Skip to content

Custom forms

Custom forms are the platform's data-driven system for capturing information during emission event investigation. Instead of hardcoding which fields appear when an operator investigates and resolves a detected emission, the platform defines named, reusable custom forms (groups of fields) in the database. Custom forms are attached to individual events, then rendered and validated dynamically on both the frontend and backend.

The emission form side panel showing custom form sections and the "+ Add a new form section" picker

Custom forms replace the older, per-company custom event fields feature, which is now deprecated.

Core concepts

A custom form is made of three pieces:

  • Form definition — a named form with a description and a priority (sort order). This is the reusable template.
  • Field definitions — the individual fields that belong to a form (root cause, notes, dates, etc.), each with a type, validation, and display rules.
  • Event form instance — the link between a specific event and a form, representing "this form is attached to this event." The answers themselves are stored on the event's form_data, keyed by field name.

Because answers are keyed by field_name, field names are effectively shared: two fields with the same field_name — even across different forms — must use the same field_type, and they read and write the same value in form_data. When more than one attached form defines the same field, it is treated as required if any of them require it.

Global vs. operator-owned forms

A form definition has an optional owner (a company):

  • Global forms have no owner. They are authored by Aerscape and apply to every operator. These are core platform primitives, not customizations. The seeded global forms are:
    • AERSHEDCore event investigation fields (root cause / rejection reason, notes, start and end dates).
    • OGMP — OGMP reporting category.
    • Operator Feedback — actionability score and free-text feedback.
  • Operator-owned forms belong to a single company and are only visible to that operator (plus staff). Use these for operator-specific data capture.

Custom forms are a core part of how the platform models event data — alongside emission states and workflows — not merely an operator customization. Operator ownership is one option, not the defining trait.

Filling out forms in the app

Operator end-users work with forms in the emission form side panel on the Site page while investigating an event:

  • Custom forms attached to an event render as grouped sections in the panel.
  • Users can attach additional forms with "+ Add a new form section", which lists the global forms plus their company's own forms.
  • Custom forms can only be added or edited while the event is in an editable status (not Archived, Linked, Rejected, Waiting Approval, or Completed).

Custom form data also feeds the emission PDF exports and CSV Exports.

Field types

Each field has a field_type:

TypePurpose
numberNumeric input (frontend variant star_rating available via options).
stringText input (frontend variant textarea available via options).
choiceSelection from a fixed list of choices; allow_any permits free text too.
booleanTrue/false (frontend variant checkbox available via options).
dateDate only.
datetimeDate and time.
warningDisplay-only warning banner shown conditionally. Does not validate or save a value.
tableA mini-table with its own column definitions. Rows store the base field types. Not validated.
fileReference to an event attachment.
userReference to a user.

Fields are grouped into named sections via group_name and ordered with order (closer to 1 = closer to the top of the form).

field_options

Per-field configuration stored as a JSON object. In Django Admin this field has a visual editor, so you rarely edit it by hand. Common keys:

  • choices — list of allowed values (for choice).
  • allow_any — accept free text alongside the choices (for choice).
  • input_type — frontend widget variant (star_rating for numbers, checkbox for booleans).
  • text_typetextarea for multi-line strings.
  • columns — column definitions for table fields.

Validation and dynamic logic

Three fields drive conditional behavior. In Django Admin these are edited as raw JSON (they have no visual widget), so the shapes are documented below with real examples.

Two of them — required_condition and suggestion_rule — use a JSON Logic envelope: an object wrapping a JSON Logic rule under logic, so metadata can live alongside it.

json
{
  "logic": { /* JSON Logic rule */ },
  "comment": "human-readable note",
  "autofill": true
}

There are two variable namespaces you can read inside a rule:

  • values.* — the current form and event state (e.g. values.formData.<field_name>, values.site). Used for validation and display.
  • emissionRecord.* — the source emission data (e.g. emissionRecord.dataPoint.detectedRate, emissionRecord.site.siteName). Used for value suggestions.

The same condition engine backs the workflows feature.

When validation runs

Form validation runs on every event update, but it operates in one of two modes depending on the target event status:

  • Strict mode — enforced only when moving an event into one of these statuses:

    • WAITING_APPROVAL
    • COMPLETED
    • REJECTED

    In strict mode, all requirements are enforced: is_required fields, required_condition rules that evaluate truthy, and boolean fields with no explicit value. If any fail, the save is rejected with validation errors and the status change does not happen.

  • Non-strict mode — every other transition (e.g. saving progress while in WORK_IN_PROGRESS). Field values are still cleaned and normalized (dates converted to ISO strings, unknown/display-only keys dropped) and saved, but required and conditional-required checks do not block the save. This lets operators save partial progress and only fully complete the form when they submit for approval, completion, or rejection.

Two things always apply regardless of mode:

  • table structure validation — a table's rows are always checked against its column definitions.
  • Custom forms cannot be attached to a completed event.

The practical rule: fields are only required at the moment an operator tries to submit the event for approval, mark it completed, or reject it. Before that, the form can be saved incrementally.

