workflows: don't restore document model files when browsing snapshots (#...)

This commit is contained in:
Frédéric Péters 2020-10-19 16:17:36 +02:00
parent 19d17c7c03
commit 4dd7bc4aaa
Notes: Frédéric Péters 2020-10-20 13:36:39 +02:00
actually: don't overwrite document models when browsing snapshots (#47310)
17 changed files with 117 additions and 59 deletions

View File

@ -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()

View File

@ -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:

View File

@ -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 = []

View File

@ -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'

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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', '')

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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