From b5b214f1c7d8fab66d2af3267ae3e8e9e96bf593 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 12 Apr 2021 23:33:27 +0200
Subject: [PATCH] misc: include object url in XML exports (#51899)

---
 tests/admin_pages/test_form.py     |  1 +
 tests/admin_pages/test_workflow.py |  6 +++---
 wcs/admin/blocks.py                | 13 +++++--------
 wcs/admin/data_sources.py          | 15 +++++----------
 wcs/admin/forms.py                 | 12 ++++--------
 wcs/admin/mail_templates.py        | 15 +++++----------
 wcs/admin/workflows.py             | 11 ++++-------
 wcs/admin/wscalls.py               | 13 ++++---------
 wcs/qommon/misc.py                 | 12 ++++++++++++
 9 files changed, 43 insertions(+), 55 deletions(-)

diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py
index f3d4e6edf..51f9b0d59 100644
--- a/tests/admin_pages/test_form.py
+++ b/tests/admin_pages/test_form.py
@@ -1018,6 +1018,7 @@ def test_form_export(pub):
 
     resp = resp.click(href='export')
     xml_export = resp.text
+    assert ET.fromstring(xml_export).attrib['url'] == 'http://example.net/backoffice/forms/1/'
 
     fd = io.StringIO(xml_export)
     formdef2 = FormDef.import_from_xml(fd)
diff --git a/tests/admin_pages/test_workflow.py b/tests/admin_pages/test_workflow.py
index d121dc222..56be4ade7 100644
--- a/tests/admin_pages/test_workflow.py
+++ b/tests/admin_pages/test_workflow.py
@@ -350,7 +350,7 @@ def test_workflows_export_import(pub):
     app = login(get_app(pub))
     resp = app.get('/backoffice/workflows/1/')
     resp = resp.click('Export')
-    assert resp.content_type == 'application/x-wcs-form'
+    assert resp.content_type == 'application/x-wcs-workflow'
     wf_export = resp.body
 
     resp = app.get('/backoffice/workflows/')
@@ -381,7 +381,7 @@ def test_workflows_import_from_url(pub):
     app = login(get_app(pub))
     resp = app.get('/backoffice/workflows/1/')
     resp = resp.click('Export')
-    assert resp.content_type == 'application/x-wcs-form'
+    assert resp.content_type == 'application/x-wcs-workflow'
     wf_export = resp.body
 
     resp = app.get('/backoffice/workflows/')
@@ -421,7 +421,7 @@ def test_workflows_export_import_create_role(pub):
     app = login(get_app(pub))
     resp = app.get('/backoffice/workflows/1/')
     resp = resp.click('Export')
-    assert resp.content_type == 'application/x-wcs-form'
+    assert resp.content_type == 'application/x-wcs-workflow'
     wf_export = resp.body
 
     resp = app.get('/backoffice/workflows/')
diff --git a/wcs/admin/blocks.py b/wcs/admin/blocks.py
index d260eb4a2..ea0337827 100644
--- a/wcs/admin/blocks.py
+++ b/wcs/admin/blocks.py
@@ -14,8 +14,6 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-import xml.etree.ElementTree as ET
-
 from quixote import get_publisher, get_response, get_session, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
@@ -128,12 +126,11 @@ class BlockDirectory(FieldsDirectory):
             return redirect('..')
 
     def export(self):
-        x = self.objectdef.export_to_xml(include_id=True)
-        misc.indent_xml(x)
-        response = get_response()
-        response.set_content_type('application/x-wcs-form')
-        response.set_header('content-disposition', 'attachment; filename=block-%s.wcs' % self.objectdef.slug)
-        return '<?xml version="1.0"?>\n' + ET.tostring(x).decode('utf-8')
+        return misc.xml_response(
+            self.objectdef,
+            filename='block-%s.wcs' % self.objectdef.slug,
+            content_type='application/x-wcs-form',
+        )
 
     def settings(self):
         get_response().breadcrumb.append(('settings', _('Settings')))
diff --git a/wcs/admin/data_sources.py b/wcs/admin/data_sources.py
index 46184547a..6da4332e4 100644
--- a/wcs/admin/data_sources.py
+++ b/wcs/admin/data_sources.py
@@ -14,8 +14,6 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-import xml.etree.ElementTree as ET
-
 from quixote import get_publisher, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
@@ -31,7 +29,7 @@ from wcs.data_sources import (
     has_chrono,
 )
 from wcs.formdef import get_formdefs_of_all_kinds
-from wcs.qommon import _, errors, force_str, misc, template
+from wcs.qommon import _, errors, misc, template
 from wcs.qommon.backoffice.menu import html_top
 from wcs.qommon.form import (
     CheckboxWidget,
@@ -371,14 +369,11 @@ class NamedDataSourcePage(Directory):
             return redirect('..')
 
     def export(self):
-        x = self.datasource.export_to_xml(include_id=True)
-        misc.indent_xml(x)
-        response = get_response()
-        response.set_content_type('application/x-wcs-datasource')
-        response.set_header(
-            'content-disposition', 'attachment; filename=datasource-%s.wcs' % self.datasource.slug
+        return misc.xml_response(
+            self.datasource,
+            filename='datasource-%s.wcs' % self.datasource.slug,
+            content_type='application/x-wcs-datasource',
         )
-        return '<?xml version="1.0"?>\n' + force_str(ET.tostring(x))
 
 
 class NamedDataSourcesDirectory(Directory):
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index 04b04e946..3563d4e43 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -1466,15 +1466,11 @@ class FormDefPage(Directory):
         return r.getvalue()
 
     def export(self):
-        x = self.formdef.export_to_xml(include_id=True)
-        misc.indent_xml(x)
-        response = get_response()
-        response.set_content_type('application/x-wcs-form')
-        response.set_header(
-            'content-disposition',
-            'attachment; filename=%s-%s.wcs' % (self.formdef_export_prefix, self.formdef.url_name),
+        return misc.xml_response(
+            self.formdef,
+            filename='%s-%s.wcs' % (self.formdef_export_prefix, self.formdef.url_name),
+            content_type='application/x-wcs-form',
         )
-        return '<?xml version="1.0"?>\n' + force_str(ET.tostring(x))
 
     def archive(self):
         if get_publisher().is_using_postgresql():
diff --git a/wcs/admin/mail_templates.py b/wcs/admin/mail_templates.py
index 0ba9f22aa..0cc5a64bf 100644
--- a/wcs/admin/mail_templates.py
+++ b/wcs/admin/mail_templates.py
@@ -14,14 +14,12 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-import xml.etree.ElementTree as ET
-
 from quixote import get_publisher, get_response, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
 from wcs.mail_templates import MailTemplate
-from wcs.qommon import _, errors, force_str, misc, template
+from wcs.qommon import _, errors, misc, template
 from wcs.qommon.backoffice.menu import html_top
 from wcs.qommon.form import (
     ComputedExpressionWidget,
@@ -279,11 +277,8 @@ class MailTemplatePage(Directory):
             return redirect('..')
 
     def export(self):
-        x = self.mail_template.export_to_xml(include_id=True)
-        misc.indent_xml(x)
-        response = get_response()
-        response.set_content_type('application/x-wcs-mail-template')
-        response.set_header(
-            'content-disposition', 'attachment; filename=mail-template-%s.wcs' % self.mail_template.slug
+        return misc.xml_response(
+            self.mail_template,
+            filename='mail-template-%s.wcs' % self.mail_template.slug,
+            content_type='application/x-wcs-mail-template',
         )
-        return '<?xml version="1.0"?>\n' + force_str(ET.tostring(x))
diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py
index 017123c3f..ec0bdba38 100644
--- a/wcs/admin/workflows.py
+++ b/wcs/admin/workflows.py
@@ -1696,14 +1696,11 @@ class WorkflowPage(Directory):
         )
 
     def export(self):
-        x = self.workflow.export_to_xml(include_id=True)
-        misc.indent_xml(x)
-        response = get_response()
-        response.set_content_type('application/x-wcs-form')
-        response.set_header(
-            'content-disposition', 'attachment; filename=workflow-%s.wcs' % misc.simplify(self.workflow.name)
+        return misc.xml_response(
+            self.workflow,
+            filename='workflow-%s.wcs' % misc.simplify(self.workflow.name),
+            content_type='application/x-wcs-workflow',
         )
-        return '<?xml version="1.0"?>\n' + force_str(ET.tostring(x))
 
     def get_new_status_form(self):
         r = TemplateIO(html=True)
diff --git a/wcs/admin/wscalls.py b/wcs/admin/wscalls.py
index e6d28889c..64c99d20d 100644
--- a/wcs/admin/wscalls.py
+++ b/wcs/admin/wscalls.py
@@ -14,15 +14,13 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-import xml.etree.ElementTree as ET
-
 from quixote import get_publisher, get_response, get_session, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
 from wcs.admin import utils
 from wcs.backoffice.snapshots import SnapshotsDirectory
-from wcs.qommon import _, errors, force_str, misc, template
+from wcs.qommon import _, errors, misc, template
 from wcs.qommon.backoffice.menu import html_top
 from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, StringWidget, TextWidget
 from wcs.wscalls import NamedWsCall, WsCallRequestWidget
@@ -177,12 +175,9 @@ class NamedWsCallPage(Directory):
             return redirect('..')
 
     def export(self):
-        x = self.wscall.export_to_xml(include_id=True)
-        misc.indent_xml(x)
-        response = get_response()
-        response.set_content_type('application/x-wcs-wscall')
-        response.set_header('content-disposition', 'attachment; filename=wscall-%s.wcs' % self.wscall.slug)
-        return '<?xml version="1.0"?>\n' + force_str(ET.tostring(x))
+        return misc.xml_response(
+            self.wscall, filename='wscall-%s.wcs' % self.wscall.slug, content_type='application/x-wcs-wscall'
+        )
 
 
 class NamedWsCallsDirectory(Directory):
diff --git a/wcs/qommon/misc.py b/wcs/qommon/misc.py
index a8f1e30bc..ea11d644e 100644
--- a/wcs/qommon/misc.py
+++ b/wcs/qommon/misc.py
@@ -28,6 +28,7 @@ import subprocess
 import time
 import unicodedata
 import urllib.parse
+import xml.etree.ElementTree as ET
 
 import requests
 from requests.adapters import HTTPAdapter
@@ -996,3 +997,14 @@ def get_document_type_value_options(current_document_type):
     options = [({}, '---', '')]
     options += [(doc_type, doc_type['label'], key) for key, doc_type in document_types.items()]
     return options
+
+
+def xml_response(obj, filename, content_type='text/xml'):
+    etree = obj.export_to_xml(include_id=True)
+    if hasattr(obj, 'get_admin_url'):
+        etree.attrib['url'] = obj.get_admin_url()
+    indent_xml(etree)
+    response = get_response()
+    response.set_content_type(content_type)
+    response.set_header('content-disposition', 'attachment; filename=%s' % filename)
+    return '<?xml version="1.0"?>\n' + ET.tostring(etree).decode('utf-8')
-- 
2.31.0

