Core: submission rendering

Submission rendering is the concept of outputting the submitted data for a given form in a particular format. A couple of examples:

  • An overview of the information and submitted data in a confirmation PDF

  • A summary of the submitted data in an e-mail confirmation

  • Overview of the submitted data in a registration e-mail

  • Exporting the submission data to a particular format (CSV, JSON…)

Rendering submissions is dependent on implementation details. E.g., for the e-mails and PDF you typically output to HTML (the HTML gets converted to a PDF), but for e-mail you also want a plain text variant for e-mail clients that can’t render rich text.

Additionally, the markup for the e-mail and PDF is different due to the difference in CSS and layout-capabilities. And, for exports you want the raw values with their proper data-types, while for other render modes you need the human-readable presentation of the data.

All these challenges are solved by leveraging a Renderer class. You specify the desired render mode and whether HTML is desired or not, and the underlying data-structures can format themselves in a suitable fashion.

Command-line interface

The management command render_report allows you do debug render mode specific implementations. By default it outputs in the CLI render mode, but you can override this.

Note

This command always groups the formio component labels and values in a table, independent from the selected render mode. The visibility rules of components depending on the render mode are respected though.

To get full command documentation, run:

python src/manage.py render_report --help

Example command and output:

LOG_LEVEL=WARNING src/manage.py render_report 563
Submission 563 - Development
    Stap 1
        ----------------------  --------------------------
        Eenvoudig tekstveld     Veld 1 ingevuld
        Toon veldengroep?       Ja
        Veldengroep met logica
        Nested 1                Nested veld in veldengroep
        Toon stap 2?            ja
        ----------------------  --------------------------
    Stap 2
        -------------------  -----------------------
        Vrije tekst, stap 2  Stap 2 ingevuld
        WYSIWYG              WYSIWYG content
        Bijlage              bijlage: sample.pdf
        Ondertekening        handtekening toegevoegd
        -------------------  -----------------------

Reference

Renderer class

Example usage:

renderer = Renderer(submission, mode=RenderModes.pdf, as_html=False)
for node in renderer:
    print(node.render())
class openforms.submissions.rendering.Renderer(submission: openforms.submissions.models.submission.Submission, mode: openforms.submissions.rendering.constants.RenderModes, as_html: bool)

A submission renderer.

Instantiate an object of this class with the desired render mode, and then you can use this object in template or python code to emit the desired markup/ formatting.

__iter__() Iterator[openforms.submissions.rendering.base.Node]

Yield the nodes to visualize a complete submission.

property form: openforms.forms.models.form.Form

Get the associated openforms.forms.models.Form instance.

get_children() Iterator[openforms.submissions.rendering.nodes.SubmissionStepNode | openforms.variables.rendering.nodes.VariablesNode]

Produce only the direct child nodes.

property steps

Return the submission steps in the correct order.

Node types

class openforms.submissions.rendering.nodes.FormNode(renderer: Renderer)

Render node for the submission form.

get_children() Iterator

Emit no child nodes.

property is_visible

Determine if the node is visible for the given render mode and context.

render() str

Emit the (public) name of the submission form.

class openforms.submissions.rendering.nodes.SubmissionStepNode(renderer: Renderer, step: openforms.submissions.models.submission_step.SubmissionStep)

Render node for a single submission step.

This node is only considered ‘visible’ if the step is applicable (determined by form logic). If the step is not visible, no child nodes are emitted.

Rendering this node outputs the name of the step within the form.

get_children() Iterator[openforms.submissions.rendering.base.Node]

Yield the specific child nodes in the node tree.

property is_visible: bool

Determine if the node is visible for the given render mode and context.

render() str

Output the result of rendering the particular type of node.

Formio integration

The renderers extend to the FormIO component types.

You can extend your custom FormIO types by using the register hook, the mechanism is identical to the usual plugin system.

Example

from openforms.formio.rendering.nodes import ComponentNode
from openforms.formio.rendering.registry import register


@register("my-custom-component-type")
class MyCustomComponentType(ComponentNode):
    ...
