From be90ef3490f371b34c36bdc2a84c5a026c322c1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 19 Oct 2020 16:17:36 +0200
Subject: [PATCH] workflows: don't overwrite document models when browsing
 snapshots (#47310)

---
 tests/test_snapshots.py     | 50 +++++++++++++++++++++++++++++++++++++
 wcs/blocks.py               |  2 +-
 wcs/fields.py               | 18 ++++++-------
 wcs/formdef.py              |  3 ++-
 wcs/qommon/xml_storage.py   |  2 +-
 wcs/snapshots.py            |  5 +++-
 wcs/wf/attachment.py        |  2 +-
 wcs/wf/backoffice_fields.py |  4 +--
 wcs/wf/create_formdata.py   |  2 +-
 wcs/wf/dispatch.py          | 12 ++++-----
 wcs/wf/export_to_model.py   |  8 ++++--
 wcs/wf/form.py              |  4 +--
 wcs/wf/jump.py              |  2 +-
 wcs/wf/profile.py           |  4 +--
 wcs/wf/roles.py             |  4 +--
 wcs/wf/wscall.py            |  4 +--
 wcs/workflows.py            | 50 ++++++++++++++++++-------------------
 17 files changed, 117 insertions(+), 59 deletions(-)

diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py
index a96a220b8..ba62401d9 100644
--- a/tests/test_snapshots.py
+++ b/tests/test_snapshots.py
@@ -1,10 +1,19 @@
+import os
+import shutil
+import xml.etree.ElementTree as ET
+
 import pytest
 
+from django.utils.six import BytesIO
+from quixote.http_request import Upload
+
 from wcs.blocks import BlockDef
 from wcs.carddef import CardDef
 from wcs.data_sources import NamedDataSource
 from wcs.formdef import FormDef
+from wcs.qommon.form import UploadedFile
 from wcs.workflows import Workflow
+from wcs.workflows import ExportToModel
 from wcs.wscalls import NamedWsCall
 
 from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
@@ -302,6 +311,47 @@ def test_workflow_snapshot_browse(pub):
     assert 'This workflow is readonly' in resp
 
 
+def test_workflow_with_model_snapshot_browse(pub):
+    create_superuser(pub)
+    create_role()
+
+    Workflow.wipe()
+    if os.path.exists(os.path.join(pub.app_dir, 'models')):
+        shutil.rmtree(os.path.join(pub.app_dir, 'models'))
+    workflow = Workflow(name='test')
+    st1 = workflow.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.label = 'test'
+    upload = Upload('/foo/bar', content_type='application/vnd.oasis.opendocument.text')
+    file_content = b'''PK\x03\x04\x14\x00\x00\x08\x00\x00\'l\x8eG^\xc62\x0c\'\x00'''
+    upload.fp = BytesIO()
+    upload.fp.write(file_content)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile('models', 'tmp', upload)
+    st1.items.append(export_to)
+    export_to.parent = st1
+
+    # export/import to get models stored in the expected way
+    workflow.store()
+    workflow = Workflow.import_from_xml_tree(
+            ET.fromstring(ET.tostring(workflow.export_to_xml(include_id=True))), include_id=True)
+    assert len(os.listdir(os.path.join(pub.app_dir, 'models'))) == 2
+
+    workflow = Workflow.import_from_xml_tree(
+            ET.fromstring(ET.tostring(workflow.export_to_xml(include_id=True))), include_id=True)
+    assert len(os.listdir(os.path.join(pub.app_dir, 'models'))) == 2
+
+    app = login(get_app(pub))
+
+    for i in range(3):
+        # check document model is not overwritten
+        resp = app.get('/backoffice/workflows/%s/history/' % workflow.id)
+        snapshot = pub.snapshot_class.select_object_history(workflow)[0]
+        resp = resp.click(href='%s/view/' % snapshot.id)
+        assert 'This workflow is readonly' in resp
+        assert len(os.listdir(os.path.join(pub.app_dir, 'models'))) == 3 + i
+
+
 def test_wscall_snapshot_browse(pub):
     create_superuser(pub)
     create_role()
diff --git a/wcs/blocks.py b/wcs/blocks.py
index 91797bcc8..e683fe840 100644
--- a/wcs/blocks.py
+++ b/wcs/blocks.py
@@ -157,7 +157,7 @@ class BlockDef(StorableObject):
         return blockdef
 
     @classmethod
-    def import_from_xml_tree(cls, tree, include_id=False):
+    def import_from_xml_tree(cls, tree, include_id=False, snapshot=False):
         charset = 'utf-8'
         blockdef = cls()
         if tree.find('name') is None or not tree.find('name').text:
diff --git a/wcs/fields.py b/wcs/fields.py
index b9a98032a..877f448b9 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -279,12 +279,12 @@ class Field(object):
                     el.text = str(val)
         return field
 
-    def init_with_xml(self, elem, charset, include_id=False):
+    def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         for attribute in self.get_admin_attributes():
             el = elem.find(attribute)
             if hasattr(self, '%s_init_with_xml' % attribute):
                 getattr(self, '%s_init_with_xml' % attribute)(el, charset,
-                        include_id=include_id)
+                        include_id=include_id, snapshot=False)
                 continue
             if el is None:
                 continue
