Form set-up

Form configuration

Form configuration is typically stored in AuditFormConfig model. Some options can be configured in config json field.

Example form configuration JSON
{
    "check_add_observation": true,
    "workflow_web_request_log_display_fields": ["status_code", "body"],
    "default_review_cycle": "monthly",
    "originator_editable_days": 7,
    "expiry_date_field": "date_field",
    "default_ward": 1
}

Advanced Configuration

Check add observation

Once need to restrict form submissions to users with specific permissions, an additional configuration option can be added as a JSON field in the advanced tab of the AuditFormConfig. When enabled, users must have the add_customobservation permission to submit observations through this form, providing an additional layer of access control.

Type: Boolean

Default: false

Available Options:

  • false (default): Users can submit observations without explicit add_customobservation permission

  • true: Users must have add_customobservation permission to submit observations

Example JSON Configuration:

{
    "check_add_observation": true
}

In this example, the form is configured so that only users with the add_customobservation permission can submit observations.

Related Permissions:

  • audit_builder.add_customobservation - Required when this option is enabled

Display web request logs in the review screen

Some workflows can make web requests to external APIs. The response is stored in TrackerLogEntry. This response can be displayed on the review screen in a format similar to the changelog. To enable this you configure which fields from the response data you want to display in the JSON config field of the form config:

{
    "workflow_web_request_log_display_fields": ["status_code", "body"]
}

The available options are body, url, status_code and request_data.

Explicit Ward Access

By default, users inherit ward access globally based on their assigned wards. When explicit_ward_access is enabled, users will only see observations in wards that are explicitly granted via Settings > Users > [User] > Form Permissions.

Type: Boolean

Default: false

Available Options:

  • false (default): Users inherit their global ward access for this form

  • true: Users only see observations in wards explicitly assigned in their form permissions

Example JSON Configuration:

{
    "explicit_ward_access": true
}

How to grant ward access:

  1. Go to Settings > Users > [User] > Form Permissions

  2. Add specific wards to the user’s form permissions for this form

Note: When enabled with no explicit wards assigned, users will see no observations for that form.

Note: This ringfencing is overridden if the user is selected via a custom user field within the form (e.g., assigned auditor field).

Observation review

This feature allows scheduling an observation to be reviewed at some date in the future. It must be enabled in the AuditFormConfig before the option to schedule a review will display in the review screen.

Note

This feature is only available in custom forms.

Once enabled you can schedule an observation review from the observation edit page. Observation review schedules will display in the calendar.

Default review cycle

Once the observations review feature has been enabled in the AuditFormConfig, an additional configuration option can be added as a JSON field in the advanced tab. If appropriately filled in, once a new observation with this configuration is submitted, a review schedule will be automatically created.

Type: Choice (String)

Available Options:

  • once-off: Reviews are performed once.

  • daily: Reviews are performed daily.

  • weekly: Reviews are performed weekly.

  • biweekly (every two weeks): Reviews are performed every two weeks.

  • monthly: Reviews are performed monthly.

  • bimonthly (every two months): Reviews are performed every two months.

  • quarterly: Reviews are performed every quarter (three months).

  • every 6 months: Reviews are performed every six months.

  • yearly: Reviews are performed yearly.

  • every two years: Reviews are performed every two years.

Example JSON Configuration:

{
    "default_review_cycle": "monthly"
}

In this example, the form is configured so that the observations are reviewable on a monthly basis. The default_review_cycle value must be one of the predefined choices listed above. Otherwise, no ReviewSchedule will be created.

Notifications

You can configure the review to trigger a reminder e-mail to the associated auditors on the morning of the day the review is scheduled to take place.

Marking observations as reviewed

Once an observation has been reviewed by the reviewer they can mark it as reviewed on the observation edit page by clicking the button Mark As Reviewed. This creates a TrackerLogEntry logging the time that the observation was reviewed at.

Allowing Originators Updating their own submissions

