diff --git a/src/openforms/emails/tests/test_confirmation_email.py b/src/openforms/emails/tests/test_confirmation_email.py index fa09e93b90..47dcf90556 100644 --- a/src/openforms/emails/tests/test_confirmation_email.py +++ b/src/openforms/emails/tests/test_confirmation_email.py @@ -323,7 +323,7 @@ def test_checkboxes_ordering(self): context = get_confirmation_email_context_data(submission) rendered_content = render_email_template("{% summary %}", context) - self.assertIn("Value 1; Value 2", rendered_content) + self.assertInHTML("", rendered_content) def test_get_confirmation_email_templates(self): email_template1 = ConfirmationEmailTemplateFactory.create( diff --git a/src/openforms/formio/formatters/base.py b/src/openforms/formio/formatters/base.py index b27a0fb7d5..fc38c224a7 100644 --- a/src/openforms/formio/formatters/base.py +++ b/src/openforms/formio/formatters/base.py @@ -3,7 +3,7 @@ from typing import Any, Generic, Iterable, Sequence, TypeVar from django.utils.encoding import force_str -from django.utils.html import format_html_join +from django.utils.html import format_html, format_html_join from ..typing import Component @@ -51,12 +51,18 @@ def normalise_value_to_list(self, component: ComponentT, value: Any): def join_formatted_values( self, component: Component, formatted_values: Iterable[str] ) -> str: - if self.as_html: - args_generator = ((formatted,) for formatted in formatted_values) - return format_html_join(self.multiple_separator, "{}", args_generator) - else: + if not self.as_html: return self.multiple_separator.join(formatted_values) + args_generator = ((formatted,) for formatted in formatted_values) + if component.get("multiple", False): + return format_html( + "", + values=format_html_join("", "
  • {}
  • ", args_generator), + ) + + return format_html_join(self.multiple_separator, "{}", args_generator) + def process_result(self, component: ComponentT, formatted: str) -> str: return formatted diff --git a/src/openforms/formio/formatters/formio.py b/src/openforms/formio/formatters/formio.py index 52767bef28..d17f56be9d 100644 --- a/src/openforms/formio/formatters/formio.py +++ b/src/openforms/formio/formatters/formio.py @@ -5,7 +5,7 @@ from django.template.defaultfilters import date as fmt_date, time as fmt_time, yesno from django.utils.dateparse import parse_date, parse_time from django.utils.formats import number_format -from django.utils.html import format_html +from django.utils.html import format_html, format_html_join from django.utils.translation import gettext, gettext_lazy as _ from glom import glom @@ -120,6 +120,18 @@ def format(self, component: SelectBoxesComponent, value: dict[str, bool]) -> str selected_labels = [ entry["label"] for entry in component["values"] if value.get(entry["value"]) ] + if self.as_html: + # For the html output, wrap the values in li tags and put it inside an ul tag. + # The selectboxes formatter handles all values at the same time, + # so handle the full html formatting here. + return format_html( + "", + values=format_html_join( + "", + "
  • {}
  • ", + ((selected_label,) for selected_label in selected_labels), + ), + ) return self.multiple_separator.join(selected_labels) diff --git a/src/openforms/formio/rendering/tests/test_component_node.py b/src/openforms/formio/rendering/tests/test_component_node.py index 0cb7f4c9c4..371df802ef 100644 --- a/src/openforms/formio/rendering/tests/test_component_node.py +++ b/src/openforms/formio/rendering/tests/test_component_node.py @@ -533,3 +533,412 @@ def test_render_mode_confirmation_email(self): "Input 6", ] self.assertEqual(labels, expected_labels) + + def test_render_mode_pdf_with_list_values(self): + config = { + "components": [ + { + "type": "bsn", + "key": "bsn", + "label": "BSN", + "multiple": True, + }, + { + "type": "date", + "key": "date", + "label": "Date", + "multiple": True, + }, + { + "type": "datetime", + "key": "datetime", + "label": "DateTime", + "multiple": True, + }, + { + "type": "email", + "key": "email", + "label": "Email", + "multiple": True, + }, + { + "type": "iban", + "key": "iban", + "label": "IBAN", + "multiple": True, + }, + { + "type": "licenseplate", + "key": "licenseplate", + "label": "LicensePlate", + "multiple": True, + }, + { + "type": "phoneNumber", + "key": "phoneNumber", + "label": "PhoneNumber", + "multiple": True, + }, + { + "type": "postcode", + "key": "postcode", + "label": "Postcode", + "multiple": True, + }, + { + "type": "textarea", + "key": "textarea", + "label": "Textarea", + "multiple": True, + }, + { + "type": "time", + "key": "time", + "label": "Time", + "multiple": True, + }, + { + "type": "selectboxes", + "key": "selectboxes", + "label": "Selectboxes", + "values": [ + { + "value": "option1", + "label": "Option 1", + }, + { + "value": "option2", + "label": "Option 2", + }, + { + "value": "option3", + "label": "Option 3", + }, + ], + }, + { + "type": "radio", + "key": "radio", + "label": "Radio", + "values": [ + { + "value": "option1", + "label": "Option 1", + }, + { + "value": "option2", + "label": "Option 2", + }, + ], + }, + { + "type": "select", + "key": "select-single", + "label": "Select single", + "multiple": False, + "openForms": { + "dataSrc": "manual", + }, + "data": { + "values": [ + {"label": "Option 1", "value": "option1"}, + {"label": "Option 2", "value": "option2"}, + ] + }, + }, + { + "type": "select", + "key": "select-multiple", + "label": "Select multiple", + "multiple": True, + "openForms": { + "dataSrc": "manual", + }, + "data": { + "values": [ + { + "label": "Option 1", + "value": "option1", + }, + { + "label": "Option 2", + "value": "option2", + }, + { + "label": "Option 3", + "value": "option3", + }, + ] + }, + }, + { + "type": "textfield", + "key": "textfield", + "label": "Textfield", + "multiple": True, + }, + ] + } + data = { + "bsn": ["123456782", "987654321"], + "date": ["2024-11-11", "2024-11-13", "2024-11-16"], + "datetime": ["2022-09-08T12:00:00+02:00", "2022-10-08T15:00:00+02:00"], + "email": ["foo@mail.com", "bar@mail.com"], + "iban": ["RO09 BCYP 0000 0012 3456 7890", "RO09 BCYP 0000 0056 3456 7890"], + "licenseplate": ["123-aa-1", "12-aa-31"], + "phoneNumber": ["0612345678", "0645678923"], + "postcode": ["1234aa", "5678bb"], + "textarea": ["foo", "bar"], + "time": ["12:12:00", "14:02:00", "19:21:00"], + "selectboxes": {"option1": True, "option2": True}, + "radio": "option1", + "select-single": "option1", + "select-multiple": ["option2", "option3"], + "textfield": ["Foo", "Bar"], + } + + submission = SubmissionFactory.from_components( + components_list=config["components"], submitted_data=data + ) + step = submission.steps[0] + register = Registry() + + nodelist = [] + with patch("openforms.formio.rendering.registry.register", new=register): + renderer = Renderer(submission, mode=RenderModes.pdf, as_html=True) + for component in step.form_step.form_definition.configuration["components"]: + component_node = ComponentNode.build_node( + step_data=step.data, component=component, renderer=renderer + ) + nodelist += list(component_node) + + self.assertEqual(len(nodelist), 15) + + expected = [ + ("BSN", ""), + ( + "Date", + "", + ), + ( + "DateTime", + "", + ), + ("Email", ""), + ( + "IBAN", + "", + ), + ("LicensePlate", ""), + ("PhoneNumber", ""), + ("Postcode", ""), + ("Textarea", ""), + ("Time", ""), + ("Selectboxes", ""), + ("Radio", "Option 1"), + ("Select single", "Option 1"), + ("Select multiple", ""), + ("Textfield", ""), + ] + values = [(node.label, node.display_value) for node in nodelist] + self.assertEqual(values, expected) + + def test_render_mode_summary_with_list_values(self): + config = { + "components": [ + { + "type": "bsn", + "key": "bsn", + "label": "BSN", + "multiple": True, + }, + { + "type": "date", + "key": "date", + "label": "Date", + "multiple": True, + }, + { + "type": "datetime", + "key": "datetime", + "label": "DateTime", + "multiple": True, + }, + { + "type": "email", + "key": "email", + "label": "Email", + "multiple": True, + }, + { + "type": "iban", + "key": "iban", + "label": "IBAN", + "multiple": True, + }, + { + "type": "licenseplate", + "key": "licenseplate", + "label": "LicensePlate", + "multiple": True, + }, + { + "type": "phoneNumber", + "key": "phoneNumber", + "label": "PhoneNumber", + "multiple": True, + }, + { + "type": "postcode", + "key": "postcode", + "label": "Postcode", + "multiple": True, + }, + { + "type": "textarea", + "key": "textarea", + "label": "Textarea", + "multiple": True, + }, + { + "type": "time", + "key": "time", + "label": "Time", + "multiple": True, + }, + { + "type": "selectboxes", + "key": "selectboxes", + "label": "Selectboxes", + "values": [ + { + "value": "option1", + "label": "Option 1", + }, + { + "value": "option2", + "label": "Option 2", + }, + { + "value": "option3", + "label": "Option 3", + }, + ], + }, + { + "type": "radio", + "key": "radio", + "label": "Radio", + "values": [ + { + "value": "option1", + "label": "Option 1", + }, + { + "value": "option2", + "label": "Option 2", + }, + ], + }, + { + "type": "select", + "key": "select-single", + "label": "Select single", + "multiple": False, + "openForms": { + "dataSrc": "manual", + }, + "data": { + "values": [ + {"label": "Option 1", "value": "option1"}, + {"label": "Option 2", "value": "option2"}, + ] + }, + }, + { + "type": "select", + "key": "select-multiple", + "label": "Select multiple", + "multiple": True, + "openForms": { + "dataSrc": "manual", + }, + "data": { + "values": [ + { + "label": "Option 1", + "value": "option1", + }, + { + "label": "Option 2", + "value": "option2", + }, + { + "label": "Option 3", + "value": "option3", + }, + ] + }, + }, + { + "type": "textfield", + "key": "textfield", + "label": "Textfield", + "multiple": True, + }, + ] + } + data = { + "bsn": ["123456782", "987654321"], + "date": ["2024-11-11", "2024-11-13", "2024-11-16"], + "datetime": ["2022-09-08T12:00:00+02:00", "2022-10-08T15:00:00+02:00"], + "email": ["foo@mail.com", "bar@mail.com"], + "iban": ["RO09 BCYP 0000 0012 3456 7890", "RO09 BCYP 0000 0056 3456 7890"], + "licenseplate": ["123-aa-1", "12-aa-31"], + "phoneNumber": ["0612345678", "0645678923"], + "postcode": ["1234aa", "5678bb"], + "textarea": ["foo", "bar"], + "time": ["12:12:00", "14:02:00", "19:21:00"], + "selectboxes": {"option1": True, "option2": True}, + "radio": "option1", + "select-single": "option1", + "select-multiple": ["option2", "option3"], + "textfield": ["Foo", "Bar"], + } + + submission = SubmissionFactory.from_components( + components_list=config["components"], submitted_data=data + ) + step = submission.steps[0] + register = Registry() + + nodelist = [] + with patch("openforms.formio.rendering.registry.register", new=register): + renderer = Renderer(submission, mode=RenderModes.summary, as_html=False) + for component in step.form_step.form_definition.configuration["components"]: + component_node = ComponentNode.build_node( + step_data=step.data, component=component, renderer=renderer + ) + nodelist += list(component_node) + + self.assertEqual(len(nodelist), 15) + + expected = [ + ("BSN", "123456782; 987654321"), + ("Date", "11 november 2024; 13 november 2024; 16 november 2024"), + ("DateTime", "8 september 2022 12:00; 8 oktober 2022 15:00"), + ("Email", "foo@mail.com; bar@mail.com"), + ("IBAN", "RO09 BCYP 0000 0012 3456 7890; RO09 BCYP 0000 0056 3456 7890"), + ("LicensePlate", "123-aa-1; 12-aa-31"), + ("PhoneNumber", "0612345678; 0645678923"), + ("Postcode", "1234aa; 5678bb"), + ("Textarea", "foo; bar"), + ("Time", "12:12; 14:02; 19:21"), + ("Selectboxes", "Option 1; Option 2"), + ("Radio", "Option 1"), + ("Select single", "Option 1"), + ("Select multiple", "Option 2; Option 3"), + ("Textfield", "Foo; Bar"), + ] + values = [(node.label, node.display_value) for node in nodelist] + self.assertEqual(values, expected) diff --git a/src/openforms/js/components/errors/ErrorMessage.js b/src/openforms/js/components/errors/ErrorMessage.js index 620a515115..5443ff9a94 100644 --- a/src/openforms/js/components/errors/ErrorMessage.js +++ b/src/openforms/js/components/errors/ErrorMessage.js @@ -6,7 +6,7 @@ import {ErrorIcon} from 'components/admin/icons'; const ErrorMessage = ({children}) => { if (!children) return null; return ( -
    +
    diff --git a/src/openforms/registrations/contrib/email/tests/test_backend.py b/src/openforms/registrations/contrib/email/tests/test_backend.py index d23c1142ce..ec6dcf0293 100644 --- a/src/openforms/registrations/contrib/email/tests/test_backend.py +++ b/src/openforms/registrations/contrib/email/tests/test_backend.py @@ -217,7 +217,9 @@ def test_submission_with_email_backend(self): self.assertTagWithTextIn("td", "foo", message_html) self.assertTagWithTextIn("td", "bar", message_html) self.assertTagWithTextIn("td", "some_list", message_html) - self.assertTagWithTextIn("td", "value1; value2", message_html) + self.assertTagWithTextIn( + "td", "", message_html + ) cosigner_line = f"{_('Co-signed by')}: Demo Person" @@ -777,7 +779,7 @@ def test_regression_nested_components_columns(self): message = mail.outbox[0] message_html = message.alternatives[0][0] - self.assertIn("Backend; Frontend", message_html) + self.assertInHTML("", message_html) @patch("openforms.registrations.contrib.email.plugin.EmailConfig.get_solo") def test_with_global_config_attach_files(self, mock_get_solo): diff --git a/src/openforms/submissions/templates/report/submission_report.html b/src/openforms/submissions/templates/report/submission_report.html index 5b51d10f1f..774db1eee7 100644 --- a/src/openforms/submissions/templates/report/submission_report.html +++ b/src/openforms/submissions/templates/report/submission_report.html @@ -25,13 +25,21 @@ {% include 'includes/design-tokens.html' with skip_csp=True %} + {% if config.organization_name %} + {% blocktranslate with name=config.organization_name asvar logo_alt trimmed %} + Logo {{ name }} + {% endblocktranslate %} + {% else %} + {% trans "Logo" as logo_alt %} + {% endif %} +
    diff --git a/src/openforms/submissions/tests/test_submission_report.py b/src/openforms/submissions/tests/test_submission_report.py index 2a80608246..6c53b56368 100644 --- a/src/openforms/submissions/tests/test_submission_report.py +++ b/src/openforms/submissions/tests/test_submission_report.py @@ -138,7 +138,7 @@ def test_report_is_generated_in_same_language_as_submission(self): ("postcode", "3744 AA"), ("radio", "Radio number one"), ("select", "A fine selection"), - ("selectboxes", "This; That; The Other"), + ("selectboxes", "
    • This
    • That
    • The Other
    "), ("signature", SIGNATURE), ("textarea", "Largish predetermined ASCII"), ("textfield", "Short predetermined ASCII"), diff --git a/src/openforms/submissions/tests/test_tasks_pdf.py b/src/openforms/submissions/tests/test_tasks_pdf.py index c0f8eb7f49..5ae5d4ce9c 100644 --- a/src/openforms/submissions/tests/test_tasks_pdf.py +++ b/src/openforms/submissions/tests/test_tasks_pdf.py @@ -262,6 +262,201 @@ def test_timestamp_included(self): self.assertEqual(reference_node.text, "Report created on: Jan. 1, 2024, 1 a.m.") + def test_textfield_component_with_multiple_is_rendered_as_html(self): + submission = SubmissionFactory.from_components( + [ + { + "type": "textfield", + "key": "textfield-single", + "label": "Textfield single", + "multiple": False, + }, + { + "type": "textfield", + "key": "textfield-multiple", + "label": "Textfield multiple", + "multiple": True, + }, + ], + submitted_data={ + "textfield-single": "foo", + "textfield-multiple": ["foo", "bar"], + }, + with_report=True, + ) + html = submission.report.generate_submission_report_pdf() + + expected = format_html( + """ +
    +
    Textfield single
    +
    + foo +
    +
    +
    +
    Textfield multiple
    +
    +
    • foo
    • bar
    +
    +
    + """, + ) + self.assertInHTML(expected, html, count=1) + + def test_select_component_with_multiple_is_rendered_as_html(self): + submission = SubmissionFactory.from_components( + [ + { + "type": "select", + "key": "select-single", + "label": "Select single", + "multiple": False, + "openForms": { + "dataSrc": "manual", + }, + "data": { + "values": [ + {"label": "Single select Option 1", "value": "option1"}, + {"label": "Single select Option 2", "value": "option2"}, + ] + }, + }, + { + "type": "select", + "key": "select-multiple", + "label": "Select multiple", + "multiple": True, + "openForms": { + "dataSrc": "manual", + }, + "data": { + "values": [ + { + "label": "Multiple select Option 1", + "value": "option1", + }, + { + "label": "Multiple select Option 2", + "value": "option2", + }, + { + "label": "Multiple select Option 3", + "value": "option3", + }, + ] + }, + }, + ], + submitted_data={ + "select-single": "option1", + "select-multiple": ["option1", "option3"], + }, + with_report=True, + ) + html = submission.report.generate_submission_report_pdf() + + expected = format_html( + """ +
    +
    Select single
    +
    + Single select Option 1 +
    +
    +
    +
    Select multiple
    +
    +
    • Multiple select Option 1
    • Multiple select Option 3
    +
    +
    + """, + ) + self.assertInHTML(expected, html, count=1) + + def test_selectboxes_component_is_rendered_as_html(self): + submission = SubmissionFactory.from_components( + [ + { + "type": "selectboxes", + "key": "selectboxes", + "label": "Selectboxes", + "values": [ + { + "value": "option1", + "label": "Selectbox Option 1", + }, + { + "value": "option2", + "label": "Selectbox Option 2", + }, + { + "value": "option3", + "label": "Selectbox Option 3", + }, + ], + }, + ], + submitted_data={ + "selectboxes": { + "option1": True, + "option3": True, + }, + }, + with_report=True, + ) + html = submission.report.generate_submission_report_pdf() + + expected = format_html( + """ +
    +
    Selectboxes
    +
    +
    • Selectbox Option 1
    • Selectbox Option 3
    +
    +
    + """, + ) + self.assertInHTML(expected, html, count=1) + + def test_radio_component_is_rendered_as_plain_text(self): + submission = SubmissionFactory.from_components( + [ + { + "type": "radio", + "key": "radio", + "label": "Radio", + "values": [ + { + "value": "firstradiooption", + "label": "First radio option", + }, + { + "value": "secondradiooption", + "label": "Second radio option", + }, + ], + }, + ], + submitted_data={ + "radio": "secondradiooption", + }, + with_report=True, + ) + html = submission.report.generate_submission_report_pdf() + + expected = format_html( + """ +
    +
    Radio
    +
    + Second radio option +
    +
    + """, + ) + self.assertInHTML(expected, html, count=1) + @temp_private_root() class SubmissionReportCoSignTests(TestCase): diff --git a/src/openforms/templates/includes/page-header.html b/src/openforms/templates/includes/page-header.html index b55218212d..992cb9e3af 100644 --- a/src/openforms/templates/includes/page-header.html +++ b/src/openforms/templates/includes/page-header.html @@ -8,9 +8,13 @@ {% get_solo 'config.GlobalConfiguration' as config %} {% get_theme as theme %} -{% blocktranslate with name=config.organization_name asvar logo_alt trimmed %} -Back to website of {{ name }} -{% endblocktranslate %} +{% if config.organization_name %} + {% blocktranslate with name=config.organization_name asvar logo_alt trimmed %} + Back to website of {{ name }} + {% endblocktranslate %} +{% else %} + {% trans "Back to website" as logo_alt %} +{% endif %} diff --git a/src/openforms/utils/pdf.py b/src/openforms/utils/pdf.py index bc9324d251..a92aca08c3 100644 --- a/src/openforms/utils/pdf.py +++ b/src/openforms/utils/pdf.py @@ -106,5 +106,5 @@ def render_to_pdf(template_name: str, context: dict) -> tuple[str, bytes]: url_fetcher=UrlFetcher(), base_url=settings.BASE_URL, ) - pdf: bytes = html_object.write_pdf() + pdf: bytes = html_object.write_pdf(pdf_variant="pdf/ua-1") return rendered_html, pdf