@@ -316,7 +316,7 @@ class Field(object):
             except:
                 pass
 
-    def condition_init_with_xml(self, node, charset, include_id=False):
+    def condition_init_with_xml(self, node, charset, include_id=False, snapshot=False):
         self.condition = None
         if node is None:
             return
@@ -328,7 +328,7 @@ class Field(object):
         elif node.text:
             self.condition = {'type': 'python', 'value': force_str(node.text).strip()}
 
-    def data_source_init_with_xml(self, node, charset, include_id=False):
+    def data_source_init_with_xml(self, node, charset, include_id=False, snapshot=False):
         self.data_source = {}
         if node is None:
             return
@@ -342,7 +342,7 @@ class Field(object):
             elif self.data_source.get('value') is None:
                 del self.data_source['value']
 
-    def prefill_init_with_xml(self, node, charset, include_id=False):
+    def prefill_init_with_xml(self, node, charset, include_id=False, snapshot=False):
         self.prefill = {}
         if node is not None and node.findall('type'):
             self.prefill = {
@@ -867,7 +867,7 @@ class StringField(WidgetField):
             changed = True
         return changed
 
-    def init_with_xml(self, element, charset, include_id=False):
+    def init_with_xml(self, element, charset, include_id=False, snapshot=False):
         super(StringField, self).init_with_xml(element, charset, include_id=include_id)
         self.migrate()
 
@@ -1263,7 +1263,7 @@ class FileField(WidgetField):
             self.document_type['mimetypes'] = old_value
         return result
 
-    def init_with_xml(self, element, charset, include_id=False):
+    def init_with_xml(self, element, charset, include_id=False, snapshot=False):
         super(FileField, self).init_with_xml(element, charset, include_id=include_id)
         # translate fields flattened to strings
         if self.document_type and self.document_type.get('mimetypes'):
@@ -1443,7 +1443,7 @@ class ItemField(WidgetField):
             changed = True
         return changed
 
-    def init_with_xml(self, element, charset, include_id=False):
+    def init_with_xml(self, element, charset, include_id=False, snapshot=False):
         super(ItemField, self).init_with_xml(element, charset, include_id=include_id)
         if getattr(element.find('show_as_radio'), 'text', None) == 'True':
             self.display_mode = 'radio'
@@ -1964,7 +1964,7 @@ class PageField(Field):
 
     post_conditions = None
 
-    def post_conditions_init_with_xml(self, node, charset, include_id=False):
+    def post_conditions_init_with_xml(self, node, charset, include_id=False, snapshot=False):
         if node is None:
             return
         self.post_conditions = []
diff --git a/wcs/formdef.py b/wcs/formdef.py
index 9db0dcdbe..e8972acfc 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -1071,7 +1071,8 @@ class FormDef(StorableObject):
         return formdef
 
     @classmethod
-    def import_from_xml_tree(cls, tree, include_id=False, charset=None, fix_on_error=False):
+    def import_from_xml_tree(cls, tree, include_id=False, charset=None,
+            fix_on_error=False, snapshot=False):
         if charset is None:
             charset = get_publisher().site_charset
         assert charset == 'utf-8'
diff --git a/wcs/qommon/xml_storage.py b/wcs/qommon/xml_storage.py
index 714323b00..84ac19505 100644
--- a/wcs/qommon/xml_storage.py
+++ b/wcs/qommon/xml_storage.py
@@ -86,7 +86,7 @@ class XmlStorableObject(StorableObject):
                 include_id=include_id)
 
     @classmethod
-    def import_from_xml_tree(cls, tree, include_id=False, charset=None):
+    def import_from_xml_tree(cls, tree, include_id=False, charset=None, snapshot=False):
         if charset is None:
             charset = get_publisher().site_charset
         obj = cls()
