From 3831954233dee6c7ae277070ac25a5076fdccc5a Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 26 Nov 2020 15:35:19 +0100 Subject: [PATCH 2/2] misc: remap statuses in a transaction (#38579) --- tests/admin_pages/test_form.py | 7 ++++++ wcs/admin/forms.py | 39 +++++++++++------------------ wcs/formdef.py | 45 +++++++++++++++++++++++++++++++++- wcs/sql.py | 22 +++++++++++++++++ 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py index 1eb3f778..e2adb8a4 100644 --- a/tests/admin_pages/test_form.py +++ b/tests/admin_pages/test_form.py @@ -513,6 +513,10 @@ def test_form_workflow_remapping(pub): formdata2.status = 'draft' formdata2.store() + formdata3 = data_class() + formdata3.status = 'wf-1' + formdata3.store() + Workflow.wipe() workflow = Workflow(name='Workflow One') workflow.store() @@ -534,9 +538,11 @@ def test_form_workflow_remapping(pub): assert len(resp.forms[0]['mapping-%s' % status.id].options) == 1 assert data_class.get(formdata1.id).status == 'wf-new' assert data_class.get(formdata2.id).status == 'draft' + assert data_class.get(formdata3.id).status == 'wf-1' resp = resp.forms[0].submit() assert data_class.get(formdata1.id).status == 'wf-finished' assert data_class.get(formdata2.id).status == 'draft' + assert data_class.get(formdata3.id).status == 'wf-1-invalid-_default' # change to another workflow, with no mapping change workflow2 = workflow @@ -557,6 +563,7 @@ def test_form_workflow_remapping(pub): resp = resp.forms[0].submit() assert data_class.get(formdata1.id).status == 'wf-finished' assert data_class.get(formdata2.id).status == 'draft' + assert data_class.get(formdata3.id).status == 'wf-1-invalid-_default' def test_form_submitter_roles(pub): diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py index bc88ba0d..8b6050ba 100644 --- a/wcs/admin/forms.py +++ b/wcs/admin/forms.py @@ -864,8 +864,7 @@ class FormDefPage(Directory): if workflow_id is None: workflow_id = self.formdef_default_workflow return redirect('workflow-status-remapping?new=%s' % workflow_id) - self.formdef.workflow_id = workflow_id - self.formdef.store(comment=_('Workflow change')) + self.formdef.change_workflow(workflow_id) return redirect('.') def workflow_status_remapping(self): @@ -898,32 +897,22 @@ class FormDefPage(Directory): r += form.render() return r.getvalue() else: - get_logger().info('admin - form "%s", workflow is now "%s" (was "%s")' % ( - self.formdef.name, new_workflow.name, self.formdef.workflow.name)) - self.workflow_status_remapping_submit(form) - if new_workflow.id == self.formdef_default_workflow: - self.formdef.workflow_id = None - else: - self.formdef.workflow_id = new_workflow.id - self.formdef.store(comment=_('Workflow change')) - # instruct formdef to update its security rules - self.formdef.data_class().rebuild_security() - return redirect('.') + return self.workflow_status_remapping_submit(form, new_workflow) + + def workflow_status_remapping_submit(self, form, new_workflow): + get_logger().info('admin - form "%s", workflow is now "%s" (was "%s")' % ( + self.formdef.name, new_workflow.name, self.formdef.workflow.name)) - def workflow_status_remapping_submit(self, form): status_mapping = {} for status in self.formdef.workflow.possible_status: - status_mapping['wf-%s' % status.id] = 'wf-%s' % \ - form.get_widget('mapping-%s' % status.id).parse() - if any([x[0] != x[1] for x in status_mapping.items()]): - # if there are status changes, update all formdatas (except drafts) - status_mapping.update({'draft': 'draft'}) - for item in self.formdef.data_class().select([NotEqual('status', 'draft')]): - item.status = status_mapping.get(item.status) - if item.evolution: - for evo in item.evolution: - evo.status = status_mapping.get(evo.status) - item.store() + status_mapping[status.id] = form.get_widget('mapping-%s' % status.id).parse() + + if new_workflow.id == self.formdef_default_workflow: + new_workflow_id = None + else: + new_workflow_id = new_workflow.id + self.formdef.change_workflow(new_workflow_id, status_mapping) + return redirect('.') def get_preview(self): form = Form(action='#', use_tokens=False) diff --git a/wcs/formdef.py b/wcs/formdef.py index fdd305a7..28fae1b3 100644 --- a/wcs/formdef.py +++ b/wcs/formdef.py @@ -33,7 +33,7 @@ from quixote import get_request, get_publisher from quixote.http_request import Upload from .qommon import _, N_, force_str, PICKLE_KWARGS -from .qommon.storage import StorableObject, fix_key, Equal +from .qommon.storage import StorableObject, fix_key, Equal, NotEqual from .qommon.cron import CronJob from .qommon.form import * from .qommon.misc import simplify, get_as_datetime, xml_node_text @@ -1517,6 +1517,49 @@ class FormDef(StorableObject): # chunk contains the fields. return pickle.dumps(object, protocol=2) + pickle.dumps(object.fields, protocol=2) + def change_workflow(self, new_workflow_id, status_mapping=None): + old_workflow = self.get_workflow() + + formdata_count = self.data_class().count() + if formdata_count: + assert status_mapping, 'status mapping is required if there are formdatas' + assert all(status.id in status_mapping for status in old_workflow.possible_status), ( + 'a status was not mapped' + ) + + mapping = {} + for old_status, new_status in status_mapping.items(): + mapping['wf-%s' % old_status] = 'wf-%s' % new_status + mapping['draft'] = 'draft' + + if any([x[0] != x[1] for x in mapping.items()]): + # if there are status changes, update all formdatas (except drafts) + if get_publisher().is_using_postgresql(): + from . import sql + sql.formdef_remap_statuses(self, mapping) + else: + def map_status(status): + if status is None: + return None + elif status in mapping: + return mapping[status] + elif '-invalid-' in status: + return status + else: + return '%s-invalid-%s' % (status, old_workflow.id) + for formdata in self.data_class().select([NotEqual('status', 'draft')]): + formdata.status = map_status(formdata.status) + if formdata.evolution: + for evo in formdata.evolution: + evo.status = map_status(evo.status) + formdata.store() + + self.workflow_id = new_workflow_id + self.store(comment=_('Workflow change')) + if formdata_count: + # instruct formdef to update its security rules + self.data_class().rebuild_security() + from .qommon.admin.emails import EmailsDirectory diff --git a/wcs/sql.py b/wcs/sql.py index 57f37fda..5d05d5fb 100644 --- a/wcs/sql.py +++ b/wcs/sql.py @@ -2874,3 +2874,25 @@ def reindex(): conn.commit() cur.close() + + +@guard_postgres +def formdef_remap_statuses(formdef, mapping): + conn, cur = get_connection_and_cursor() + table_name = get_formdef_table_name(formdef) + evolutions_table_name = table_name + '_evolutions' + old_workflow_id = formdef.workflow_id or '_default' + args = [] + case_expression = '(CASE ' + case_expression += 'WHEN status IS NULL THEN NULL ' + for old_id, new_id in mapping.items(): + case_expression += "WHEN status = %s THEN %s " + args.extend([old_id, new_id]) + case_expression += " WHEN status LIKE '%%-invalid-%%' THEN status " + case_expression += " ELSE (status || '-invalid-' || %s) END)" + args.append(old_workflow_id) + + cur.execute('BEGIN') + cur.execute('UPDATE %s SET status = %s WHERE status <> \'draft\'' % (table_name, case_expression), args) + cur.execute('UPDATE %s SET status = %s' % (evolutions_table_name, case_expression), args) + cur.execute('COMMIT') -- 2.29.2