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

---
 tests/test_formdata.py     |  4 +-
 tests/test_workflows.py    | 94 ++++++++++++++++++++++++++++++++++++--
 wcs/formdata.py            |  2 +-
 wcs/sql.py                 |  2 +-
 wcs/wf/create_formdata.py  |  2 +-
 wcs/wf/register_comment.py | 42 ++++++++++++++---
 wcs/wf/wscall.py           |  2 +-
 wcs/workflows.py           |  2 +-
 8 files changed, 132 insertions(+), 18 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 76a9599c..117399a0 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -423,17 +423,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()
 
@@ -463,17 +463,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_workflows.py b/tests/test_workflows.py
index 4aa15684..023b7f9e 100644
--- a/tests/test_workflows.py
+++ b/tests/test_workflows.py
@@ -1048,17 +1048,17 @@ def test_register_comment_with_attachment_file(pub):
         assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
 
     assert len(formdata.evolution[-1].parts) == 2
     assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
     assert formdata.evolution[-1].parts[0].orig_filename == upload.orig_filename
 
     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())
+    comment_view = str(formdata.evolution[-1].parts[1].view(formdata))
     assert comment_view == '<p>%s</p>' % comment_text
 
     if os.path.exists(os.path.join(get_publisher().app_dir, 'attachments')):
         shutil.rmtree(os.path.join(get_publisher().app_dir, 'attachments'))
 
     formdata.evolution[-1].parts = []
     formdata.store()
 
@@ -1084,20 +1084,104 @@ def test_register_comment_with_attachment_file(pub):
         assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
 
     assert len(formdata.evolution[-1].parts) == 2
     assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
     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())
+    comment_view = str(formdata.evolution[-1].parts[1].view(formdata))
     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_last_part():
+        rendered = formdata.evolution[-1].parts[-1].view(formdata)
+        return str(rendered)
+
+    register_commenter.comment = 'all'
+    register_commenter.to = None
+    register_commenter.perform(formdata)
+    assert len(formdata.evolution[-1].parts) == 1
+    assert display_last_part() == '<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 not display_last_part()
+    pub._request._user = user
+    assert not display_last_part()
+    user.roles = [role.id]
+    register_commenter.perform(formdata)
+    assert display_last_part() == '<p>to-role</p>'
+
+    user.roles = []
+    register_commenter.comment = 'to-submitter'
+    register_commenter.to = ['_submitter']
+    register_commenter.perform(formdata)
+    assert not display_last_part()
+    formdata.user_id = user.id
+    assert display_last_part() == '<p>to-submitter</p>'
+
+    register_commenter.comment = 'to-role-or-submitter'
+    register_commenter.to = [role.id, '_submitter']
+    register_commenter.perform(formdata)
+    assert display_last_part() == '<p>to-role-or-submitter</p>'
+    formdata.user_id = None
+    assert not display_last_part()
+    user.roles = [role.id]
+    assert display_last_part() == '<p>to-role-or-submitter</p>'
+    user.roles = []
+    formdata.user_id = user.id
+    assert display_last_part() == '<p>to-role-or-submitter</p>'
+    register_commenter.to = [role2.id]
+    register_commenter.perform(formdata)
+    assert not display_last_part()
+
+    register_commenter.comment = 'd1'
+    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_commenter.perform(formdata)
+    register_commenter2.perform(formdata)
+    display_parts = formdata.evolution[-1].display_parts()
+    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()
 
@@ -1719,29 +1803,29 @@ def test_webservice_call(http_requests, pub):
     formdata.workflow_data = None
 
     item = WebserviceCallStatusItem()
     item.url = 'http://remote.example.net/400-json'
     item.record_errors = True
     item.action_on_4xx = ':stop'
     with pytest.raises(AbortActionException):
         item.perform(formdata)
-    rendered = formdata.evolution[-1].parts[-1].view()
+    rendered = formdata.evolution[-1].parts[-1].view(formdata)
     assert not rendered # empty if not in backoffice
     req = HTTPRequest(None, {'SERVER_NAME': 'example.net', 'SCRIPT_NAME': '/backoffice/'})
     pub._set_request(req)
-    rendered = formdata.evolution[-1].parts[-1].view()
+    rendered = formdata.evolution[-1].parts[-1].view(formdata)
     assert 'Error during webservice call' in str(rendered)
     assert 'Error Code: 1' in str(rendered)
     assert 'Error Description: :(' in str(rendered)
 
     item.label = 'do that'
     with pytest.raises(AbortActionException):
         item.perform(formdata)
