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']
)