When strict validation fails, the side panel lists the errors and flags the offending sections and fields, and the status change is blocked until they are resolved:

The emission form side panel showing a strict validation failure: an error summary and the required Root cause field flagged in red

is_required and required_condition

is_required makes a field unconditionally required. required_condition makes it required only when its JSON Logic rule is truthy. Examples:

json
// Required only when the event has a site (AERSHED: ae_root_cause)
{"logic": {"!!": [{"var": "values.site"}]}, "comment": "Required when the event has a site."}
json
// Required when another form field equals a value (PL956: datetime_of_emission)
{"logic": {"==": [{"var": "values.formData.emissions_found"}, true]}, "comment": ""}
json
// Numeric comparison (a missing trigger resolves to null and does not fire)
{"logic": {">": [{"var": "values.formData.some_number"}, 10]}, "comment": "Numeric comparison."}
json
// AND of two conditions
{"logic": {"and": [
  {"==": [{"var": "values.formData.trigger_bool"}, true]},
  {"==": [{"var": "values.formData.custom_form_cause"}, "Valve Leak"]}
]}, "comment": "AND of two formData conditions."}

display_when

Controls whether a field is shown. Unlike the others, this uses a legacy array format (not JSON Logic — a migration to JSON Logic is planned). It is a list of conditions that are ANDed together; the field is shown only when all match.

json
// Show only when a boolean field is true (PL956: repair_description)
[{"is": true, "field": "formData.repair_needed"}]
json
// Show only for a real event that has a site (AERSHED: ae_root_cause)
[{"is": "null", "field": "similarEmission"}, {"is": "notNull", "field": "site"}]
json
// Date comparison against an emission field (warning field: warn_start_date)
[
  {"is": "gt", "type": "date", "field": "formData.start_date",
   "emissionField": "dataPoint.detectionTimestamp"},
  {"is": "null", "field": "similarEmission"}
]

Vocabulary:

  • is — one of true, null, notNull, gt, lt.
  • field — a values-relative path (formData.<name>, site, similarEmission, …).
  • type: "date" + emissionField — compare the field against a value from the emission data.

suggestion_rule

Computes a suggested value for a field from the emission data. Uses the JSON Logic envelope, plus autofill:

  • autofill: true — prefill the value into an empty field automatically on load.
  • autofill: false — offer the computed value as a suggestion only.
json
// Passthrough of an emission value (site_name_suggest)
{"logic": {"var": "emissionRecord.site.siteName"}, "comment": "siteName passthrough", "autofill": false}
json
// Computed number: detected rate g/h -> kg/h (rate_kg_h_autofill)
{"logic": {"/": [{"var": "emissionRecord.dataPoint.detectedRate"}, 1000]},
 "comment": "rate / 1000", "autofill": true}
json
// Tiered choice derived from the detected rate (rate_tier_autofill)
{"logic": {"if": [
  {">=": [{"/": [{"var": "emissionRecord.dataPoint.detectedRate"}, 1000]}, 100]}, "High (>=100)",
  {">=": [{"/": [{"var": "emissionRecord.dataPoint.detectedRate"}, 1000]}, 30]},  "Medium (>=30)",
  "Low (<30)"
]}, "comment": "rate/1000 -> tier", "autofill": true}

In the app, suggested and autofilled values appear directly in the field. Below, the "Suggestion Engine Test" section shows a tier and a follow-up flag both derived from the emission's detected rate:

Authoring custom forms

Custom forms are created and edited by staff (and per-operator authors) through Django Admin:

  1. Log in to Django Admin (see Admin Operations).
  2. Go to Event management → Form definitions. This lists every form definition with its priority and owner.
  3. Create or edit a form definition. Set its name, description, priority, and (optionally) an owner company. Leave the owner blank for a global form.
  4. Add fields via the inline field editor. Fields can be drag-reordered, and field_options has a visual editor. Validation, display, and suggestion rules are entered as JSON using the shapes documented above.
  5. Save.

The Django admin "Form definitions" list, showing each form's priority and owner

Add and reorder fields with the inline field editor:

Each field has a Field type and a visual Field options editor (for example, adding choices to a choice field), alongside the raw-JSON display_when and validation inputs:

Exporting and importing forms as JSON

A form definition can be copied between environments (for example, from a staging database to production) using the JSON tools on its admin page:

  • Download form as JSON — exports the complete form definition, including all of its fields and their options/validation/display rules, as a single JSON file.
  • Update form from JSON — replaces the current form's fields with those from an uploaded JSON file (in the format produced by the download button).

The "Download form as JSON" and "Update form from JSON" buttons on the change form definition page

This is the recommended way to move a form between environments or to share a form definition, rather than re-creating its fields by hand.

Relationship to custom event fields

Custom forms supersede the deprecated custom event fields feature, which stored a raw JSON blob on the company and supported only text and boolean fields. Custom forms are proper relational models with many field types, grouped sections, per-event attachment, and full JSON Logic for validation and suggestions.