Skip to content

Commit 9c16952

Browse files
committed
Handled multiple sessions for one context
1 parent 94c7b5e commit 9c16952

File tree

5 files changed

+137
-23
lines changed

5 files changed

+137
-23
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Changelog
2929
[chris-adam]
3030
- Added external watchers for esign sessions.
3131
[chris-adam, sgeulette]
32+
- Added possibility to have elements of the same context to belong to different sessions.
33+
[chris-adam]
3234

3335
1.0a2 (2026-02-06)
3436
------------------

src/imio/esign/browser/templates/macros.pt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<tr metal:define-macro="signers" i18n:domain="imio.esign">
4141
<td class="table_widget_label"><label i18n:translate="">Signers</label></td>
4242
<td class="table_widget_value">
43-
<table id="context_viewlet_signers_table" class="no-border no-style-table listing"
43+
<table class="no-border no-style-table listing context_viewlet_signers_table"
4444
tal:define="signers python:session.get('signers', [])">
4545
<thead>
4646
<tr>

src/imio/esign/browser/templates/session_info.pt

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
55
lang="en"
66
i18n:domain="imio.esign">
7-
<div class="faceted-session-info" tal:condition="python: view.available()">
8-
<div id="session-info"
9-
onclick="toggleDetails('collapsible-session-info', toggle_parent_active=true);"
10-
tal:attributes="class python: view.collapsible_css_default()">
7+
<div class="faceted-session-info"
8+
tal:condition="python: view.available()"
9+
tal:define="session_id python: str(view.session.get('id', ''))">
10+
<div tal:attributes="id string:session-info-${session_id};
11+
onclick string:toggleDetails('collapsible-session-info-${session_id}', toggle_parent_active=true);
12+
class python: view.collapsible_css_default()">
1113
<span i18n:translate="">Session information (<img i18n:name="icon_url" tal:attributes="src string:${view/portal_url}/++resource++imio.esign/parapheo.svg" /> Parapheo)</span>
1214
</div>
13-
<div id="collapsible-session-info"
14-
tal:attributes="class python: view.collapsible_content_css_default()">
15+
<div tal:attributes="id string:collapsible-session-info-${session_id};
16+
class python: view.collapsible_content_css_default()">
1517
<div class="collapsible-inner-content session-info-container"
1618
tal:define="template python: context.unrestrictedTraverse('@@esign-macros').index;
1719
session python: view.session">
1820
<div class="session-info-column">
19-
<table id="context_viewlet_session_table" class="no-style-table table-view-widgets">
21+
<table class="no-style-table table-view-widgets">
2022
<tbody>
2123
<tal:block tal:repeat="macro_name python:view.get_table_rows(1)">
2224
<metal:render_cell use-macro="python: template.macros[macro_name]" />
@@ -26,15 +28,15 @@
2628
</div>
2729

2830
<div class="session-info-column">
29-
<table id="context_viewlet_session_signers" class="no-style-table table-view-widgets">
31+
<table class="no-style-table table-view-widgets">
3032
<tbody>
3133
<tal:block tal:repeat="macro_name python:view.get_table_rows(2)">
3234
<metal:render_cell use-macro="python: template.macros[macro_name]" />
3335
</tal:block>
3436
</tbody>
3537
</table>
3638
</div>
37-
<script>$('table#context_viewlet_signers_table').each(setoddeven);</script>
39+
<script>$('table.context_viewlet_signers_table').each(setoddeven);</script>
3840
</div>
3941
</div>
4042
</div>

src/imio/esign/browser/views.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -252,27 +252,43 @@ def get_state_description(self, state):
252252

253253

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

257257
def available(self):
258258
"""Global availability of the viewlet."""
259259
return True
260260

261-
def render(self):
262-
"""Render the viewlet."""
263-
if self.session:
264-
return self.index()
265-
return ""
266-
267261
@property
268-
def session(self):
262+
def sessions(self):
263+
"""Return all sessions that contain files from this context."""
269264
annot = get_session_annotation()
265+
result = []
266+
seen = set()
270267
for f_uid in annot["c_uids"].get(self.context.UID(), []):
271-
if f_uid in annot["uids"]:
272-
session = annot["sessions"].get(annot["uids"][f_uid], {})
273-
session["id"] = annot["uids"][f_uid]
274-
return session
275-
return {}
268+
session_id = annot["uids"].get(f_uid)
269+
if session_id is not None and session_id not in seen:
270+
seen.add(session_id)
271+
session = dict(annot["sessions"].get(session_id, {}))
272+
session["id"] = session_id
273+
result.append(session)
274+
return result
275+
276+
@property
277+
def session(self):
278+
"""Current session during render loop; also used by base template."""
279+
return getattr(self, "_current_session", None) or {}
280+
281+
def render(self):
282+
"""Render viewlet once per session linked to this context."""
283+
sessions = self.sessions
284+
if not sessions:
285+
return ""
286+
parts = []
287+
for s in sessions:
288+
self._current_session = s
289+
parts.append(self.index())
290+
self._current_session = None
291+
return "\n".join(parts)
276292

