diff --git a/opengever/api/configure.zcml b/opengever/api/configure.zcml
index 851a1b096f1..83228a0e535 100644
--- a/opengever/api/configure.zcml
+++ b/opengever/api/configure.zcml
@@ -6,11 +6,14 @@
i18n_domain="opengever.api">
+
+
+
diff --git a/opengever/api/schema/__init__.py b/opengever/api/schema/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/opengever/api/schema/adapters.py b/opengever/api/schema/adapters.py
new file mode 100644
index 00000000000..d94ef863555
--- /dev/null
+++ b/opengever/api/schema/adapters.py
@@ -0,0 +1,201 @@
+from opengever.api.schema.schema import TYPE_TO_BE_ADDED_KEY
+from opengever.base.interfaces import IOpengeverBaseLayer
+from plone.restapi.types.adapters import ChoiceJsonSchemaProvider
+from plone.restapi.types.adapters import CollectionJsonSchemaProvider
+from plone.restapi.types.adapters import ListJsonSchemaProvider
+from plone.restapi.types.adapters import SetJsonSchemaProvider
+from plone.restapi.types.adapters import TupleJsonSchemaProvider
+from plone.restapi.types.interfaces import IJsonSchemaProvider
+from plone.restapi.types.z3crelationadapter import ChoiceslessRelationListSchemaProvider
+from z3c.relationfield.interfaces import IRelationList
+from zope.annotation import IAnnotations
+from zope.component import adapter
+from zope.component import getMultiAdapter
+from zope.component.hooks import getSite
+from zope.interface import implementer
+from zope.interface import Interface
+from zope.schema.interfaces import IChoice
+from zope.schema.interfaces import ICollection
+from zope.schema.interfaces import IList
+from zope.schema.interfaces import ISet
+from zope.schema.interfaces import ITuple
+
+
+@adapter(IChoice, Interface, IOpengeverBaseLayer)
+@implementer(IJsonSchemaProvider)
+class GEVERChoiceJsonSchemaProvider(ChoiceJsonSchemaProvider):
+ """Customized ChoiceJsonSchemaProvider that renders schema-intent
+ aware URLs when used by the @schema endpoint.
+ """
+
+ def additional(self):
+ result = super(GEVERChoiceJsonSchemaProvider, self).additional()
+
+ # Get information about parent field so that we can use its name to
+ # render URLs to sources on anonymous inner value_type Choice fields
+ parent_field = getattr(self, 'parent_field', None)
+
+ # Postprocess the ChoiceJsonSchemaProvider to re-build the vocabulary
+ # like URLs with (possibly) schema-intent aware ones.
+
+ if 'source' in result:
+ result['source']['@id'] = get_source_url(self.field, self.context, self.request,
+ parent_field=parent_field)
+
+ if 'querysource' in result:
+ result['querysource']['@id'] = get_querysource_url(self.field, self.context, self.request,
+ parent_field=parent_field)
+
+ if 'vocabulary' in result:
+ # Extract vocab_name from URL
+ # (it's not always just self.field.vocabularyName)
+ vocab_url = result['vocabulary']['@id']
+ vocab_name = vocab_url.split('/')[-1]
+ result['vocabulary']['@id'] = get_vocabulary_url(vocab_name, self.context, self.request)
+
+ return result
+
+# These IJsonSchemaProviders below are customized so that we can retain
+# a link to the parent field. We do this so that we can use the parent field's
+# name to render URLs to sources on anonymous inner value_type Choice fields.
+
+
+@adapter(ICollection, Interface, IOpengeverBaseLayer)
+@implementer(IJsonSchemaProvider)
+class GEVERCollectionJsonSchemaProvider(CollectionJsonSchemaProvider):
+
+ def get_items(self):
+ """Get items properties."""
+ value_type_adapter = getMultiAdapter(
+ (self.field.value_type, self.context, self.request), IJsonSchemaProvider
+ )
+
+ # Retain information about parent field
+ value_type_adapter.parent_field = self.field
+ return value_type_adapter.get_schema()
+
+
+@adapter(ITuple, Interface, IOpengeverBaseLayer)
+@implementer(IJsonSchemaProvider)
+class GEVERTupleJsonSchemaProvider(TupleJsonSchemaProvider):
+
+ def get_items(self):
+ """Get items properties."""
+ value_type_adapter = getMultiAdapter(
+ (self.field.value_type, self.context, self.request), IJsonSchemaProvider
+ )
+
+ # Retain information about parent field
+ value_type_adapter.parent_field = self.field
+ return value_type_adapter.get_schema()
+
+
+@adapter(ISet, Interface, IOpengeverBaseLayer)
+@implementer(IJsonSchemaProvider)
+class GEVERSetJsonSchemaProvider(SetJsonSchemaProvider):
+
+ def get_items(self):
+ """Get items properties."""
+ value_type_adapter = getMultiAdapter(
+ (self.field.value_type, self.context, self.request), IJsonSchemaProvider
+ )
+
+ # Retain information about parent field
+ value_type_adapter.parent_field = self.field
+ return value_type_adapter.get_schema()
+
+
+@adapter(IList, Interface, IOpengeverBaseLayer)
+@implementer(IJsonSchemaProvider)
+class GEVERListJsonSchemaProvider(ListJsonSchemaProvider):
+
+ def get_items(self):
+ """Get items properties."""
+ value_type_adapter = getMultiAdapter(
+ (self.field.value_type, self.context, self.request), IJsonSchemaProvider
+ )
+
+ # Retain information about parent field
+ value_type_adapter.parent_field = self.field
+ return value_type_adapter.get_schema()
+
+
+@adapter(IRelationList, Interface, IOpengeverBaseLayer)
+@implementer(IJsonSchemaProvider)
+class GEVERChoiceslessRelationListSchemaProvider(ChoiceslessRelationListSchemaProvider):
+ def get_items(self):
+ """Get items properties."""
+ value_type_adapter = getMultiAdapter(
+ (self.field.value_type, self.context, self.request), IJsonSchemaProvider
+ )
+
+ # Prevent rendering all choices.
+ value_type_adapter.should_render_choices = False
+
+ # Retain information about parent field
+ value_type_adapter.parent_field = self.field
+
+ return value_type_adapter.get_schema()
+
+
+def get_vocab_like_url(endpoint, locator, context, request):
+ """Construct a schema-intent aware URL to a vocabulary-like endpoint.
+
+ (@vocabularies, @sources or @querysources)
+
+ The `locator` is, dependent on the endpoint, either the vocabulary name
+ or a field name.
+
+ If a TYPE_TO_BE_ADDED_KEY is present in the request annotation, this
+ signals add-intent and will be used as the portal_type of the object
+ to be added.
+
+ If TYPE_TO_BE_ADDED_KEY is missing from request annotations, edit-intent
+ will be assumed.
+ """
+ portal_type = IAnnotations(request).get(TYPE_TO_BE_ADDED_KEY)
+
+ try:
+ context_url = context.absolute_url()
+ except AttributeError:
+ portal = getSite()
+ context_url = portal.absolute_url()
+
+ if portal_type is None:
+ # edit - context is the object to be edited
+ url = '/'.join((context_url, endpoint, locator))
+ else:
+ # add - context is the container where the obj will be added
+ url = '/'.join((context_url, endpoint, portal_type, locator))
+
+ return url
+
+
+def get_vocabulary_url(vocab_name, context, request, portal_type=None):
+ return get_vocab_like_url('@vocabularies', vocab_name, context, request)
+
+
+def get_querysource_url(field, context, request, portal_type=None, parent_field=None):
+ field_name = field.getName()
+ if parent_field:
+ # If we're getting passed a parent_field, we assume that our actual
+ # field is an anonymous inner Choice field that's being used as the
+ # value_type for the multivalued parent_field. In that case, we omit
+ # the inner field's empty string name from the URL, and instead
+ # construct an URL that points to the parent field.
+ field_name = parent_field.getName()
+
+ return get_vocab_like_url('@querysources', field_name, context, request)
+
+
+def get_source_url(field, context, request, portal_type=None, parent_field=None):
+ field_name = field.getName()
+ if parent_field:
+ # If we're getting passed a parent_field, we assume that our actual
+ # field is an anonymous inner Choice field that's being used as the
+ # value_type for the multivalued parent_field. In that case, we omit
+ # the inner field's empty string name from the URL, and instead
+ # construct an URL that points to the parent field.
+ field_name = parent_field.getName()
+
+ return get_vocab_like_url('@sources', field_name, context, request)
diff --git a/opengever/api/schema/configure.zcml b/opengever/api/schema/configure.zcml
new file mode 100644
index 00000000000..cfeb1b4c30d
--- /dev/null
+++ b/opengever/api/schema/configure.zcml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opengever/api/schema/querysources.py b/opengever/api/schema/querysources.py
new file mode 100644
index 00000000000..8b6136f78d6
--- /dev/null
+++ b/opengever/api/schema/querysources.py
@@ -0,0 +1,109 @@
+from opengever.api.schema.sources import get_field_by_name
+from opengever.api.schema.sources import GEVERSourcesGet
+from opengever.base.interfaces import IDuringContentCreation
+from plone.dexterity.utils import iterSchemata
+from plone.dexterity.utils import iterSchemataForType
+from plone.restapi.batching import HypermediaBatch
+from plone.restapi.interfaces import ISerializeToJson
+from z3c.formwidget.query.interfaces import IQuerySource
+from zope.component import getMultiAdapter
+from zope.interface import alsoProvides
+from zope.schema.interfaces import ICollection
+
+
+class GEVERQuerySourcesGet(GEVERSourcesGet):
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ super(GEVERQuerySourcesGet, self).__init__(context, request)
+
+ def reply(self):
+ if len(self.params) not in (1, 2):
+ return self._error(
+ 400, "Bad Request",
+ "Must supply either one (fieldname) or two (portal_type, fieldname) parameters"
+ )
+
+ if len(self.params) == 1:
+ # Edit intent
+ # - context is the object to be edited
+ # - schemata need to be determined via context
+ self.intent = 'edit'
+ portal_type = None
+ fieldname = self.params[0]
+ schemata = iterSchemata(self.context)
+
+ else:
+ # Add intent
+ # - context is the container where the object will be created
+ # - portal_type is the type of object to be created
+ # - schemata need to be determined via portal_type
+ self.intent = 'add'
+ portal_type = self.params[0]
+ fieldname = self.params[1]
+ schemata = iterSchemataForType(portal_type)
+ alsoProvides(self.request, IDuringContentCreation)
+
+ field = get_field_by_name(fieldname, schemata)
+ if field is None:
+ return self._error(
+ 404, "Not Found",
+ "No such field: %r" % fieldname
+ )
+ bound_field = field.bind(self.context)
+
+ # Look for a source directly on the field first
+ source = getattr(bound_field, 'source', None)
+
+ # Handle ICollections (like Tuples, Lists and Sets). These don't have
+ # sources themselves, but instead are multivalued, and their
+ # items are backed by a value_type of Choice with a source
+ if ICollection.providedBy(bound_field):
+ source = self._get_value_type_source(bound_field)
+ if not source:
+ ftype = bound_field.__class__.__name__
+ return self._error(
+ 404, "Not Found",
+ "%r Field %r does not have a value_type of Choice with "
+ "an IQuerySource" % (ftype, fieldname))
+
+ if not IQuerySource.providedBy(source):
+ return self._error(
+ 404, "Not Found",
+ "Field %r does not have an IQuerySource" % fieldname
+ )
+
+ if 'query' not in self.request.form:
+ return self._error(
+ 400, "Bad Request",
+ u'Enumerating querysources is not supported. Please search '
+ u'the source using the ?query= QS parameter'
+ )
+
+ query = self.request.form['query']
+
+ result = source.search(query)
+
+ terms = []
+ for term in result:
+ terms.append(term)
+
+ batch = HypermediaBatch(self.request, terms)
+
+ serialized_terms = []
+ for term in batch:
+ serializer = getMultiAdapter(
+ (term, self.request), interface=ISerializeToJson
+ )
+ serialized_terms.append(serializer())
+
+ result = {
+ "@id": batch.canonical_url,
+ "items": serialized_terms,
+ "items_total": batch.items_total,
+ }
+ links = batch.links
+ if links:
+ result["batching"] = links
+ return result
diff --git a/opengever/api/schema/schema.py b/opengever/api/schema/schema.py
new file mode 100644
index 00000000000..5a25ff5aa25
--- /dev/null
+++ b/opengever/api/schema/schema.py
@@ -0,0 +1,57 @@
+from plone.restapi.services import Service
+from plone.restapi.services.types.get import check_security
+from plone.restapi.types.utils import get_jsonschema_for_portal_type
+from zope.annotation import IAnnotations
+from zope.interface import implementer
+from zope.publisher.interfaces import IPublishTraverse
+
+
+TYPE_TO_BE_ADDED_KEY = 'plone.restapi.portal_type_to_be_added'
+
+
+@implementer(IPublishTraverse)
+class GEVERSchemaGet(Service):
+ """Endpoint that serializes intent-aware schemas.
+ """
+
+ def __init__(self, context, request):
+ super(GEVERSchemaGet, self).__init__(context, request)
+ self.params = []
+
+ def publishTraverse(self, request, name):
+ # Treat any path segments after /@types as parameters
+ self.params.append(name)
+ return self
+
+ def reply(self):
+ if len(self.params) == 0:
+ # Edit intent
+ self.intent = 'edit'
+ portal_type = self.context.portal_type
+
+ elif len(self.params) == 1:
+ # Add intent
+ self.intent = 'add'
+ portal_type = self.params[0]
+ request_annotations = IAnnotations(self.request)
+ request_annotations[TYPE_TO_BE_ADDED_KEY] = portal_type
+
+ else:
+ return self._error(
+ 400, "Bad Request",
+ "Must supply either zero or one (portal_type) parameters"
+ )
+
+ check_security(self.context)
+ self.content_type = "application/json+schema"
+ try:
+ return get_jsonschema_for_portal_type(
+ portal_type, self.context, self.request
+ )
+ except KeyError:
+ self.content_type = "application/json"
+ self.request.response.setStatus(404)
+ return {
+ "type": "NotFound",
+ "message": 'Type "{}" could not be found.'.format(portal_type),
+ }
diff --git a/opengever/api/schema/sources.py b/opengever/api/schema/sources.py
new file mode 100644
index 00000000000..76638ee403b
--- /dev/null
+++ b/opengever/api/schema/sources.py
@@ -0,0 +1,108 @@
+from opengever.base.interfaces import IDuringContentCreation
+from plone.dexterity.utils import iterSchemata
+from plone.dexterity.utils import iterSchemataForType
+from plone.restapi.interfaces import ISerializeToJson
+from plone.restapi.services.sources.get import SourcesGet
+from zope.component import getMultiAdapter
+from zope.interface import alsoProvides
+from zope.schema import getFieldsInOrder
+from zope.schema.interfaces import IChoice
+from zope.schema.interfaces import ICollection
+from zope.schema.interfaces import IIterableSource
+from zope.schema.interfaces import ISource
+
+
+class GEVERSourcesGet(SourcesGet):
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ super(GEVERSourcesGet, self).__init__(context, request)
+
+ def _get_value_type_source(self, field):
+ """Get the source of a Choice field that is used as the `value_type`
+ for a multi-valued ICollection field, like ITuple.
+ """
+ value_type = getattr(field, 'value_type', None)
+ value_type_source = getattr(value_type, 'source', None)
+ if not value_type or not IChoice.providedBy(value_type) or not value_type_source:
+ return None
+ return value_type_source
+
+ def reply(self):
+ if len(self.params) not in (1, 2):
+ return self._error(
+ 400, "Bad Request",
+ "Must supply either one (fieldname) or two (portal_type, fieldname) parameters"
+ )
+
+ if len(self.params) == 1:
+ # Edit intent
+ # - context is the object to be edited
+ # - schemata need to be determined via context
+ self.intent = 'edit'
+ portal_type = None
+ fieldname = self.params[0]
+ schemata = iterSchemata(self.context)
+
+ else:
+ # Add intent
+ # - context is the container where the object will be created
+ # - portal_type is the type of object to be created
+ # - schemata need to be determined via portal_type
+ self.intent = 'add'
+ portal_type = self.params[0]
+ fieldname = self.params[1]
+ schemata = iterSchemataForType(portal_type)
+ alsoProvides(self.request, IDuringContentCreation)
+
+ field = get_field_by_name(fieldname, schemata)
+ if field is None:
+ return self._error(
+ 404, "Not Found",
+ "No such field: %r" % fieldname
+ )
+
+ bound_field = field.bind(self.context)
+
+ # Look for a source directly on the field first
+ source = getattr(bound_field, 'source', None)
+
+ # Handle ICollections (like Tuples, Lists and Sets). These don't have
+ # sources themselves, but instead are multivalued, and their
+ # items are backed by a value_type of Choice with a source
+ if ICollection.providedBy(bound_field):
+ source = self._get_value_type_source(bound_field)
+ if not source:
+ ftype = bound_field.__class__.__name__
+ return self._error(
+ 404, "Not Found",
+ "%r Field %r does not have a value_type of Choice with "
+ "an ISource" % (ftype, fieldname))
+
+ if not ISource.providedBy(source):
+ return self._error(
+ 404, "Not Found",
+ "Field %r does not have a source" % fieldname
+ )
+
+ if not IIterableSource.providedBy(source):
+ return self._error(
+ 400, "Bad Request",
+ "Source for field %r is not iterable. " % fieldname
+ )
+
+ serializer = getMultiAdapter(
+ (source, self.request), interface=ISerializeToJson
+ )
+ return serializer(
+ "{}/@sources/{}".format(self.context.absolute_url(), fieldname)
+ )
+
+
+def get_field_by_name(fieldname, schemata):
+ for schema in schemata:
+ fields = getFieldsInOrder(schema)
+ for fn, field in fields:
+ if fn == fieldname:
+ return field
diff --git a/opengever/api/schema/vocabularies.py b/opengever/api/schema/vocabularies.py
new file mode 100644
index 00000000000..5d504f56004
--- /dev/null
+++ b/opengever/api/schema/vocabularies.py
@@ -0,0 +1,67 @@
+from opengever.base.interfaces import IDuringContentCreation
+from plone.restapi.interfaces import ISerializeToJson
+from plone.restapi.services.vocabularies.get import VocabulariesGet
+from zope.component import ComponentLookupError
+from zope.component import getMultiAdapter
+from zope.component import getUtilitiesFor
+from zope.component import getUtility
+from zope.interface import alsoProvides
+from zope.schema.interfaces import IVocabularyFactory
+
+
+class GEVERVocabulariesGet(VocabulariesGet):
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ super(GEVERVocabulariesGet, self).__init__(context, request)
+
+ def reply(self):
+ if len(self.params) == 0:
+ # Listing of existing vocabularies
+ return [
+ {
+ "@id": "{}/@vocabularies/{}".format(
+ self.context.absolute_url(), vocab[0]
+ ),
+ "title": vocab[0],
+ }
+ for vocab in getUtilitiesFor(IVocabularyFactory)
+ ]
+
+ elif len(self.params) == 1:
+ # Edit intent
+ # - context is the object to be edited
+ self.intent = 'edit'
+ vocab_name = self.params[0]
+ elif len(self.params) == 2:
+ # Add intent
+ # - context is the container where the object will be created
+ # - first parameter is the portal_type
+ # (which will be ignored by this endpoint, but is accepted
+ # for consistency with @sources and @querysources)
+ self.intent = 'add'
+ vocab_name = self.params[1]
+ alsoProvides(self.request, IDuringContentCreation)
+ else:
+ return self._error(
+ 400, "Bad Request",
+ "Must supply either zero, one (vocab_name) or "
+ "two (portal_type, vocab_name) parameters"
+ )
+
+ try:
+ factory = getUtility(IVocabularyFactory, name=vocab_name)
+ except ComponentLookupError:
+ return self._error(
+ 404, "Not Found", "The vocabulary '{}' does not exist".format(vocab_name)
+ )
+
+ vocabulary = factory(self.context)
+
+ serializer = getMultiAdapter(
+ (vocabulary, self.request), interface=ISerializeToJson
+ )
+ return serializer(
+ "{}/@vocabularies/{}".format(self.context.absolute_url(), vocab_name)
+ )
diff --git a/opengever/api/tests/test_schema.py b/opengever/api/tests/test_schema.py
new file mode 100644
index 00000000000..07e4c839668
--- /dev/null
+++ b/opengever/api/tests/test_schema.py
@@ -0,0 +1,39 @@
+from ftw.testbrowser import browsing
+from opengever.testing import IntegrationTestCase
+
+
+class TestSchemaEndpoint(IntegrationTestCase):
+
+ @browsing
+ def test_schema_endpoint_id_for_vocabulary(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.document.absolute_url() + '/@schema'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+ expected_url = "/".join(
+ (self.document.absolute_url(),
+ '@vocabularies/classification_classification_vocabulary'))
+ self.assertEqual(
+ expected_url,
+ response['properties']['classification']['vocabulary']['@id']
+ )
+
+ @browsing
+ def test_schema_endpoint_id_for_querysource(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.document.absolute_url() + '/@schema'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+ expected_url = "/".join(
+ (self.document.absolute_url(),
+ '@querysources/keywords'))
+ self.assertEqual(
+ expected_url,
+ response['properties']['keywords']['items']['querysource']['@id']
+ )
diff --git a/opengever/api/tests/test_vocabularies.py b/opengever/api/tests/test_vocabularies.py
index c7fdb1fa714..c93fba05764 100644
--- a/opengever/api/tests/test_vocabularies.py
+++ b/opengever/api/tests/test_vocabularies.py
@@ -1,13 +1,9 @@
from ftw.testbrowser import browsing
+from opengever.base.behaviors.classification import IClassification
from opengever.testing import IntegrationTestCase
from plone import api
-def http_headers():
- return {'Accept': 'application/json',
- 'Content-Type': 'application/json'}
-
-
NON_SENSITIVE_VOCABUALRIES = [
'Behaviors',
'classification_classification_vocabulary',
@@ -119,7 +115,7 @@ def test_vocabularies_endpoint_does_not_provide_sensitive_data(self, browser):
response = browser.open(
self.portal.absolute_url() + '/@vocabularies',
method='GET',
- headers=http_headers(),
+ headers=self.api_headers,
).json
self.assertItemsEqual(
@@ -141,7 +137,7 @@ def assert_permission_for_non_sensitive_vocabulaires(self, browser, role):
browser.open(
self.portal.absolute_url() + '/@vocabularies/{}'.format(vocabulary),
method='GET',
- headers=http_headers())
+ headers=self.api_headers)
if browser.status_code != 200:
not_accessable.append((vocabulary, browser.status_code))
@@ -159,3 +155,187 @@ def test_all_non_sensitive_vocabularies_are_accessable_by_a_member(self, browser
@browsing
def test_all_non_sensitive_vocabularies_are_accessable_by_a_contributor(self, browser):
self.assert_permission_for_non_sensitive_vocabulaires(browser, 'Contributor')
+
+
+class TestGetVocabularies(IntegrationTestCase):
+
+ @browsing
+ def test_get_vocabulary_for_edit(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.empty_dossier.absolute_url() + '/@vocabularies/opengever.document.document_types'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(8, response.get('items_total'))
+ expected_tokens = [u'contract', u'directive', u'offer', u'protocol',
+ u'question', u'regulations', u'report', u'request']
+ self.assertItemsEqual(expected_tokens,
+ [item['token'] for item in response.get('items')])
+
+ @browsing
+ def test_get_vocabulary_for_add(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.empty_dossier.absolute_url() + '/@vocabularies/opengever.document.document/opengever.document.document_types'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(8, response.get('items_total'))
+ expected_tokens = [u'contract', u'directive', u'offer', u'protocol',
+ u'question', u'regulations', u'report', u'request']
+ self.assertItemsEqual(expected_tokens,
+ [item['token'] for item in response.get('items')])
+
+ @browsing
+ def test_get_restricted_vocabulary_for_add(self, browser):
+ self.login(self.regular_user, browser)
+
+ url = self.leaf_repofolder.absolute_url() + '/@vocabularies/opengever.dossier.businesscasedossier/classification_classification_vocabulary'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ field = IClassification['classification']
+ field.set(field.interface(self.leaf_repofolder), u'confidential')
+ restricted_response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(url, restricted_response.get('@id'))
+ self.assertTrue(
+ response.get('items_total') > restricted_response.get('items_total'))
+ self.assertIn(
+ u'unprotected',
+ [item.get('token') for item in response.get('items')]
+ )
+ self.assertNotIn(
+ u'unprotected',
+ [item.get('token') for item in restricted_response.get('items')]
+ )
+
+ @browsing
+ def test_get_restricted_vocabulary_for_edit(self, browser):
+ self.login(self.regular_user, browser)
+
+ url = self.dossier.absolute_url() + '/@vocabularies/classification_classification_vocabulary'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ field = IClassification['classification']
+ field.set(field.interface(self.leaf_repofolder), u'confidential')
+ restricted_response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(url, restricted_response.get('@id'))
+ self.assertTrue(
+ response.get('items_total') > restricted_response.get('items_total'))
+ self.assertIn(
+ u'unprotected',
+ [item.get('token') for item in response.get('items')]
+ )
+ self.assertNotIn(
+ u'unprotected',
+ [item.get('token') for item in restricted_response.get('items')]
+ )
+
+
+class TestGetQuerySources(IntegrationTestCase):
+
+ @browsing
+ def test_get_querysource_for_edit(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.empty_dossier.absolute_url() + '/@querysources/responsible?query=nicole'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(1, response.get('items_total'))
+ self.assertItemsEqual([u'nicole.kohler'],
+ [item['token'] for item in response.get('items')])
+
+ @browsing
+ def test_get_vocabulary_for_add(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.leaf_repofolder.absolute_url() + '/@querysources/opengever.dossier.businesscasedossier/responsible?query=nicole'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(1, response.get('items_total'))
+ self.assertItemsEqual([u'nicole.kohler'],
+ [item['token'] for item in response.get('items')])
+
+ @browsing
+ def test_get_keywords_querysource_for_edit(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.empty_dossier.absolute_url() + '/@querysources/keywords?query=secret'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(1, response.get('items_total'))
+ self.assertItemsEqual([u'secret'],
+ [item['token'] for item in response.get('items')])
+
+
+class TestGetSources(IntegrationTestCase):
+
+ @browsing
+ def test_get_source_for_edit(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.document.absolute_url() + '/@sources/document_type'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(8, response.get('items_total'))
+ expected_tokens = [u'contract', u'directive', u'offer', u'protocol',
+ u'question', u'regulations', u'report', u'request']
+ self.assertItemsEqual(expected_tokens,
+ [item['token'] for item in response.get('items')])
+
+ @browsing
+ def test_get_vocabulary_for_add(self, browser):
+ self.login(self.regular_user, browser)
+ url = self.empty_dossier.absolute_url() + '/@sources/opengever.document.document/document_type'
+ response = browser.open(
+ url,
+ method='GET',
+ headers=self.api_headers,
+ ).json
+
+ self.assertEqual(url, response.get('@id'))
+ self.assertEqual(8, response.get('items_total'))
+ expected_tokens = [u'contract', u'directive', u'offer', u'protocol',
+ u'question', u'regulations', u'report', u'request']
+ self.assertItemsEqual(expected_tokens,
+ [item['token'] for item in response.get('items')])
diff --git a/opengever/base/configure.zcml b/opengever/base/configure.zcml
index 4583fe4c8bc..cf76edc8d62 100644
--- a/opengever/base/configure.zcml
+++ b/opengever/base/configure.zcml
@@ -28,6 +28,15 @@
+
+