diff --git a/wcs/snapshots.py b/wcs/snapshots.py
index edb776076..4534a51b8 100644
--- a/wcs/snapshots.py
+++ b/wcs/snapshots.py
@@ -79,7 +79,10 @@ class Snapshot:
     def instance(self):
         if self._instance is None:
             tree = ET.fromstring(self.serialization)
-            self._instance = self.get_object_class().import_from_xml_tree(tree, include_id=True)
+            self._instance = self.get_object_class().import_from_xml_tree(
+                    tree,
+                    include_id=True,
+                    snapshot=True)
             self._instance.readonly = True
         return self._instance
 
diff --git a/wcs/wf/attachment.py b/wcs/wf/attachment.py
index a611ce02b..c470fed43 100644
--- a/wcs/wf/attachment.py
+++ b/wcs/wf/attachment.py
@@ -224,7 +224,7 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem):
             ET.SubElement(node, 'mimetype').text = mimetype
         return node
 
-    def document_type_init_with_xml(self, node, charset, include_id=False):
+    def document_type_init_with_xml(self, node, charset, include_id=False, snapshot=False):
         self.document_type = {}
         if node is None:
             return
diff --git a/wcs/wf/backoffice_fields.py b/wcs/wf/backoffice_fields.py
index 1b314daae..82215d390 100644
--- a/wcs/wf/backoffice_fields.py
+++ b/wcs/wf/backoffice_fields.py
@@ -180,14 +180,14 @@ class SetBackofficeFieldsWorkflowStatusItem(WorkflowStatusItem):
 
         return fields_node
 
-    def fields_init_with_xml(self, elem, charset, include_id=False):
+    def fields_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         fields = []
         if elem is None:
             return
         for field_xml_node in elem.findall('field'):
             field_node = FieldNode()
             field_node.init_with_xml(field_xml_node, charset,
-                    include_id=include_id)
+                    include_id=include_id, snapshot=snapshot)
             fields.append(field_node.as_dict())
         if fields:
             self.fields = fields
diff --git a/wcs/wf/create_formdata.py b/wcs/wf/create_formdata.py
index 9fa0f3f45..8317104cb 100644
--- a/wcs/wf/create_formdata.py
+++ b/wcs/wf/create_formdata.py
@@ -465,7 +465,7 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
             item.attrib['field_id'] = str(mapping.field_id)
             item.text = mapping.expression
 
-    def mappings_init_with_xml(self, container, charset, include_id=False):
+    def mappings_init_with_xml(self, container, charset, include_id=False, snapshot=False):
         self.mappings = []
         for child in container:
             field_id = child.attrib.get('field_id', '')
diff --git a/wcs/wf/dispatch.py b/wcs/wf/dispatch.py
index f76dfc74d..c7cb9c1e9 100644
--- a/wcs/wf/dispatch.py
+++ b/wcs/wf/dispatch.py
@@ -73,9 +73,9 @@ class RuleNode(XmlSerialisable):
         self._role_export_to_xml('role_id', item, charset,
                 include_id=include_id)
 
-    def role_id_init_with_xml(self, elem, charset, include_id=False):
+    def role_id_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self._role_init_with_xml('role_id', elem, charset,
-                include_id=include_id)
+                include_id=include_id, snapshot=snapshot)
 
 
 class DispatchWorkflowStatusItem(WorkflowStatusItem):
@@ -97,9 +97,9 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem):
         self._role_export_to_xml('role_id', item, charset,
                 include_id=include_id)
 
-    def role_id_init_with_xml(self, elem, charset, include_id=False):
+    def role_id_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self._role_init_with_xml('role_id', elem, charset,
-                include_id=include_id)
+                include_id=include_id, snapshot=snapshot)
 
     def rules_export_to_xml(self, item, charset, include_id=False):
         if self.dispatch_type != 'automatic' or not self.rules:
@@ -112,14 +112,14 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem):
 
         return rules_node
 
-    def rules_init_with_xml(self, elem, charset, include_id=False):
+    def rules_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         rules = []
         if elem is None:
             return
         for rule_xml_node in elem.findall('rule'):
             rule_node = RuleNode()
             rule_node.init_with_xml(rule_xml_node, charset,
-                    include_id=include_id)
+                    include_id=include_id, snapshot=snapshot)
             rules.append(rule_node.as_dict())
         if rules:
             self.rules = rules
