-
Notifications
You must be signed in to change notification settings - Fork 0
Added SessionAnnotationInfoView #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,22 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from AccessControl import Unauthorized | ||
| from html import escape | ||
| from imio.esign.utils import persistent_to_native | ||
| from imio.esign import _ | ||
| from imio.esign.adapters import ISignable | ||
| from imio.esign.utils import add_files_to_session | ||
| from imio.esign.utils import get_session_annotation | ||
| from imio.esign.utils import remove_context_from_session | ||
| from imio.esign.utils import remove_files_from_session | ||
| from imio.helpers.content import uuidToObject | ||
| from imio.helpers.security import check_zope_admin | ||
| from plone import api | ||
| from Products.CMFPlone.utils import safe_unicode | ||
| from Products.Five import BrowserView | ||
| from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile | ||
|
|
||
| import pprint | ||
| import re | ||
|
|
||
|
|
||
| class AddToSessionView(BrowserView): | ||
|
|
@@ -121,3 +131,67 @@ def available(self): | |
| """Defines if the action is available or not.""" | ||
| annot = get_session_annotation() | ||
| return self.context.UID() in annot.get("uids", {}) | ||
|
|
||
|
|
||
| class SessionAnnotationInfoView(BrowserView): | ||
| """Admin-only view displaying imio.esign session annotations for a specific context item.""" | ||
|
|
||
| index = ViewPageTemplateFile("templates/session_annotation_info.pt") | ||
|
|
||
| def __call__(self): | ||
| if not check_zope_admin(): | ||
| raise Unauthorized | ||
| return self.index() | ||
|
|
||
| def _uid_to_link(self, uid): | ||
| """Return an HTML link for an object UID, or the UID if not found.""" | ||
| obj = uuidToObject(uid, unrestricted=True) | ||
| if obj is None: | ||
| return u"<span title='not found'>{}</span>".format(safe_unicode(uid)) | ||
| url = escape(obj.absolute_url() + "/view", quote=True) | ||
| path = escape(u"/".join(obj.getPhysicalPath())) | ||
| title = escape(safe_unicode(getattr(obj, "title", "") or path)) | ||
| return u"<a href='{}' title='{}'>{}</a>".format(url, path, title) | ||
|
|
||
| def _render_value(self, value, indent=u""): | ||
| """Render a value, replacing UIDs with links where possible.""" | ||
| inner = indent + u" " | ||
| if isinstance(value, dict): | ||
| if not value: | ||
| return u"{}" | ||
| lines = [u"{"] | ||
| for k, v in sorted(value.items()): | ||
| key = escape(safe_unicode(pprint.pformat(k))) | ||
| lines.append(u"{}{}: {},".format(inner, key, self._render_value(v, inner))) | ||
| lines.append(u"{}}}".format(indent)) | ||
| return u"\n".join(lines) | ||
| elif isinstance(value, (list, tuple)): | ||
| if not value: | ||
| return u"[]" | ||
| lines = [u"["] | ||
| for item in value: | ||
| lines.append(u"{}{},".format(inner, self._render_value(item, inner))) | ||
| lines.append(u"{}]".format(indent)) | ||
| return u"\n".join(lines) | ||
| elif isinstance(value, basestring) and re.match(r"^[0-9a-f]{32}$", value): | ||
| # Looks like a UUID | ||
| return self._uid_to_link(value) | ||
| else: | ||
| return escape(safe_unicode(pprint.pformat(value))) | ||
|
|
||
| @property | ||
| def esign_sessions(self): | ||
| """Return list of (session_id, session_data) for all sessions.""" | ||
| annot = get_session_annotation() | ||
| c_uid = self.context.UID() | ||
| result = [] | ||
| for session_id in annot['sessions']: | ||
| session = annot.get("sessions", {}).get(session_id) | ||
| # If any file in this session is in this context | ||
| if any(f['context_uid'] == c_uid for f in session['files']): | ||
| result.append((session_id, persistent_to_native(session))) | ||
| return sorted(result) | ||
|
Comment on lines
+182
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
sed -n '115,121p' src/imio/esign/browser/configure.zcml
sed -n '179,190p' src/imio/esign/browser/actions.py
rg -n 'name="session-annotation-info"|for="\*"|UID\(' src/imio/esign/browser/configure.zcml src/imio/esign/browser/actions.pyRepository: IMIO/imio.esign Length of output: 1649 Guard contexts that do not expose This property assumes every traversal target has Suggested fix `@property`
def esign_sessions(self):
"""Return list of (session_id, session_data) for all sessions."""
+ uid_getter = getattr(self.context, "UID", None)
+ if uid_getter is None:
+ return []
annot = get_session_annotation()
- c_uid = self.context.UID()
+ c_uid = uid_getter()
result = []
- for session_id in annot['sessions']:
- session = annot.get("sessions", {}).get(session_id)
+ for session_id, session in annot.get("sessions", {}).items():
# If any file in this session is in this context
if any(f['context_uid'] == c_uid for f in session['files']):
result.append((session_id, persistent_to_native(session)))
return sorted(result)🤖 Prompt for AI Agents |
||
|
|
||
| def esign_session_html(self, session_data): | ||
| """Renders esign session annot in HTML""" | ||
| return self._render_value(session_data) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" | ||
| xmlns:tal="http://xml.zope.org/namespaces/tal" | ||
| xmlns:metal="http://xml.zope.org/namespaces/metal" | ||
| xmlns:i18n="http://xml.zope.org/namespaces/i18n" | ||
| metal:use-macro="context/main_template/macros/master" | ||
| i18n:domain="imio.esign"> | ||
| <body> | ||
| <metal:bodytext fill-slot="content-core"> | ||
| <tal:esign tal:repeat="session view/esign_sessions"> | ||
| <h2 tal:content="python:'imio.esign session (id: {})'.format(session[0])"></h2> | ||
| <pre style="background: #f5f5f5; color: #333; padding: 1em; overflow: auto;" | ||
| tal:content="structure python:view.esign_session_html(session[1])" /> | ||
| </tal:esign> | ||
| </metal:bodytext> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """actions tests for this package.""" | ||
| from AccessControl import Unauthorized | ||
| from collective.iconifiedcategory.utils import calculate_category_id | ||
| from imio.esign.browser.actions import SessionAnnotationInfoView | ||
| from imio.esign.testing import IMIO_ESIGN_INTEGRATION_TESTING | ||
| from imio.esign.utils import add_files_to_session | ||
| from imio.esign.utils import get_session_annotation | ||
| from plone import api | ||
| from plone.app.testing import login | ||
| from plone.app.testing import setRoles | ||
| from plone.app.testing import TEST_USER_ID | ||
| from plone.namedfile.file import NamedBlobFile | ||
|
|
@@ -17,6 +20,17 @@ | |
| import unittest | ||
|
|
||
|
|
||
| try: | ||
| from html import unescape | ||
| except ImportError: # Python 2 | ||
| from HTMLParser import HTMLParser | ||
| unescape = HTMLParser().unescape | ||
|
|
||
| try: | ||
| string_types = (basestring,) | ||
| except NameError: | ||
| string_types = (str,) | ||
|
|
||
| class TestRemoveItemFromSessionView(unittest.TestCase): | ||
| """Test RemoveItemFromSessionView browser view.""" | ||
|
|
||
|
|
@@ -124,3 +138,173 @@ def test_finished_shows_message_and_redirects(self): | |
| self.assertEqual(len(messages), 1) | ||
| self.assertIn("removed from session", messages[0].message) | ||
| self.assertEqual(self.request.RESPONSE.getHeader("location"), self.annexes[0].absolute_url()) | ||
|
|
||
|
|
||
| class TestSessionAnnotationInfoView(unittest.TestCase): | ||
| """Test SessionAnnotationInfoView""" | ||
|
|
||
| layer = IMIO_ESIGN_INTEGRATION_TESTING | ||
|
|
||
| def setUp(self): | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gbastien Je sais que tu m'as dit de mettre les contenus d'exemple pour les tests dans la layer. Je gérerai ça dans une PR séparée
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chris-adam ce n'est pas du tout urgent, en effet quand çà se calmera dans une v3 on pourra refactorer les tests pour les rendre + rapides mais c'est loin d'être prioritaire 👍 |
||
| self.portal = self.layer["portal"] | ||
| 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", "icône1.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ône1.png"), | ||
| id="to_sign", | ||
| predefined_title="To be signed", | ||
| to_sign=True, | ||
| show_preview=False, | ||
| ) | ||
| self.folder = api.content.create( | ||
| container=self.portal, type="Folder", id="test_session_folder", title="Test Session 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=u"Annex {}".format(i), | ||
| content_category=calculate_category_id(self.portal["annexes_types"]["annexes"]["to_sign"]), | ||
| scan_id="012345600000{: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", u"User 1", u"Position 1"), | ||
| ("user2", "user2@sign.com", u"User 2", u"Position 2"), | ||
| ] | ||
| self.view = SessionAnnotationInfoView(self.folder, self.portal.REQUEST) | ||
|
|
||
| def test_call(self): | ||
| setRoles(self.portal, TEST_USER_ID, ["Member"]) | ||
| with self.assertRaises(Unauthorized): | ||
| self.view() | ||
| login(self.layer["app"], "admin") | ||
| self.assertIsInstance(self.view(), string_types) | ||
|
|
||
| def test_render_value(self): | ||
| # Dict | ||
| self.assertEqual(self.view._render_value({}), u"{}") | ||
| self.assertEqual( | ||
| self.view._render_value({"key": "val"}), | ||
| u"{\n 'key': 'val',\n}", | ||
| ) | ||
|
|
||
| # Indentation: nested value increases indent level | ||
| self.assertEqual( | ||
| self.view._render_value({"key": ["a"]}), | ||
| u"{\n 'key': [\n 'a',\n ],\n}", | ||
| ) | ||
|
|
||
| # List | ||
| self.assertEqual(self.view._render_value([]), u"[]") | ||
| self.assertEqual( | ||
| self.view._render_value(["a", "b"]), | ||
| u"[\n 'a',\n 'b',\n]", | ||
| ) | ||
|
|
||
| # Tuple | ||
| self.assertEqual(self.view._render_value(()), u"[]") | ||
|
|
||
| # String | ||
| self.assertEqual(self.view._render_value(u"hello"), u"u'hello'") | ||
|
|
||
| def test_uid_to_link(self): | ||
| uid = self.folder.UID() | ||
| result = self.view._uid_to_link(uid) | ||
| self.assertEqual( | ||
| result, | ||
| u"<a href='http://nohost/plone/test_session_folder/view' title='/plone/test_session_folder'>Test Session Folder</a>", | ||
| ) | ||
|
|
||
| result = self.view._uid_to_link(u"a" * 32) | ||
| self.assertEqual( | ||
| result, | ||
| u"<span title='not found'>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>", | ||
| ) | ||
|
|
||
| def test_esign_sessions(self): | ||
| uids = [a.UID() for a in self.annexes] | ||
| add_files_to_session(self.signers, uids, title=u"[ia.parapheo] Session {sign_id}") | ||
| view = SessionAnnotationInfoView(self.folder, self.portal.REQUEST) | ||
|
|
||
| esign_sessions = view.esign_sessions | ||
| self.assertEqual(len(esign_sessions), 1) | ||
| esign_session = esign_sessions[0] | ||
| self.assertIsInstance(esign_session, tuple) | ||
| self.assertEqual(esign_session[0], 0) | ||
|
|
||
| self.assertEqual( | ||
| unescape(view.esign_session_html(esign_session[1])), | ||
| u"""{{ | ||
| 'acroform': True, | ||
| 'client_id': '0123456', | ||
| 'discriminators': [], | ||
| 'files': [ | ||
| {{ | ||
| 'context_uid': <a href='http://nohost/plone/test_session_folder/view' title='/plone/test_session_folder'>Test Session Folder</a>, | ||
| 'filename': u'annex0.pdf', | ||
| 'scan_id': '01234560000000', | ||
| 'status': '', | ||
| 'title': u'Annex 0', | ||
| 'uid': <a href='http://nohost/plone/test_session_folder/annex0/view' title='/plone/test_session_folder/annex0'>Annex 0</a>, | ||
| }}, | ||
| {{ | ||
| 'context_uid': <a href='http://nohost/plone/test_session_folder/view' title='/plone/test_session_folder'>Test Session Folder</a>, | ||
| 'filename': u'annex1.pdf', | ||
| 'scan_id': '01234560000001', | ||
| 'status': '', | ||
| 'title': u'Annex 1', | ||
| 'uid': <a href='http://nohost/plone/test_session_folder/annex1/view' title='/plone/test_session_folder/annex1'>Annex 1</a>, | ||
| }}, | ||
| ], | ||
| 'last_update': {}, | ||
| 'returns': [], | ||
| 'seal': None, | ||
| 'sign_id': '012345600000', | ||
| 'sign_url': None, | ||
| 'signers': [ | ||
| {{ | ||
| 'email': 'user1@sign.com', | ||
| 'fullname': u'User 1', | ||
| 'position': u'Position 1', | ||
| 'status': '', | ||
| 'userid': 'user1', | ||
| }}, | ||
| {{ | ||
| 'email': 'user2@sign.com', | ||
| 'fullname': u'User 2', | ||
| 'position': u'Position 2', | ||
| 'status': '', | ||
| 'userid': 'user2', | ||
| }}, | ||
| ], | ||
| 'size': 13936, | ||
| 'state': 'draft', | ||
| 'title': u'[ia.parapheo] Session 012345600000', | ||
| 'watchers': [], | ||
| }}""".format( | ||
| repr(esign_session[1]['last_update']), | ||
| ), | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
basestringis undefined in Python 3.This will raise a
NameErrorwhen running under Python 3. Use a compatibility check:🐛 Proposed fix
Add at the top of the file (after other imports):
Then update line 173:
🧰 Tools
🪛 Ruff (0.15.4)
[error] 173-173: Undefined name
basestring(F821)
🤖 Prompt for AI Agents