-    rendered = formdata.evolution[-1].parts[-1].view()
+    rendered = formdata.evolution[-1].parts[-1].view(formdata)
     assert 'Error during webservice call &quot;do that&quot;' in str(rendered)
 
     item = WebserviceCallStatusItem()
     item.method = 'GET'
     item.url = 'http://remote.example.net?in_url=1'
     item.qs_data = {
         'str': 'abcd',
         'one': '=1',
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 2df1ddfd..96643958 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -149,17 +149,17 @@ class Evolution(object):
 
         if not self.parts:
             return []
 
         l = []
         for p in self.parts:
             if not hasattr(p, 'view'):
                 continue
-            text = p.view()
+            text = p.view(self.formdata)
             if text:
                 l.append(text)
         self._display_parts = l
         return self._display_parts
 
     def get_json_export_dict(self, user, anonymise=False):
         data = {
             'time': datetime.datetime(*self.time[:6]) if self.time else None,
diff --git a/wcs/sql.py b/wcs/sql.py
index 57f37fda..73521e0d 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -1733,17 +1733,17 @@ 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'):
-                        html_part = part.view()
+                        html_part = part.view(self)
                         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 = '''UPDATE %s SET fts = to_tsvector( %%(fts)s)
                             WHERE id = %%(id)s''' % self._table_name
diff --git a/wcs/wf/create_formdata.py b/wcs/wf/create_formdata.py
index 8317104c..d57f7d16 100644
--- a/wcs/wf/create_formdata.py
+++ b/wcs/wf/create_formdata.py
@@ -158,17 +158,17 @@ class LinkedFormdataEvolutionPart(object):
         d = {}
         for part in formdata.iter_evolution_parts():
             if not isinstance(part, cls):
                 continue
             if part.formdata:
                 d['form_links_%s' % (part.varname or '*')] = part
         return d
 
-    def view(self):
+    def view(self, filled):
         if self.attach_to_history:
             return htmltext('<p class="wf-links">%s <a href="%s">%s %s</a>') % (
                 _('Created new form'),
                 self.formdata and self.formdata.get_url(
                     backoffice=bool(get_request() and get_request().is_in_backoffice())),
                 self.formdef.name if self.formdef else _('Deleted'),
                 self.formdata_id)
         else:
diff --git a/wcs/wf/register_comment.py b/wcs/wf/register_comment.py
index 84b16cc0..36bddaa5 100644
--- a/wcs/wf/register_comment.py
+++ b/wcs/wf/register_comment.py
@@ -9,33 +9,37 @@
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+from quixote import get_request
+
 from ..qommon import _, N_, ezt
 from ..qommon.form import *
 from ..qommon.template import TemplateError
 from ..qommon import get_logger
 
 from wcs.workflows import (WorkflowStatusItem, register_item_class, template_on_formdata,
-                           AttachmentEvolutionPart)
+                           AttachmentEvolutionPart, get_role_translation)
 
 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
@@ -44,54 +48,80 @@ class JournalEvolutionPart: #pylint: disable=C1001
             # treat it as html, escape strings from ezt variables
             self.content = template_on_formdata(formdata, message,
                     ezt_format=ezt.FORMAT_HTML)
             return
 
         # treat is as text/plain
         self.content = template_on_formdata(formdata, message)
 
-    def view(self):
-        if not self.content:
+    def is_for_current_user(self, filled):
+        if not self.to:
+            return True
+        if not get_request():
+            return False
+        user = get_request().user
+        for role in self.to or []:
+            if role == '_submitter':
+                if filled.is_submitter(user):
+                    return True
+            elif user:
+                role = get_role_translation(filled, role)
+                if role in user.get_roles():
+                    return True
+        return False
+
+    def view(self, filled):
+        if not (self.content and self.is_for_current_user(filled)):
             return ''
         if self.content.startswith('<'):
             return htmltext(self.content)
         else:
             # use empty lines to mark paragraphs
             return 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()
@@ -110,17 +140,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)))
 
 
 register_item_class(RegisterCommenterWorkflowStatusItem)
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index 02f7060c..a57ca4cf 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -42,17 +42,17 @@ class JournalWsCallErrorPart: #pylint: disable=C1001
     label = None
 
     def __init__(self, summary, label=None, data=None):
         self.summary = summary
         self.label = label
         if data:
             self.data = data[:10000] # beware of huge responses
 
-    def view(self):
+    def view(self, filled):
         if not (get_request() and get_request().get_path().startswith('/backoffice/')):
             return ''
         r = TemplateIO(html=True)
         r += htmltext('<div class="ws-error">')
         r += htmltext('<h4 class="foldable folded">')
         if self.label:
             r += _('Error during webservice call "%s"') % self.label
         else:
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 94cade90..97504343 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -226,17 +226,17 @@ class AttachmentEvolutionPart: #pylint: disable=C1001
             if not os.path.exists(dirname):
                 os.mkdir(dirname)
             odict['filename'] = os.path.join(dirname, filename)
             self.filename = odict['filename']
             self.fp.seek(0)
             atomic_write(self.filename, self.fp)
         return odict
 
-    def view(self):
+    def view(self, filled):
         show_link = True
         if self.has_redirect_url():
             is_in_backoffice = bool(get_request() and get_request().is_in_backoffice())
             show_link = bool(self.get_redirect_url(backoffice=is_in_backoffice))
         if show_link:
             return htmltext('<p class="wf-attachment"><a href="attachment?f=%s">%s</a></p>' % (
                         os.path.basename(self.filename), self.orig_filename))
         else:
-- 
2.29.2

