Skip to content

Commit

Permalink
[#3718] PR feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
vaszig committed Mar 12, 2024
1 parent 6c6797a commit d150cc4
Show file tree
Hide file tree
Showing 7 changed files with 536 additions and 27 deletions.
10 changes: 0 additions & 10 deletions src/openforms/formio/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,8 @@ def inject_variables(
continue

match property_value:
case str():
property_value = property_value
case [str(), *_]:
property_value = [s for s in property_value if isinstance(s, str)]
case [{"label": _}, *_]:
for item in property_value:
if "label" in item:
item["label"] = item["label"]
case {"values": [*defined_values]}:
for item in defined_values:
if "label" in item:
item["label"] = item["label"]

try:
templated_value = render(property_value, values)
Expand Down
17 changes: 17 additions & 0 deletions src/openforms/forms/api/serializers/form_definition.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import warnings

from django.urls import reverse
from django.utils.translation import gettext_lazy as _

Expand All @@ -9,6 +11,7 @@
from openforms.formio.service import rewrite_formio_components_for_request
from openforms.translations.api.serializers import ModelTranslationsSerializer

from ...fd_translations_converter import process_component_tree
from ...models import Form, FormDefinition
from ...validators import (
validate_form_definition_is_reusable,
Expand Down Expand Up @@ -134,6 +137,20 @@ def validate(self, attrs):
self.instance, new_value=attrs.get("is_reusable")
)

# during import, process legacy format component translations
if self.context.get("is_import", False) and (
translations_store := attrs.get("component_translations")
):
warnings.warn(
"Form-definition component translations are deprecated, the "
"compatibility layer wil be removed in Open Forms 3.0.",
DeprecationWarning,
)
process_component_tree(
components=attrs["configuration"]["components"],
translations_store=translations_store,
)

return attrs


Expand Down
177 changes: 177 additions & 0 deletions src/openforms/forms/fd_translations_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
Implements the translation converter for form definitions.
Github issue: https://github.com/open-formulieren/open-forms/issues/2958
Before, translations for formio components were saved in the JSONField
``FormDefinition.component_translations``, which is a key-value mapping of language
code to translations mapping, which itself is a key-value mapping of source string to
translation string. This had the drawback that if different components happen to use the
same literal, they also shared the same translation.
The utilities here help in converting these translations to component-local
translations, where the translations are stored on each component inside the formio
schema. The exact set of supported fields for translation varies per component, the
reference for this is the typescript type definitions:
https://open-formulieren.github.io/types/modules/index.html
"""

import logging
from typing import TypedDict

from glom import assign

from openforms.formio.service import iter_components
from openforms.formio.typing import Component

logger = logging.getLogger(__name__)


class Option(TypedDict):
value: str
label: str


def _set_translations(
obj: Component | Option,
translations_from_store: dict[str, str],
locale: str,
fields: list[str],
) -> None:
translations = {
key: translation
for key in fields
if (literal := obj.get(key))
and (translation := translations_from_store.get(literal))
}
if not translations:
return
assign(obj, f"openForms.translations.{locale}", translations, missing=dict)


def _move_translations(component: Component, locale: str, translations: dict[str, str]):
"""
Given a component and translation store, mutate the component to bake in translations.
This mutates the ``component`` parameter!
"""
assert "type" in component

match component:
case {"type": "textfield" | "textarea"}:
_set_translations(
component,
translations,
locale,
fields=[
"label",
"description",
"tooltip",
"defaultValue",
"placeholder",
],
)
case {
"type": "email"
| "date"
| "datetime"
| "time"
| "phoneNumber"
| "file"
| "checkbox"
| "currency"
| "iban"
| "licenseplate"
| "bsn"
| "addressNL"
| "npFamilyMembers"
| "cosign"
| "map"
| "postcode"
| "password"
}:
_set_translations(
component,
translations,
locale,
fields=["label", "description", "tooltip"],
)

case {"type": "number"}:
_set_translations(
component,
translations,
locale,
fields=["label", "description", "tooltip", "suffix"],
)

case {"type": "select" | "selectboxes" | "radio", **rest}:
_set_translations(
component,
translations,
locale,
fields=["label", "description", "tooltip"],
)

# process options, which have their translations inside of each option
match rest:
case {"data": {"values": values}} | {"values": values}:
values: list[Option]
for value in values:
_set_translations(value, translations, locale, fields=["label"])

case {"type": "signature"}:
_set_translations(
component,
translations,
locale,
fields=["label", "description", "tooltip", "footer"],
)

case {"type": "coSign"}:
_set_translations(
component, translations, locale, fields=["label", "description"]
)

case {"type": "editgrid"}:
_set_translations(
component,
translations,
locale,
fields=[
"label",
"description",
"tooltip",
"groupLabel",
"addAnother",
"saveRow",
"removeRow",
],
)

case {"type": "content"}:
# label is legacy and no longer exposed in the new form builder, but pre-existing
# form definitions may have it set.
_set_translations(component, translations, locale, fields=["label", "html"])

case {"type": "columns"}:
pass

case {"type": "fieldset"}:
_set_translations(component, translations, locale, fields=["label"])

case _: # pragma: no cover
# should not happen
logger.warning(
"Could not move translations for unknown component type %s",
component["type"],
)


def process_component_tree(
components: list[Component], translations_store: dict[str, dict[str, str]]
):
tree = {"components": components}
for component in iter_components(tree, recursive=True): # type: ignore
for lang_code, translations in translations_store.items():
_move_translations(component, lang_code, translations)

This file was deleted.

9 changes: 9 additions & 0 deletions src/openforms/forms/models/form_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ class FormDefinition(models.Model):
help_text=_("The total number of Formio components used in the configuration"),
)

# this field is deprecated and will be removed in Open Forms v3.0
# the translations are handled by the new formio builder instead
component_translations = models.JSONField(
verbose_name=_("Component translations"),
help_text=_("Translations for literals used in components"),
blank=True,
default=dict,
)

class Meta:
verbose_name = _("Form definition")
verbose_name_plural = _("Form definitions")
Expand Down
70 changes: 70 additions & 0 deletions src/openforms/forms/tests/test_import_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,3 +988,73 @@ def test_rountrip_form_with_theme_override(self):

imported_form = Form.objects.exclude(pk=form.pk).get()
self.assertIsNone(imported_form.theme)

def test_import_form_with_legacy_formio_component_translations_format(self):
"""
Legacy translations need to be converted to the new format.
"""
resources = {
"forms": [
{
"active": True,
"authentication_backends": [],
"is_deleted": False,
"login_required": False,
"maintenance_mode": False,
"name": "Test Form 1",
"internal_name": "Test Form Internal 1",
"product": None,
"show_progress_indicator": True,
"slug": "translations",
"url": "http://testserver/api/v2/forms/324cadce-a627-4e3f-b117-37ca232f16b2",
"uuid": "324cadce-a627-4e3f-b117-37ca232f16b2",
}
],
"formSteps": [
{
"form": "http://testserver/api/v2/forms/324cadce-a627-4e3f-b117-37ca232f16b2",
"form_definition": "http://testserver/api/v2/form-definitions/f0dad93b-333b-49af-868b-a6bcb94fa1b8",
"index": 0,
"slug": "test-step-1",
"uuid": "3ca01601-cd20-4746-bce5-baab47636823",
},
],
"formDefinitions": [
{
"configuration": {
"components": [
{
"key": "textfield",
"type": "textfield",
"label": "TEXTFIELD_LABEL",
},
]
},
"name": "Def 1 - With condition",
"slug": "test-definition-1",
"url": "http://testserver/api/v2/form-definitions/f0dad93b-333b-49af-868b-a6bcb94fa1b8",
"uuid": "f0dad93b-333b-49af-868b-a6bcb94fa1b8",
"component_translations": {
"nl": {
"TEXTFIELD_LABEL": "Tekstveld",
}
},
},
],
}

with zipfile.ZipFile(self.filepath, "w") as zip_file:
for name, data in resources.items():
zip_file.writestr(f"{name}.json", json.dumps(data))

call_command("import", import_file=self.filepath)

fd = FormDefinition.objects.get()
textfield = fd.configuration["components"][0]

self.assertIn("openForms", textfield)
self.assertIn("translations", textfield["openForms"])
self.assertIn("nl", textfield["openForms"]["translations"])
nl_translations = textfield["openForms"]["translations"]["nl"]
self.assertIn("label", nl_translations)
self.assertEqual(nl_translations["label"], "Tekstveld")
Loading

0 comments on commit d150cc4

Please sign in to comment.