class openforms.formio.rendering.nodes.ComponentNode(renderer: 'Renderer', component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
apply_to_labels(f: Callable[[str], str]) None

Apply a function f to all labels.

static build_node(step_data: dict[str, Any], component: openforms.formio.typing.base.Component, renderer: Renderer, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', depth: int = 0, parent_node: openforms.submissions.rendering.base.Node | None = None) ComponentNode

Instantiate the most specific node type for a given component type.

property display_value: Union[str, Any]

Format the value according to the render mode and/or output content type.

This applies the registry of Formio formatters to the value based on the component type, using openforms.formio.service.format_value().

get_children() Iterator[openforms.formio.rendering.nodes.ComponentNode]

Yield the child components if this component is a container type.

property is_visible: bool

Implement the logic to determine if a component is visible.

See https://github.com/open-formulieren/open-forms/issues/1451#issuecomment-1077506877 for a diagram of the logic powering this.

Summarized, a component is visible for a given render mode if the component-level configuration says so (while falling back to some defaults for older configurations).

The exceptions to this are:

  • fieldsets are visible if:

    • any of the children is visible (no render_mode dependency)

    • not hidden (no render_mode dependency)

    • (not hideHeader) -> render children, but not the label

  • fieldsets:

    • never render the label

  • wysiwyg:

    • in PDF and summary if visible

These exceptions are handled in more specific subclasses to avoid massive if-else branches again, see openforms.formio.rendering.default.

property key_as_path: glom.core.Path

See https://glom.readthedocs.io/en/latest/api.html?highlight=Path#glom.Path Using Path(“a.b”) in glom will not use the nested path, but will look for a key “a.b”

property label: str

Obtain the (human-readable) label for the Formio component.

property layout_modifier: str

For HTML based rendering, potentially emit a layout modifier.

render() str

Output a simple key-value pair of label and value.

property spans_full_width: bool

Whether the display value spans the full width rather than 2 columns.

property value: Any

Obtain the value from the submission for this component.

Note that this returns an unformatted value. There also has not been done any Formio type -> Python type casting, so a datetime will be an ISO-8601 datestring for example.

TODO: build and use the type conversion for Formio components.

class openforms.formio.rendering.nodes.FormioNode(renderer: 'Renderer', step: openforms.submissions.models.submission_step.SubmissionStep)
get_children() Iterator[openforms.formio.rendering.nodes.ComponentNode]

Yield the specific child nodes in the node tree.

render() Literal['']

Output the result of rendering the particular type of node.

class openforms.formio.rendering.nodes.RenderConfiguration(key: str | None, default: bool)

Component-level property configuration to control output.

Whether a component should be emitted or not is/can be configured on the component in the form designer. In the event that this key is missing from the component ( because it is not supported or is a form definition from before this feature landed), then fall back using the default.

Vanilla FormIO components

The following component types are automatically picked up by Open Forms

class openforms.formio.rendering.default.ChoicesNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
apply_to_labels(f: Callable[[str], str]) None

Apply a function f to all labels.

class openforms.formio.rendering.default.ColumnsNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
get_children() Iterator[openforms.formio.rendering.nodes.ComponentNode]

Columns has an extra nested structure contained within.

{
    "type": "columns",
    "columns": [
        {
            "size": 6,
            "components": [...],
        },
        {
            "size": 6,
            "components": [...],
        }
    ],
}
value: None = None
class openforms.formio.rendering.default.EditGridGroupNode(renderer: 'Renderer', component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None, group_index: int = 0, layout_modifier: str = 'editgrid-group', display_value: str = '', default_label: str = 'Item')
apply_to_labels(f: Callable[[str], str]) None

Apply a function f to all labels.

get_children() Iterator[openforms.formio.rendering.nodes.ComponentNode]

Yield the child components if this component is a container type.

property label: str

Obtain the (human-readable) label for the Formio component.

render() str

Output a simple key-value pair of label and value.

class openforms.formio.rendering.default.EditGridNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
get_children() Iterator[openforms.formio.rendering.nodes.ComponentNode]

Return children as many times as they are repeated in the data

The editgrid component is special because it may have a configuration such as:

{
    "key": "children",
    "components": [
        {"key": "name", ...},
        {"key": "surname", ...},
    ],
    ...
}

But the data submitted with the form will be:

{
    "children": [
        {"name": "Jon", "surname": "Doe"},
        {"name": "Jane", "surname": "Doe"},
        ...
    ]
}

So we need to repeat the child nodes of the configuration and associate them with the data provided by the user.

property value

Obtain the value from the submission for this component.

Note that this returns an unformatted value. There also has not been done any Formio type -> Python type casting, so a datetime will be an ISO-8601 datestring for example.

TODO: build and use the type conversion for Formio components.

class openforms.formio.rendering.default.FieldSetNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
property label: str

Obtain the (human-readable) label for the Formio component.

render() str

Output a simple key-value pair of label and value.

class openforms.formio.rendering.default.FileNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
property display_value: str

Format the value according to the render mode and/or output content type.

This applies the registry of Formio formatters to the value based on the component type, using openforms.formio.service.format_value().

class openforms.formio.rendering.default.SelectNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
apply_to_labels(f: Callable[[str], str]) None

Apply a function f to all labels.

class openforms.formio.rendering.default.WYSIWYGNode(renderer: Renderer, component: openforms.formio.typing.base.Component, step_data: dict[str, Any], depth: int = 0, path: glom.core.Path | None = None, json_renderer_path: glom.core.Path | None = None, configuration_path: str = '', parent_node: openforms.submissions.rendering.base.Node | None = None)
property is_visible: bool

Implement the logic to determine if a component is visible.

See https://github.com/open-formulieren/open-forms/issues/1451#issuecomment-1077506877 for a diagram of the logic powering this.

Summarized, a component is visible for a given render mode if the component-level configuration says so (while falling back to some defaults for older configurations).

The exceptions to this are:

  • fieldsets are visible if:

    • any of the children is visible (no render_mode dependency)

    • not hidden (no render_mode dependency)

    • (not hideHeader) -> render children, but not the label

  • fieldsets:

    • never render the label

  • wysiwyg:

    • in PDF and summary if visible

These exceptions are handled in more specific subclasses to avoid massive if-else branches again, see openforms.formio.rendering.default.

property label: str

Obtain the (human-readable) label for the Formio component.

property spans_full_width: bool

Whether the display value spans the full width rather than 2 columns.

property value: str | django.utils.safestring.SafeString

Obtain the value from the submission for this component.

Note that this returns an unformatted value. There also has not been done any Formio type -> Python type casting, so a datetime will be an ISO-8601 datestring for example.

TODO: build and use the type conversion for Formio components.