diff --git a/wcs/wf/export_to_model.py b/wcs/wf/export_to_model.py
index c9c3538e0..39b85453d 100644
--- a/wcs/wf/export_to_model.py
+++ b/wcs/wf/export_to_model.py
@@ -606,7 +606,7 @@ class ExportToModel(WorkflowStatusItem):
         ET.SubElement(el, 'b64_content').text = force_text(base64.encodebytes(
                 self.model_file.get_file().read()))
 
-    def model_file_init_with_xml(self, elem, charset, include_id=False):
+    def model_file_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         if elem is None:
             return
         base_filename = elem.find('base_filename').text
@@ -616,8 +616,12 @@ class ExportToModel(WorkflowStatusItem):
         if elem.find('content') is not None:
             content = elem.find('content').text
 
-        if self.parent.parent.id:
+        if self.parent.parent.id and not snapshot:
             ids = (self.parent.parent.id, self.parent.id, self.id)
+        elif snapshot:
+            # use snapshot prefix so they can eventually be cleaned
+            # automatically
+            ids = ('snapshot%i' % random.randint(0, 1000000), self.parent.id, self.id)
         else:
             # hopefully this will be random enough.
             ids = ('i%i' % random.randint(0, 1000000), self.parent.id, self.id)
diff --git a/wcs/wf/form.py b/wcs/wf/form.py
index 9ff7519d8..6548acd30 100644
--- a/wcs/wf/form.py
+++ b/wcs/wf/form.py
@@ -139,14 +139,14 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
             fields.append(field.export_to_xml(charset=charset, include_id=include_id))
         return item
 
-    def init_with_xml(self, elem, charset, include_id=False):
+    def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         WorkflowStatusItem.init_with_xml(self, elem, charset)
         el = elem.find('formdef')
         if el is None:
             return
         # we can always include id in the formdef export as it lives in
         # a different space, isolated from other formdefs.
-        imported_formdef = FormDef.import_from_xml_tree(el, include_id=True)
+        imported_formdef = FormDef.import_from_xml_tree(el, include_id=True, snapshot=snapshot)
         self.formdef = WorkflowFormFieldsFormDef(item=self)
         self.formdef.fields = imported_formdef.fields
         if self.formdef.max_field_id is None and self.formdef.fields:
diff --git a/wcs/wf/jump.py b/wcs/wf/jump.py
index bd403667d..9ad98ec93 100644
--- a/wcs/wf/jump.py
+++ b/wcs/wf/jump.py
@@ -119,7 +119,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
     directory_name = 'jump'
     directory_class = JumpDirectory
 
-    def timeout_init_with_xml(self, elem, charset, include_id=False):
+    def timeout_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         if elem is None or elem.text is None:
             self.timeout = None
         else:
diff --git a/wcs/wf/profile.py b/wcs/wf/profile.py
index 63622dc81..73b1d01bd 100644
--- a/wcs/wf/profile.py
+++ b/wcs/wf/profile.py
@@ -132,14 +132,14 @@ class UpdateUserProfileStatusItem(WorkflowStatusItem):
 
         return fields_node
 
-    def fields_init_with_xml(self, elem, charset, include_id=False):
+    def fields_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         fields = []
         if elem is None:
             return
         for field_xml_node in elem.findall('field'):
             field_node = FieldNode()
             field_node.init_with_xml(field_xml_node, charset,
-                    include_id=include_id)
+                    include_id=include_id, snapshot=snapshot)
             fields.append(field_node.as_dict())
         if fields:
             self.fields = fields
diff --git a/wcs/wf/roles.py b/wcs/wf/roles.py
index 76fd1a18e..55e993dba 100644
--- a/wcs/wf/roles.py
+++ b/wcs/wf/roles.py
@@ -68,9 +68,9 @@ class AddRoleWorkflowStatusItem(WorkflowStatusItem):
         self._role_export_to_xml('role_id', item, charset,
                 include_id=include_id)
 
-    def role_id_init_with_xml(self, elem, charset, include_id=False):
+    def role_id_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self._role_init_with_xml('role_id', elem, charset,
-                include_id=include_id)
+                include_id=include_id, snapshot=snapshot)
 
     def perform(self, formdata):
         if not self.role_id:
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index 7c81fa976..58a9681ea 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -497,13 +497,13 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
         self._kv_data_export_to_xml(xml_item, charset, include_id=include_id,
                                     attribute='post_data')
 