277293

278294
@implementer(IPublishTraverse)

src/imio/esign/tests/test_browser_views.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from datetime import timedelta
77
from imio.esign.browser.views import DownloadFileView
88
from imio.esign.browser.views import ExternalSessionCreateView
9+
from imio.esign.browser.views import ItemSessionInfoViewlet
910
from imio.esign.browser.views import SessionDeleteView
1011
from imio.esign.testing import IMIO_ESIGN_FUNCTIONAL_TESTING
1112
from imio.esign.testing import IMIO_ESIGN_INTEGRATION_TESTING
@@ -393,3 +394,96 @@ def test_download_file_view(self):
393394
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)
394395
browser.open("{}/download-file/{}".format(portal_url, "aabbccddee?param=value"))
395396
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)
397+
398+
399+
class TestItemSessionInfoViewlet(unittest.TestCase):
400+
"""Test ItemSessionInfoViewlet multi-session support."""
401+
402+
layer = IMIO_ESIGN_INTEGRATION_TESTING
403+
404+
def setUp(self):
405+
self.portal = self.layer["portal"]
406+
self.request = self.portal.REQUEST
407+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
408+
at_folder = api.content.create(
409+
container=self.portal, id="annexes_types", title="Annexes Types",
410+
type="ContentCategoryConfiguration", exclude_from_nav=True,
411+
)
412+
category_group = api.content.create(
413+
type="ContentCategoryGroup", title="Annexes",
414+
container=at_folder, id="annexes",
415+
)
416+
icon_path = os.path.join(
417+
os.path.dirname(collective.iconifiedcategory.__file__), "tests", u"ic\xf4ne1.png"
418+
)
419+
with open(icon_path, "rb") as fl:
420+
api.content.create(
421+
type="ContentCategory", title="To sign",
422+
container=category_group,
423+
icon=NamedBlobImage(fl.read(), filename=u"ic\xf4ne1.png"),
424+
id="to_sign", predefined_title="To be signed",
425+
to_sign=True, show_preview=False,
426+
)
427+
api.user.create(email="user1@sign.com", username="user1", password="password1")
428+
self.folder = api.content.create(
429+
container=self.portal, type="Folder",
430+
id="test_folder", title="Test Folder",
431+
)
432+
tests_dir = os.path.dirname(__file__)
433+
self.annexes = []
434+
for i in range(2):
435+
with open(os.path.join(tests_dir, "annex1.pdf"), "rb") as f:
436+
annex = api.content.create(
437+
container=self.folder, type="annex",
438+
id="annex{}".format(i), title="Annex {}".format(i),
439+
content_category=calculate_category_id(
440+
self.portal["annexes_types"]["annexes"]["to_sign"]
441+
),
442+
scan_id="0123456000000{:02d}".format(i),
443+
file=NamedBlobFile(
444+
data=f.read(), filename=u"annex{}.pdf".format(i),
445+
contentType="application/pdf",
446+
),
447+
)
448+
self.annexes.append(annex)
449+
self.signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
450+
for key in list(self.request.form.keys()):
451+
del self.request.form[key]
452+
453+
def test_sessions_empty(self):
454+
"""No files in esign annotation → sessions returns empty list."""
455+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
456+
self.assertEqual(viewlet.sessions, [])
457+
self.assertEqual(viewlet.render(), "")
458+
459+
def test_sessions_single_session(self):
460+
"""All context files in one session → sessions returns one dict."""
461+
uids = [a.UID() for a in self.annexes]
462+
add_files_to_session(self.signers, uids)
463+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
464+
sessions = viewlet.sessions
465+
self.assertEqual(len(sessions), 1)
466+
self.assertEqual(sessions[0]["id"], 0)
467+
468+
def test_sessions_multiple_sessions(self):
469+
"""Files in two sessions (different discriminators) → sessions returns two dicts."""
470+
add_files_to_session(self.signers, [self.annexes[0].UID()], discriminators=("a",))
471+
add_files_to_session(self.signers, [self.annexes[1].UID()], discriminators=("b",))
472+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
473+
sessions = viewlet.sessions
474+
self.assertEqual(len(sessions), 2)
475+
session_ids = {s["id"] for s in sessions}
476+
self.assertEqual(session_ids, {0, 1})
477+
478+
rendered_ids = []
479+
def mock_index():
480+
rendered_ids.append(viewlet._current_session["id"])
481+
return u"<div>session {}</div>".format(viewlet._current_session["id"])
482+
viewlet.index = mock_index
483+
result = viewlet.render()
484+
self.assertEqual(len(rendered_ids), 2)
485+
self.assertIn(0, rendered_ids)
486+
self.assertIn(1, rendered_ids)
487+
self.assertIn(u"<div>session 0</div>", result)
488+
self.assertIn(u"<div>session 1</div>", result)
489+
self.assertIsNone(viewlet._current_session)

0 commit comments

Comments
 (0)