misc: remap statuses in a transaction (#38579)

This commit is contained in:
Benjamin Dauvergne 2020-11-26 15:35:19 +01:00 committed by Frédéric Péters
parent 8f8d4bf481
commit dfbe3123ef
4 changed files with 114 additions and 32 deletions

View File

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

View File

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

View File

@ -42,7 +42,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
@ -506,11 +506,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)
@ -1667,6 +1668,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',

View File

@ -25,6 +25,7 @@ import uuid
import psycopg2
import psycopg2.extensions
import psycopg2.extras
from psycopg2.sql import SQL, Identifier, Literal
import unidecode
try:
@ -3672,3 +3673,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()