Form structure¶
This chapter covers how to organise and render forms — grouping fields, building multi-page flows, and adapting the admin interface.
Collapsible sections¶
Group form fields under collapsible headings using the subregion mechanism:
class Group(ConfiguredFormPlugin):
subregion = "group"
title = models.CharField(
"title",
max_length=200,
blank=True,
help_text=(
"Use an empty title to finish an existing group "
"without starting a new one."
),
)
class Meta:
verbose_name = "group"
def __str__(self):
return self.title
renderer.register(models.Group, "")
Handle groups in a custom Regions class:
from feincms3.regions import Regions, matches
from feincms3.renderer import render_in_context
class FormRegions(Regions):
def handle_group(self, items, context):
group = items.popleft()
if not group.title:
return []
content = []
while items and not matches(items[0], subregions={"group"}):
content.append(
self.renderer.render_plugin_in_context(items.popleft(), context)
)
return render_in_context(
context, "forms/group.html", {"group": group, "content": content}
)
# In your view:
context["form_regions"] = FormRegions.from_contents(contents, renderer=renderer)
Dynamic regions from the database¶
Build form regions dynamically from a related model — useful for fully configurable questionnaires:
from content_editor.models import Region
from admin_ordering.models import OrderableModel
class ConfiguredForm(forms_models.ConfiguredForm):
FORMS = [
forms_models.FormType(
key="questionnaire",
label="questionnaire",
regions=lambda cf: [
Region(key="cover", title="Cover"),
] + [group.region for group in cf.groups.all()],
form_class="app.tools.forms.Form",
process="app.forms.forms.process_questionnaire_form",
),
]
class Group(OrderableModel):
parent = models.ForeignKey(
ConfiguredForm, on_delete=models.CASCADE, related_name="groups"
)
title = models.CharField(max_length=200)
@property
def region(self):
return Region(key=f"group_{self.pk}", title=self.title)
Add an inline for managing groups in the admin:
from admin_ordering.admin import OrderableAdmin
class GroupInline(OrderableAdmin, admin.TabularInline):
model = models.Group
extra = 0
@admin.register(models.ConfiguredForm)
class ConfiguredFormAdmin(ConfiguredFormAdmin):
inlines = [GroupInline, ...]
Rendering specific regions (multi-page forms)¶
Render only a subset of a form’s regions for step-by-step forms:
def start(request):
cf = get_configured_form()
contents = contents_for_item(cf, plugins=renderer.plugins(), regions=cf.regions[:1])
form = create_form(contents, form_class=cf.type.form_class, form_kwargs={...})
if form.is_valid():
return HttpResponseRedirect(...)
def questionnaire(request):
cf = get_configured_form()
contents = contents_for_item(cf, plugins=renderer.plugins(), regions=cf.regions[1:])
form = create_form(contents, form_class=cf.type.form_class, form_kwargs={...})
Multiple renderers (form input vs. report view)¶
Use separate renderers for data entry and viewing submitted data:
# Form renderer for data entry
form_renderer = RegionRenderer()
form_renderer.register(
models.SimpleField,
template_renderer("forms/simple-field.html", simple_field_context),
)
# Report renderer for viewing submitted data
def report_simple_field_context(plugin, context):
return {
"plugin": plugin,
"rows": [loader(context["submission"].data) for loader in plugin.get_loaders()],
}
report_renderer = RegionRenderer()
report_renderer.register(
models.SimpleField,
template_renderer("forms/report-simple-field.html", report_simple_field_context),
)
Conditional inlines by form type¶
Show different admin inlines depending on the selected form type:
@admin.register(models.ConfiguredForm)
class ConfiguredFormAdmin(ConfiguredFormAdmin):
def get_inlines(self, request, obj):
if not obj:
return []
inlines = [
ContentEditorInline.create(models.RichText),
SimpleFieldInline.create(models.Text),
SimpleFieldInline.create(models.Email),
]
if obj.type.key == "consulting":
return [StepInline, *inlines]
return inlines