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()
|