Skip to content
Merged
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 @@ -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
8 changes: 4 additions & 4 deletions src/imio/esign/browser/templates/macros.pt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<tr metal:define-macro="session_id" i18n:domain="imio.esign">
<td class="table_widget_label"><label i18n:translate="">Session ID</label></td>
<td class="table_widget_value" tal:define="can_link python:view.can_display_sessions_listing_link()">
<span tal:condition="not: can_link" tal:content="python: session['id']">25452</span>
<span tal:condition="not: can_link" tal:content="python: session_id">25452</span>
<a tal:condition="can_link" href="#" target="_blank"
tal:content="python: session['id']"
tal:attributes="href python:'{}#{}'.format(view.session_listing_url, session['id'])">25452 - College</a>
tal:content="python: session_id"
tal:attributes="href python:'{}#{}'.format(view.session_listing_url, session_id)">25452 - College</a>
</td>
</tr>
<tr metal:define-macro="state" i18n:domain="imio.esign">
Expand Down 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
51 changes: 29 additions & 22 deletions src/imio/esign/browser/templates/session_info.pt
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,37 @@
</div>
<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">
<div class="session-info-column">
<table id="context_viewlet_session_table" 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]" />
</tal:block>
</tbody>
</table>
</div>
<tal:loop repeat="session_infos python:view.sessions.items()">
<div class="collapsible-inner-content session-info-container"
tal:attributes="class python:'collapsible-inner-content session-info-container ' + classOddEven"
tal:define="oddrow repeat/session_infos/odd;
classOddEven python: oddrow and 'even' or 'odd';
template python: context.unrestrictedTraverse('@@esign-macros').index">
<tal:session_defines define="session_id python: session_infos[0];
session python: session_infos[1]">
<div class="session-info-column">
<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]" />
</tal:block>
</tbody>
</table>
</div>

<div class="session-info-column">
<table id="context_viewlet_session_signers" 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 class="session-info-column">
<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>
</tal:session_defines>
</div>
<script>$('table#context_viewlet_signers_table').each(setoddeven);</script>
</div>
</tal:loop>
<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
83 changes: 83 additions & 0 deletions src/imio/esign/tests/test_browser_views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
"""Browser views tests for this package."""
from AccessControl import Unauthorized
from collections import OrderedDict
from collective.iconifiedcategory.utils import calculate_category_id
from datetime import datetime
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.browser.views import SigningUsersCsv
from imio.esign.config import set_registry_signing_users_email_content
Expand Down Expand Up @@ -673,3 +675,84 @@ def test_render_email_content(self):
view = SigningUsersCsv(self.portal, self.request)
result = view._render_email_content(template, user_data)
self.assertEqual(result, u"<p>John Smith</p>")


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, OrderedDict())
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.keys(), [0])
self.assertEqual(len(sessions[0]["files"]), len(uids))

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 = sessions.keys()
self.assertEqual(session_ids, [0, 1])
26 changes: 26 additions & 0 deletions src/imio/esign/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""utils tests for this package."""
from collections import OrderedDict
from collective.iconifiedcategory.utils import calculate_category_id
from datetime import date
from datetime import timedelta
Expand All @@ -14,6 +15,7 @@
from imio.esign.utils import get_max_download_date
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_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 @@ -502,6 +504,30 @@ def test_get_session_info(self):
sid, session = add_files_to_session(signers, (self.uids[0], self.uids[1]))
self.assertEqual(get_session_info(sid), session)

def test_get_sessions_for(self):
"""Test getting sessions for a given context_uid."""
# no session
context_uid = self.folders[0].UID()
self.assertEqual(get_sessions_for(context_uid), OrderedDict())
# one session
signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
sid, session = add_files_to_session(signers, (self.uids[0],))
self.assertEqual(get_sessions_for(context_uid).keys(), [0])
# two sessions
signers = [("user2", "user2@sign.com", "User 2", "Position 2")]
sid, session = add_files_to_session(signers, (self.uids[2],))
self.assertEqual(get_sessions_for(context_uid).keys(), [0, 1])
# readonly=True
sessions = get_sessions_for(context_uid)
self.assertEqual(get_session_info(0)['watchers'], [])
sessions[0]['watchers'] = ["watcher@sign.com"]
self.assertEqual(get_session_info(0)['watchers'], [])
# readonly=False
sessions = get_sessions_for(context_uid, readonly=False)
self.assertEqual(get_session_info(0)['watchers'], [])
sessions[0]['watchers'] = ["watcher@sign.com"]
self.assertEqual(get_session_info(0)['watchers'], ["watcher@sign.com"])

def test_get_file_download_url(self):
"""Test generating file download URL from UID."""
uid = "f40682caafc045b4b81973bd82ea9ab6"
Expand Down
19 changes: 19 additions & 0 deletions src/imio/esign/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-

from collections import OrderedDict
from copy import deepcopy
from datetime import datetime
from datetime import timedelta
from imio.esign import _tr as _
Expand Down Expand Up @@ -516,3 +519,19 @@ 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()
sessions = OrderedDict()
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)
sessions[session_id] = session
return sessions
Loading