Once the “originator editable days” feature has been enabled in the AuditFormConfig, auditors without review permissions can be allowed to edit their own submissions within a specific timeframe.

Example JSON Configuration:

{
    "originator_editable_days": 7
}

Field configuration

Conditional logic

Conditional logic, a.k.a. nested questions allows adding conditions to the forms. The logic is interpreted by the client app (or Web form) at the time the form is being filled in.

Example of a conditional field setup
[
  {
    "field_name": "staff_type",
    "has_value": "Dr",
    "show": true,
    "set_value": null,
    "make_readonly": false,
    "make_required": true,
    "limit_choices": ["Periprosthetic #", "Diaphysis Clavicle #", "Lateral Clavicle #"],
    "always_update": false,
    "set_image": "https://megit.com/static/syringe.png"
  }
]

Note

Conditional logic is primarily executed on the client side during data entry. However: * set_value action that for fields that do not display at the initial submission, is evaluated automatically after submission by the back-end. * During the server-side calculation pass, a field’s visibility is evaluated with the same rules; conditionally hidden fields are not calculated.

Conditional logic is added to the “conditions” field in a question. It is a json list of conditions that when met, have one of the pre-defined effects on the field:

Supported condition options

field_name

designates name of another field within the same form or a hardcoded field like (institution, department or ward) (required). The selected field will decide about the outcome of the condition

search

search type for has_value check (optional).

Search types: 1. exact_match (default): check exact match between source and target field

2. contains: Check that target field contains source field value

has_value

predefined value. When field designated by field_name has this value, the condition evaluates true (condition is met) and triggers its associated actions.

