Plone.superform/autoform directives and documentation

I need to make a form that has subgroups within fieldsets. It's the same concept as a fieldset, it should have a list of fields, a label, and a description. From what I've read nested fieldsets are not a good idea, and using the fieldset directive recursively doesn't work. That's fine.

I thought maybe I could create a new MetadataListDirective with plone.supermodel API, and I can, but I have no idea how to actually get that data when building a form. I can tell that the fieldset directive is integrating with zope.schema 'groups' but the documentation for how plone.supermodel actually does this is not detailed enough to help me. Is this approach even a good idea, or too impractical?

What I'm doing now is just making a custom edit form, building out each fieldset with subgroups, and manually putting each field's widget/@@ploneform-render-widget into that subgroup. It's not ideal because it does not require the schema's order to be set properly, and I need that for some other behaviors

Usually schema directives store some data in "tagged values" on the schema interface. Then code in plone.autoform reads from there when setting up the form. The code which processes fieldsets is in plone.autoform/plone/autoform/utils.py at master · plone/plone.autoform · GitHub; I'd suggest stepping through there in a debugger to see how it works.

Thanks, I think even if all I do is store that 'subgroup' data within the schema, instead of somewhere else, it's worth it. I know you can't set non-Attribute/Method values on a schema, so this seems like the way to do that.

Yeah, I assume that's why tagged values were used to implement schema directives.

This is what I've implemented so far. It seems to be working, but is not fully tested - I'm posting this only as a reference for anyone looking to develop something similar.

The subgroup directive is mostly a copy of the fieldset directive:

from collections import OrderedDict

from plone.autoform.form import AutoExtensibleForm
from plone.supermodel.directives import MetadataListDirective
from plone.supermodel.interfaces import DEFAULT_ORDER
from plone.supermodel.model import Fieldset
from plone.supermodel.utils import mergedTaggedValueList

SUBGROUPS_KEY = 'plone.autoform.subgroups'


class SubGroup(Fieldset):
    def __init__(
            self, __name__, fieldset=None, label=None, description=None, fields=None, order=DEFAULT_ORDER
    ):
        super().__init__(__name__, label, description, fields, order)
        self.label = label or None
        self.fieldset = fieldset

    def __repr__(self):
        return "<Subgroup '{}' order {:d} of {}>".format(
            self.__name__, self.order, ", ".join(self.fields)
        )


class subgroup(MetadataListDirective):
    """Directive used to create subgroups within fieldsets """

    key = SUBGROUPS_KEY

    def factory(
            self, name, fieldset=None, label=None, description=None, fields=None, order=DEFAULT_ORDER, **kw
    ):
        subgroup = SubGroup(
            name,
            fieldset=fieldset,
            label=label,
            description=description,
            fields=fields,
            order=order,
        )
        for key, value in kw.items():
            setattr(subgroup, key, value)
        return [subgroup]

I have a custom form class to actually process the subgroups. It seemed easier to do this rather than trying to get AutoExtensibleForm to recognize and know how to process subgroups.


class SubGroupAutoExtensibleForm(AutoExtensibleForm):
    template = ViewPageTemplateFile('templates/project_edit.pt')
    default_fieldset_label = 'General'

    def updateFieldsFromSchemata(self):
        super().updateFieldsFromSchemata()
        self._process_subgroups()

    def _process_subgroups(self):
        """ Because subgroups are tied to the schemata and not a fieldset, we have to do a lot of work
            to build it in the order defined by the schema (as opposed to the order of the fields declared
            by subgroup directive
        """
        subgroups = {'default': OrderedDict()}

        def add_subgroup_field(field, subgroup):
            if subgroup.fieldset not in subgroups:
                subgroups[subgroup.fieldset] = OrderedDict()
            if subgroup.__name__ not in subgroups[subgroup.fieldset]:
                subgroups[subgroup.fieldset][subgroup.__name__] = {
                    'label': subgroup.label,
                    'description': subgroup.description,
                    'fields': []  # we don't use the subgroup's fields for order
                }
            subgroups[subgroup.fieldset][subgroup.__name__]['fields'].append(field.__name__)

        subgroups_by_fieldset = {}
        for subgroup in mergedTaggedValueList(self.schema, SUBGROUPS_KEY):
            if subgroup.fieldset not in subgroups_by_fieldset:
                subgroups_by_fieldset[subgroup.fieldset] = []
            subgroups_by_fieldset[subgroup.fieldset].append(subgroup)

        # make dict where {fieldset: {subgroup: [fields]}
        extra = {'default': []}
        for field in self.fields.values():
            _subgrouped = False
            for subgroup in subgroups_by_fieldset['default']:
                if field.__name__ in [f for f in subgroup.fields]:
                    _subgrouped = True
                    add_subgroup_field(field, subgroup)
            if not _subgrouped:
                extra['default'].append(field.__name__)
        for group in self.groups:
            for field in group.fields.values():
                _subgrouped = False
                for subgroup in subgroups_by_fieldset[group.__name__]:
                    if field.__name__ in [f for f in subgroup.fields]:
                        _subgrouped = True
                        add_subgroup_field(field, subgroup)
                if not _subgrouped:
                    if group.__name__ not in extra:
                        extra[group.__name__] = []
                    extra[group.__name__].append(field.__name__)

        for k, v, in extra.items():
            subgroups[k]['extra'] = {
                'label': '',
                'description': '',
                'fields': v
            }
        self.subgroups = subgroups

To be sure, it would be better if subgroups were declared as a property of a fieldset instead of a property of the schema, as there's more complexity to build it out this way. This first approach is very much about trying to stay out of the way of whatever is going on with plone.autoform so I don't break it.

Relevant template section:

<metal:define define-macro="widget_rendering"
                    tal:define="fieldset_name fieldset_name|string:default">
                  <tal:subgroup repeat="subgroup python:formview.subgroups[fieldset_name].values()">
                      <div class="subgroup border-bottom mb-4">
                          <h2 tal:condition="subgroup/label">${subgroup/label}</h2>
                          <p tal:replace="structure subgroup/description"/>
                          <tal:widgets repeat="field subgroup/fields">
                              <metal:field-slot define-slot="field"
                                tal:define="widget python:view.widgets[field]">
                                  <metal:field define-macro="field">
                                      <tal:widget tal:replace="structure widget/@@ploneform-render-widget" />
                                  </metal:field>
                              </metal:field-slot>
                          </tal:widgets>
                      </div>
                  </tal:subgroup>
                </metal:define>

End result is I can put something like this right in the schema

    subgroup(
        'prod',
        fieldset='default',
        label='Production Environment',
        description='some plain text or html here'
        fields=['title', 'description', 'status', 'live_url', 'api_url']
    )
3 Likes