Skip to content

Commit 593c386

Browse files
committed
Added action to add a item to a specific session WIP
1 parent 9039b53 commit 593c386

8 files changed

Lines changed: 333 additions & 3 deletions

File tree

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Changelog
1111
[chris-adam]
1212
- Added action to create a custom session.
1313
[chris-adam]
14+
- Added action to add a item to a specific session.
15+
[chris-adam]
1416

1517
1.0b8 (2026-05-08)
1618
------------------

src/imio/esign/browser/actions.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from imio.esign import _
55
from imio.esign.adapters import ISignable
66
from imio.esign.audit import audit
7+
from imio.esign.browser.table import FilteredSessionsTable
78
from imio.esign.utils import add_files_to_session
89
from imio.esign.utils import get_session_annotation
910
from imio.esign.utils import get_sessions_for
@@ -149,6 +150,87 @@ def available(self):
149150
return self.context.UID() in annot.get("uids", {})
150151

151152

153+
class AddToCustomEsignSessionView(BrowserView):
154+
"""Overlay view listing draft e-sign sessions with radio buttons,
155+
letting the user assign the current file to a chosen session."""
156+
157+
template = ViewPageTemplateFile("templates/add_to_custom_esign_session.pt")
158+
159+
def __call__(self):
160+
if self.request.method == "POST" and "form.buttons.submit" in self.request.form:
161+
return self.handle_submit()
162+
return self.template()
163+
164+
def render_table(self):
165+
table = FilteredSessionsTable(self.context, self, self.request)
166+
table.update()
167+
return table.render()
168+
169+
def has_draft_sessions(self):
170+
annot = get_session_annotation()
171+
return any(s.get("state") == "draft" for s in annot.get("sessions", {}).values())
172+
173+
def get_current_session(self):
174+
"""Return (session_id, session) if the file is already in a session, else (None, None)."""
175+
annot = get_session_annotation()
176+
session_id = annot.get("uids", {}).get(self.context.UID())
177+
if session_id is not None:
178+
session = annot["sessions"].get(session_id)
179+
if session:
180+
return session_id, session
181+
return None, None
182+
183+
def handle_submit(self):
184+
session_id_str = self.request.form.get("session_id")
185+
if not session_id_str:
186+
api.portal.show_message(
187+
_(u"No session selected!"), request=self.request, type="warning"
188+
)
189+
return self.template()
190+
try:
191+
session_id = int(session_id_str)
192+
except (ValueError, TypeError):
193+
api.portal.show_message(
194+
_(u"Invalid session!"), request=self.request, type="error"
195+
)
196+
return self.template()
197+
annot = get_session_annotation()
198+
session = annot["sessions"].get(session_id)
199+
if not session or session.get("state") != "draft":
200+
api.portal.show_message(
201+
_(u"Session not found or no longer draft!"),
202+
request=self.request,
203+
type="error",
204+
)
205+
return self.template()
206+
file_uid = self.context.UID()
207+
old_session_id = annot.get("uids", {}).get(file_uid)
208+
if old_session_id is not None and old_session_id != session_id:
209+
remove_files_from_session([file_uid])
210+
signers = [
211+
(s["userid"], s["email"], s["fullname"], s["position"])
212+
for s in session.get("signers", [])
213+
]
214+
add_files_to_session(
215+
signers=signers,
216+
files_uids=[file_uid],
217+
session_id=session_id,
218+
seal=session.get("seal"),
219+
title=session.get("title", ""),
220+
)
221+
api.portal.show_message(
222+
_(u"File added to session!"), request=self.request, type="info"
223+
)
224+
self.request.RESPONSE.redirect(self.context.absolute_url())
225+
226+
def available(self):
227+
return True
228+
229+
@property
230+
def portal_url(self):
231+
return api.portal.get().absolute_url()
232+
233+
152234
class SessionAnnotationInfoView(BrowserView):
153235
"""Admin-only view displaying imio.esign session annotations for a specific context item."""
154236

src/imio/esign/browser/configure.zcml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@
112112
allowed_attributes="available"
113113
/>
114114

115+
<browser:page
116+
name="add-to-custom-esign-session"
117+
for="*"
118+
class=".actions.AddToCustomEsignSessionView"
119+
permission="imio.esign.ManageSessions"
120+
i18n:domain="imio.esign"
121+
allowed_attributes="available"
122+
/>
123+
115124
<browser:page
116125
for="*"
117126
name="session-annotation-info"

