Skip to content

Commit f49a996

Browse files
authored
Merge pull request #21 from IMIO/PARAF-359_file_added_to_same_session_again
Manage file added again to same session, data is updated.
2 parents 6730ff2 + e68d5b6 commit f49a996

File tree

11 files changed

+274
-16
lines changed

11 files changed

+274
-16
lines changed

.github/workflows/main.yml

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- name: Install dependencies
2727
run: |
2828
sudo apt-get update
29-
sudo apt-get install -y libjpeg-dev libbz2-dev
29+
sudo apt-get install -y libjpeg8-dev libbz2-dev liblzma-dev
3030
- name: Set up pyenv and Python
3131
uses: "gabrielfalcao/pyenv-action@v18"
3232
with:
@@ -50,6 +50,56 @@ jobs:
5050
- name: test
5151
run: |
5252
bin/test -t !robot
53+
coverage:
54+
runs-on: ubuntu-24.04
55+
strategy:
56+
fail-fast: false
57+
matrix:
58+
include:
59+
- python: 2.7.18
60+
plone: 4.3
61+
steps:
62+
- name: Checkout
63+
uses: actions/checkout@v5
64+
- name: Setup Env
65+
run: |
66+
sudo apt-get update -qqy
67+
sudo apt-get install -y libjpeg8-dev libbz2-dev liblzma-dev libreadline-dev
68+
- name: Set up pyenv and Python
69+
uses: "gabrielfalcao/pyenv-action@v18"
70+
with:
71+
default: 2.7.18
72+
versions: 3.8.16
73+
command: pyenv -v
74+
- name: Setup Env
75+
run: |
76+
sudo apt-get update
77+
pip install -r requirements-4.3.txt coverage==5.3.1
78+
- name: Cache eggs
79+
uses: actions/cache@v4
80+
env:
81+
cache-name: cache-eggs
82+
with:
83+
path: ~/buildout-cache/eggs
84+
key: ${{ runner.os }}-coverage-${{ env.cache-name }}
85+
restore-keys: ${{ runner.os }}-coverage-${{ env.cache-name }}
86+
- name: buildout
87+
run: |
88+
sed -ie "s#test.cfg#test-${{matrix.plone}}.cfg#" gha.cfg
89+
buildout -c gha.cfg
90+
- name: test coverage
91+
run: |
92+
bin/coverage run bin/test
93+
bin/coverage report
94+
- name: Install Coveralls
95+
run: |
96+
pip3 install -U pip setuptools --no-cache-dir
97+
pip3 install -U "coveralls>=3.0.0" coverage==5.3.1 --no-cache-dir
98+
- name: Publish to Coveralls
99+
env:
100+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101+
run: |
102+
coveralls --service=github
53103
# coverage:
54104
# runs-on: ubuntu-24.04
55105
# strategy:

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,18 @@ Changelog
3131
[chris-adam, sgeulette]
3232
- Added possibility to have elements of the same context to belong to different sessions.
3333
[chris-adam]
34+
- Manage file added again to same session, data is updated.
35+
[gbastien]
3436
- Configured the `@@remove-from-esign-session` the same way as `@@remove-item-from-esign-session`
3537
so relying on an `available` method to show it only if a context is in a session.
3638
[gbastien]
3739
- Fixed faceted viewlet broken because sessions format changed from list to OrderedDict.
3840
[gbastien]
41+
- Turned file info in session annotation from `dict` to `PersistentMapping`.
42+
Entire annotation structure is now persistent, remove `_p_changed`.
43+
[gbastien]
44+
- Use `@CachedProperty` for `FacetedSessionInfoViewlet.sessions` and `ItemSessionInfoViewlet.sessions`.
45+
[gbastien]
3946

4047
1.0a2 (2026-02-06)
4148
------------------

checkouts.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[buildout]
22
always-checkout = force
33
auto-checkout +=
4+
collective.iconifiedcategory
45
imio.annex
56
imio.helpers
67
imio.pyutils
@@ -19,6 +20,7 @@ collective = https://github.com/collective
1920
collective_push = git@github.com:collective
2021