-    def post_data_init_with_xml(self, elem, charset, include_id=False):
+    def post_data_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self._kv_data_init_with_xml(elem, charset, include_id=include_id, attribute='post_data')
 
     def qs_data_export_to_xml(self, xml_item, charset, include_id=False):
         self._kv_data_export_to_xml(xml_item, charset, include_id=include_id, attribute='qs_data')
 
-    def qs_data_init_with_xml(self, elem, charset, include_id=False):
+    def qs_data_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self._kv_data_init_with_xml(elem, charset, include_id=include_id, attribute='qs_data')
 
 register_item_class(WebserviceCallStatusItem)
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 4d4fc6e13..ba401ddb3 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -607,7 +607,7 @@ class Workflow(StorableObject):
         return cls.import_from_xml_tree(tree, include_id=include_id)
 
     @classmethod
-    def import_from_xml_tree(cls, tree, include_id=False):
+    def import_from_xml_tree(cls, tree, include_id=False, snapshot=False):
         charset = get_publisher().site_charset
         workflow = cls()
         if tree.find('name') is None or not tree.find('name').text:
@@ -641,7 +641,7 @@ class Workflow(StorableObject):
         for status in tree.find('possible_status'):
             status_o = WorkflowStatus()
             status_o.parent = workflow
-            status_o.init_with_xml(status, charset, include_id=include_id)
+            status_o.init_with_xml(status, charset, include_id=include_id, snapshot=snapshot)
             workflow.possible_status.append(status_o)
 
         workflow.global_actions = []
@@ -650,7 +650,7 @@ class Workflow(StorableObject):
             for action in global_actions:
                 action_o = WorkflowGlobalAction()
                 action_o.parent = workflow
-                action_o.init_with_xml(action, charset, include_id=include_id)
+                action_o.init_with_xml(action, charset, include_id=include_id, snapshot=snapshot)
                 workflow.global_actions.append(action_o)
 
         workflow.criticality_levels = []
@@ -664,14 +664,14 @@ class Workflow(StorableObject):
         variables = tree.find('variables')
         if variables is not None:
             formdef = variables.find('formdef')
-            imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True)
+            imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True, snapshot=snapshot)
             workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
             workflow.variables_formdef.fields = imported_formdef.fields
 
         variables = tree.find('backoffice-fields')
         if variables is not None:
             formdef = variables.find('formdef')
-            imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True)
+            imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True, snapshot=snapshot)
             workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow=workflow)
             workflow.backoffice_fields_formdef.fields = imported_formdef.fields
 
@@ -905,14 +905,14 @@ class XmlSerialisable(object):
                     el.text = str(val)
         return node
 
-    def init_with_xml(self, elem, charset, include_id=False):
+    def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         if include_id and elem.attrib.get('id'):
             self.id = elem.attrib.get('id')
         for attribute in self.get_parameters():
             el = elem.find(attribute)
             if getattr(self, '%s_init_with_xml' % attribute, None):
                 getattr(self, '%s_init_with_xml' % attribute)(el, charset,
-                        include_id=include_id)
+                        include_id=include_id, snapshot=snapshot)
                 continue
             if el is None:
                 continue
@@ -957,7 +957,7 @@ class XmlSerialisable(object):
             sub.attrib['role_id'] = role_id
             sub.text = role
 
-    def _roles_init_with_xml(self, attribute, elem, charset, include_id=False):
+    def _roles_init_with_xml(self, attribute, elem, charset, include_id=False, snapshot=False):
         if elem is None:
             setattr(self, attribute, [])
         else:
@@ -1021,7 +1021,7 @@ class XmlSerialisable(object):
         role.store()
         return role.id
 
-    def _role_init_with_xml(self, attribute, elem, charset, include_id=False):
+    def _role_init_with_xml(self, attribute, elem, charset, include_id=False, snapshot=False):
         setattr(self, attribute, self._get_role_id_from_xml(elem, charset,
             include_id=include_id))
 
@@ -1071,8 +1071,8 @@ class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
     def roles_export_to_xml(self, item, charset, include_id=False):
         self._roles_export_to_xml('roles', item, charset, include_id=include_id)
 
-    def roles_init_with_xml(self, elem, charset, include_id=False):
-        self._roles_init_with_xml('roles', elem, charset, include_id=include_id)
+    def roles_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
+        self._roles_init_with_xml('roles', elem, charset, include_id=include_id, snapshot=snapshot)
 
 
 class WorkflowGlobalActionTimeoutTriggerMarker(object):
@@ -1413,7 +1413,7 @@ class WorkflowGlobalAction(object):
 
         return status
 