src/imio/esign/browser/static/esign.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
jQuery(function($) {
2+
// Existing: create-custom-session overlay (on @@parapheo page)
23
$('a.esign-create-custom-session').prepOverlay({
34
subtype: 'ajax',
45
filter: '#content>*',
@@ -8,4 +9,60 @@ jQuery(function($) {
89
window.location.reload();
910
}
1011
});
12+
13+
// Add-to-custom-esign-session overlay (bound to actionspanel links)
14+
function initCustomSessionOverlays() {
15+
$('a.apButtonAction_form_add_to_custom_esign_session').each(function() {
16+
if ($(this).data('pbo') === undefined) {
17+
$(this).prepOverlay({
18+
subtype: 'ajax',
19+
filter: '#content>*',
20+
formselector: '#add-to-custom-esign-session-form',
21+
closeselector: '[name="form.buttons.cancel"]',
22+
noform: function(el, pbo) {
23+
window.location.reload();
24+
}
25+
});
26+
}
27+
});
28+
}
29+
initCustomSessionOverlays();
30+
$(document).ajaxComplete(initCustomSessionOverlays);
31+
32+
// After the choose-session overlay loads, bind the "Create new session"
33+
// link so it opens @@create-custom-session in a nested prepOverlay.
34+
// On successful creation, the outer overlay content is refetched and
35+
// the newest session is auto-selected.
36+
$(document).bind('loadInsideOverlay', function(e, el) {
37+
var $el = $(el);
38+
var $form = $el.find('#add-to-custom-esign-session-form');
39+
if (!$form.length) return;
40+
41+
var sessionListUrl = $form.attr('action');
42+
var $pbAjax = $el.hasClass('pb-ajax') ? $el : $el.find('.pb-ajax');
43+
if (!$pbAjax.length) $pbAjax = $el;
44+
45+
$el.find('a.esign-create-custom-session-from-overlay').each(function() {
46+
if ($(this).data('pbo') !== undefined) return;
47+
$(this).prepOverlay({
48+
subtype: 'ajax',
49+
filter: '#content>*',
50+
formselector: '#form',
51+
closeselector: '[name="form.buttons.cancel"]',
52+
noform: function(innerEl, innerPbo) {
53+
// Session created. Refetch the session list into the
54+
// outer overlay and auto-select the newest session.
55+
$pbAjax.load(
56+
sessionListUrl + '?ajax_load=' + (new Date().getTime()) + ' #content>*',
57+
function() {
58+
$pbAjax.find('input[name="session_id"]:last').prop('checked', true);
59+
// Re-initialise nested handlers inside the refreshed content
60+
$(document).trigger('loadInsideOverlay', [el]);
61+
}
62+
);
63+
return 'close';
64+
}
65+
});
66+
});
67+
});
1168
});

src/imio/esign/browser/table.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from imio.esign.config import get_esign_registry_max_session_size
77
from imio.esign.config import get_esign_registry_seal_code
88
from imio.esign.config import get_esign_registry_seal_email
9+
from imio.esign.utils import get_session_annotation
910
from imio.esign.utils import get_state_description
1011
from imio.helpers.security import check_zope_admin
1112
from imio.pyutils.utils import safe_encode
@@ -273,3 +274,60 @@ def setUpColumns(self):
273274
seal_col = SealColumn(ctx, req, tbl)
274275
columns.insert(4, seal_col)
275276
return columns
277+
278+
279+
class RadioColumn(Column):
280+
header = u""
281+
weight = 5
282+
cssClasses = {"th": "th_header_sessions_radio nosort",
283+
"td": "radio-column"}
284+
285+
def renderCell(self, item):
286+
sid = item.get("id")
287+
return u'<input type="radio" name="session_id" value="{0}" id="session-{0}" />'.format(sid)
288+
289+
290+
class FilteredSessionsTable(Table):
291+
cssClassEven = "even"
292+
cssClassOdd = "odd"
293+
cssClasses = {"table": "listing sessions-table width-full"}
294+
sortOn = None
295+
results = []
296+
297+
def __init__(self, context, view, request, items=None):
298+
super(FilteredSessionsTable, self).__init__(context, request)
299+
self.view = view
300+
self.portal_url = api.portal.get().absolute_url()
301+
self._items = items
302+
303+
def filter_session(self, session):
304+
if session.get("state") != "draft":
305+
return False
306+
return True
307+
308+
@property
309+
def values(self):
310+
if self._items is not None:
311+
return self._items
312+
annot = get_session_annotation()
313+
result = []
314+
for session_id, session in sorted(annot.get("sessions", {}).items()):
315+
if not self.filter_session(session):
316+
continue
317+
s = dict(session)
318+
s["id"] = session_id
319+
result.append(s)
320+
return result
321+
322+
def setUpColumns(self):
323+
ctx, req, tbl = self.context, self.request, self
324+
columns = [
325+
RadioColumn(ctx, req, tbl),
326+
IdColumn(ctx, req, tbl),
327+
TitleColumn(ctx, req, tbl),
328+
SignersColumn(ctx, req, tbl),
329+
FilesColumn(ctx, req, tbl),
330+
]
331+
if get_esign_registry_seal_code() and get_esign_registry_seal_email():
332+
columns.insert(4, SealColumn(ctx, req, tbl))
333+
return columns
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
2+
xmlns:tal="http://xml.zope.org/namespaces/tal"
3+
xmlns:metal="http://xml.zope.org/namespaces/metal"
4+
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5+
lang="en"
6+
metal:use-macro="context/main_template/macros/master"
7+
i18n:domain="imio.esign">
8+
<body>
9+
10+
<metal:custom_title fill-slot="content-title">
11+
<h1 class="documentFirstHeading" i18n:translate="">
12+
Choose an e-sign session
13+
</h1>
14+
</metal:custom_title>
15+
<metal:description fill-slot="content-description">
16+
</metal:description>
17+
18+
<metal:content-core fill-slot="content-core">
19+
20+
<style>
21+
#add-to-custom-esign-session-form .sessions-table { table-layout: fixed; width: 100%; }
22+
#add-to-custom-esign-session-form .radio-column { width: 30px; }
23+
#add-to-custom-esign-session-form .id-column { width: 40px; }
24+
#add-to-custom-esign-session-form .title-column { width: 30%; }
25+
#add-to-custom-esign-session-form .documents-column { width: 120px !important; }
26+
#add-to-custom-esign-session-form .seal-column { width: 50px; }
27+
</style>
28+
29+
<form method="POST" id="add-to-custom-esign-session-form"
30+
tal:attributes="action string:${context/absolute_url}/@@add-to-custom-esign-session">
31+
32+
<tal:already define="cur_info python:view.get_current_session()"
33+
condition="python:cur_info[0] is not None">
34+
<tal:warn define="cur_sid python:cur_info[0];
35+
cur_session python:cur_info[1];
36+
cur_title python:cur_session.get('title', '') or '-';
37+
cur_state python:cur_session.get('state', '')">
38+
<div class="portalMessage warning" role="alert">
39+
<strong i18n:translate="">Warning</strong>
40+
<span i18n:translate="">
41+
This file is currently in session #<span tal:replace="cur_sid" i18n:name="session_id" />
42+
(<span tal:replace="cur_title" i18n:name="title" />, <span tal:replace="cur_state" i18n:name="state" />).
43+
Selecting a different session will move the file.
44+
</span>
45+
</div>
46+
</tal:warn>
47+
</tal:already>
48+
49+
<tal:has condition="view/has_draft_sessions">
50+
<div tal:replace="structure view/render_table" />
51+
</tal:has>
52+
<tal:no condition="not:view/has_draft_sessions">
53+
<p class="discreet" i18n:translate="">
54+
No draft sessions available.
55+
</p>
56+
</tal:no>
57+
58+
<div class="formControls" style="margin-top:1em">
59+
<input type="submit" class="context" name="form.buttons.submit"
60+
value="Add to session"
61+
i18n:attributes="value" />
62+
<input type="button" class="standalone" name="form.buttons.cancel"
63+
value="Cancel"
64+
i18n:attributes="value" />
65+
</div>
66+
67+
<div style="margin-top:1em; text-align:center">
68+
<a class="esign-create-custom-session-from-overlay"
69+
tal:attributes="href string:${view/portal_url}/@@create-custom-session"
70+
i18n:translate="">
71+
Create new session
72+
</a>
73+
</div>
74+
75+
</form>
76+
77+
</metal:content-core>
78+
79+
</body>
80+
</html>

src/imio/esign/profiles/default/actions.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@
3535
</property>
3636
<property name="visible">True</property>
3737
</object>
38+
<!-- Headless by default. Downstream packages activate it by adding a visible action
39+
entry in the target content type's XML profile (e.g. dmsappendixfile.xml). -->
40+
<object name="add_to_custom_esign_session" meta_type="CMF Action" i18n:domain="imio.esign">
41+
<property name="title" i18n:translate="">Add to custom esign session</property>
42+
<property name="description" i18n:translate="">Choose a draft session to assign this file to</property>
43+
<property name="url_expr">string:${object/absolute_url}/@@add-to-custom-esign-session</property>
44+
<property name="icon_expr">string:$portal_url/++resource++imio.esign/add_to_esign_session.png</property>
45+
<property name="available_expr">object/@@add-to-custom-esign-session/available</property>
46+
<property name="permissions">
47+
<element value="Manage portal"/>
48+
</property>
49+
<property name="visible">False</property>
50+
</object>
3851
</object>
3952
<object name="portal_tabs" meta_type="CMF Action Category">
4053
<object name="parapheo" meta_type="CMF Action">

0 commit comments

Comments
 (0)