IContextSourceBinder and validation in Plone 5.2

I have a lot of dynamic sources along the lines shown in the Dexterity developer manual here https://docs.plone.org/external/plone.app.dexterity/docs/advanced/vocabularies.html#dynamic-sources. Just a function that is context-aware and returns a vocabulary. This works fine in the usual add/edit forms for the content type. But I also have some of code that attempts to build a new form based on parts of a content schema, to do mass edits. It will get a widget like this which can be rendered directly:

field = IMyInterface['field']
widget = getMultiAdapter((field, self.request), IFieldWidget)
widget.update()

This works in 5.1 but in 5.2 it fails validation, due to what looks like an update in zope.schema._field.Choice's validation.

Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 155, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 337, in publish_module
  Module ZPublisher.WSGIPublisher, line 255, in publish
  Module ZPublisher.mapply, line 85, in mapply
  Module ZPublisher.WSGIPublisher, line 61, in call_object
  Module ims.fieldupdater.browser.mass, line 28, in __call__
  Module Products.Five.browser.pagetemplatefile, line 126, in __call__
  Module Products.Five.browser.pagetemplatefile, line 61, in __call__
  Module zope.pagetemplate.pagetemplate, line 135, in pt_render
  Module Products.PageTemplates.engine, line 88, in __call__
  Module z3c.pt.pagetemplate, line 173, in render
  Module chameleon.zpt.template, line 306, in render
  Module chameleon.template, line 209, in render
  Module chameleon.utils, line 75, in raise_with_traceback
  Module chameleon.template, line 187, in render
  Module 6dffeae47701c9dc8464c4b9dccef94e.py, line 2050, in render
  Module 5c4dfcdbef155018902625d254565f06.py, line 284, in render_master
  Module 927f9f167c8e9d53f313dd3edc43e383.py, line 687, in render_master
  Module 5c4dfcdbef155018902625d254565f06.py, line 264, in __fill_content
  Module 927f9f167c8e9d53f313dd3edc43e383.py, line 1273, in render_content
  Module 5c4dfcdbef155018902625d254565f06.py, line 255, in __fill_main
  Module 6dffeae47701c9dc8464c4b9dccef94e.py, line 1917, in __fill_prefs_configlet_main
  Module ims.fieldupdater.browser.mass, line 346, in replacement_widget
  Module z3c.form.browser.select, line 51, in update
  Module z3c.form.browser.widget, line 171, in update
  Module z3c.form.widget, line 233, in update
  Module z3c.form.widget, line 227, in updateTerms
  Module zope.component._api, line 95, in getMultiAdapter
  Module zope.component._api, line 108, in queryMultiAdapter
  Module zope.interface.registry, line 359, in queryMultiAdapter
  Module zope.interface.adapter, line 552, in queryMultiAdapter
  Module z3c.form.term, line 105, in ChoiceTerms
  Module zope.schema._field, line 507, in bind
  Module zope.schema._field, line 498, in _resolve_vocabulary
zope.schema._field.InvalidVocabularyError: zope.schema._field.InvalidVocabularyError: Invalid vocabulary <function contact_categories at 0x7f5a186a7f28>

It fails because the returned vocabulary does not provide ISource, which again looks like a new addition to the validation. So it's not clear to me if I should be converting my IContextSourceBinder functions into a class that also implements ISource (or convert it into a named vocabulary), or if the problem is somehow with the mass edit code that tries to extract and render a single widget. Or, perhaps, the problem is an overly aggressive validator in zope.schema but I don't know what I'd be able to do about that.

IMO ISource/IContextSource to difficult. Instead I use the vocabulary factory:

from zope.schema.interfaces import IVocabularyFactory
from plone.app.vocabularies.terms import safe_simplevocabulary_from_values

@implementer(IVocabularyFactory)
class MyVocabularyFactory(object):
    """Vocabulary factory listing something context aware
    """

    def __call__(self, context, query=None):
        # fetch some values from somewhere where context and maybe query is needed, 
        # for this example here a static list is used.
        values = ["foo", "bar", "baz"]
        return safe_simplevocabulary_from_values(values)

registered as utility:


  <utility
    component=".vocabularies.MyVocabularyFactory"
    name="myproject.myvocabulary"
    />

and use it in my schema


    myfield = schema.Tuple(
        title=u"Myfield",
        value_type=schema.TextLine(),
        required=False,
        missing_value=(),
    )
    directives.widget(
        "myfield",
        AjaxSelectFieldWidget,
        vocabulary="myproject.myvocabulary"
    )

Ok, I can convert my existing instances of this. Is there a benefit to using a class with @implementer as opposed to @provider on a function?

No, my example is an anti-pattern for the simple case. I copied from a project where the class does much more and subclasses are used too.

We use IContextSourceBinder frequently e.g.

    form.widget("gl_refs_my_onkopedia", SelectWidget, multiple="multiple")
    gl_refs_my_onkopedia = schema.List(
        title=_("References to My-Onkopedia"),
        description=_("References to My-Onkopedia"),
        required=False,
        value_type=schema.Choice(
            source=OnkopediaPathBinder(["my-onkopedia", "mein-onkopedia"])
        ),
        default=[],
    )

with

@implementer(IContextSourceBinder)
class OnkopediaPathBinder(object):
    """ Returns all linkable objects in a given subarea of Onkopedia"""

    def __init__(self, areas, subpath="guidelines", drug_assessment=False):
        super(OnkopediaPathBinder, self).__init__()
        assert isinstance(areas, (list, tuple)), "areas must be tuple or list"
        self.areas = areas
        self.subpath = subpath
        self.drug_assessment = drug_assessment

    def __call__(self, context):

        items = list()
        portal = plone.api.portal.get()

        uuids_seen = set()
        for area in self.areas:
            language = (
                "de" if "/onkopedia/de" in "/".join(context.getPhysicalPath()) else "en"
            )
            path = "{}/{}".format(language, area)
            if self.subpath:
                path += "/{}".format(self.subpath)
            target_folder = portal.restrictedTraverse(path, None)
            if target_folder is not None:
                for brain in target_folder.getFolderContents():

                    if brain.UID in uuids_seen:
                        continue

                    if self.drug_assessment:
                        title = safe_unicode(brain.Title)
                        title += " (" + safe_unicode(brain.topic_str)
                        if brain.specifications_str:
                            title += ", {}".format(
                                safe_unicode(brain.specifications_str)
                            )
                        if brain.specifications2_str:
                            title += ", {}".format(
                                safe_unicode(brain.specifications2_str)
                            )
                        title += ")"
                        items.append(SimpleTerm(brain.UID, brain.UID, title))
                    else:
                        items.append(SimpleTerm(brain.UID, brain.UID, brain.Title))
                    uuids_seen.add(brain.UID)

        items = sorted(items, key=operator.attrgetter("title"))
        return VocabularyWrapper(SimpleVocabulary(items))


class VocabularyWrapper(object):
    """ Wraps a SimpleVocabulary into a searchable source """

    def __init__(self, vocabulary):
        self.vocabulary = vocabulary

    def getTermByToken(self, token):
        return self.vocabulary.getTermByToken(token)

    def __contains__(self, name):
        return name in self.vocabulary

    def getTerm(self, term):
        return self.vocabulary.getTerm(term)

    def __iter__(self):
        for item in self.vocabulary:
            yield item

    def search(self, query_string):
        items = []
        for term in self.vocabulary:
            if query_string.lower() in term.title.lower():
                items.append(term)
        return items
~
1 Like