From 022b7f7b2b953f658bbc13270999791d0764b814 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 11 Jul 2022 22:49:30 +0200
Subject: [PATCH] workflows: force inclusion of openformula namespace in ods
 documents (#66352)

---
 tests/form_pages/test_formdata.py |  48 ++++++++++++++++++++++++++++++
 tests/template.ods                | Bin 0 -> 8232 bytes
 wcs/wf/export_to_model.py         |  13 ++++++++
 3 files changed, 61 insertions(+)
 create mode 100644 tests/template.ods

diff --git a/tests/form_pages/test_formdata.py b/tests/form_pages/test_formdata.py
index 8bacab855..8aefe0c4a 100644
--- a/tests/form_pages/test_formdata.py
+++ b/tests/form_pages/test_formdata.py
@@ -658,6 +658,54 @@ def test_formdata_generated_document_odt_download_with_substitution_variable(pub
     assert resp.request.environ['PATH_INFO'].endswith(file2.filename)
 
 
+def test_formdata_generated_document_ods_download(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = st1.add_action('export_to_model', id='_export_to')
+    export_to.convert_to_pdf = False
+    export_to.label = 'create doc'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.ods')
+    with open(template_filename, 'rb') as fd:
+        template = fd.read()
+    upload = QuixoteUpload('/foo/template.ods', content_type='application/octet-stream')
+    upload.fp = io.BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.by = ['_submitter']
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test_formdata_generated_document_ods_download'
+    formdef.url_name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')  # -> validation
+    resp = resp.forms[0].submit('submit')  # -> submit
+    resp = resp.follow()
+
+    resp = resp.form.submit('button_export_to')
+
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    with io.BytesIO(resp.body) as generated_ods:
+        with zipfile.ZipFile(generated_ods) as z_ods:
+            for name in z_ods.namelist():
+                if name != 'content.xml':
+                    continue
+                content_bytes = z_ods.read(name)
+                assert b' xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" ' in content_bytes
+                assert b'>test_formdata_generated_document_ods_download<' in content_bytes
+                # check it's valid XML
+                ET.tostring(ET.XML(content_bytes))
+
+
 @pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found')
 def test_formdata_generated_document_odt_to_pdf_download(pub):
     create_user(pub)
diff --git a/tests/template.ods b/tests/template.ods
new file mode 100644
index 0000000000000000000000000000000000000000..5cc98a519aba5baad60374f25775547262fcf5c0
GIT binary patch
diff --git a/wcs/wf/export_to_model.py b/wcs/wf/export_to_model.py
index 16717aa66..bbfe3dd9f 100644
--- a/wcs/wf/export_to_model.py
+++ b/wcs/wf/export_to_model.py
@@ -144,6 +144,19 @@ def transform_opendocument(instream, outstream, process):
             root = ET.fromstring(content)
             process(root, new_images)
             content = ET.tostring(root)
+            if (
+                root.find(f'{{{OO_OFFICE_NS}}}body/{{{OO_OFFICE_NS}}}spreadsheet')
+                and b'xmlns:of=' not in content
+            ):
+                # force xmlns:of namespace inclusion in spreadsheet files, as it may be
+                # required for proper handling of table:formula attributes.
+                # (there is no easy way to have ElementTree include namespace declarations
+                # if there are no elements of that namespace)
+                content = content.replace(
+                    b':document-content ',
+                    b':document-content xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" ',
+                    1,
+                )
             zout.writestr(filename, content)
 
         for filename in zin.namelist():
-- 
2.36.1

