Skip to content

Commit

Permalink
Handle (query)sources for multivalued zope.schema fields:
Browse files Browse the repository at this point in the history
Multivalued fields don't directly have sources themselves, but
their items do.

We therefore need to handle that case in the @sources and
@querysources endpoints, and look for a source on the Choice
field used as the value_type for the multivalued field.

Similarly we need to construct appropriate URLs during
schema serialization for these types of fields: The Choice
fields used as the value_type for multivalued fields don't
usually have a name. We therefore omit their empty string
name from the URL, and instead construct URLs that look
like the source was actually on the multivalued fields
themselves.
  • Loading branch information
lukasgraf committed Nov 4, 2019
1 parent b66585b commit aeb3b06
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 19 deletions.
129 changes: 123 additions & 6 deletions opengever/api/schema/adapters.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
5 changes: 5 additions & 0 deletions opengever/api/schema/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,10 @@
/>

<adapter factory=".adapters.GEVERChoiceJsonSchemaProvider" />
<adapter factory=".adapters.GEVERCollectionJsonSchemaProvider" />
<adapter factory=".adapters.GEVERTupleJsonSchemaProvider" />
<adapter factory=".adapters.GEVERSetJsonSchemaProvider" />
<adapter factory=".adapters.GEVERListJsonSchemaProvider" />
<adapter factory=".adapters.GEVERChoiceslessRelationListSchemaProvider" />

</configure>
19 changes: 15 additions & 4 deletions opengever/api/schema/querysources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand Down
34 changes: 25 additions & 9 deletions opengever/api/schema/sources.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions opengever/base/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
<include file="schema.zcml" />
<include file="widgets.zcml" />

<plone:CORSPolicy
allow_origin="*"
allow_methods="DELETE,GET,OPTIONS,PATCH,POST,PUT"
allow_credentials="true"
expose_headers="Content-Length,X-My-Header"
allow_headers="Accept,Authorization,Content-Type,X-Custom-Header"
max_age="3600"
/>

<i18n:registerTranslations directory="locales" />

<genericsetup:registerProfile
Expand Down

0 comments on commit aeb3b06

Please sign in to comment.