Skip to content

Commit f891418

Browse files
committed
Handled multiple sessions for one context
1 parent 78b4e46 commit f891418

File tree

5 files changed

+139
-23
lines changed

5 files changed

+139
-23
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Changelog
2323
[sgeulette]
2424
- Style sessions state-column to display a question circle.
2525
[gbastien]
26+
- Added possibility to have elements of the same context to belong to different sessions.
27+
[chris-adam]
2628

2729
1.0a2 (2026-02-06)
2830
------------------

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
@@ -244,27 +244,43 @@ def get_state_description(self, state):
244244

245245

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

249249
def available(self):
250250
"""Global availability of the viewlet."""
251251
return True
252252

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

269285

270286
@implementer(IPublishTraverse)

src/imio/esign/tests/test_browser_views.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# -*- coding: utf-8 -*-
22
"""Browser views tests for this package."""
3+
from collective.iconifiedcategory.utils import calculate_category_id
34
from datetime import datetime
45
from datetime import timedelta
56
from imio.esign.browser.views import DownloadFileView
7+
from imio.esign.browser.views import ItemSessionInfoViewlet
68
from imio.esign.testing import IMIO_ESIGN_FUNCTIONAL_TESTING
9+
from imio.esign.testing import IMIO_ESIGN_INTEGRATION_TESTING
10+
from imio.esign.utils import add_files_to_session
711
from imio.pyutils.utils import shortuid_encode_id
812
from plone import api
913
from plone.app.testing import logout
@@ -158,3 +162,95 @@ def test_download_file_view(self):
158162
browser.open("{}/download-file/{}".format(portal_url, "aabbccddee?param=value"))
159163
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)
160164

165+
166+
class TestItemSessionInfoViewlet(unittest.TestCase):
167+
"""Test ItemSessionInfoViewlet multi-session support."""
168+
169+
layer = IMIO_ESIGN_INTEGRATION_TESTING
170+
171+
def setUp(self):
172+
self.portal = self.layer["portal"]
173+
self.request = self.portal.REQUEST
174+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
175+
at_folder = api.content.create(
176+
container=self.portal, id="annexes_types", title="Annexes Types",
177+
type="ContentCategoryConfiguration", exclude_from_nav=True,
178+
)
179+
category_group = api.content.create(
180+
type="ContentCategoryGroup", title="Annexes",
181+
container=at_folder, id="annexes",
182+
)
183+
icon_path = os.path.join(
184+
os.path.dirname(collective.iconifiedcategory.__file__), "tests", u"ic\xf4ne1.png"
185+
)
186+
with open(icon_path, "rb") as fl:
187+
api.content.create(
188+
type="ContentCategory", title="To sign",
189+
container=category_group,
190+
icon=NamedBlobImage(fl.read(), filename=u"ic\xf4ne1.png"),
191+
id="to_sign", predefined_title="To be signed",
192+
to_sign=True, show_preview=False,
193+
)
194+
api.user.create(email="user1@sign.com", username="user1", password="password1")
195+
self.folder = api.content.create(
196+
container=self.portal, type="Folder",
197+
id="test_folder", title="Test Folder",
198+
)
199+
tests_dir = os.path.dirname(__file__)
200+
self.annexes = []
201+
for i in range(2):
202+
with open(os.path.join(tests_dir, "annex1.pdf"), "rb") as f:
203+
annex = api.content.create(
204+
container=self.folder, type="annex",
205+
id="annex{}".format(i), title="Annex {}".format(i),
206+
content_category=calculate_category_id(
207+
self.portal["annexes_types"]["annexes"]["to_sign"]
208+
),
209+
scan_id="0123456000000{:02d}".format(i),
210+
file=NamedBlobFile(
211+
data=f.read(), filename=u"annex{}.pdf".format(i),
212+
contentType="application/pdf",
213+
),
214+
)
215+
self.annexes.append(annex)
216+
self.signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
217+
for key in list(self.request.form.keys()):
218+
del self.request.form[key]
219+
220+
def test_sessions_empty(self):
221+
"""No files in esign annotation → sessions returns empty list."""
222+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
223+
self.assertEqual(viewlet.sessions, [])
224+
self.assertEqual(viewlet.render(), "")
225+
226+
def test_sessions_single_session(self):
227+
"""All context files in one session → sessions returns one dict."""
228+
uids = [a.UID() for a in self.annexes]
229+
add_files_to_session(self.signers, uids)
230+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
231+
sessions = viewlet.sessions
232+
self.assertEqual(len(sessions), 1)
233+
self.assertEqual(sessions[0]["id"], 0)
234+
235+
def test_sessions_multiple_sessions(self):
236+
"""Files in two sessions (different discriminators) → sessions returns two dicts."""
237+
add_files_to_session(self.signers, [self.annexes[0].UID()], discriminators=("a",))
238+
add_files_to_session(self.signers, [self.annexes[1].UID()], discriminators=("b",))
239+
viewlet = ItemSessionInfoViewlet(self.folder, self.request, None, None)
240+
sessions = viewlet.sessions
241+
self.assertEqual(len(sessions), 2)
242+
session_ids = {s["id"] for s in sessions}
243+
self.assertEqual(session_ids, {0, 1})
244+
245+
rendered_ids = []
246+
def mock_index():
247+
rendered_ids.append(viewlet._current_session["id"])
248+
return u"<div>session {}</div>".format(viewlet._current_session["id"])
249+
viewlet.index = mock_index
250+
result = viewlet.render()
251+
self.assertEqual(len(rendered_ids), 2)
252+
self.assertIn(0, rendered_ids)
253+
self.assertIn(1, rendered_ids)
254+
self.assertIn(u"<div>session 0</div>", result)
255+
self.assertIn(u"<div>session 1</div>", result)
256+
self.assertIsNone(viewlet._current_session)

0 commit comments

Comments
 (0)