Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -29,6 +29,8 @@ Changelog
[chris-adam]
- Added external watchers for esign sessions.
[chris-adam, sgeulette]
- Added possibility to have elements of the same context to belong to different sessions.
[chris-adam]

1.0a2 (2026-02-06)
------------------
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 @@ -40,7 +40,7 @@
<tr metal:define-macro="signers" i18n:domain="imio.esign">
<td class="table_widget_label"><label i18n:translate="">Signers</label></td>
<td class="table_widget_value">
<table id="context_viewlet_signers_table" class="no-border no-style-table listing"
<table class="context_viewlet_signers_table no-border no-style-table listing"
tal:define="signers python:session.get('signers', [])">
<thead>
<tr>
Expand Down
12 changes: 6 additions & 6 deletions src/imio/esign/browser/templates/session_info.pt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
<div id="collapsible-session-info"
tal:attributes="class python: view.collapsible_content_css_default()">
<div class="collapsible-inner-content session-info-container"
tal:define="template python: context.unrestrictedTraverse('@@esign-macros').index;
session python: view.session">
tal:repeat="session python:view.sessions"
tal:define="template python: context.unrestrictedTraverse('@@esign-macros').index">
<div class="session-info-column">
<table id="context_viewlet_session_table" class="no-style-table table-view-widgets">
<table tal:attributes="id string:context_viewlet_session_table_${session/id}" class="no-style-table table-view-widgets">
<tbody>
<tal:block tal:repeat="macro_name python:view.get_table_rows(1)">
<metal:render_cell use-macro="python: template.macros[macro_name]" />
Expand All @@ -26,16 +26,16 @@
</div>

<div class="session-info-column">
<table id="context_viewlet_session_signers" class="no-style-table table-view-widgets">
<table tal:attributes="id string:context_viewlet_session_signers_${session/id}" class="no-style-table table-view-widgets">
<tbody>
<tal:block tal:repeat="macro_name python:view.get_table_rows(2)">
<metal:render_cell use-macro="python: template.macros[macro_name]" />
</tal:block>
</tbody>
</table>
</div>
<script>$('table#context_viewlet_signers_table').each(setoddeven);</script>
</div>
</div>
<script>$('table.context_viewlet_signers_table').each(setoddeven);</script>
</div>
</div>

Expand Down
35 changes: 17 additions & 18 deletions src/imio/esign/browser/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-

from AccessControl import Unauthorized
from copy import deepcopy
from datetime import datetime
from datetime import timedelta
from imio.esign import _
Expand All @@ -12,6 +13,7 @@
from imio.esign.config import get_registry_signing_users_email_content
from imio.esign.utils import create_external_session
from imio.esign.utils import get_session_annotation
from imio.esign.utils import get_sessions_for
from imio.esign.utils import get_state_description
from imio.esign.utils import remove_session
from imio.helpers.content import uuidToObject
Expand Down Expand Up @@ -206,23 +208,25 @@ def sessions_collection_uid(self):
def render(self):
"""Render the viewlet."""
if self.request.form.get("c1[]", None) == self.sessions_collection_uid:
if self.session:
if self.sessions:
return self.index()
return self.sessions_listing_view(self.context, self.request).render_table()
return ""

@property
def session(self):
session = None
def sessions(self):
session_id = self.request.form.get("esign_session_id[]", None)
if not session_id:
return
try:
session_id = int(session_id)
except (TypeError, ValueError):
return []
sessions = get_session_annotation()["sessions"]
session = sessions.get(int(session_id))
session = sessions.get(session_id)
if not session:
return
return []
session = deepcopy(session)
session["id"] = session_id
return session
return [session]

def get_table_rows(self, column):
"""Get the table rows following the column"""
Expand Down Expand Up @@ -252,27 +256,22 @@ def get_state_description(self, state):


class ItemSessionInfoViewlet(FacetedSessionInfoViewlet):
"""Show selected session info for an item."""
"""Show session info for all sessions linked to a context item."""

def available(self):
"""Global availability of the viewlet."""
return True

def render(self):
"""Render the viewlet."""
if self.session:
if self.sessions:
return self.index()
return ""

@property
def session(self):
annot = get_session_annotation()
for f_uid in annot["c_uids"].get(self.context.UID(), []):
if f_uid in annot["uids"]:
session = annot["sessions"].get(annot["uids"][f_uid], {})
session["id"] = annot["uids"][f_uid]
return session
return {}
def sessions(self):
"""Return all sessions that contain files from this context."""
return get_sessions_for(self.context.UID())