-    def init_with_xml(self, elem, charset, include_id=False):
+    def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self.id = xml_node_text(elem.find('id'))
         self.name = xml_node_text(elem.find('name'))
         if elem.find('backoffice_info_text') is not None:
@@ -1425,7 +1425,7 @@ class WorkflowGlobalAction(object):
             self.append_item(item_type)
             item_o = self.items[-1]
             item_o.parent = self
-            item_o.init_with_xml(item, charset, include_id=include_id)
+            item_o.init_with_xml(item, charset, include_id=include_id, snapshot=snapshot)
 
         self.triggers = []
         for trigger in elem.find('triggers'):
@@ -1433,7 +1433,7 @@ class WorkflowGlobalAction(object):
             self.append_trigger(trigger_type)
             trigger_o = self.triggers[-1]
             trigger_o.parent = self
-            trigger_o.init_with_xml(trigger, charset, include_id=include_id)
+            trigger_o.init_with_xml(trigger, charset, include_id=include_id, snapshot=snapshot)
 
 
 class WorkflowCriticalityLevel(object):
@@ -1454,7 +1454,7 @@ class WorkflowCriticalityLevel(object):
             ET.SubElement(level, 'colour').text = force_text(self.colour, charset)
         return level
 
-    def init_with_xml(self, elem, charset, include_id=False):
+    def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self.id = xml_node_text(elem.find('id'))
         self.name = xml_node_text(elem.find('name'))
         if elem.find('colour') is not None:
@@ -1732,7 +1732,7 @@ class WorkflowStatus(object):
                 include_id=include_id))
         return status
 
-    def init_with_xml(self, elem, charset, include_id=False):
+    def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         self.id = xml_node_text(elem.find('id'))
         self.name = xml_node_text(elem.find('name'))
         if elem.find('colour') is not None:
@@ -1754,7 +1754,7 @@ class WorkflowStatus(object):
             self.append_item(item_type)
             item_o = self.items[-1]
             item_o.parent = self
-            item_o.init_with_xml(item, charset, include_id=include_id)
+            item_o.init_with_xml(item, charset, include_id=include_id, snapshot=snapshot)
 
     def __repr__(self):
         return '<%s %s %r>' % (self.__class__.__name__, self.id, self.name)
@@ -2086,16 +2086,16 @@ class WorkflowStatusItem(XmlSerialisable):
     def by_export_to_xml(self, item, charset, include_id=False):
         self._roles_export_to_xml('by', item, charset, include_id=include_id)
 
-    def by_init_with_xml(self, elem, charset, include_id=False):
-        self._roles_init_with_xml('by', elem, charset, include_id=include_id)
+    def by_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
+        self._roles_init_with_xml('by', elem, charset, include_id=include_id, snapshot=snapshot)
 
     def to_export_to_xml(self, item, charset, include_id=False):
         self._roles_export_to_xml('to', item, charset, include_id=include_id)
 
-    def to_init_with_xml(self, elem, charset, include_id=False):
-        self._roles_init_with_xml('to', elem, charset, include_id)
+    def to_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
+        self._roles_init_with_xml('to', elem, charset, include_id, snapshot=snapshot)
 
-    def condition_init_with_xml(self, node, charset, include_id=False):
+    def condition_init_with_xml(self, node, charset, include_id=False, snapshot=False):
         self.condition = None
         if node is None:
             return
@@ -2117,7 +2117,7 @@ class WorkflowStatusItem(XmlSerialisable):
             del odict['parent']
         return odict
 
-    def mail_template_init_with_xml(self, elem, charset, include_id=False):
+    def mail_template_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         if elem is None:
             self.mail_template = None
             return
@@ -2128,7 +2128,7 @@ class WorkflowStatusItem(XmlSerialisable):
         self.mail_template = value
         return
 
-    def attachments_init_with_xml(self, elem, charset, include_id=False):
+    def attachments_init_with_xml(self, elem, charset, include_id=False, snapshot=False):
         if elem is None:
             self.attachments = None
         else:
@@ -2396,7 +2396,7 @@ class CommentableWorkflowStatusItem(WorkflowStatusItem):
             el = ET.SubElement(xml_item, 'button_label')
             el.text = self.button_label
 
-    def button_label_init_with_xml(self, element, charset, include_id=False):
+    def button_label_init_with_xml(self, element, charset, include_id=False, snapshot=False):
         if element is None:
             return
         # this can be None if element is self-closing, <button_label />, which
-- 
2.28.0

