Skip to content

Dynamic forms (how it works)

This page explains how Dynamic Forms work end-to-end, in plain language.

Developer? See Dynamic Forms - Developer Guide for how to register new hydration targets and extend the system.

The big picture

Dynamic Forms add a configurable layer on top of the platform: 1. A form is defined (and published) per municipality 2. A submission is stored as an immutable record 3. Optionally, the submission is “hydrated” into a real domain object (e.g., an Occurrence)

The three stages

1) Define

Administrators build a form using the Form Builder and publish versions.

Key ideas: - Draft schema is editable - Publish creates an immutable version (a snapshot)

2) Collect

When a user submits a form: - A Form Submission is saved (payload + metadata like IP/user agent) - The submission always references the exact form version used

3) Hydrate (optional)

Hydration is the “conversion step”: turning the submission payload into an actual model instance.

Hydration is controlled by: - a Use Case (what you’re trying to create), - a list of Field Bindings (how payload keys map to target fields), - and a hydration target implementation registered by the destination app.

What “registry-based hydration” means

Dynamic Forms stay generic by using a registry: - Target apps (like Occurrences) register themselves as “hydration targets” - Each target provides: - field discovery (what can be mapped), - optional suggestions, - and the hydrate function (how to create the target object).

In practice: - The Use Case stores a target model path (example: occurrences.Occurrence) - The registry resolves that model path to the correct hydration handler

Field mapping UX

The mapping page is designed to avoid memorizing internal field names: - Form fields are read directly from the current form schema - Target fields come from the registered hydration target and are grouped: - required - auto-filled - optional - dynamic (type-specific)

Suggested mappings are computed using simple patterns (e.g., “email” → reporter email).

Dynamic fields (Occurrence example)

Occurrences can have type-specific dynamic fields.

How it works: - The administrator selects an Occurrence Type (or configures it in the Use Case) - The target field list is expanded with that type’s dynamic fields - Mappings can target keys like dynamic:<field_name>

Audit trail

Every hydration attempt can store a result record: - success/failure - target object reference (e.g., occurrences.Occurrence:126) - error details when hydration fails

Files (uploads) and “file tokens”

To avoid sending large files inside the same JSON request as the submission, Dynamic Forms use a two-step pattern:

  1. The portal uploads the file first (either directly to Django in LOCAL mode, or to S3/GCS via a presigned URL).
  2. The upload returns a file_token.
  3. The portal includes that file_token inside the submission payload.
  4. When the submission is created, the backend consumes those tokens into SubmissionFile rows.

Payload convention: - A file reference is an object containing a file_token, for example: - {"file_token": "<uuid>", "name": "photo.jpg", "content_type": "image/jpeg", "size": 12345}

Preset portal blocks (map + attachments)

For high-UX inputs like maps, it is common to: - render a Leaflet map outside Form.io, and - write values into hidden payload keys like location.latitude / location.longitude / location.address.

Attachments follow the same idea: the portal manages selection + upload and writes attachments[] token objects into the payload.

Behind the scenes (grounded in code)

  • Models: multivertical_django/apps/dynamic_forms/models.py
  • Public API:
  • schema fetch: multivertical_django/apps/dynamic_forms/views.py (FormPublicViewSet.retrieve)
  • submission create: multivertical_django/apps/dynamic_forms/views.py (FormPublicViewSet.submissions)
  • Hydration orchestration: multivertical_django/apps/dynamic_forms/services.py
  • Upload flow + token consumption: multivertical_django/apps/dynamic_forms/uploads.py
  • Registry:
  • definitions: multivertical_django/apps/dynamic_forms/registry.py
  • occurrence target: multivertical_django/apps/occurrences/hydration.py
  • Admin mapping UI + APIs:
  • template: multivertical_django/apps/dynamic_forms/templates/admin/dynamic_forms/formdefinition/mappings.html
  • endpoints: multivertical_django/apps/dynamic_forms/admin_api.py