@implementer(IPublishTraverse)
Expand Down
81 changes: 81 additions & 0 deletions src/imio/esign/tests/test_browser_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from datetime import timedelta
from imio.esign.browser.views import DownloadFileView
from imio.esign.browser.views import ExternalSessionCreateView
from imio.esign.browser.views import ItemSessionInfoViewlet
from imio.esign.browser.views import SessionDeleteView
from imio.esign.testing import IMIO_ESIGN_FUNCTIONAL_TESTING
from imio.esign.testing import IMIO_ESIGN_INTEGRATION_TESTING
Expand Down Expand Up @@ -393,3 +394,83 @@ def test_download_file_view(self):
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)
browser.open("{}/download-file/{}".format(portal_url, "aabbccddee?param=value"))
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)


class TestItemSessionInfoViewlet(unittest.TestCase):
"""Test ItemSessionInfoViewlet multi-session support."""

layer = IMIO_ESIGN_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
self.request = self.portal.REQUEST
setRoles(self.portal, TEST_USER_ID, ["Manager"])
at_folder = api.content.create(
container=self.portal, id="annexes_types", title="Annexes Types",
type="ContentCategoryConfiguration", exclude_from_nav=True,
)
category_group = api.content.create(
type="ContentCategoryGroup", title="Annexes",
container=at_folder, id="annexes",
)
icon_path = os.path.join(
os.path.dirname(collective.iconifiedcategory.__file__), "tests", u"ic\xf4ne1.png"
)
with open(icon_path, "rb") as fl:
api.content.create(
type="ContentCategory", title="To sign",
container=category_group,
icon=NamedBlobImage(fl.read(), filename=u"ic\xf4ne1.png"),
id="to_sign", predefined_title="To be signed",
to_sign=True, show_preview=False,
)
api.user.create(email="user1@sign.com", username="user1", password="password1")
self.folder = api.content.create(
container=self.portal, type="Folder",
id="test_folder", title="Test Folder",
)
tests_dir = os.path.dirname(__file__)
self.annexes = []
for i in range(2):
with open(os.path.join(tests_dir, "annex1.pdf"), "rb") as f:
annex = api.content.create(
container=self.folder, type="annex",
id="annex{}".format(i), title="Annex {}".format(i),
content_category=calculate_category_id(
self.portal["annexes_types"]["annexes"]["to_sign"]
),
scan_id="0123456000000{:02d}".format(i),
file=NamedBlobFile(
data=f.read(), filename=u"annex{}.pdf".format(i),
contentType="application/pdf",
),
)
self.annexes.append(annex)
self.signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
for key in list(self.request.form.keys()):
del self.request.form[key]

def test_sessions_empty(self):
"""No files in esign annotation → sessions returns empty list."""
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
self.assertEqual(viewlet.sessions, [])
self.assertEqual(viewlet.render(), "")

def test_sessions_single_session(self):
"""All context files in one session → sessions returns one dict."""
uids = [a.UID() for a in self.annexes]
add_files_to_session(self.signers, uids)
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
sessions = viewlet.sessions
self.assertEqual(len(sessions), 1)
self.assertEqual(sessions[0]["id"], 0)

def test_sessions_multiple_sessions(self):
"""Files in two sessions (different discriminators) → sessions returns two dicts."""
add_files_to_session(self.signers, [self.annexes[0].UID()], discriminators=("a",))
add_files_to_session(self.signers, [self.annexes[1].UID()], discriminators=("b",))
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
sessions = viewlet.sessions
self.assertEqual(len(sessions), 2)
session_ids = {s["id"] for s in sessions}
self.assertEqual(session_ids, {0, 1})
18 changes: 18 additions & 0 deletions src/imio/esign/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from copy import deepcopy
from datetime import datetime
from datetime import timedelta
from imio.esign import _tr as _
Expand Down Expand Up @@ -516,3 +517,20 @@ def get_state_description(state):
'returned': u'The session is finished and signed documents are on the way back to the application.',
'finalized': u'The session is finished and signed documents have been sent back to the application.',
}.get(state, "")


def get_sessions_for(context_uid, readonly=True):
"""Returns a list of all sessions involving the provided context_uid"""
annot = get_session_annotation()
result = []
seen = set()
for f_uid in annot["c_uids"].get(context_uid, []):
session_id = annot["uids"].get(f_uid)
if session_id is not None and session_id not in seen:
seen.add(session_id)
session = annot["sessions"][session_id]
if readonly:
session = deepcopy(session)
session["id"] = session_id
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ici, je ne suis pas sûr de l'implémentation. J'ai besoin de l'ID dans la session mais je n'ai pas envie de modifier l'annotation. je fais donc un deepcopy pour chaque session, et il y a une différence dans l'output si on est en readonly ou non

result.append(session)
return result