Skip to content

Commit 76b0cbd

Browse files
authored
Merge pull request #15 from IMIO/PARAF-345/1_context_multiple_sessions
Handled multiple sessions for one context
2 parents 42311dc + 5966478 commit 76b0cbd

File tree

7 files changed

+180
-44
lines changed

7 files changed

+180
-44
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<tr metal:define-macro="session_id" i18n:domain="imio.esign">
22
<td class="table_widget_label"><label i18n:translate="">Session ID</label></td>
33
<td class="table_widget_value" tal:define="can_link python:view.can_display_sessions_listing_link()">
4-
<span tal:condition="not: can_link" tal:content="python: session['id']">25452</span>
4+
<span tal:condition="not: can_link" tal:content="python: session_id">25452</span>
55
<a tal:condition="can_link" href="#" target="_blank"
6-
tal:content="python: session['id']"
7-
tal:attributes="href python:'{}#{}'.format(view.session_listing_url, session['id'])">25452 - College</a>
6+
tal:content="python: session_id"
7+
tal:attributes="href python:'{}#{}'.format(view.session_listing_url, session_id)">25452 - College</a>
88
</td>
99
</tr>
1010
<tr metal:define-macro="state" i18n:domain="imio.esign">
@@ -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="context_viewlet_signers_table no-border no-style-table listing"
4444
tal:define="signers python:session.get('signers', [])">
4545
<thead>
4646
<tr>

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

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,37 @@
1212
</div>
1313
<div id="collapsible-session-info"
1414
tal:attributes="class python: view.collapsible_content_css_default()">
15-
<div class="collapsible-inner-content session-info-container"
16-
tal:define="template python: context.unrestrictedTraverse('@@esign-macros').index;
17-
session python: view.session">
18-
<div class="session-info-column">
19-
<table id="context_viewlet_session_table" class="no-style-table table-view-widgets">
20-
<tbody>
21-
<tal:block tal:repeat="macro_name python:view.get_table_rows(1)">
22-
<metal:render_cell use-macro="python: template.macros[macro_name]" />
23-
</tal:block>
24-
</tbody>
25-
</table>
26-
</div>
15+
<tal:loop repeat="session_infos python:view.sessions.items()">
16+
<div class="collapsible-inner-content session-info-container"
17+
tal:attributes="class python:'collapsible-inner-content session-info-container ' + classOddEven"
18+
tal:define="oddrow repeat/session_infos/odd;
19+
classOddEven python: oddrow and 'even' or 'odd';
20+
template python: context.unrestrictedTraverse('@@esign-macros').index">
21+
<tal:session_defines define="session_id python: session_infos[0];
22+
session python: session_infos[1]">
23+
<div class="session-info-column">
24+
<table tal:attributes="id string:context_viewlet_session_table_${session_id}" class="no-style-table table-view-widgets">
25+
<tbody>
26+
<tal:block tal:repeat="macro_name python:view.get_table_rows(1)">
27+
<metal:render_cell use-macro="python: template.macros[macro_name]" />
28+
</tal:block>
29+
</tbody>
30+
</table>
31+
</div>
2732

28-
<div class="session-info-column">
29-
<table id="context_viewlet_session_signers" class="no-style-table table-view-widgets">
30-
<tbody>
31-
<tal:block tal:repeat="macro_name python:view.get_table_rows(2)">
32-
<metal:render_cell use-macro="python: template.macros[macro_name]" />
33-
</tal:block>
34-
</tbody>
35-
</table>
33+
<div class="session-info-column">
34+
<table tal:attributes="id string:context_viewlet_session_signers_${session_id}" class="no-style-table table-view-widgets">
35+
<tbody>
36+
<tal:block tal:repeat="macro_name python:view.get_table_rows(2)">
37+
<metal:render_cell use-macro="python: template.macros[macro_name]" />
38+
</tal:block>
39+
</tbody>
40+
</table>
41+
</div>
42+
</tal:session_defines>
3643
</div>
37-
<script>$('table#context_viewlet_signers_table').each(setoddeven);</script>
38-
</div>
44+
</tal:loop>
45+
<script>$('table.context_viewlet_signers_table').each(setoddeven);</script>
3946
</div>
4047
</div>
4148

src/imio/esign/browser/views.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
from AccessControl import Unauthorized
4+
from copy import deepcopy
45
from datetime import datetime
56
from datetime import timedelta
67
from imio.esign import _
@@ -12,6 +13,7 @@
1213
from imio.esign.config import get_registry_signing_users_email_content
1314
from imio.esign.utils import create_external_session
1415
from imio.esign.utils import get_session_annotation
16+
from imio.esign.utils import get_sessions_for
1517
from imio.esign.utils import get_state_description
1618
from imio.esign.utils import remove_session
1719
from imio.helpers.content import uuidToObject
@@ -206,23 +208,25 @@ def sessions_collection_uid(self):
206208
def render(self):
207209
"""Render the viewlet."""
208210
if self.request.form.get("c1[]", None) == self.sessions_collection_uid:
209-
if self.session:
211+
if self.sessions:
210212
return self.index()
211213
return self.sessions_listing_view(self.context, self.request).render_table()
212214
return ""
213215