has_value can be given dynamic value instead of specific value in source field.

  1. null can be used for self-referencing conditions with set_value logic. this will use the condition’s set_value as the default for this field:

    Example of setting email field default value to “email@example.com
      [
         {
           "field_name": "email",
           "has_value": null,
           "set_value": "email@example.com"
         }
      ]
    
  2. * can be used for conditions with set_value where the condition will be met for any value in the source field. this will also use set_value as the default value:

    Set ward field to be ward manager on any value.
       [
         {
           "field_name": "ward",
           "has_value": "*",
           "set_value": "{ward.manager}"
         }
       ]
    
  3. !*` can be used for conditions with ``set_value where the condition will be met for any value that is not null in the source field. This will also use set_value as the default value:

    Set ward field to be ward manager on any value.
       [
         {
           "field_name": "ward",
           "has_value": "!*",
           "set_value": "{ward.manager}"
         }
       ]
    
  4. using a custom search value to look inside field text value:

    Example of setting email field value to “staff@meg.com” when email field has an email that contains @meg.com
       [
         {
           "field_name": "email",
           "search": "contains",
           "has_value": "@meg.com",
           "set_value": "staff@meg.com"
         }
       ]
    

Supported actions

The following actions are supported. To enable action, set its value to true or false. Leave it null if action is not used by the condition. Some actions (limit_choices) may accept a different type of value.

show

show the field (makes it hidden by default). See Visibility semantics (client & server).

make_required

makes the field required. This requires that field is initially non-required by default

make_readonly

makes the field read-only when condition is met. Its value cannot be changed by the user. This is commonly applied in the review page to freeze value.

limit_choices

for choice fields, this can be set to a list of strings representing a subset of choices. When condition is met, the choice items will be limited.

always_update:

by default in review page, “set_value” conditions will only run when the field is empty, with always_update set to “true” the field will always update regardless of it’s current value

an example of conditional logic with “always_update”
[
  {
    "field_name": "email",
    "has_value": "*",
    "always_update": true,
    "set_value": "email@example.com"
  }
]
set_image

Adds image to the field. The value should contain the image url (string). This action can also be generated automatically by using image_from field config.

set_value

Sets given value to the field.

First item is an example of “set_value” setup, when email is empty the value “email@example.com” will be used by default. second is an example of “set_value” setup with placeholder, when name is empty the value of the current auditor’s name will be used by default, if a public web form without logged in user is used, the value will stay empty, third is an an example of “set_value” setup with an UserProfile field with
[
  {
    "field_name": "email",
    "has_value": null,
    "set_value": "email@example.com"
  },
  {
    "field_name": "name",
    "has_value": null,
    "set_value": "{auditor.id}"
  },
  {
    "field_name": "ward",
    "has_value": "*",
    "set_value": "{ward.manager}"
  }
]

Note

if set_value is set and has_value is not, or if has_value is None, it is assumed that the value is default and it will be used as the initial value (Task #25396).

Note

If the field is not exposed to the client app (at initial submission), the value will be still set after submission by the server.

Note

“set_value” accepts certain placeholders, once configured the populated value would be used for the field in a web form, client app or server if not exposed at the initial submission.

  • {auditor.id}

  • {auditor.name}

  • {auditor.phone_number}

  • {auditor.email}

  • {ward.manager} the condition field_name should be ward to map it correctly

Note

When a hardcoded field is used, “set_value” should contain the id of the needed object.

Visibility semantics (client & server)

These rules apply to visibility both in the UI and during the server calculation pass:

  • If there are any rules with "show": true, the field is visible only if at least one of those rules matches.

  • If there are no rules with "show": true → the field is visible by default.

  • Conditions with "show": false are ignored for visibility and do not hide the field.

  • Conditions without an explicit show key do not affect visibility (they may still perform other actions like set_value, limit_choices, etc.).

Server-side evaluation

The back end evaluates a subset of conditional logic:

  • During calculation: when server-side field calculations run, each calculated field’s visibility is evaluated using the rules above. If a field is hidden, its calculation is skipped..

Join multiple conditions with “AND” logic

Typical conditional logic setup iterates over conditions and executes those that match the condition. If you require more than one condition to match to invoke an action, replace field_name value with a nested condition object:

an example of conditional logic involving AND operator
[
  {
    "field_name": {
      "fields": [
        {"answer": "Upper Limb", "field_name": "axial_limb"},
        {"answer": "Fracture", "field_name": "injury_type"}
      ],
      "operator": "AND"
    },
    "has_value": null,
    "show": true,
    "set_value": null,
    "limit_choices": ["Periprosthetic #", "Diaphysis Clavicle #", "Lateral Clavicle #"],
    "make_readonly": false,
    "make_required": true
  }
]

Nested conditional logic was implemented in Task #26313.

Calculation logic

Field calculations are triggered on the server-side after the initial submission, and after observation is modified in the review page.

Calculation configuration is a json object containing the following:

operator: string

Name of the function applied to the named fields Supported operators include: “sum”, “difference”, “mean”, “product”, “multiply”, “quotient”, “divide”, “round”, “map”, “floor”. More up-to-date list can be found in the implementation in meg_forms/audit_builder/calculator.py:OPERATORS.

fields: list

A list of one or more field names whose value will be passed to the operator function. A field name can be a string (field name), a literal value, a pleceholder, or a nested calculation object.

Supported field placeholders:
  • {today}

  • {now}

  • ward

  • auditor

  • created

  • date

  • session__start_time

  • session__end_time

Simple example demonstrating subtraction between two fields
{
  "operator": "difference",
  "fields": [
    "field1",
    "field2"
  ]
}

Visibility during calculation

Before calculating each field that has calculation logic, the back end evaluates that field’s visibility using the conditions defined. If a field is deemed hidden, its calculation is skipped (the field is not updated).

Key points:

  • If there are any condition with "show": true, the field is visible only if at least one of those rules matches.

  • If there are no condition with "show": true, the field is visible by default.

  • Conditions with "show": false are ignored for visibility (they do not make a field hidden).

  • Conditions without an explicit show key do not control visibility (but may still perform other actions like set_value or limit_choices elsewhere).

Answer context used for visibility checks

For visibility evaluation during calculation, the server constructs an answers map that includes:

  • The observation’s own custom_answers.

  • For subforms: the sub-observation’s answers per subform.

This allows visibility to respond to both observation-level and subform answers during the calculation pass.

Operators

sum: (1 or more values)

add up values from provided fields

base64_encode: (1 or more values)

Return a base64 encoded string of the provided values.

difference:

Take first field’s value and subtract all the remaining ones. If the value type is a date or datetime, there must be exactly two values

mean:

Average of values

product, multiply:

Multiply all values

quotient, divide:

Takes value from first field, and multiplies by each value from remaining fields

round: (1 value)

Rounds the first passed value. If second value is provided, it determines number of decimal digits.

map: (2 values)

Takes first value and uses mapping passed as the second value to transform it to another value.

Map example
{
  "operator": "map",
  "fields": [
    "value_field",
    {
      "value1": "new value if 'value1' was selected",
      "value2": "new value if 'value2' was selected"
    }]
}
map_multiple: (2 values)

Identical to map, but for mapping from multichoice fields to other multichoice fields. Takes a list of values from the source field, for each value in that list, if a transformation is available then that transformed value is included in the output.

floor: (1 value)

Rounds the number down to the nearest whole number (integer)

Calculate max for a specific field

Max calculation example showing how to add “1” to the fetched max value from same form
{
  "fields": [
    1,
    {
      "data": { "form": 10 },
      "field": "test_numberinput",
      "calculation": "max"
    }
  ],
  "operator": "sum"
}

Note

The form can be a related form or the custom field’s form itself.

Note

The max field can be an Integer, Float or a Date Field.

Periodically re-calculate field value

A field can be scheduled to automatically update its value. Create an update schedule for the form to update its fields daily. The values defined in the schedule are updated daily at midnight server’s time. This is typically Dublin, but depending on region, it can be Sydney time (Australia).

The re-calculation can be triggered manually in django admin. Last re-calculation date is visible in admin as well.

Note

fields updated this way do not trigger other calculations, even for fields that use the scheduled calculation field as the source value

Custom field Latest Changes

This feature allows viewing latest change on a specific custom file field or multiple file field. when enabled on custom field and form, the latest change date and user will appear in as a small text under the field.

../_images/custom_field_latest_changes.png

Custom field file field would show the latest change date and user.

Note

This feature is only available in review page.

Enable latest change on custom field

The latest change can be enabled or disabled through the Custom field’s Advanced Configuration field.

Example of a custom file field with latest change history enabled
{
    "display_field_latest_change": true
}

Enable latest change on BaseAuditForm

By default BaseAuditForm sub-classes would have the latest changes feature disabled form wide, to enable it for specific form you can either:

  1. Override Form and megforms.models.BaseAuditForm.show_field_latest_change. attribute

Custom form with show_field_latest_change attribute override
import megforms.forms import BaseAuditForm

class CustomObservationForm(BaseAuditForm):
    show_field_latest_change = True
    # Rest of the form implementation
    ...
  1. pass show_field_latest_change in form kwargs when initializing.

Custom form with show_field_latest_change kwarg override
form_kwargs = {...}
form = CustomObservationForm(show_field_latest_change=True, **form_kwargs)

Custom Usage

The latest changes on a specific field can be retrieved using get_latest_changes():

Example of fetching a specific custom field changes in view and returns a list of ObservationChange.
from megforms.models import TrackerLogEntry, ObservationChange
from audit_builder.models import CustomField, CustomObservation

observation: CustomObservation
custom_field: CustomField
latest_changes: list[ObservationChange] = TrackerLogEntry.objects.get_latest_changes(observation, custom_field)

print(latest_changes)

If passed to template they could be rendered using the audit_builder/field_changelog.html template:

Example of rendering latest_changes values in context.
<span>
    {% include 'audit_builder/field_changelog.html' with latest_changes=latest_changes %}
</span>

Remote form field

a CustomField can be configured such that it can use data from another form as the select choices. the data from the remote form can be searched and fetched using an API that the widget uses.

Field Configuration

To configure the custom field, you need to use “Remote Form Select (single choice)” or RemoteFormSelect widget and specify the following parameters in the JSON configuration:

remote_form: int

the ID of the form that will be used as the data source

remote_label_field_name: string

the CustomField field_name on remote form that will be used as the choice label.

remote_value_field_name: string

the CustomField field_name on remote form that will be used as the choice value. and as the value that will be saved to the observation custom answers.

remote_metadata_field_names: array of string (optional)

the CustomField field_names on remote form that will be added to the label as metadata ex: “{label} [{metadata_field1}, {metadata_field2}]”

Example configuration of an remote audit form with (id=1) on he Custom field’s Advanced Configuration field.
{
    "remote_form": 1,
    "remote_label_field_name": "label",
    "remote_value_field_name": "value",
    "remote_metadata_field_names": ["metadata_field1", "metadata_field2"]
}

Note

The widget will use a “startswith” search filter on label field and “exact” search on value field.

Bypass Form Validation

It is occasionally required that a CustomField will need to be edited in an incomplete submission before data is gathered for all required fields. By setting the bypass_form_validation flag to True in the Advanced Configuration, validation for the rest of the AuditFormConfig will be skipped when changes are made to only these flagged fields. If any conditional logic affects the values or visibility of a field without this flag, that field will be considered a changed field without the bypass_form_validation flag, and validation will not be bypassed.

Example configuration of the validation bypass flag on the Custom field’s Advanced Configuration field.
{
    "bypass_form_validation": true
}

Field Default value

A CustomField can be configured such that it can use a default value from another existing observations.

Field Configuration

To configure the custom field to use a default value, specify the following parameters in the JSON configuration:

enabled: boolean

whether the default value is enabled or not (required)

match_fields: array of string

the custom fields values to match when fetching default value from previous observations

limit: int

limit observations defaults to x entries

order: string

order of the default observations defaults to latest first

nulls: boolean

whether a null value should be used if it was the latest found

Example configuration of a default value on the Custom field’s Advanced Configuration field.
{
    "default_value": {
        "enabled": true,
        "match_fields": ["test_select", "test_radioselect"]
        "limit": 1,
        "order_by": "-pk",
        "nulls": true
    }
}

Example API Call

MEG provides an API for fetching default value for any valid CustomField. A GET request can be sent to /api/v2/custom-field/<CUSTOM_FIELD_ID>/default-value/ to fetch the default values.

curl -X GET localhost:8000/api/v2/custom-field/<CUSTOM_FIELD_ID>/default-value/ -H "Content-Type: application/json" -H "Authorization: Token <TOKEN>"
Example Response
[
    "default value"
]

Sentiment Analysis

Sentiment analysis is the process of analyzing digital text to determine if the emotional tone of the message is positive, negative, or neutral. Sentiment analysis runs on audit form text fields once setup. It is run after a form is filled in and submitted (as a background task). The output is saved to a single choice widget, which should not be shown to the user when they fill in the form.

Example of a sentiment analysis field setup
[
  {
    "sentiment_text_field_name": "comment",
    "examples": [
      "El servicio fue excelente y la comida deliciosa.", "positive",
      "Terrible experience, would not recommend.", "negative",
      "La tienda está cerrada los domingos.", "neutral"
    ]
  }
]

Note

The sentiment_text_field_name field must be a text input field (TextArea or CharField).

The result from sentiment analysis is stored in a single choice field containing the above config.

Supported condition options

sentiment_text_field_name

designates name of another field within the same form (required). The selected field must be a TextArea of CharField.

Required setup

The following options must be set as follows

make_readonly

must be set / true. The results field must only be set by the sentiment task.

editable

must be unset / false. The user should not be able to change sentiment result.

show_in_app

must be unset / false. Sentiment field should not be shown to the user on form submission.

choices

field values must be positive, negative, neutral.

For example:

negative,negativo
positive,positivo
neutral,neutral
unknown,desconocido'
widget

must be single choice (dropdown) or single choice (radio)

Title Generation

Title generation analyzes detailed text fields to create concise, clear titles. The process runs as a background task after form submission, storing the result in a multi-line text field hidden from users during form completion. To setup title generation you need a text / multi-line text custom field which the user enters text into. Then you need a text custom field which stores the title and whose config sets up title generation. Setting up this title text field is detailed below.

Example of a config for title generation custom field
[
  {
    "generate_title_from": "description",
    "examples": [
        ["Patient reported severe headache and nausea after workplace incident", "Workplace Head Injury Report"],
        ["Monthly safety audit found multiple fire exits blocked by storage boxes", "Blocked Fire Exit Safety Violation"]
    ]
  }
]

Note

The generate_title_from field must be a text input field (TextArea or CharField).

The result from title generation is stored in a text field (TextArea or CharField) containing the above config.

Supported config options

generate_title_from

Name of source field within same form (required) Must be TextArea or CharField

examples (optional)

List of [description, title] pairs Used to guide title generation style Each pair must contain two strings

General options

show_in_app

must be unset / false. To be hidden during form submission.

widget

must be TextArea (multi-line text field)

Model field filters

For custom fields that use a database model for their choices, you can specify a filter statement in the config json field of the audit_builder.models.CustomField. The available choices for the field will be filtered based on the filter statement provided. You can filter the model options based on any field or related field the model has.

Simple example demonstrating filtering an auditor custom field
{
    "filters": {
        "title__icontains": "consultant",
    }
}

Ringfencing auditor model select field by ward

For custom fields that use a Auditor database model for their choices, you can specify a ringfence statement in the config json field of the audit_builder.models.CustomField.

Available ring-fence options:
  1. ward: database model fields can be ringfenced by their ward and current auditor that’s adding or editing the observation, such that he can see only the objects he has access to.

Example of CustomField.config
{
    "ringfence_by": "ward"
}

Include inactive auditors

For custom fields that use Auditor for their choices, you can control whether inactive/deactivated auditors are included in the available options. Type: Boolean

Default: false

Behavior:

  • true: When editing an existing observation, both active and inactive auditors are shown. Any previously selected inactive auditors remain selectable in valid choices. Deactivated auditors are displayed with a “[Removed]” prefix.

  • false: Only active auditors are shown in the field as valid choices, even when editing existing observations. Previously assigned inactive auditors will appear as deprecated choices (showing only their ID).

Example JSON Configuration:

Example of CustomField.config to exclude inactive auditors
{
    "include_inactive_auditors": false
}

This configuration is particularly useful when want to prevent selection of deactivated auditors, ensuring that only currently active auditors can be assigned to new or existing observations.

AI Auto Classification Of Reviews

This feature was implemented to leverage the power of chatGPT to auto classify customer reviews into any class without the need to train a specific classification model. More specifically it gives use the ability to create a multiple choice widget, with any desired classes, which stores the classes or topics present in a custom text field of the same form. These classes or topics are extracted using chatGPT 4o.

Example of a AI auto classification setup in CustomField.config
{
  "examples": [
    [
      "My nurse was lovely and I enjoyed the tea and toast after my operation. However I was given penicilin to bring home and I am allergic!",
      "Clinical"
    ],
    [
      "I was given tea and toast after my scope but I am celiac.",
      "Management"
    ],
    [
      "Great experience, lovely hospital.",
      ""
    ],
    [
      "I was waiting ages in A&E and I kept being ignored by the nurses, horrible experience would have been better off driving to Dublin.",
      "Management, Relationships"
    ]
  ],
  "extra_prompt": "You are analysing hospital reviews from patients. Some more info on the topics/classes: Clinical refers to surgical, doctor error or medicine issues. Management refers to food service or ward management issues. Relationships refers to issues related to staff attitude or caring issues.",
  "to_classify_field_name": "customer_review"
}

Note

The to_classify_field_name field must be a text input field (TextArea or CharField). The extra_prompt field must be a string and is optional. The examples field must follow the format above a list of lists of format [example, answer].

The result from auto classification is stored in a multiple choice field containing the above config.

Base prompt

Base prompt
messages_for_llm = [
    {
        "role": "system",
        "content": "You are an AI model who detects topics in customer reviews. You are to only report negative topics as these are flagged to management."
    },
    {
        "role": "system",
        "content": "When you receive a customer review, you will determine which of the following topics are present in that review: " + ", ".join(topics) + ". You can select as many topics as you like, including all or None."
    },
    {
        "role": "system",
        "content": "Return a python list of the selected topics with no other text or explaination. If you detected no topics return an empty list '[]'."
    },
]

This is the prompt fed to Chat GPT before asking for a text to be classified. It gives it more information on it’s task, specifies the style of the output and the classes or topics which it can choose from. Note that the model is set up to detect topics in reviews and to only highlight negative topics. Examples & extra prompt are also added to this as extra messages.

Supported config options

to_classify_field_name

string, designates name of another field within the same form (required). The selected field must be a TextArea of CharField.

extra_prompt

string, extra info on the specific task or classes to feed to chatGPT for more context on the task.

examples

list, examples [question, answer] to help chatGPT understand its role better. In general giving more examples increases accuracy (few shot learning).

Required setup

The following field options must be set as follows

show_in_app

must be unset / false. Results field should not be shown to the user while filling out the form.

choices

any list of strings of minimum length 2

widget

must be multiple choice widget

Audio Transcription

Audio transcription converts recorded audio fields into text, storing the transcribed content in a separate field. The process runs as a background task after form submission, storing the result in a multi-line text field hidden from users during form completion. To audio transcription you need an audio custom field which is used to record audio. Then you need a text custom field which stores the transcription and whose config sets up audio transcription. Setting up this transcription / text custom field is detailed below.

Example of a configuration (Advanced subsection) for transcription custom field
{
    "audio_transcription_field_name": "recording_field"
}

Note

The audio_transcription_field_name must reference an audio input field (AudioWidget).

The transcription result is stored in a multi-line text field (TextArea) containing the above config.

Supported field configuration options

audio_transcription_field_name

Name of source audio field within same form (required) Must use AudioWidget

General field options

The following options must be set as follows

show_in_app

must be unset / false. To be hidden during form submission.

widget

must be TextArea (multi-line text field)

Assigned Subforms

This configuration allows subforms to be highlighted when a specific auditor is selected in a custom field. When enabled, the subforms listed in a field’s configuration will display an “ASSIGNED TO YOU” label when that auditor is identified in the field’s answer.

Type: Array of strings (subform names)

Default: Not set

How It Works

  1. Create a custom field in a subform (e.g., a ModelSelect field pointing to the Auditor model in subform A)

  2. In that field’s advanced configuration, add an assigned_subforms setting with a list of subform names to highlight

  3. If the current auditor’s ID appears in that field’s custom answers, the subforms listed in assigned_subforms will be highlighted

  4. The highlighting persists across the observation, so navigating away and back will maintain the highlighted state

Example Field Configuration

In a subform A with a field assigned_auditor, set the field’s config to:

{
    "assigned_subforms": ["subform_b", "subform_c"]
}

If the current auditor’s ID is found in the assigned_auditor field, subforms with names subform_b and subform_c will be highlighted.

Visual Indicators

When a subform is highlighted:

  • An orange dashed border appears around the subform tab

  • An “ASSIGNED TO YOU” label is displayed above the tab in both desktop and responsive views

  • The highlighted subform receives visual emphasis with hover effects

Phone number field

You can create a custom field to accept phone number data by selecting “Phone number” as the widget. It will display a dropdown of country codes and validates the user input to ensure it’s a valid phone number. In the config of the field you can restrict the country codes that are displayed

Only show the country codes for Ireland and Great Britian
{
    "country_codes": [
        "IE",
        "GB"
    ]
}

By default if no country code exists in the phone number the package may not be able to parse the number format. You can set a default region to assist this parsing at the field level:

Set Great Britian as the default region
{
    "phone_number_default_region": "GB"
}

File upload fields

You can add file upload fields to a form in MEG. In the config of the field you can specify the allowed file types.

Only allow csv files
{
    "allowed_extensions": ["csv"]
}

Data Expiry Classes

class megforms.tasks.DataExpiry(auditor: Auditor, expiry_date_field: CustomField | None = None, task_name_unpublish: str = 'schedule-data-unpublish-observation-{id}', task_name_delete: str = 'schedule-data-delete-observation-{id}')

Schedule observation deletion/unpublishing based on expiry date field configuration. Handles different form layouts:

  • Flat forms: Unpublishes the observation instance

  • Subforms (accordion layout): Unpublishes all related subobservations and parent

  • Subforms (default layout): Unpublishes only subobservations with expiry field

Parameters:
  • auditor – The auditor instance

  • expiry_date_field – Field containing the expiry date answer

  • task_name_unpublish – Template for unpublish task naming

schedule_data_unpublish(observation: CustomObservation | CustomSubObservationInstance) None

Evaluates and schedules the data unpublishing process based on the observation to evaluate If the observation has subforms, it will query the subobservation instance based on the current observation and evaluate the expiry date fiels Schedules the data_expiry_unpublish_data function as a periodic task

Parameters:

observation – current observation to evaluate - can be either a custom observation or custom subobservation

Automatic Data Expiry

The system supports automatic unpublishing of observations based on configurable expiry date fields. This is handled through the DataExpiry class which schedules unpublishment tasks.

Expiry Behavior by Form Layout:

  • Flat forms: Unpublishes the entire observation instance when the expiry date is reached

  • Subforms (accordion layout): Unpublishes all related subobservations and the parent observation

  • Subforms (default layout): Unpublishes only the specific subobservations that contain the expiry field

Usage Example:

data_expiry = DataExpiry(
    auditor=auditor_instance,
    expiry_date_field="expiry_date",
    observation=observation_instance
)
data_expiry.schedule_data_unpublish()

Subform Handling:

For forms with subforms, the system automatically finds the earliest expiry date across all subobservations and schedules unpublishment based on that date. Only subobservations containing the specified expiry field are considered.

Task Scheduling:

The expiry system uses Celery’s Periodic Tasks to schedule unpublishment tasks. Tasks are named using the pattern schedule-data-unpublish-observation-{id} and will overwrite existing tasks with the same name.

Data Expiry Tasks

megforms.tasks.data_expiry_unpublish_data(auditor_id: int, object_id: int) None

unpublishes data when invoked by the given periodic task schedule

Parameters:
  • auditor_id – the id of the auditor that has initiated the unpublish process

  • object_id – the id (observation) of the object that will be queried and unpublished

Task Behavior:

The unpublishment task runs with force=True to ensure data is unpublished regardless of dependencies. A log entry is created with the message format: "Unpublished {obj}: {object_id} from data expiry policy"

megforms.tasks.delete_custom_observation(observation_id: int) None

Permanently deletes an unpublished custom observation and all associated data.

Removes the observation, its sub-observations, issues and attached files related comments, log entries, tracker logs, model links and revision history in a single atomic transaction. This task is typically used for data retention policies to clean up draft or expired observations.

Parameters:

observation_id – the id of the custom observation to delete

Task Behavior: - Atomic deletion of unpublished custom observations - Used for data retention policies on draft/expired observations

Deletes: - Custom observation + sub-observations - Related issues + attached files (photos/docs/audio) - Comments, log entries, tracker logs - Model links and revision history - Observation compliance records - Audit session (if no other observations exist)

Safety: - Transaction-wrapped for data consistency - Exception handling for file deletion failures - Sentry error tracking - Conditional cleanup to prevent orphaned data