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.