How to make Dexterity fields required (or not), depending on the workflow state?

I have a Dexterity content type for which I'd like to make certain fields required in the edit form at specific workflow states.

The z3c.form documentation at https://docs.plone.org/develop/plone/forms/z3c.form.html#making-widgets-required-conditionally mentions one method (a bit cryptically) that requires modifying the form field. Since I'm defining the content type schema as an Interface and am using the default Dexterity add and edit forms, I'm not sure how to use this method.

Would another workable method be to use invariant validators as per https://docs.plone.org/develop/plone/forms/z3c.form.html#invariant-validators ? As long as I can get to the content item itself, I can check what workflow state is in, and then raise the Invalid exception if the field value is empty.

This works, but displays the same error in every field set except the default one, and does not indicate visually where the field is that violated the requiredness:

from zope.interface import Invalid
from zope.interface import invariant
from plone import api

class IMyContentType(Interface):
    @invariant
    def verify_per_state_requiredness(data):
        state = api.content.get_state(obj=data.__context__)
        if state == 'initial': # this is the ID of any state you want to check
            if not data.title: # every field is an attribute of this data object
                raise Invalid(_(u'Title is required in the ''%s'' state' % state))

You see the usual "There were some errors" message but on the Default fieldset there is nothing else indicating what the error was (ie. in the above example, the string "Title is required in the `initial' state"):

If you click on any other fieldset, you see the expected error message:

take a look at collective.elections (check the README file first to understand how it works).

the code is old and currently unsupported, but I think it covers exactly the same use case.

at some point I wanted to manage the election of the Plone board using it, but I think is way too complicated :slight_smile:

Sounds like a total power grab! :smiley:

It was easier with Archetypes because you could add arbitrary attributes to a field .... e.g. this required_by_state = ['seatAssigned'] attribute of a field

which would be checked as part of a transition guard

@hvelarde do you mean this state_changed event subscriber?

OK this method works nicely:

https://docs.plone.org/develop/plone/forms/z3c.form.html#form-widget-validators

I also found this nugget by @davisagli:

which I will try if the above method doesn't cut it.

sound like you found something that could be an enhancement for Dexterity; open an issue.

no, @frapell was the one who wrote most of the code; IIRC, he added some checks to the read_permission and write_permission attributes based on the workflow state.

@tkimnguyen IIRC There are some changes where a zope.interface subclassed Invalid works for zope.formlib based forms but not for z3c.form. But you already found that in the z3c.for docs now.

Here's what I will proceed with. The (somewhat contrived) example code below enforces requiredness for title and description in the state having the ID initial

from plone import api
from z3c.form import validator

class TitleRequiredValidator(validator.SimpleFieldValidator):
    def validate(self, value):
        state = api.content.get_state(obj=self.context)
        if state == 'initial':
            if not value or not value.strip():
                raise Invalid(_(u'Title is required in state ''%s''' % state))
validator.WidgetValidatorDiscriminators(TitleRequiredValidator, field=IOIEStudyAbroadProgram['title'])

class DescriptionRequiredValidator(validator.SimpleFieldValidator):
    def validate(self, value):
        state = api.content.get_state(obj=self.context)
        if state == 'initial':
            if not value or not value.strip():
                raise Invalid(_(u'Description is required in state ''%s''' % state))
validator.WidgetValidatorDiscriminators(DescriptionRequiredValidator, field=IOIEStudyAbroadProgram['description'])

and these two lines added in configure.zcml:

  <adapter factory="uwosh.oie.studyabroadstudent.interfaces.studyabroadprogram.TitleRequiredValidator" />
  <adapter factory="uwosh.oie.studyabroadstudent.interfaces.studyabroadprogram.DescriptionRequiredValidator" />

If there is a validation error, the usual "There were some errors" message appears, and you have to click onto each field set to find the field that had the problem. Not ideal, but it works.