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 @@ + +