From b45bfc40dfb709caadb16d7f9717cc1f036cf740 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 26 Nov 2020 15:35:19 +0100 Subject: [PATCH] misc: remap statuses in a transaction (#38579) --- tests/admin_pages/test_form.py | 7 +++++ wcs/admin/forms.py | 42 ++++++++------------------- wcs/formdef.py | 52 ++++++++++++++++++++++++++++++++-- wcs/sql.py | 45 +++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 32 deletions(-) diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py index 264fadce..c0f390a1 100644 --- a/tests/admin_pages/test_form.py +++ b/tests/admin_pages/test_form.py @@ -602,6 +602,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() @@ -627,6 +631,7 @@ 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() # run a SQL SELECT and we known all columns are defined. @@ -634,6 +639,7 @@ def test_form_workflow_remapping(pub): 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 @@ -658,6 +664,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' # run a SQL SELECT and we known all columns are defined. FormDef.get(formdef.id).data_class().select() diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py index 59aa4ba2..a5a44c58 100644 --- a/wcs/admin/forms.py +++ b/wcs/admin/forms.py @@ -1100,14 +1100,11 @@ class FormDefPage(Directory): r += form.render() return r.getvalue() else: - workflow_id = form.get_widget('workflow_id').parse() - if self.formdef.data_class().keys(): + workflow_id = form.get_widget('workflow_id').parse() or self.formdef_default_workflow + if self.formdef.data_class().count(): # there are existing formdata, status will have to be mapped - if workflow_id is None: - workflow_id = self.formdef_default_workflow return redirect('workflow-status-remapping?new=%s' % workflow_id) - self.formdef.workflow = Workflow.get(workflow_id) if workflow_id else None - self.formdef.store(comment=_('Workflow change')) + self.formdef.change_workflow(Workflow.get(workflow_id)) return redirect('.') def workflow_status_remapping(self): @@ -1146,33 +1143,18 @@ 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 = None - else: - self.formdef.workflow = Workflow.get(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): + 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) + ) 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() + self.formdef.change_workflow(new_workflow, 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 8f2b6679..e644b3e5 100644 --- a/wcs/formdef.py +++ b/wcs/formdef.py @@ -41,7 +41,7 @@ from .qommon.cron import CronJob from .qommon.form import Form, HtmlWidget, UploadedFile from .qommon.misc import JSONEncoder, get_as_datetime, simplify, xml_node_text from .qommon.publisher import get_publisher_class -from .qommon.storage import Equal, StorableObject, fix_key +from .qommon.storage import Equal, NotEqual, StorableObject, fix_key from .qommon.substitution import Substitutions from .qommon.template import Template from .roles import logged_users_role @@ -505,11 +505,12 @@ class FormDef(StorableObject): return workflow def set_workflow(self, workflow): - if workflow: + if workflow and workflow.id not in ['_carddef_default', '_default']: self.workflow_id = workflow.id self._workflow = workflow elif self.workflow_id: self.workflow_id = None + self._workflow = None workflow = property(get_workflow, set_workflow) @@ -1666,6 +1667,53 @@ 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, 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' + + unmapped_status_suffix = '-invalid-%s' % str(self.workflow_id or 'default') + 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%s' % (status, unmapped_status_suffix) + + 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 = new_workflow + self.store(comment=_('Workflow change')) + if formdata_count: + # instruct formdef to update its security rules + self.data_class().rebuild_security() + EmailsDirectory.register( 'new_user', diff --git a/wcs/sql.py b/wcs/sql.py index 751c0426..f680c440 100644 --- a/wcs/sql.py +++ b/wcs/sql.py @@ -25,6 +25,7 @@ import uuid import psycopg2 import psycopg2.extensions import psycopg2.extras +from psycopg2.sql import SQL, Identifier, Literal import unidecode try: @@ -3669,3 +3670,47 @@ def reindex(): conn.commit() cur.close() + + +@guard_postgres +def formdef_remap_statuses(formdef, mapping): + table_name = get_formdef_table_name(formdef) + evolutions_table_name = table_name + '_evolutions' + unmapped_status_suffix = str(formdef.workflow_id or 'default') + + # build the case expression + status_cases = [] + for old_id, new_id in mapping.items(): + status_cases.append( + SQL('WHEN status = {old_status} THEN {new_status}').format( + old_status=Literal(old_id), new_status=Literal(new_id) + ) + ) + case_expression = SQL( + '(CASE WHEN status IS NULL THEN NULL ' + '{status_cases} ' + # keep status alread marked as invalid + 'WHEN status LIKE {pattern} THEN status ' + # mark unknown statuses as invalid + 'ELSE (status || {suffix}) END)' + ).format( + status_cases=SQL('').join(status_cases), + pattern=Literal('%-invalid-%'), + suffix=Literal('-invalid-' + unmapped_status_suffix), + ) + + conn, cur = get_connection_and_cursor() + # update formdatas statuses + cur.execute( + SQL('UPDATE {table_name} SET status = {case_expression} WHERE status <> {draft_status}').format( + table_name=Identifier(table_name), case_expression=case_expression, draft_status=Literal('draft') + ) + ) + # update evolutions statuses + cur.execute( + SQL('UPDATE {table_name} SET status = {case_expression}').format( + table_name=Identifier(evolutions_table_name), case_expression=case_expression + ) + ) + conn.commit() + cur.close() -- 2.33.0