214216
@property
215-
def session(self):
216-
session = None
217+
def sessions(self):
217218
session_id = self.request.form.get("esign_session_id[]", None)
218-
if not session_id:
219-
return
219+
try:
220+
session_id = int(session_id)
221+
except (TypeError, ValueError):
222+
return []
220223
sessions = get_session_annotation()["sessions"]
221-
session = sessions.get(int(session_id))
224+
session = sessions.get(session_id)
222225
if not session:
223-
return
226+
return []
227+
session = deepcopy(session)
224228
session["id"] = session_id
225-
return session
229+
return [session]
226230

227231
def get_table_rows(self, column):
228232
"""Get the table rows following the column"""
@@ -252,27 +256,22 @@ def get_state_description(self, state):
252256

253257

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

257261
def available(self):
258262
"""Global availability of the viewlet."""
259263
return True
260264

261265
def render(self):
262266
"""Render the viewlet."""
263-
if self.session:
267+
if self.sessions:
264268
return self.index()
265269
return ""
266270

267271
@property
268-
def session(self):
269-
annot = get_session_annotation()
270-
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 {}
272+
def sessions(self):
273+
"""Return all sessions that contain files from this context."""
274+
return get_sessions_for(self.context.UID())
276275

277276

278277
@implementer(IPublishTraverse)

src/imio/esign/tests/test_browser_views.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# -*- coding: utf-8 -*-
22
"""Browser views tests for this package."""
33
from AccessControl import Unauthorized
4+
from collections import OrderedDict
45
from collective.iconifiedcategory.utils import calculate_category_id
56
from datetime import datetime
67
from datetime import timedelta
78
from imio.esign.browser.views import DownloadFileView
89
from imio.esign.browser.views import ExternalSessionCreateView
10+
from imio.esign.browser.views import ItemSessionInfoViewlet
911
from imio.esign.browser.views import SessionDeleteView
1012
from imio.esign.browser.views import SigningUsersCsv
1113
from imio.esign.config import set_registry_signing_users_email_content
@@ -673,3 +675,84 @@ def test_render_email_content(self):
673675
view = SigningUsersCsv(self.portal, self.request)
674676
result = view._render_email_content(template, user_data)
675677
self.assertEqual(result, u"<p>John Smith</p>")
678+
679+
680+
class TestItemSessionInfoViewlet(unittest.TestCase):
681+
"""Test ItemSessionInfoViewlet multi-session support."""
682+
683+
layer = IMIO_ESIGN_INTEGRATION_TESTING
684+
685+
def setUp(self):
686+
self.portal = self.layer["portal"]
687+
self.request = self.portal.REQUEST
688+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
689+
at_folder = api.content.create(
690+
container=self.portal, id="annexes_types", title="Annexes Types",
691+
type="ContentCategoryConfiguration", exclude_from_nav=True,
692+
)
693+
category_group = api.content.create(
694+
type="ContentCategoryGroup", title="Annexes",
695+
container=at_folder, id="annexes",
696+
)
697+
icon_path = os.path.join(
698+
os.path.dirname(collective.iconifiedcategory.__file__), "tests", u"ic\xf4ne1.png"
699+
)
700+
with open(icon_path, "rb") as fl:
701+
api.content.create(
702+
type="ContentCategory", title="To sign",
703+
container=category_group,
704+
icon=NamedBlobImage(fl.read(), filename=u"ic\xf4ne1.png"),
705+
id="to_sign", predefined_title="To be signed",
706+
to_sign=True, show_preview=False,
707+
)
708+
api.user.create(email="user1@sign.com", username="user1", password="password1")
709+
self.folder = api.content.create(
710+
container=self.portal, type="Folder",
711+
id="test_folder", title="Test Folder",
712+
)
713+
tests_dir = os.path.dirname(__file__)
714+
self.annexes = []
715+
for i in range(2):
716+
with open(os.path.join(tests_dir, "annex1.pdf"), "rb") as f:
717+
annex = api.content.create(
718+
container=self.folder, type="annex",
719+
id="annex{}".format(i), title="Annex {}".format(i),
720+
content_category=calculate_category_id(
721+
self.portal["annexes_types"]["annexes"]["to_sign"]
722+
),
723+
scan_id="0123456000000{:02d}".format(i),
724+
file=NamedBlobFile(
725+
data=f.read(), filename=u"annex{}.pdf".format(i),
726+
contentType="application/pdf",
727+
),
728+
)
729+
self.annexes.append(annex)
730+
self.signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
731+
for key in list(self.request.form.keys()):
732+
del self.request.form[key]
733+
734+
def test_sessions_empty(self):
735+
"""No files in esign annotation → sessions returns empty list."""
736+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
737+
self.assertEqual(viewlet.sessions, OrderedDict())
738+
self.assertEqual(viewlet.render(), "")
739+
740+
def test_sessions_single_session(self):
741+
"""All context files in one session → sessions returns one dict."""
742+
uids = [a.UID() for a in self.annexes]
743+
add_files_to_session(self.signers, uids)
744+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
745+
sessions = viewlet.sessions
746+
self.assertEqual(len(sessions), 1)
747+
self.assertEqual(sessions.keys(), [0])
748+
self.assertEqual(len(sessions[0]["files"]), len(uids))
749+
750+
def test_sessions_multiple_sessions(self):
751+
"""Files in two sessions (different discriminators) → sessions returns two dicts."""
752+
add_files_to_session(self.signers, [self.annexes[0].UID()], discriminators=("a",))
753+
add_files_to_session(self.signers, [self.annexes[1].UID()], discriminators=("b",))
754+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
755+
sessions = viewlet.sessions
756+
self.assertEqual(len(sessions), 2)
757+
session_ids = sessions.keys()
758+
self.assertEqual(session_ids, [0, 1])

