diff --git a/opengever/api/schema/adapters.py b/opengever/api/schema/adapters.py index 2f8e231c024..d94ef863555 100644 --- a/opengever/api/schema/adapters.py +++ b/opengever/api/schema/adapters.py @@ -1,13 +1,24 @@ 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) @@ -20,14 +31,20 @@ class GEVERChoiceJsonSchemaProvider(ChoiceJsonSchemaProvider): 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) + 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) + 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 @@ -38,6 +55,88 @@ def additional(self): 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. @@ -76,9 +175,27 @@ 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): - return get_vocab_like_url('@querysources', field.getName(), 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() -def get_source_url(field, context, request, portal_type=None): - return get_vocab_like_url('@sources', field.getName(), context, request) + return get_vocab_like_url('@sources', field_name, context, request) diff --git a/opengever/api/schema/configure.zcml b/opengever/api/schema/configure.zcml index ed463ac9bbc..cfeb1b4c30d 100644 --- a/opengever/api/schema/configure.zcml +++ b/opengever/api/schema/configure.zcml @@ -83,5 +83,10 @@ /> + + + + + diff --git a/opengever/api/schema/querysources.py b/opengever/api/schema/querysources.py index 65296f0b413..8b6136f78d6 100644 --- a/opengever/api/schema/querysources.py +++ b/opengever/api/schema/querysources.py @@ -8,6 +8,7 @@ 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): @@ -52,10 +53,20 @@ def reply(self): ) bound_field = field.bind(self.context) - if hasattr(bound_field, "value_type"): - source = bound_field.value_type.source - else: - source = bound_field.source + # 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( diff --git a/opengever/api/schema/sources.py b/opengever/api/schema/sources.py index 98dae455d58..76638ee403b 100644 --- a/opengever/api/schema/sources.py +++ b/opengever/api/schema/sources.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from opengever.base.interfaces import IDuringContentCreation from plone.dexterity.utils import iterSchemata from plone.dexterity.utils import iterSchemataForType @@ -7,6 +6,8 @@ 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 @@ -18,10 +19,15 @@ def __init__(self, context, request): self.request = request super(GEVERSourcesGet, self).__init__(context, request) - def publishTraverse(self, request, name): - # Treat any path segments after /@sources as parameters - self.params.append(name) - return self + 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): @@ -59,10 +65,20 @@ def reply(self): bound_field = field.bind(self.context) - if hasattr(bound_field, "value_type"): - source = bound_field.value_type.source - else: - source = bound_field.source + # 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( 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 @@ + +