How do I validate in datagridfield?

What is the 'correct way' to validate DataGridfields ?

In 'normal field', I can validate with

def company_letter_kodeConstraint(value):
    """Check that the company_3 letter code is upperclass
    """
    if not value.isupper():
        #Does not work with datagridfield, gives error in browser
        raise Invalid(_(u"Only capital letters for Company 3 letter code"))
        #Works with datagridfield, but will show error message 'Constraint not satisfied /The system could not process the given value.'
        return False
    return True

UPDATE: This might be due to 'bootstrap stuff', because the following works (but will return the error message)

def company_letter_kodeConstraint(value):
"""Check that the company_3 letter code is upperclass
"""
if not value.isupper():
raise InvalidValue()
#raise InvalidValue(_(u"This message will not show "))
return True

I'm running into the same issue I think, in Plone 5.2, DGF 1.5.3.

def filesize_limit(data, limit=5):
    mbs = data.size/1024/1024
    if mbs > limit:
        raise Invalid(f'File must not exceed {limit} MB')

def validate_presentation(val):
    filesize_limit(val, 5)

class IPresentation(model.Schema):
    file = NamedBlobFile(
        title='Attachment',
        description='If available, upload an image of the presenter(s) at the event or a flyer '
                    'of the event (less than 5 MB).',
        required=False,
        constraint=validate_presentation
    )

class IMySchema(model.Schema):
    directives.widget(presentations=BlockDataGridFieldFactory)
    presentations = schema.List(
        title='Presentations',
        value_type=DictRow(title="Presentation", schema=IPresentation),
        required=False,
    )

In the Dexterity add form, if I set the schema just to IPresentation (no DGF) it works as expected. With the schema set IMySchema, validation fails but instead of being caught and a warning shown it just displays the traceback.

I tried raising a ValidationError instead. This led to the message "The system could not process the given value." on the presentations field and "Raised if the Validation process fails." on the presentations sub field.

I also tried setting a constraint on the DGF field itself (presentations) but the method was never reached.

Here's one solution, though not ideal: remove constraints from the schema and validate instead on the form's handler.

def validate_presentation(val):
    if not filesize_limit(val['file'], 5):
        raise WidgetActionExecutionError('presentations',
                                         Invalid(f'File size must not exceed 5 MB'))
...

    @button.buttonAndHandler(_("Save"), name="save")
    def handleAdd(self, action):
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return
        for presentation in data['presentations']:
            validate_presentation(presentation)

Not ideal, as it highlights the entire DGF.

edit:
This doesn't work if I move the field "presentations" into a fieldset. Ideally I still want to assign validation directory to a schema field anyway.

It looks to me that it is possible to use InvalidValue() ONLY if the other fields does not have any 'normal validation present'.

In other words:

  • validation and raise InvalidValue() works for 'field A', but does not give 'proper error message'.

  • if we add in field B

    fieldB = schema.TextLine(
    max_length=3,

things 'break'


Will report back if / when I find out more

Here's another DGF validation snippet

We used the same approach with the widget-based validator introduced by @petschki; however, we went a step further by displaying multiple errors while validating the DataGridField. This might be useful for others.

First, you need to register a widget-level validator for your DataGridField (DGF).

from z3c.form import validator

validator.WidgetValidatorDiscriminators(
    MyDGFValidator,
    field=IMySchema["presentations"],
)
component.provideAdapter(MyDGFValidator)

register the validator in ZCML

<configure
    xmlns="http://namespaces.zope.org/zope"
    i18n_domain="">

  <adapter factory=".MyDGFValidator" name="presentations" />

</configure>

Implement a validation object. To me it seems important to apply rules to each row of the DataGridField (DGF). I particularly like the monadic approach described in this article: Object-Oriented vs. Functional Form Validation.

class MyDGFValidator(validator.SimpleFieldValidator):

    def validate(self, value):
        
        try:
            # Some validation call which applies validation rules to each row or whole table
            # and returs errors
            # I prefer more functional approach
            result = ValidatedData(value).run(*validators)
        except Exception as err:
            raise Invalid("Validation chain internal error: {}".format(err))
        errors = list(result['errors'].values())
        if errors:
            raise MultiInvalid([Invalid(e) for e in errors])

If you're using Zope version lower than 5.0, the MultiInvalid exception is not available. In this case, you need

  1. Raise a z3c.form.error.MultipleErrors object from the validate method instead of MultiInvalid. This MultipleErrors object will be passed to your custom ErrorViewSnippet.

  2. Register a custom ErrorViewDiscriminator for your field and the exception raised (in this case, MultipleErrors). The discriminator defines and calls your custom ErrorView to handle multiple errors.

from z3c.form import error

error.ErrorViewDiscriminators(
    MyDGFFieldValidationErrorView,
    error=error.MultipleErrors,
    field=IMySchema["presentations"],
)
component.provideAdapter(InterimsFieldValidationErrorView)

class SingleRowErrorViewSnippet(ErrorViewSnippet):

    def __init__(self, error, request, widget, field, form, content):
        super(SingleRowErrorViewSnippet, self).__init__(
            error, request, widget, field, form, content)
        self.message = self.content

    def update(self):
        pass


class MyDGFFieldValidationErrorView(MultipleErrorViewSnippet):

    def __init__(self, error, request, widget, field, form, content):
        super(MyDGFFieldValidationErrorView, self).__init__(
            error, request, widget, field, form, content)
        err_snippet = partial(SingleRowErrorViewSnippet,
                              error, request, widget, field, form)
        self.error.errors = (err_snippet(err) for err in self.error.errors)