From dd812551dbee566b30a7523fda858c10e1bba51f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 2 Nov 2020 14:55:45 +0100
Subject: [PATCH] misc: keep track of first formdata when running a mass action
 (#47878)

---
 tests/backoffice_pages/test_all.py | 56 ++++++++++++++++++++++++++++++
 wcs/backoffice/management.py       | 14 +++++++-
 wcs/formdata.py                    |  7 ++--
 wcs/qommon/substitution.py         |  3 ++
 4 files changed, 77 insertions(+), 3 deletions(-)

diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py
index 0657c4f1e..471bc2429 100644
--- a/tests/backoffice_pages/test_all.py
+++ b/tests/backoffice_pages/test_all.py
@@ -1846,6 +1846,62 @@ def test_backoffice_multi_actions_jump(pub):
         assert formdef.data_class().get(id).status == 'wf-accepted'
 
 
+def test_backoffice_multi_actions_first_form(pub):
+    create_superuser(pub)
+    create_environment(pub)
+    formdef = FormDef.get_by_urlname('form-title')
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'id="multi-actions"' in resp.text  # always there
+
+    workflow = Workflow.get_default_workflow()
+    workflow.id = '2'
+    action = workflow.add_global_action('Mark as duplicates')
+    jump = action.append_item('jump')
+    jump.condition = {'type': 'django', 'value': "mass_action_index != 0"}
+    jump.status = 'rejected'
+
+    jump2 = action.append_item('jump')
+    jump2.condition = {'type': 'django', 'value': "mass_action_index == 0"}
+    jump2.status = 'accepted'
+
+    register_comment = RegisterCommenterWorkflowStatusItem()
+    register_comment.id = '_comment'
+    register_comment.comment = '<p>Original form: {{ first_form_number }}.</p>'
+    assert workflow.possible_status[2].id == 'rejected'
+    workflow.possible_status[2].items.append(register_comment)
+    register_comment.parent = workflow.possible_status[2]
+
+    trigger = action.triggers[0]
+    trigger.roles = [x.id for x in Role.select() if x.name == 'foobar']
+
+    workflow.store()
+    formdef.workflow_id = workflow.id
+    formdef.store()
+
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'id="multi-actions"' in resp.text
+    ids = []
+    for checkbox in resp.forms[0].fields['select[]'][1:6]:
+        ids.append(checkbox._value)
+        checkbox.checked = True
+    resp = resp.forms[0].submit('button-action-1')
+    assert '?job=' in resp.location
+    resp = resp.follow()
+    assert 'Executing task &quot;Mark as duplicates&quot; on forms' in resp.text
+    assert '>completed<' in resp.text
+    first_formdata = None
+    for i, id in enumerate(sorted(ids, key=lambda x: int(x))):
+        if i == 0:
+            first_formdata = formdef.data_class().get(id)
+            assert formdef.data_class().get(id).status == 'wf-accepted'
+        else:
+            assert formdef.data_class().get(id).status == 'wf-rejected'
+            assert (formdef.data_class().get(id).evolution[-1].parts[0].content ==
+                    '<p>Original form: %s.</p>' % first_formdata.get_display_id())
+
+
 def test_backoffice_statistics_with_no_formdefs(pub):
     create_user(pub)
     create_environment(pub)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 8dbb2726c..97a6e8717 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1949,15 +1949,27 @@ class FormPage(Directory):
                 self.user = get_request().user
 
             def execute(self, job=None):
-                formdatas = self.formdef.data_class().get_ids(self.item_ids)
+                # sort formdata IDs so we know for sure the processing order
+                # (they are also converted as integers as it's required for
+                # keep_order when using postgresql).
+                item_ids = sorted([int(x) for x in self.item_ids])
+                formdatas = self.formdef.data_class().get_ids(item_ids, keep_order=True)
                 job.completion_status = '{}/{}'.format(0, len(formdatas))
                 job.store()
                 publisher = get_publisher()
+                # conserver initial_formdata = formdatas[0]
+                if formdatas:
+                    first_lazy_form = formdatas[0].get_as_lazy()
                 for i, formdata in enumerate(formdatas):
                     publisher.substitutions.reset()
                     publisher.substitutions.feed(publisher)
                     publisher.substitutions.feed(self.formdef)
                     publisher.substitutions.feed(formdata)
+                    publisher.substitutions.feed({
+                        'first_form': first_lazy_form,
+                        'mass_action_index': i,
+                        'mass_action_length': len(formdatas),
+                    })
                     if getattr(self.action['action'], 'status_action', False):
                         # manual jump action
                         from wcs.wf.jump import jump_and_perform
diff --git a/wcs/formdata.py b/wcs/formdata.py
index afa2b8661..226f927ae 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -840,12 +840,15 @@ class FormData(StorableObject):
 
         return d
 
+    def get_as_lazy(self):
+        from wcs.variables import LazyFormData
+        return LazyFormData(self)
+
     def get_substitution_variables(self, minimal=False):
         from .qommon.substitution import CompatibilityNamesDict
-        from wcs.variables import LazyFormData
         from wcs.workflows import AttachmentsSubstitutionProxy
         variables = CompatibilityNamesDict({
-            'form': LazyFormData(self),
+            'form': self.get_as_lazy(),
             'attachments': AttachmentsSubstitutionProxy(self),
         })
         if self.formdef.category:
diff --git a/wcs/qommon/substitution.py b/wcs/qommon/substitution.py
index 3ee852237..5a1d9eec4 100644
--- a/wcs/qommon/substitution.py
+++ b/wcs/qommon/substitution.py
@@ -119,6 +119,9 @@ class Substitutions(object):
             return d
         d = CompatibilityNamesDict()
         for source in self.sources:
+            if isinstance(source, dict):
+                d.update(source)
+                continue
             d.update(source.get_substitution_variables())
             if not lazy and hasattr(source, 'get_static_substitution_variables'):
                 d.update(source.get_static_substitution_variables())
-- 
2.29.1