2122
[sources]
23+
collective.iconifiedcategory = git ${remotes:collective}/collective.iconifiedcategory.git pushurl=${remotes:collective_push}/collective.iconifiedcategory.git
2224
imio.annex = git ${remotes:imio}/imio.annex.git pushurl=${remotes:imio_push}/imio.annex.git
2325
imio.helpers = git ${remotes:imio}/imio.helpers.git pushurl=${remotes:imio_push}/imio.helpers.git
2426
imio.pyutils = git ${remotes:imio}/imio.pyutils.git pushurl=${remotes:imio_push}/imio.pyutils.git

src/imio/esign/browser/actions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ def get_signers(self):
5050
"""
5151
try:
5252
signers = ISignable(self.context).get_signers()
53-
except ValueError, msg:
53+
except ValueError as msg:
5454
signers = []
5555
api.portal.show_message(
5656
_(
5757
"Problem getting signers: \"${error}\")!",
58-
mapping={"error": msg},
58+
mapping={"error": str(msg)},
5959
),
6060
request=self.request,
6161
type="warning",

src/imio/esign/browser/views.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from Products.Five import BrowserView
3131
from Products.PageTemplates.Expressions import SecureModuleImporter
3232
from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
33+
from zope.cachedescriptors.property import CachedProperty
3334
from zope.component import getMultiAdapter
3435
from zope.i18n import translate
3536
from zope.interface import implementer
@@ -213,11 +214,8 @@ def render(self):
213214
return self.sessions_listing_view(self.context, self.request).render_table()
214215
return ""
215216

216-
@property
217+
@CachedProperty
217218
def sessions(self):
218-
# caching
219-
if hasattr(self, "_cached_session"):
220-
return self._cached_session
221219
session_id = self.request.form.get("esign_session_id[]", None)
222220
try:
223221
session_id = int(session_id)
@@ -227,8 +225,6 @@ def sessions(self):
227225
session_info = get_session_info(session_id)
228226
if session_info:
229227
session = {session_id: session_info}
230-
# caching
231-
self._cached_session = session
232228
return session
233229

234230
def get_table_rows(self, column):
@@ -271,7 +267,7 @@ def render(self):
271267
return self.index()
272268
return ""
273269

274-
@property
270+
@CachedProperty
275271
def sessions(self):
276272
"""Return all sessions that contain files from this context."""
277273
return get_sessions_for(self.context.UID())

src/imio/esign/configure.zcml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<i18n:registerTranslations directory="locales" />
1010

11+
<include file="events.zcml" />
1112
<include file="permissions.zcml" />
1213

1314
<include package=".browser" />

src/imio/esign/events.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from imio.esign.utils import get_file_info
4+
from imio.esign.utils import get_sessions_for
5+
from imio.helpers.transmogrifier import get_correct_id
6+
from os import path
7+
8+
9+
def on_categorized_annex_updated(annex, event):
10+
'''When an annex is modified, update check if need to update esign session.'''
11+
old_values = event.old_values
12+
# we are creating a new annex, not in a session
13+
if not old_values:
14+
return
15+
16+
sessions = get_sessions_for(event.parent.UID(), readonly=False)
17+
if not sessions:
18+
return
19+
20+
# make sure annex_uid is in a session
21+
annex_uid = annex.UID()
22+
file_infos = []
23+
for session_id in sessions:
24+
file_info = get_file_info(session_id, annex_uid)
25+
if file_info:
26+
file_infos.append(file_info)
27+
if not file_infos:
28+
return
29+
30+
# here we are sure that annex is in a session, we need to update data
31+
# if something usefull changed, we will update the session
32+
new_values = event.new_values
33+
update = False
34+
checked_keys = ['title', 'filesize', 'relative_url']
35+
for checked_key in checked_keys:
36+
if new_values[checked_key] != old_values[checked_key]:
37+
update = True
38+
break
39+
# check scan_id and filename
40+
if update is False:
41+
for file_info in file_infos:
42+
if file_info and (annex.scan_id != file_info['scan_id'] or \
43+
annex.file.filename != file_info['filename']):
44+
update = True
45+
break
46+
47+
if update is True:
48+
for session_id, session in sessions.items():
49+
# size
50+
size_diff = new_values['filesize'] - old_values['filesize']
51+
session['size'] += size_diff
52+
# title and filename
53+
for file_data in session['files']:
54+
if file_data['uid'] == annex_uid:
55+
file_data['title'] = new_values['title']
56+
file_data['scan_id'] = annex.scan_id
57+
# filename changed, need to make sure new filename is unique
58+
if annex.file.filename != file_data['filename']:
59+
existing_files = [path.splitext(f["filename"])[0]
60+
for f in session["files"]]
61+
filename, ext = path.splitext(annex.file.filename)
62+
new_filename = get_correct_id(existing_files, filename)
63+
file_data['filename'] = new_filename + ext
64+
# file_uid is only there one time per session
65+
break

src/imio/esign/events.zcml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<configure
2+
xmlns="http://namespaces.zope.org/zope">
3+
4+
<subscriber for="imio.annex.content.annex.IAnnex
5+
collective.iconifiedcategory.interfaces.ICategorizedElementUpdatedEvent"
6+
handler=".events.on_categorized_annex_updated" />
7+
8+
</configure>

src/imio/esign/tests/test_utils.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from imio.esign.utils import add_files_to_session
1212
from imio.esign.utils import create_external_session
1313
from imio.esign.utils import get_file_download_url
14+
from imio.esign.utils import get_file_info
1415
from imio.esign.utils import get_filesize
1516
from imio.esign.utils import get_max_download_date
1617
from imio.esign.utils import get_session_annotation
@@ -30,6 +31,8 @@
3031
from plone.namedfile.file import NamedBlobFile
3132
from plone.namedfile.file import NamedBlobImage
3233
from zope.annotation import IAnnotations
34+
from zope.event import notify
35+
from zope.lifecycleevent import ObjectModifiedEvent
3336

3437
import collective.iconifiedcategory
3538
import json
@@ -452,6 +455,77 @@ def test_add_files_with_duplicate_filenames(self):
452455
self.assertIn("same_filename-1.pdf", filenames)
453456
self.assertIn("same_filename-2.pdf", filenames)
454457

458+
def test_add_files_already_exist_is_updated(self):
459+
"""When adding a file to the same session, it is updated."""
460+
annot = get_session_annotation()
461+
self.assertEqual(len(annot["sessions"]), 0)
462+
463+
signers = [
464+
("user1", "user1@sign.com", "User 1", "Position 1"),
465+
]
466+
467+
annex0_uid = self.uids[0]
468+
annex0 = api.content.get(UID=annex0_uid)
469+
470+
sid, session = add_files_to_session(signers, (annex0_uid,))
471+
self.assertEqual(sid, 0)
472+
self.assertEqual(len(session["files"]), 1)
473+
self.assertEqual(session["files"][0]["filename"], "annex0.pdf")
474+
self.assertEqual(session["files"][0]["title"], "Annex 0")
475+
self.assertEqual(session["size"], 6968)
476+
# edit annex and add again, still one annex in session and data are updated
477+
annex0.file.filename = u"new_annex0.pdf"
478+
annex0.setTitle('New Annex 0')
479+
sid, session = add_files_to_session(signers, (annex0_uid,))
480+
# same session_id
481+
self.assertEqual(sid, 0)
482+
self.assertEqual(len(session["files"]), 1)
483+
self.assertEqual(session["files"][0]["filename"], "new_annex0.pdf")
484+
self.assertEqual(session["files"][0]["title"], "New Annex 0")
485+
self.assertEqual(session["size"], 6968)
486+
# add again exact same file
487+
sid, session = add_files_to_session(signers, (annex0_uid,))
488+
self.assertEqual(sid, 0)
489+
self.assertEqual(len(session["files"]), 1)
490+
self.assertEqual(session["files"][0]["filename"], "new_annex0.pdf")
491+
self.assertEqual(session["files"][0]["title"], "New Annex 0")
492+
self.assertEqual(session["size"], 6968)
493+
# add second file 2 times
494+
annex1_uid = self.uids[1]
495+
annex1 = api.content.get(UID=annex1_uid)
496+
sid, session = add_files_to_session(signers, (annex1_uid,))
497+
self.assertEqual(sid, 0)
498+
self.assertEqual(len(session["files"]), 2)
499+
self.assertEqual(session["files"][1]["filename"], "annex1.pdf")
500+
self.assertEqual(session["files"][1]["title"], "Annex 1")
501+
self.assertEqual(session["size"], 13982)
502+
# edit and add again
503+
annex1.setTitle('New Annex 1')
504+
# edit file, filename and content so size changed
505+
with open(os.path.join(os.path.dirname(__file__), "annex1.pdf"), "rb") as f:
506+
annex1.file = NamedBlobFile(data=f.read(), filename=u"new_annex1.pdf", contentType="application/pdf")
507+
notify(ObjectModifiedEvent(annex1))
508+
# data were already updated
509+
self.assertEqual(len(session["files"]), 2)
510+
self.assertEqual(session["files"][1]["filename"], "new_annex1.pdf")
511+
self.assertEqual(session["files"][1]["title"], "New Annex 1")
512+
self.assertEqual(session["files"][1]["scan_id"], "012345600000001")
513+
self.assertEqual(session["size"], 13936)
514+
# change file but add a filename already used, change scan_id as well
515+
with open(os.path.join(os.path.dirname(__file__), "annex1.pdf"), "rb") as f:
516+
annex1.file = NamedBlobFile(data=f.read(), filename=u"new_annex0.pdf", contentType="application/pdf")
517+
annex1.scan_id = "012345600000002"
518+
notify(ObjectModifiedEvent(annex1))
519+
self.assertEqual(session["files"][1]["filename"], "new_annex0-1.pdf")
520+
self.assertEqual(session["files"][1]["scan_id"], "012345600000002")
521+
# edit annex out of any session
522+
annex2_uid = self.uids[2]
523+
annex2 = api.content.get(UID=annex2_uid)
524+
notify(ObjectModifiedEvent(annex2))
525+
# just to check, remove annex0
526+
remove_files_from_session((annex0_uid,))
527+
self.assertEqual(session["size"], 6968)
528+
455529
def test_remove_context_from_session(self):
456530
"""Test removing a context from a session."""
457531
annot = get_session_annotation()
@@ -528,6 +602,30 @@ def test_get_sessions_for(self):
528602
sessions[0]['watchers'] = ["watcher@sign.com"]
529603
self.assertEqual(get_session_info(0)['watchers'], ["watcher@sign.com"])
530604

605+
def test_get_file_info(self):
606+
"""Test getting infos for a given file."""
607+
annex0_uid = self.uids[0]
608+
annex1_uid = self.uids[1]
609+
# no session
610+
self.assertIsNone(get_file_info(0, annex0_uid))
611+
# create session
612+
signers = [("user1", "user1@sign.com", "User 1", "Position 1")]
613+
sid, session = add_files_to_session(signers, (annex0_uid,))
614+
self.assertEqual(get_file_info(0, annex0_uid)['uid'], annex0_uid)
615+
self.assertIsNone(get_file_info(0, annex1_uid))
616+
# readonly=True by default
617+
file_info = get_file_info(0, annex0_uid)
618+
file_info['title'] = u'New title annex 0'
619+
# not changed in the annotation
620+
self.assertEqual(
621+
get_session_annotation()['sessions'][0]['files'][0]['title'], u'Annex 0')
622+
# readonly=False
623+
file_info = get_file_info(0, annex0_uid, readonly=False)
624+
file_info['title'] = u'New title annex 0'
625+
# changed in the annotation
626+
self.assertEqual(
627+
get_session_annotation()['sessions'][0]['files'][0]['title'], u'New title annex 0')
628+
531629
def test_get_file_download_url(self):
532630
"""Test generating file download URL from UID."""
533631
uid = "f40682caafc045b4b81973bd82ea9ab6"

0 commit comments

Comments
 (0)