Skip to content

Commit 02dbf92

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

9 files changed

Lines changed: 478 additions & 0 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: 72 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,77 @@ def available(self):
149150
return self.context.UID() in annot.get("uids", {})
150151

151152

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

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.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
/* Add-to-custom-esign-session overlay table */
2+
#add-to-custom-esign-session-form .sessions-table {
3+
table-layout: fixed;
4+
width: 100%;
5+
word-wrap: break-word;
6+
overflow-wrap: break-word;
7+
}
8+
9+
#add-to-custom-esign-session-form .th_header_sessions_radio { width: 4%; }
10+
#add-to-custom-esign-session-form .th_header_sessions_id { width: 6%; }
11+
#add-to-custom-esign-session-form .th_header_sessions_title { width: 25%; }
12+
#add-to-custom-esign-session-form .th_header_sessions_signers { width: 30%; }
13+
#add-to-custom-esign-session-form .th_header_sessions_seal { width: 7%; }
14+
#add-to-custom-esign-session-form .th_header_sessions_documents { width: 28%; }
15+
16+
#add-to-custom-esign-session-form .signers-column ol {
17+
margin: 0;
18+
padding-left: 1.2em;
19+
}
20+
21+
#add-to-custom-esign-session-form .documents-column .collapsible {
22+
white-space: normal;
23+
}
24+
25+
/* Create-custom-session form */
126
.create-custom-session .fieldset-level-1 {
227
margin-bottom: 0;
328
}

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

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

src/imio/esign/browser/table.py

Lines changed: 56 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,58 @@ 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):
298+
super(FilteredSessionsTable, self).__init__(context, request)
299+
self.view = view
300+
self.portal_url = api.portal.get().absolute_url()
301+
self._items = None
302+
303+
def filter_session(self, session):
304+
return session.get("state") == "draft"
305+
306+
@property
307+
def values(self):
308+
if self._items is not None:
309+
return self._items
310+
annot = get_session_annotation()
311+
result = []
312+
for session_id, session in sorted(annot.get("sessions", {}).items(), reverse=True):
313+
if not self.filter_session(session):
314+
continue
315+
s = dict(session)
316+
s["id"] = session_id
317+
result.append(s)
318+
return result
319+
320+
def setUpColumns(self):
321+
ctx, req, tbl = self.context, self.request, self
322+
columns = [
323+
RadioColumn(ctx, req, tbl),
324+
IdColumn(ctx, req, tbl),
325+
TitleColumn(ctx, req, tbl),
326+
SignersColumn(ctx, req, tbl),
327+
FilesColumn(ctx, req, tbl),
328+
]
329+
if get_esign_registry_seal_code() and get_esign_registry_seal_email():
330+
columns.append(SealColumn(ctx, req, tbl))
331+
return columns
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
<form method="POST" id="add-to-custom-esign-session-form"
21+
tal:attributes="action string:${context/absolute_url}/@@add-to-custom-esign-session">
22+
23+
<tal:has condition="view/has_sessions">
24+
<div tal:replace="structure view/render_table" />
25+
</tal:has>
26+
<tal:no condition="not:view/has_sessions">
27+
<p class="discreet" i18n:translate="">
28+
No draft sessions available.
29+
</p>
30+
</tal:no>
31+
32+
<div class="formControls" style="margin-top:1em">
33+
<input type="submit" class="context" name="form.buttons.submit"
34+
value="Add to session" disabled="disabled"
35+
id="esign-submit-btn"
36+
i18n:attributes="value" />
37+
<input type="button" class="standalone" name="form.buttons.cancel"
38+
value="Cancel"
39+
i18n:attributes="value" />
40+
</div>
41+
42+
<div style="margin-top:1em; text-align:center">
43+
<a class="esign-create-custom-session-from-overlay"
44+
tal:attributes="href string:${view/portal_url}/@@create-custom-session"
45+
i18n:translate="">
46+
Create new session
47+
</a>
48+
</div>
49+
50+
</form>
51+
52+
</metal:content-core>
53+
54+
</body>
55+
</html>

0 commit comments

Comments
 (0)