From 39ad065722fceb886484fa6a2662cd916bdf29e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 7 Feb 2022 14:57:36 +0100
Subject: [PATCH 2/3] sql: add workflow_merged_roles_dict jsonb column (#58881)

---
 tests/test_sql.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++
 wcs/formdata.py   | 17 ++++++++++++++
 wcs/sql.py        | 32 ++++++++++++++++++++-----
 3 files changed, 103 insertions(+), 6 deletions(-)

diff --git a/tests/test_sql.py b/tests/test_sql.py
index 6e1915e21..e281ec9d5 100644
--- a/tests/test_sql.py
+++ b/tests/test_sql.py
@@ -2073,6 +2073,66 @@ def test_migration_40_user_is_active(pub):
     assert sql.SqlUser.get(id=user2.id).is_active is True
 
 
+def test_migration_58_workflow_roles_dict(pub):
+    formdef = FormDef()
+    formdef.name = 'tests migration 58'
+    formdef.fields = []
+    formdef.workflow_roles = {'_receiver': '123'}
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.store()
+
+    formdata = formdef.data_class()()
+    formdata.workflow_roles = {'_receiver': ['_user:123', '_user:456']}
+    formdata.store()
+
+    conn, cur = sql.get_connection_and_cursor()
+    cur.execute('UPDATE wcs_meta SET value = 57 WHERE key = %s', ('sql_level',))
+    conn.commit()
+    cur.close()
+
+    conn, cur = sql.get_connection_and_cursor()
+    cur.execute('ALTER TABLE %s DROP COLUMN workflow_merged_roles_dict' % formdef.table_name)
+    sql.migrate()
+    assert column_exists_in_table(cur, formdef.table_name, 'workflow_merged_roles_dict')
+    assert migration_level(cur) >= 58
+    assert sql.is_reindex_needed('formdata', conn=conn, cur=cur) is True
+    assert formdef.data_class().count([st.Null('workflow_merged_roles_dict')]) == 2
+    sql.reindex()
+    assert formdef.data_class().count([st.Null('workflow_merged_roles_dict')]) == 0
+
+
+def test_workflow_roles_dict_change(pub):
+    formdef = FormDef()
+    formdef.name = 'test_workflow_roles_dict_change'
+    formdef.fields = []
+    formdef.workflow_roles = {'_receiver': '123'}
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.workflow_roles = {'_receiver': ['_user:123', '_user:456']}
+    formdata.store()
+
+    conn, cur = sql.get_connection_and_cursor()
+    cur.execute('SELECT workflow_merged_roles_dict FROM %s WHERE id = %s' % (formdef.table_name, formdata.id))
+    merged_roles_dict = cur.fetchone()[0]
+    assert merged_roles_dict == {'_receiver': ['123', '_user:123', '_user:456']}
+    conn.commit()
+    cur.close()
+
+    formdef.workflow_roles = {'_receiver': '234'}
+    formdef.store()
+    formdef.data_class().rebuild_security()
+
+    conn, cur = sql.get_connection_and_cursor()
+    cur.execute('SELECT workflow_merged_roles_dict FROM %s WHERE id = %s' % (formdef.table_name, formdata.id))
+    merged_roles_dict = cur.fetchone()[0]
+    assert merged_roles_dict == {'_receiver': ['234', '_user:123', '_user:456']}
+    conn.commit()
+    cur.close()
+
+
 def test_logged_error_store_without_integrity_error(pub, sql_queries):
     sql.LoggedError.record('there was an error')
 
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 02b20bb17..fa16ab5a9 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -1098,6 +1098,23 @@ class FormData(StorableObject):
             status = self.status
         return status == 'draft'
 
+    @property
+    def workflow_merged_roles_dict(self):
+        merged_dict = {}
+        for k, v in (self.formdef.workflow_roles or {}).items():
+            merged_dict[k] = [v]
+        for k, v in (self.workflow_roles or {}).items():
+            if k not in merged_dict:
+                merged_dict[k] = []
+            merged_dict[k].extend(v)
+        return merged_dict
+
+    @workflow_merged_roles_dict.setter
+    def workflow_merged_roles_dict(self, value):
+        # do not do anything, this setter is just there as the SQL retrieval will
+        # try to set the property.
+        pass
+
     def get_concerned_roles(self):
         if self.is_draft():
             # drafts are only visible to submitter
diff --git a/wcs/sql.py b/wcs/sql.py
index 82cb18346..22b21b465 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -587,6 +587,9 @@ def do_formdef_tables(formdef, conn=None, cur=None, rebuild_views=False, rebuild
         'page_no',
         'anonymised',
         'workflow_roles',
+        # workflow_merged_roles_dict combines workflow_roles from formdef and
+        # formdata and is used to filter on function assignment.
+        'workflow_merged_roles_dict',
         # workflow_roles_array is created from workflow_roles to be used in
         # get_ids_with_indexed_value
         'workflow_roles_array',
@@ -613,6 +616,8 @@ def do_formdef_tables(formdef, conn=None, cur=None, rebuild_views=False, rebuild
     if 'workflow_roles' not in existing_fields:
         cur.execute('''ALTER TABLE %s ADD COLUMN workflow_roles bytea''' % table_name)
         cur.execute('''ALTER TABLE %s ADD COLUMN workflow_roles_array text[]''' % table_name)
+    if 'workflow_merged_roles_dict' not in existing_fields:
+        cur.execute('''ALTER TABLE %s ADD COLUMN workflow_merged_roles_dict jsonb''' % table_name)
     if 'concerned_roles_array' not in existing_fields:
         cur.execute('''ALTER TABLE %s ADD COLUMN concerned_roles_array text[]''' % table_name)
     if 'actions_roles_array' not in existing_fields:
@@ -2019,6 +2024,7 @@ class SqlDataMixin(SqlMixin):
         ('prefilling_data', 'bytea'),
         ('id_display', 'varchar'),
         ('workflow_roles', 'bytea'),
+        ('workflow_merged_roles_dict', 'jsonb'),
         ('workflow_roles_array', 'text[]'),
         ('concerned_roles_array', 'text[]'),
         ('actions_roles_array', 'text[]'),
@@ -2158,6 +2164,7 @@ class SqlDataMixin(SqlMixin):
             'submission_agent_id': self.submission_agent_id,
             'submission_channel': self.submission_channel,
             'criticality_level': self.criticality_level,
+            'workflow_merged_roles_dict': self.workflow_merged_roles_dict,
         }
         if self.last_update_time:
             sql_dict['last_update_time'] = datetime.datetime.fromtimestamp(time.mktime(self.last_update_time))
@@ -2473,9 +2480,10 @@ class SqlDataMixin(SqlMixin):
         for formdata in formdatas:
             sql_statement = (
                 '''UPDATE %s
-                                  SET concerned_roles_array = %%(roles)s,
-                                      actions_roles_array = %%(actions_roles)s
-                                WHERE id = %%(id)s'''
+                      SET concerned_roles_array = %%(roles)s,
+                          actions_roles_array = %%(actions_roles)s,
+                          workflow_merged_roles_dict = %%(workflow_merged_roles_dict)s
+                    WHERE id = %%(id)s'''
                 % cls._table_name
             )
             with get_publisher().substitutions.temporary_feed(formdata):
@@ -2488,6 +2496,7 @@ class SqlDataMixin(SqlMixin):
                         'id': formdata.id,
                         'roles': [str(x) for x in formdata.concerned_roles if x],
                         'actions_roles': [str(x) for x in formdata.actions_roles if x],
+                        'workflow_merged_roles_dict': formdata.workflow_merged_roles_dict,
                     },
                 )
         conn.commit()
@@ -3656,7 +3665,7 @@ def get_period_total(
 # latest migration, number + description (description is not used
 # programmaticaly but will make sure git conflicts if two migrations are
 # separately added with the same number)
-SQL_LEVEL = (57, 'store tokens in SQL')
+SQL_LEVEL = (58, 'add workflow_merged_roles_dict')
 
 
 def migrate_global_views(conn, cur):
@@ -3782,7 +3791,7 @@ def migrate():
         # 21: (second part), store ascii_name of users
         # 23: (first part), use misc.simplify() over full text queries
         set_reindex('user', 'needed', conn=conn, cur=cur)
-    if sql_level < 51:
+    if sql_level < 58:
         # 17: store last_update_time in tables
         # 18: add user name to full-text search index
         # 21: (third part), add user ascii_names to full-text index
@@ -3852,6 +3861,16 @@ def migrate():
         # 57: store tokens in SQL
         do_tokens_table()
         migrate_legacy_tokens()
+    if sql_level < 58:
+        # 58: add workflow_merged_roles_dict as a jsonb column with
+        #     combined formdef and formdata value.
+        from wcs.carddef import CardDef
+        from wcs.formdef import FormDef
+
+        for formdef in FormDef.select() + CardDef.select():
+            do_formdef_tables(formdef, rebuild_views=False, rebuild_global_views=False)
+        migrate_views(conn, cur)
+        set_reindex('formdata', 'needed', conn=conn, cur=cur)
 
     cur.execute('''UPDATE wcs_meta SET value = %s WHERE key = %s''', (str(SQL_LEVEL[0]), 'sql_level'))
 
@@ -3868,11 +3887,12 @@ def reindex():
             user.store()
         set_reindex('user', 'done', conn=conn, cur=cur)
 
+    from wcs.carddef import CardDef
     from wcs.formdef import FormDef
 
     if is_reindex_needed('formdata', conn=conn, cur=cur):
         # load and store all formdatas
-        for formdef in FormDef.select():
+        for formdef in FormDef.select() + CardDef.select():
             for formdata in formdef.data_class().select(iterator=True):
                 try:
                     formdata.migrate()
-- 
2.34.1

