From 37bfeb6284c6600998cf1d18cc990c6778815d71 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 12 Aug 2026 14:48:10 +0200 Subject: [PATCH] PARAF-457: Displayed who refused a session and his comment in the state help tooltip --- CHANGES.rst | 2 ++ src/imio/esign/browser/table.py | 5 ++- src/imio/esign/browser/templates/macros.pt | 2 +- src/imio/esign/browser/views.py | 6 ++-- src/imio/esign/tests/test_browser_views.py | 7 +--- src/imio/esign/tests/test_utils.py | 38 ++++++++++++++++++++++ src/imio/esign/utils.py | 27 +++++++++++++++ 7 files changed, 74 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 66e282e..4f71b39 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,8 @@ Changelog [chris-adam] - When a session status is received as completed, mark each signer as signed. [sgeulette] +- Displayed who refused a session and his comment in the state help tooltip (PARAF-457). + [chris-adam] 1.0b10 (2026-06-18) ------------------- diff --git a/src/imio/esign/browser/table.py b/src/imio/esign/browser/table.py index 98d3a10..f06d934 100644 --- a/src/imio/esign/browser/table.py +++ b/src/imio/esign/browser/table.py @@ -7,7 +7,7 @@ from imio.esign.config import get_esign_registry_max_session_size from imio.esign.config import get_esign_registry_seal_code from imio.esign.config import get_esign_registry_seal_email -from imio.esign.utils import get_state_description +from imio.esign.utils import get_state_title from imio.helpers.security import check_zope_admin from imio.pyutils.utils import safe_encode from plone import api @@ -62,8 +62,7 @@ def renderCell(self, item): state = escape(translate( (item.get("state", "")), context=self.request, default=item.get("state", ""), domain="imio.esign", )) - title = escape(translate(get_state_description(item.get("state", "")), context=self.request, - domain="imio.esign")) + title = escape(get_state_title(item, self.request)) return (u"{state} " u"" u"".format(state=state, title=title, state_title_value=item.get("state"))) diff --git a/src/imio/esign/browser/templates/macros.pt b/src/imio/esign/browser/templates/macros.pt index 7dcc433..6cbbe24 100644 --- a/src/imio/esign/browser/templates/macros.pt +++ b/src/imio/esign/browser/templates/macros.pt @@ -11,7 +11,7 @@ + title python:view.get_state_title(session)"> draft diff --git a/src/imio/esign/browser/views.py b/src/imio/esign/browser/views.py index 4c3024a..5bc8a09 100644 --- a/src/imio/esign/browser/views.py +++ b/src/imio/esign/browser/views.py @@ -16,7 +16,7 @@ from imio.esign.utils import get_session_annotation from imio.esign.utils import get_session_info from imio.esign.utils import get_sessions_for -from imio.esign.utils import get_state_description +from imio.esign.utils import get_state_title from imio.esign.utils import remove_session from imio.helpers.content import uuidToObject from imio.helpers.emailer import create_html_email @@ -273,8 +273,8 @@ def collapsible_content_css_default(self): """Default CSS class to apply on the collapsible.""" return "collapsible-content" - def get_state_description(self, state): - return translate(get_state_description(state), context=self.request, domain="imio.esign") + def get_state_title(self, session): + return get_state_title(session, self.request) class ItemSessionInfoViewlet(FacetedSessionInfoViewlet): diff --git a/src/imio/esign/tests/test_browser_views.py b/src/imio/esign/tests/test_browser_views.py index bb22aa3..a39a86f 100644 --- a/src/imio/esign/tests/test_browser_views.py +++ b/src/imio/esign/tests/test_browser_views.py @@ -22,6 +22,7 @@ from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID from plone.testing import z2 +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.statusmessages import STATUSMESSAGEKEY from Products.statusmessages.interfaces import IStatusMessage from zope.annotation.interfaces import IAnnotations @@ -584,12 +585,6 @@ def test_ext_session_link(self): result = v.ext_session_link(session) self.assertEqual(result, u'My Session') - def test_get_state_description(self): - """Known state → non-empty translated string; unknown state → ''.""" - v = self._make_viewlet() - self.assertTrue(len(v.get_state_description("draft")) > 0) - self.assertEqual(v.get_state_description("unknown_state"), "") - class TestItemSessionInfoViewlet(BaseEsignTest): """Tests for ItemSessionInfoViewlet.""" diff --git a/src/imio/esign/tests/test_utils.py b/src/imio/esign/tests/test_utils.py index 83021f0..62afb15 100644 --- a/src/imio/esign/tests/test_utils.py +++ b/src/imio/esign/tests/test_utils.py @@ -18,6 +18,7 @@ from imio.esign.utils import get_session_annotation from imio.esign.utils import get_session_info from imio.esign.utils import get_sessions_for +from imio.esign.utils import get_state_title from imio.esign.utils import get_suid_from_uuid from imio.esign.utils import remove_context_from_session from imio.esign.utils import remove_files_from_session @@ -469,6 +470,43 @@ def test_get_sessions_for(self): sessions[0]["watchers"] = ["watcher@sign.com"] self.assertEqual(get_session_info(0)["watchers"], ["watcher@sign.com"]) + def test_get_state_title(self): + """get_state_title: state description, plus who refused the session and why.""" + request = self.portal.REQUEST + self.assertEqual(get_state_title({}, request), "") + self.assertEqual(get_state_title({"state": "unknown_state"}, request), "") + draft_title = get_state_title({"state": "draft"}, request) + self.assertTrue(len(draft_title) > 0) + self.assertNotIn(u"\n", draft_title) + + # refusal feedback without comment: who refused, on a new line after the state description + session = { + "state": "refused", + "signers": [{"email": "user1@sign.com", "fullname": u"Marie Dupont"}], + "returns": [(52, u"refused", {"user": u"user1@sign.com"}, u"Document has been declined", None)], + } + title = get_state_title(session, request) + self.assertIn(u"a signer refused a document.", title) + self.assertTrue(title.endswith(u"\nMarie Dupont refused the session without comment")) + + # refusal feedback with comment + session["returns"].append( + (52, u"refused", {"reason": u"Délibération corrigée.", "user": u"user1@sign.com"}, u"", None) + ) + with_comment = u'\nMarie Dupont refused the session with comment: "Délibération corrigée."' + self.assertTrue(get_state_title(session, request).endswith(with_comment)) + + # a feedback value that is not a mapping is ignored + session["returns"].append((52, u"refused", u"", u"", None)) + self.assertTrue(get_state_title(session, request).endswith(with_comment)) + + # empty fullname or unknown signer email: the email is used + session["signers"][0]["fullname"] = u"" + by_email = u'\nuser1@sign.com refused the session with comment: "Délibération corrigée."' + self.assertTrue(get_state_title(session, request).endswith(by_email)) + session["signers"] = [] + self.assertTrue(get_state_title(session, request).endswith(by_email)) + def test_get_file_info(self): """get_file_info: returns None for unknown session/file; honours readonly flag.""" annex0_uid = self.uids[0] diff --git a/src/imio/esign/utils.py b/src/imio/esign/utils.py index 2f747b1..e2ba4df 100644 --- a/src/imio/esign/utils.py +++ b/src/imio/esign/utils.py @@ -28,6 +28,7 @@ from plone import api from zope.annotation import IAnnotations from zope.component import getAdapter +from zope.i18n import translate import json import requests @@ -658,6 +659,32 @@ def get_state_description(state): }.get(state, "") +def get_state_title(session, request): + """Translated state description, followed by who refused the session and why.""" + title = translate(get_state_description(session.get("state", "")), context=request, domain="imio.esign") + # code 52 = a signer refused + refusals = [ + r[2] for r in session.get("returns") or [] if r[0] == 52 and isinstance(r[2], (PersistentMapping, dict)) + ] + if refusals: + email = refusals[-1].get("user", u"") + fullname = next( + (s["fullname"] for s in session.get("signers") or [] if s.get("email") == email and s.get("fullname")), + email, + ) + reason = refusals[-1].get("reason") + if fullname and reason: + title += u"\n\n" + _( + u'${fullname} refused the session with comment: "${reason}"', + mapping={"fullname": fullname, "reason": reason}, + ) + elif fullname: + title += u"\n\n" + _( + u"${fullname} refused the session without comment", mapping={"fullname": fullname} + ) + return title + + def get_sessions_for(context_uid, readonly=True): """Returns a list of all sessions involving the provided context_uid""" annot = get_session_annotation()