From eb3dd702a07963798deed57bb3895ead617f60bb Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Fri, 11 Dec 2020 00:10:18 +0100
Subject: [PATCH 3/3] workflows: add target roles in commenter workflow item
 (#38254)

---
 tests/form_pages/test_formdata.py | 88 +++++++++++++++++++++++++++++
 tests/test_formdata.py            |  4 +-
 tests/test_workflow_import.py     | 33 +++++++++++
 tests/test_workflows.py           | 94 +++++++++++++++++++++++++++++++
 wcs/formdata.py                   |  2 +
 wcs/sql.py                        |  2 +
 wcs/wf/register_comment.py        | 23 +++++++-
 7 files changed, 241 insertions(+), 5 deletions(-)

diff --git a/tests/form_pages/test_formdata.py b/tests/form_pages/test_formdata.py
index bec178f2..b094258e 100644
--- a/tests/form_pages/test_formdata.py
+++ b/tests/form_pages/test_formdata.py
@@ -1499,8 +1499,96 @@ def test_formdata_named_wscall_in_conditions(http_requests, pub):
     formdef.data_class().wipe()
 
     resp = login(get_app(pub), username='foo', password='foo').get('/test/')
     assert '>1st page<' in resp.text
     assert '>2nd page<' in resp.text
     assert '>3rd page<' not in resp.text
     assert '>4th page<' in resp.text
     assert len(http_requests.requests) == 1
+
+
+def test_formdata_evolution_registercommenter_to(pub):
+    user = create_user(pub)
+
+    Role.wipe()
+    role1 = Role(name='role the user does not have')
+    role1.store()
+    role2 = Role(name='role the user does have')
+    role2.store()
+    user.roles = [role2.id]
+    user.store()
+
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    comment = RegisterCommenterWorkflowStatusItem()
+    comment.id = '_comment'
+    comment.comment = 'Hello World'
+    comment.to = None
+    st1.items.append(comment)
+    comment.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    # register comment to all users
+    app = login(get_app(pub), username='foo', password='foo')
+    resp = app.get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[0].parts[0].content == 'Hello World'
+    assert formdata.evolution[0].parts[0].to is None
+    resp = app.get('/test/%s/' % formdata.id)
+    resp.status_int = 200
+    assert resp.html.find('div', {'id': 'evolution-log'}).find('p').text == 'Hello World'
+
+    # register comment to other users
+    formdef.data_class().wipe()
+    comment.to = [role1.id]
+    wf.store()
+
+    resp = app.get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[0].parts[0].content == 'Hello World'
+    assert formdata.evolution[0].parts[0].to == [role1.id]
+    resp = app.get('/test/%s/' % formdata.id)
+    resp.status_int = 200
+    assert not resp.html.find('div', {'id': 'evolution-log'}).find('p')
+
+    # register comment to this user
+    formdef.data_class().wipe()
+    comment.to = [role2.id]
+    wf.store()
+
+    resp = app.get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[0].parts[0].content == 'Hello World'
+    assert formdata.evolution[0].parts[0].to == [role2.id]
+    resp = app.get('/test/%s/' % formdata.id)
+    resp.status_int = 200
+    assert resp.html.find('div', {'id': 'evolution-log'}).find('p').text == 'Hello World'
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index f1b4d91c..bbc71d1a 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -425,17 +425,17 @@ def test_get_json_export_dict_evolution(pub, local_user):
     d.user_id = local_user.id
     d.receipt_time = time.localtime()
     evo = Evolution()
     evo.time = time.localtime()
     evo.status = 'wf-%s' % st_new.id
     evo.who = '_submitter'
     d.evolution = [evo]
     d.store()
-    evo.add_part(JournalEvolutionPart(d, "ok"))
+    evo.add_part(JournalEvolutionPart(d, "ok", None))
     evo.add_part(JournalWsCallErrorPart("summary", "label", "data"))
     evo = Evolution()
     evo.time = time.localtime()
     evo.status = 'wf-%s' % st_finished.id
     evo.who = '_submitter'
     d.evolution.append(evo)
     d.store()
 
@@ -465,17 +465,17 @@ def test_get_json_export_dict_evolution(pub, local_user):
     export = d.get_json_export_dict(anonymise=True)
     assert 'evolution' in export
     assert len(export['evolution']) == 2
     assert export['evolution'][0]['status'] == st_new.id
     assert 'time' in export['evolution'][0]
     assert 'who' not in export['evolution'][0]
     assert 'parts' in export['evolution'][0]
     assert len(export['evolution'][0]['parts']) == 2
-    assert len(export['evolution'][0]['parts'][0]) == 1
+    assert len(export['evolution'][0]['parts'][0]) == 2
     assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment'
     assert len(export['evolution'][0]['parts'][1]) == 1
     assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error'
     assert export['evolution'][1]['status'] == st_finished.id
     assert 'time' in export['evolution'][1]
     assert 'who' not in export['evolution'][0]
     assert 'parts' not in export['evolution'][1]
 
diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py
index 33f1ee5f..a2c65ffa 100644
--- a/tests/test_workflow_import.py
+++ b/tests/test_workflow_import.py
@@ -476,16 +476,49 @@ def test_global_actions(pub):
     trigger.roles = [role.id]
 
     wf2 = assert_import_export_works(wf)
     assert wf2.global_actions[0].triggers[0].roles == [role.id]
 
     wf2 = assert_import_export_works(wf, True)
 
 
+def test_register_comment_to(pub):
+    role = Role()
+    role.id = '5'
+    role.name = 'Test Role'
+    role.store()
+
+    wf = Workflow(name='global actions')
+    st1 = wf.add_status('Status1', 'st1')
+
+    add_to_journal1 = RegisterCommenterWorkflowStatusItem()
+    add_to_journal1.id = '_add_to_journal1'
+    add_to_journal1.comment = 'HELLO WORLD'
+    st1.items.append(add_to_journal1)
+    add_to_journal1.parent = st1
+
+    add_to_journal2 = RegisterCommenterWorkflowStatusItem()
+    add_to_journal2.id = '_add_to_journal2'
+    add_to_journal2.comment = 'OLA MUNDO'
+    add_to_journal2.to = [role.id]
+    st1.items.append(add_to_journal2)
+    add_to_journal2.parent = st1
+    assert wf.possible_status[0].items[0].to == None
+    assert wf.possible_status[0].items[1].to == [role.id]
+
+    xml_root = wf.export_to_xml()
+    assert 'to' not in [x.tag for x in xml_root.findall('possible_status/status/items/item[1]/')]
+    assert 'to' in [x.tag for x in xml_root.findall('possible_status/status/items/item[2]/')]
+
+    wf2 = assert_import_export_works(wf)
+    assert wf2.possible_status[0].items[0].to == []
+    assert wf2.possible_status[0].items[1].to == [role.id]
+
+
 def test_backoffice_fields(pub):
     wf = Workflow(name='bo fields')
     wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
     wf.backoffice_fields_formdef.fields = [
         StringField(id='bo1', label='1st backoffice field', type='string', varname='backoffice_blah'),
     ]
     wf2 = assert_import_export_works(wf, True)
 
diff --git a/tests/test_workflows.py b/tests/test_workflows.py
index ac9d0f2a..635cff14 100644
--- a/tests/test_workflows.py
+++ b/tests/test_workflows.py
@@ -1148,16 +1148,110 @@ def test_register_comment_with_attachment_file(pub):
     assert formdata.evolution[-1].parts[0].orig_filename == 'hello.txt'
 
     assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
     assert len(formdata.evolution[-1].parts[1].content) > 0
     comment_view = str(formdata.evolution[-1].parts[1].view())
     assert comment_view == '<p>%s</p>' % comment_text
 
 
+def test_register_comment_to(pub):
+    workflow = Workflow(name='register comment to')
+    st1 = workflow.add_status('Status1', 'st1')
+
+    role = Role(name='foorole')
+    role.store()
+    role2 = Role(name='no-one-role')
+    role2.store()
+    user = pub.user_class(name='baruser')
+    user.roles = []
+    user.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.url_name = 'foobar'
+    formdef._workflow = workflow
+
+    formdata = formdef.data_class()()
+    formdata.just_created()
+    assert formdata.status == 'wf-st1'
+    formdata.store()
+
+    register_commenter = RegisterCommenterWorkflowStatusItem()
+    register_commenter.parent = st1
+    st1.items.append(register_commenter)
+
+    def display_parts():
+        formdata.evolution[-1]._display_parts = None  # invalidate cache
+        return [str(x) for x in formdata.evolution[-1].display_parts()]
+
+    register_commenter.comment = 'all'
+    register_commenter.to = None
+    register_commenter.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 1
+    assert display_parts() == ['<p>all</p>']
+
+    register_commenter.comment = 'to-role'
+    register_commenter.to = [role.id]
+    register_commenter.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 2
+    assert len(display_parts()) == 1
+    pub._request._user = user
+    assert display_parts() == ['<p>all</p>']
+    user.roles = [role.id]
+    assert display_parts() == ['<p>all</p>', '<p>to-role</p>']
+
+    user.roles = []
+    register_commenter.comment = 'to-submitter'
+    register_commenter.to = ['_submitter']
+    register_commenter.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 3
+    assert display_parts() == ['<p>all</p>']
+    formdata.user_id = user.id
+    assert display_parts() == ['<p>all</p>', '<p>to-submitter</p>']
+
+    register_commenter.comment = 'to-role-or-submitter'
+    register_commenter.to = [role.id, '_submitter']
+    register_commenter.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 4
+    assert display_parts() == ['<p>all</p>', '<p>to-submitter</p>', '<p>to-role-or-submitter</p>']
+    formdata.user_id = None
+    assert display_parts() == ['<p>all</p>']
+    user.roles = [role.id]
+    assert display_parts() == ['<p>all</p>', '<p>to-role</p>', '<p>to-role-or-submitter</p>']
+    formdata.user_id = user.id
+    assert display_parts() == [
+        '<p>all</p>',
+        '<p>to-role</p>',
+        '<p>to-submitter</p>',
+        '<p>to-role-or-submitter</p>',
+    ]
+
+    register_commenter.comment = 'd1'
+    register_commenter.to = [role2.id]
+    register_commenter.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 5
+    assert display_parts() == [
+        '<p>all</p>',
+        '<p>to-role</p>',
+        '<p>to-submitter</p>',
+        '<p>to-role-or-submitter</p>',
+    ]
+    register_commenter2 = RegisterCommenterWorkflowStatusItem()
+    register_commenter2.parent = st1
+    st1.items.append(register_commenter2)
+    register_commenter2.comment = 'd2'
+    register_commenter2.to = [role.id, '_submitter']
+    user.roles = [role.id, role2.id]
+    register_commenter2.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 6
+    assert '<p>d1</p>' in [str(x) for x in display_parts()]
+    assert '<p>d2</p>' in [str(x) for x in display_parts()]
+
+
 def test_email(pub, emails):
     pub.substitutions.feed(MockSubstitutionVariables())
 
     formdef = FormDef()
     formdef.name = 'baz'
     formdef.fields = []
     formdef.store()
 
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 267e2661..be453873 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -152,16 +152,18 @@ class Evolution(object):
 
         if not self.parts:
             return []
 
         l = []
         for p in self.parts:
             if not hasattr(p, 'view'):
                 continue
+            if hasattr(p, 'to') and not self.formdata.is_for_current_user(p.to):
+                continue
             text = p.view()
             if text:
                 l.append(text)
         self._display_parts = l
         return self._display_parts
 
     def get_json_export_dict(self, user, anonymise=False):
         data = {
diff --git a/wcs/sql.py b/wcs/sql.py
index 2f586c3b..d1fa6ab7 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -2017,16 +2017,18 @@ class SqlDataMixin(SqlMixin):
                 elif type(value) in (tuple, list):
                     fts_strings.extend(value)
         if self._evolution:
             for evo in self._evolution:
                 if evo.comment:
                     fts_strings.append(evo.comment)
                 for part in evo.parts or []:
                     if hasattr(part, 'view'):
+                        if hasattr(part, 'to') and not self.is_for_current_user(part.to):
+                            continue
                         html_part = part.view()
                         if html_part:
                             fts_strings.append(qommon.misc.html2text(html_part))
         user = self.get_user()
         if user:
             fts_strings.append(user.get_display_name())
 
         sql_statement = (
diff --git a/wcs/wf/register_comment.py b/wcs/wf/register_comment.py
index 84d940c0..8aea5f72 100644
--- a/wcs/wf/register_comment.py
+++ b/wcs/wf/register_comment.py
@@ -26,20 +26,22 @@ from wcs.workflows import (
     AttachmentEvolutionPart,
 )
 
 import sys
 
 
 class JournalEvolutionPart:  # pylint: disable=C1001
     content = None
+    to = None
 
-    def __init__(self, formdata, message):
+    def __init__(self, formdata, message, to):
         if not message:
             return
+        self.to = to
         if '{{' in message or '{%' in message:
             # django template
             content = template_on_formdata(formdata, message)
             if content and not content.startswith('<'):
                 # add <div> to mark the string as processed as HTML
                 content = '<div>%s</div>' % content
             self.content = content
             return
@@ -63,41 +65,56 @@ class JournalEvolutionPart:  # pylint: disable=C1001
                 htmltext('<p>')
                 + htmltext('\n').join([(x or htmltext('</p><p>')) for x in self.content.splitlines()])
                 + htmltext('</p>')
             )
 
     def get_json_export_dict(self, anonymise=False):
         d = {
             'type': 'workflow-comment',
+            'to': self.to,
         }
         if not anonymise:
             d['content'] = self.content
         return d
 
 
 class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem):
     description = N_('History Message')
     key = 'register-comment'
     category = 'interaction'
 
     comment = None
+    to = None
     attachments = None
 
     def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
         super(RegisterCommenterWorkflowStatusItem, self).add_parameters_widgets(
             form, parameters, prefix=prefix, formdef=formdef
         )
         if 'comment' in parameters:
             form.add(
                 TextWidget, '%scomment' % prefix, title=_('Message'), value=self.comment, cols=80, rows=10
             )
+        if 'to' in parameters:
+            form.add(
+                WidgetList,
+                '%sto' % prefix,
+                title=_('To'),
+                element_type=SingleSelectWidget,
+                value=self.to or [],
+                add_element_label=self.get_add_role_label(),
+                element_kwargs={
+                    'render_br': False,
+                    'options': [(None, '---', None)] + self.get_list_of_roles(include_logged_in_users=False),
+                },
+            )
 
     def get_parameters(self):
-        return ('comment', 'attachments', 'condition')
+        return ('comment', 'to', 'attachments', 'condition')
 
     def attach_uploads_to_formdata(self, formdata, uploads):
         if not formdata.evolution[-1].parts:
             formdata.evolution[-1].parts = []
         for upload in uploads:
             try:
                 # useless but required to restore upload.fp from serialized state,
                 # needed by AttachmentEvolutionPart.from_upload()
@@ -115,17 +132,17 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem):
         # (with substitution vars)
         if self.attachments:
             uploads = self.convert_attachments_to_uploads()
             self.attach_uploads_to_formdata(formdata, uploads)
             formdata.store()  # store and invalidate cache, so references can be used in the comment message.
 
         # the comment can use attachments done above
         try:
-            formdata.evolution[-1].add_part(JournalEvolutionPart(formdata, self.comment))
+            formdata.evolution[-1].add_part(JournalEvolutionPart(formdata, self.comment, self.to))
             formdata.store()
         except TemplateError as e:
             url = formdata.get_url()
             get_logger().error(
                 'error in template for comment [%s], ' 'comment could not be generated: %s' % (url, str(e))
             )
 
 
-- 
2.30.0

