diff --git a/document_merge_service/api/data/xlsx-not-valid.xlsx b/document_merge_service/api/data/xlsx-not-valid.xlsx new file mode 100644 index 00000000..8bd6648e --- /dev/null +++ b/document_merge_service/api/data/xlsx-not-valid.xlsx @@ -0,0 +1 @@ +asdf diff --git a/document_merge_service/api/data/xlsx-syntax.xlsx b/document_merge_service/api/data/xlsx-syntax.xlsx new file mode 100644 index 00000000..465bfb55 Binary files /dev/null and b/document_merge_service/api/data/xlsx-syntax.xlsx differ diff --git a/document_merge_service/api/engines.py b/document_merge_service/api/engines.py index 798a7106..3c821f91 100644 --- a/document_merge_service/api/engines.py +++ b/document_merge_service/api/engines.py @@ -186,9 +186,13 @@ def merge(self, data, buf): return buf +_placeholder_match = re.compile(r"^\s*{{\s*([^{}]+)\s*}}\s*$") + + class XlsxTemplateEngine: def __init__(self, template): self.template = template + self.writer = None def validate_is_xlsx(self): try: @@ -201,10 +205,39 @@ def validate(self, available_placeholders=None, sample_data=None): self.validate_template_syntax(available_placeholders, sample_data) def validate_template_syntax(self, available_placeholders=None, sample_data=None): - pass + # We cannot use jinja to validate because xltpl uses jinja's lexer directly + if not sample_data: + sample_data = {} + buf = io.BytesIO() + try: + self.merge(sample_data, buf) + except TemplateSyntaxError as exc: + arg_str = ";".join(exc.args) + raise exceptions.ValidationError(f"Syntax error in template: {arg_str}") + if available_placeholders: + placeholders = [] + for sheet in self.writer.sheet_resource_map.sheet_state_list: + if not sheet.sheet_resource: + continue + tree = sheet.sheet_resource.sheet_tree + self.collect_placeholders(tree._children, placeholders) + missing = set(available_placeholders) - set(placeholders) + if missing: + raise exceptions.ValidationError( + f"Template uses unavailable placeholders: {str(missing)}" + ) + + def collect_placeholders(self, children, placeholders): + for child in children: + if hasattr(child, "value"): + value = str(child.value) + re_match = _placeholder_match.match(value) + if re_match: + placeholders.append(re_match.group(1)) + self.collect_placeholders(child._children, placeholders) def merge(self, data, buf): - writer = BookWriter(self.template) + self.writer = writer = BookWriter(self.template) writer.jinja_env.filters.update(get_jinja_filters()) writer.jinja_env.globals.update(dir=dir, getattr=getattr) diff --git a/document_merge_service/api/tests/test_structure.py b/document_merge_service/api/tests/test_excel.py similarity index 52% rename from document_merge_service/api/tests/test_structure.py rename to document_merge_service/api/tests/test_excel.py index dbf91cce..f140de23 100644 --- a/document_merge_service/api/tests/test_structure.py +++ b/document_merge_service/api/tests/test_excel.py @@ -1,6 +1,8 @@ import io import openpyxl +import pytest +from rest_framework import exceptions from ..data import django_file from ..engines import XlsxTemplateEngine @@ -17,6 +19,8 @@ ], } +_available = ["key0", "key1.subkey1"] + def test_structure(): tmpl = django_file("xlsx-structure.xlsx") @@ -31,3 +35,21 @@ def test_structure(): assert ws["A5"].value == "Item: mixed" assert ws["A6"].value == "Item: list" assert ws["A7"].value == "Subitem: xdata2" + engine.validate(_available, _structure) + _available.append("huhu") + with pytest.raises(exceptions.ValidationError): + engine.validate(_available, _structure) + + +def test_syntax_error(): + tmpl = django_file("xlsx-syntax.xlsx") + engine = XlsxTemplateEngine(tmpl) + with pytest.raises(exceptions.ValidationError): + engine.validate(_available, _structure) + + +def test_valid_error(): + tmpl = django_file("xlsx-not-valid.xlsx") + engine = XlsxTemplateEngine(tmpl) + with pytest.raises(exceptions.ParseError): + engine.validate(_available, _structure) diff --git a/document_merge_service/settings.py b/document_merge_service/settings.py index 470a9aef..6fee9ea8 100644 --- a/document_merge_service/settings.py +++ b/document_merge_service/settings.py @@ -10,7 +10,7 @@ django_root = environ.Path(__file__) - 2 ENV_FILE = env.str("ENV_FILE", default=django_root(".env")) -if os.path.exists(ENV_FILE): +if os.path.exists(ENV_FILE): # pragma: no cover environ.Env.read_env(ENV_FILE) # per default production is enabled for security reasons