Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
-------------------
Expand Down
5 changes: 2 additions & 3 deletions src/imio/esign/browser/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"<span class='state-title state-title-{state_title_value}' title='{title}'>{state} "
u"<span class='far fa-question-circle' />"
u"</span>".format(state=state, title=title, state_title_value=item.get("state")))
Expand Down
2 changes: 1 addition & 1 deletion src/imio/esign/browser/templates/macros.pt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<td class="table_widget_label"><label i18n:translate="">State</label></td>
<td class="table_widget_value">
<span tal:attributes="class string:state-title state-title-${session/state};
title python:view.get_state_description(session['state'])">
title python:view.get_state_title(session)">
<tal:block content="python:session['state']" i18n:translate="">draft</tal:block>
<span class='far fa-question-circle' />
</span>
Expand Down
6 changes: 3 additions & 3 deletions src/imio/esign/browser/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
7 changes: 1 addition & 6 deletions src/imio/esign/tests/test_browser_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -584,12 +585,6 @@ def test_ext_session_link(self):
result = v.ext_session_link(session)
self.assertEqual(result, u'<a href="https://sign.example.com/s/1" target="_blank">My Session</a>')

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."""
Expand Down
38 changes: 38 additions & 0 deletions src/imio/esign/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
27 changes: 27 additions & 0 deletions src/imio/esign/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
)
Comment on lines +677 to +684

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'def get_state_title|refused the session|translate\(' src/imio/esign/utils.py
fd -e po . | xargs -r rg -n -F 'refused the session'

Repository: IMIO/imio.esign

Length of output: 1262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- translation imports and helper definitions ---'
sed -n '1,90p' src/imio/esign/utils.py
rg -n -C 5 'def translate|from .* import .*translate|import .*translate|gettext|domain=' src
printf '%s\n' '--- catalogs and relevant message IDs ---'
fd -i -e po -e pot . | sort
while IFS= read -r file; do
  rg -n -C 3 'refused|session|fullname|reason' "$file" || true
done < <(fd -i -e po -e pot .)
printf '%s\n' '--- locale-related test setup and translation tests ---'
rg -n -C 5 'setLanguage|LANGUAGE|locale|translate|get_state_title|I18N|translation' src/imio/esign/tests src/imio/esign

Repository: IMIO/imio.esign

Length of output: 3911


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact call graph for get_state_title ---'
ast-grep outline src/imio/esign/utils.py
ast-grep run --pattern $'def get_state_title($$$):
  $$$' --lang python src/imio/esign/utils.py --debug-query=pattern || true
sed -n '640,690p' src/imio/esign/utils.py
printf '%s\n' '--- all message-factory and translation call sites ---'
rg -n -C 4 'MessageFactory|messageFactory|_\(|translate\(' src/imio/esign
printf '%s\n' '--- test class setup and existing locale tests ---'
sed -n '1,130p' src/imio/esign/tests/test_utils.py
sed -n '430,525p' src/imio/esign/tests/test_utils.py
rg -n -C 8 'setLanguage|set_language|preferred_languages|LANGUAGE|translation|translate' src/imio/esign/tests

Repository: IMIO/imio.esign

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- package files related to translations ---'
git ls-files | rg -i '(^|/)(locales?|i18n|translations?)(/|$)|\.(po|pot|mo)$' || true
printf '%s\n' '--- translation registrations and catalog declarations ---'
rg -n -C 6 'imio\.esign|ITranslationDomain|TranslationDomain|locales|i18n|configure\.zCML|language' setup.py setup.cfg pyproject.toml buildout.cfg src 2>/dev/null | head -500 || true
printf '%s\n' '--- focused tests and fixture setup ---'
sed -n '1,120p' src/imio/esign/tests/test_utils.py
sed -n '450,525p' src/imio/esign/tests/test_utils.py
printf '%s\n' '--- current-language helper usage ---'
rg -n -C 3 'get_current_language|set.*language|Language|preferred_languages|portal_language' src/imio/esign

Repository: IMIO/imio.esign

Length of output: 17287


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- exact package initializer ---'
cat -n src/imio/esign/__init__.py
printf '%s\n' '--- translation-related tracked files ---'
git ls-files | rg -i '(^|/)(locales?|i18n|translations?)(/|$)|\.(po|pot|mo)$' || true
printf '%s\n' '--- test base class and request language configuration ---'
rg -n -C 8 'class .*Test|setUp|request|language|locale' src/imio/esign/tests/test_utils.py | head -400
printf '%s\n' '--- setup metadata ---'
for f in setup.py setup.cfg pyproject.toml configure.zcml src/imio/esign/configure.zcml; do
  if [ -f "$f" ]; then
    echo "### $f"
    rg -n -C 5 'i18n|locale|domain|package|include' "$f" || true
  fi
done

Repository: IMIO/imio.esign

Length of output: 12278


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- imports in translation-sensitive modules ---'
sed -n '1,35p' src/imio/esign/browser/table.py
sed -n '1,25p' src/imio/esign/browser/views.py
printf '%s\n' '--- all tracked files with translation catalogs or French strings ---'
git ls-files | while IFS= read -r f; do
  case "$f" in
    *.po|*.pot|*.mo|*locales*|*locale*|*i18n*) printf '%s\n' "$f";;
  esac
done
rg -n -i -C 2 'refused|refusé|session without|session avec|signer refused' . \
  -g '!*.pyc' -g '!*.js' -g '!*.css' -g '!*.html' | head -300 || true
printf '%s\n' '--- setup and package configuration files ---'
git ls-files | rg '(^|/)(setup\.py|setup\.cfg|pyproject\.toml|configure\.zcml|metadata\.json|buildout\.cfg)$' | while IFS= read -r f; do
  echo "### $f"
  rg -n -C 4 'i18n|locale|domain|package|include' "$f" || true
done

Repository: IMIO/imio.esign

Length of output: 25243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- refusal-detail catalog entries ---'
for f in src/imio/esign/locales/imio.esign.pot src/imio/esign/locales/fr/LC_MESSAGES/imio.esign.po src/imio/esign/locales/en/LC_MESSAGES/imio.esign.po; do
  echo "### $f"
  rg -n -C 4 'refused the session|without comment|with comment|cancelled because' "$f" || true
done
printf '%s\n' '--- test base language behavior ---'
fd -i 'base.py' src/imio/esign/tests --exec sh -c 'echo "### $1"; cat -n "$1"' sh {}
printf '%s\n' '--- locale update behavior ---'
cat -n src/imio/esign/locales/update.sh
printf '%s\n' '--- static behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/imio/esign/utils.py").read_text(encoding="utf-8")
init = Path("src/imio/esign/__init__.py").read_text(encoding="utf-8")
messages = [
    '${fullname} refused the session with comment: "${reason}"',
    '${fullname} refused the session without comment',
]
print("utils imports _tr as _:", "from imio.esign import _tr as _" in source)
print("utils imports zope.i18n.translate:", "from zope.i18n import translate" in source)
print("_tr uses portal current language:", "api.portal.get_current_language()" in init)
for msg in messages:
    print("\nMSGID:", msg)
    for path in Path("src/imio/esign/locales").rglob("*.po"):
        text = path.read_text(encoding="utf-8")
        present = re.search(r'^msgid "' + re.escape(msg) + r'"$', text, re.MULTILINE)
        print(path, "present" if present else "absent")
PY

Repository: IMIO/imio.esign

Length of output: 3776


Translate both refusal-detail messages with the request context.

_ is _tr, which uses the portal language instead of request. Both refusal msgids are absent from the catalogs, so the suffix remains English in the French UI. Pass both raw msgids to translate(..., context=request, domain="imio.esign", mapping=...), add their catalog entries, and test both refusal branches with a non-English request language.

📍 Affects 2 files
  • src/imio/esign/utils.py#L677-L684 (this comment)
  • src/imio/esign/tests/test_utils.py#L473-L509
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/imio/esign/utils.py` around lines 677 - 684, Update both refusal-detail
branches in utils.py around the title-building logic to use translate with the
raw msgids, context=request, domain="imio.esign", and the existing
fullname/reason mappings instead of the portal-language _ helper; add catalog
entries for both messages. In test_utils.py lines 473-509, cover both refusal
branches using a non-English request language and assert the translated results.

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()
Expand Down
Loading