Skip to content

Commit

Permalink
[#1451] Submission rendering WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Bartvaderkin committed Mar 28, 2022
1 parent b56a331 commit 89ba323
Show file tree
Hide file tree
Showing 18 changed files with 868 additions and 28 deletions.
1 change: 1 addition & 0 deletions src/openforms/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"openforms.emails",
"openforms.formio",
"openforms.formio.formatters.apps.FormIOFormattersApp",
"openforms.formio.display.apps.FormIODisplayApp",
"openforms.forms",
"openforms.multidomain",
"openforms.products",
Expand Down
Empty file.
12 changes: 12 additions & 0 deletions src/openforms/formio/display/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class FormIODisplayApp(AppConfig):
name = "openforms.formio.display"
label = "formio_display"
verbose_name = _("FormIO display")

def ready(self):
# register the plugin
from . import default # noqa
13 changes: 13 additions & 0 deletions src/openforms/formio/display/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from enum import Enum, auto


# TODO convert to django-choices
class OutputMode(Enum):
summary = auto()
pdf = auto()
email_confirmation = auto()
email_registration = auto()

@classmethod
def all(cls):
return list(cls)
117 changes: 117 additions & 0 deletions src/openforms/formio/display/default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from typing import TYPE_CHECKING, Iterable

from openforms.formio.display.constants import OutputMode
from openforms.formio.display.elements import Element, Header, HTMLElement, LabelValue
from openforms.formio.display.registry import register
from openforms.formio.display.render import RenderContext
from openforms.formio.display.wrap import Node
from openforms.plugins.plugin import AbstractBasePlugin


class RenderBasePlugin(AbstractBasePlugin):
def create_elements(self, node: Node, context: RenderContext) -> Iterable[Element]:
# as base no output
return []

def is_visible(self, node: Node, context: RenderContext):
if not self.is_self_visible(node, context):
return False

if self.has_visible_children(node, context):
# as base show when children are visible
return True
elif self.has_children(node, context):
# as base hide container with all hidden children
return False
else:
# as base show non-containers
return True

def is_self_visible(self, node: Node, context: RenderContext):
return True

def has_children(self, node: Node, context: RenderContext):
return bool(node.children)

def has_visible_children(self, node: Node, context: RenderContext):
# verbose for override consistency
return self.has_children(node, context) and any(
n.plugin.is_visible(n, context) for n in node.children
)

def check_config(self):
pass


class FormioBasePlugin(RenderBasePlugin):
def is_self_visible(self, node: Node, context: RenderContext):
return not node.component.get("hidden", False)


class ValueBasePlugin(FormioBasePlugin):
def is_self_visible(self, node: Node, context: RenderContext):
# TODO is this the best place?
if not context.allows_value_key(node.key):
return False
return super().is_self_visible(node, context)


@register("default")
class LabelValuePlugin(ValueBasePlugin):
def create_elements(self, node: Node, context: RenderContext):
if not self.is_visible(node, context):
return
yield LabelValue(node)


@register("fieldset")
class FieldsetPlugin(FormioBasePlugin):
def is_visible(self, node: Node, context: RenderContext):
if not self.is_self_visible(node, context):
return False
if self.has_visible_children(node, context):
return True
else:
return False

def create_elements(self, node: Node, context: RenderContext):
if not self.is_visible(node, context):
return
# is it hideLabel ?
if not node.component.get("hideLabel", False):
yield Header(node.get_default_label())

for n in node.children:
yield from n.plugin.create_elements(n, context)


@register("columns")
class ColumsPlugin(FormioBasePlugin):
def is_visible(self, node: Node, context: RenderContext):
if not self.is_self_visible(node, context):
return False
if self.has_visible_children(node, context):
return True
else:
return False

def create_elements(self, node: Node, context: RenderContext):
if not self.is_visible(node, context):
return
for n in node.children:
yield from n.plugin.create_elements(n, context)


@register("content")
class ContentPlugin(ValueBasePlugin):
def is_visible(self, node: Node, context: RenderContext):
# only in PDF
return context.mode == OutputMode.pdf and super().is_visible(node, context)

def create_elements(self, node: Node, context: RenderContext):
if not self.is_visible(node, context):
return
# no label
# danger zone..
html = node.component["html"]
yield HTMLElement(html)
109 changes: 109 additions & 0 deletions src/openforms/formio/display/elements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import html
from dataclasses import dataclass
from typing import TYPE_CHECKING, Iterable

from django.utils.html import conditional_escape, escape, format_html
from django.utils.safestring import mark_safe

from openforms.formio.formatters.service import format_value

if TYPE_CHECKING:
from openforms.formio.display.render import RenderContext
from openforms.formio.display.wrap import Node


def create_elements(
nodes: Iterable["Node"], context: "RenderContext"
) -> Iterable["Element"]:
for node in nodes:
yield from node.plugin.create_elements(node, context)


class Element:
def render(self, context: "RenderContext") -> Iterable[str]:
return []


@dataclass
class NodeElement(Element):
node: "Node"

def format_value(self, context: "RenderContext") -> str:
value = format_value(self.node.component, self.node.value, context.as_html)
if context.as_html:
value = conditional_escape(value)
return value


@dataclass
class LabelValue(NodeElement):
def render(self, context):
label = self.node.get_default_label()
if context.as_html:
yield format_html(
"<td>{}</td><td>{}</td>", label, self.format_value(context)
)
else:
yield f"{label}: {self.format_value(context)}"


@dataclass
class ValueElement(NodeElement):
def render(self, context):
text = self.format_value(context)
if context.as_html:
yield format_html('<td colspan="2">{}<td>', text)
else:
yield text


@dataclass
class Header(Element):
text: str
# TODO .wrap should be something like .level:int and do something for text
wrap: str = "h1"

def render(self, context):
if context.as_html:
if self.wrap:
text = format_html(
"<{tag}>{text}</{tag}>", tag=self.wrap, text=self.text
)
else:
text = self.text
yield format_html('<td colspan="2">{}<td>', text)
else:
yield self.text


@dataclass
class TextElement(Element):
text: str

def render(self, context):
if context.as_html:
yield format_html('<td colspan="2">{}<td>', self.text)
else:
yield self.text


@dataclass
class HTMLElement(Element):
# danger
html: str

def render(self, context):
if context.as_html:
yield format_html('<td colspan="2">{}<td>', mark_safe(self.html))
else:
# TODO add whitespace cleanup from elsewhere
yield html.unescape(self.html)


@dataclass
class GroupBreakElement(Element):
def render(self, context):
if context.as_html:
yield escape('<td colspan="2">&nbsp;<td>')
else:
yield ""
14 changes: 14 additions & 0 deletions src/openforms/formio/display/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from openforms.plugins.registry import BaseRegistry


class Registry(BaseRegistry):
"""
A registry for the FormIO submission rendering component plugins.
"""

pass


# Sentinel to provide the default registry. You an easily instantiate another
# :class:`Registry` object to use as dependency injection in tests.
register = Registry()
45 changes: 45 additions & 0 deletions src/openforms/formio/display/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Iterable, Set

from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe

if TYPE_CHECKING:
from openforms.formio.display.constants import OutputMode
from openforms.formio.display.elements import Element


@dataclass
class RenderContext:
mode: "OutputMode"
as_html: bool

limit_value_keys: Set[str] = field(default_factory=set)

def allows_value_key(self, key: str):
return bool(not self.limit_value_keys or key in self.limit_value_keys)


def render_elements(elements: Iterable["Element"], context: RenderContext) -> str:
res = "\n".join(_render_elements(elements, context))
if context.as_html:
# booya
res = mark_safe(res)
return res


def _render_elements(
elements: Iterable["Element"], context: RenderContext
) -> Iterable[str]:
if context.as_html:
# TODO support more then simple table
for elem in elements:
# TODO make full generator until join
output = list(elem.render(context))
if output:
yield "<tr>"
yield from map(conditional_escape, output)
yield "</tr>"
else:
for elem in elements:
yield from elem.render(context)
17 changes: 17 additions & 0 deletions src/openforms/formio/display/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from openforms.submissions.models import Submission

from .constants import OutputMode
from .elements import create_elements
from .registry import register
from .render import RenderContext, render_elements
from .wrap import get_submission_tree

__all__ = ["render", "OutputMode"]


def render(submission: Submission, *, mode: OutputMode, as_html: bool) -> str:
context = RenderContext(mode=mode, as_html=as_html)
root = get_submission_tree(submission, register)

elements = create_elements(root.children, context)
return render_elements(elements, context)
Empty file.

0 comments on commit 89ba323

Please sign in to comment.