src/imio/esign/tests/test_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
"""utils tests for this package."""
3+
from collections import OrderedDict
34
from collective.iconifiedcategory.utils import calculate_category_id
45
from datetime import date
56
from datetime import timedelta
@@ -14,6 +15,7 @@
1415
from imio.esign.utils import get_max_download_date
1516
from imio.esign.utils import get_session_annotation
1617
from imio.esign.utils import get_session_info
18+
from imio.esign.utils import get_sessions_for
1719
from imio.esign.utils import get_suid_from_uuid
1820
from imio.esign.utils import remove_context_from_session
1921
from imio.esign.utils import remove_files_from_session
@@ -502,6 +504,30 @@ def test_get_session_info(self):
502504
sid, session = add_files_to_session(signers, (self.uids[0], self.uids[1]))
503505
self.assertEqual(get_session_info(sid), session)
504506

507+
def test_get_sessions_for(self):
508+
"""Test getting sessions for a given context_uid."""
509+
# no session
510+
context_uid = self.folders[0].UID()
511+
self.assertEqual(get_sessions_for(context_uid), OrderedDict())
512+
# one session
513+
signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
514+
sid, session = add_files_to_session(signers, (self.uids[0],))
515+
self.assertEqual(get_sessions_for(context_uid).keys(), [0])
516+
# two sessions
517+
signers = [("user2", "user2@sign.com", "User 2", "Position 2")]
518+
sid, session = add_files_to_session(signers, (self.uids[2],))
519+
self.assertEqual(get_sessions_for(context_uid).keys(), [0, 1])
520+
# readonly=True
521+
sessions = get_sessions_for(context_uid)
522+
self.assertEqual(get_session_info(0)['watchers'], [])
523+
sessions[0]['watchers'] = ["watcher@sign.com"]
524+
self.assertEqual(get_session_info(0)['watchers'], [])
525+
# readonly=False
526+
sessions = get_sessions_for(context_uid, readonly=False)
527+
self.assertEqual(get_session_info(0)['watchers'], [])
528+
sessions[0]['watchers'] = ["watcher@sign.com"]
529+
self.assertEqual(get_session_info(0)['watchers'], ["watcher@sign.com"])
530+
505531
def test_get_file_download_url(self):
506532
"""Test generating file download URL from UID."""
507533
uid = "f40682caafc045b4b81973bd82ea9ab6"

src/imio/esign/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# -*- coding: utf-8 -*-
2+
3+
from collections import OrderedDict
4+
from copy import deepcopy
25
from datetime import datetime
36
from datetime import timedelta
47
from imio.esign import _tr as _
@@ -516,3 +519,19 @@ def get_state_description(state):
516519
'returned': u'The session is finished and signed documents are on the way back to the application.',
517520
'finalized': u'The session is finished and signed documents have been sent back to the application.',
518521
}.get(state, "")
522+
523+
524+
def get_sessions_for(context_uid, readonly=True):
525+
"""Returns a list of all sessions involving the provided context_uid"""
526+
annot = get_session_annotation()
527+
sessions = OrderedDict()
528+
seen = set()
529+
for f_uid in annot["c_uids"].get(context_uid, []):
530+
session_id = annot["uids"].get(f_uid)
531+
if session_id is not None and session_id not in seen:
532+
seen.add(session_id)
533+
session = annot["sessions"][session_id]
534+
if readonly:
535+
session = deepcopy(session)
536+
sessions[session_id] = session
537+
return sessions

0 commit comments

Comments
 (0)