Skip to content

Commit f17bd0f

Browse files
committed
Included watchers in SigningUsersCsv
1 parent f2b70fe commit f17bd0f

File tree

2 files changed

+288
-3
lines changed

2 files changed

+288
-3
lines changed

src/imio/esign/browser/views.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,17 @@ def filter_user(self, user_data):
420420
portal_type="held_position",
421421
userid=user_data["userid"],
422422
)
423-
if not hps:
424-
return False
425423
for hp in hps:
426424
hp_obj = hp.getObject()
427425
if base_hasattr(hp_obj, "usages") and "signer" in hp_obj.usages:
428426
return True
427+
428+
user_obj = api.user.get(userid=user_data["userid"])
429+
if user_obj:
430+
for group in api.group.get_groups(user=user_obj):
431+
if group.getId().endswith("watchers"):
432+
return True
433+
429434
return False
430435

431436
def get_users_data(self):
@@ -515,7 +520,7 @@ def _get_selected_userids(self):
515520
# Parse JSON array
516521
try:
517522
return json.loads(selected)
518-
except (json.JSONDecodeError, ValueError, TypeError):
523+
except (ValueError, TypeError):
519524
return []
520525

521526
def _download_csv(self):

src/imio/esign/tests/test_browser_views.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from imio.esign.browser.views import DownloadFileView
88
from imio.esign.browser.views import ExternalSessionCreateView
99
from imio.esign.browser.views import SessionDeleteView
10+
from imio.esign.browser.views import SigningUsersCsv
11+
from imio.esign.config import set_registry_signing_users_email_content
1012
from imio.esign.testing import IMIO_ESIGN_FUNCTIONAL_TESTING
1113
from imio.esign.testing import IMIO_ESIGN_INTEGRATION_TESTING
1214
from imio.esign.utils import add_files_to_session
@@ -24,6 +26,7 @@
2426
from Products.statusmessages.interfaces import IStatusMessage
2527

2628
import collective.iconifiedcategory
29+
import json
2730
import os
2831
import transaction
2932
import unittest
@@ -393,3 +396,280 @@ def test_download_file_view(self):
393396
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)
394397
browser.open("{}/download-file/{}".format(portal_url, "aabbccddee?param=value"))
395398
self.assertIn("The corresponding file identifier cannot be retrieved (aabbccddee)", browser.contents)
399+
400+
401+
class TestSigningUsersCsv(unittest.TestCase):
402+
"""Tests for the SigningUsersCsv view."""
403+
404+
layer = IMIO_ESIGN_INTEGRATION_TESTING
405+
406+
def setUp(self):
407+
self.portal = self.layer["portal"]
408+
self.request = self.layer["request"]
409+
self.request.form.clear()
410+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
411+
self.user = api.user.create(email="signer@test.com", username="signer_user", password="password1") # noqa: S106
412+
413+
def _make_user_data(self, user):
414+
return {
415+
"userid": user.getId(),
416+
"email": user.getProperty("email", ""),
417+
"lastname": "",
418+
"firstname": "",
419+
"fullname": user.getProperty("fullname", ""),
420+
}
421+
422+
# --- filter_user ---
423+
424+
def test_filter_user_in_watchers_group_returns_true(self):
425+
"""User in a group ending with 'watchers' is included by default."""
426+
api.group.create(groupname="myservice_watchers")
427+
api.group.add_user(groupname="myservice_watchers", user=self.user)
428+
view = SigningUsersCsv(self.portal, self.request)
429+
self.assertTrue(view.filter_user(self._make_user_data(self.user)))
430+
431+
def test_filter_user_not_in_any_group_returns_false(self):
432+
"""User with no held_position and no watchers group is excluded."""
433+
view = SigningUsersCsv(self.portal, self.request)
434+
self.assertFalse(view.filter_user(self._make_user_data(self.user)))
435+
436+
def test_filter_user_in_non_watchers_group_returns_false(self):
437+
"""User in a group not ending with 'watchers' is excluded."""
438+
api.group.create(groupname="myservice_editors")
439+
api.group.add_user(groupname="myservice_editors", user=self.user)
440+
view = SigningUsersCsv(self.portal, self.request)
441+
self.assertFalse(view.filter_user(self._make_user_data(self.user)))
442+
443+
def test_filter_user_with_signer_held_position_returns_true(self):
444+
"""User with a held_position having 'signer' in usages is included by default."""
445+
mock_hp_obj = Mock()
446+
mock_hp_obj.usages = ["signer"]
447+
mock_brain = Mock()
448+
mock_brain.getObject.return_value = mock_hp_obj
449+
view = SigningUsersCsv(self.portal, self.request)
450+
with patch("imio.esign.browser.views.api.content.find", return_value=[mock_brain]):
451+
result = view.filter_user(self._make_user_data(self.user))
452+
self.assertTrue(result)
453+
454+
# --- get_users_data ---
455+
456+
def test_get_users_data(self):
457+
"""Returns data for all users with duplicates."""
458+
view = SigningUsersCsv(self.portal, self.request)
459+
users_data, _duplicates = view.get_users_data()
460+
self.assertEqual(
461+
users_data,
462+
[
463+
{
464+
"checked": False,
465+
"firstname": u"",
466+
"lastname": u"signer_user",
467+
"userid": "signer_user",
468+
"has_duplicate_email": False,
469+
"fullname": "",
470+
"email": "signer@test.com",
471+
},
472+
{
473+
"checked": False,
474+
"firstname": u"",
475+
"lastname": u"test_user_1_",
476+
"userid": "test_user_1_",
477+
"has_duplicate_email": False,
478+
"fullname": "",
479+
"email": "",
480+
},
481+
],
482+
)
483+
484+
# Test duplicates
485+
api.user.create(email="signer@test.com", username="signer_user2", password="password1") # noqa: S106
486+
view = SigningUsersCsv(self.portal, self.request)
487+
users_data, duplicates = view.get_users_data()
488+
self.assertEqual(
489+
users_data,
490+
[
491+
{
492+
"checked": False,
493+
"firstname": u"",
494+
"lastname": u"signer_user",
495+
"userid": "signer_user",
496+
"has_duplicate_email": True,
497+
"fullname": "",
498+
"email": "signer@test.com",
499+
},
500+
{
501+
"checked": False,
502+
"firstname": u"",
503+
"lastname": u"signer_user2",
504+
"userid": "signer_user2",
505+
"has_duplicate_email": True,
506+
"fullname": "",
507+
"email": "signer@test.com",
508+
},
509+
{
510+
"checked": False,
511+
"firstname": u"",
512+
"lastname": u"test_user_1_",
513+
"userid": "test_user_1_",
514+
"has_duplicate_email": False,
515+
"fullname": "",
516+
"email": "",
517+
},
518+
],
519+
)
520+
self.assertEqual(duplicates, {"signer@test.com": ["signer_user", "signer_user2"]})
521+
522+
# Signers and watchers users are sorted and checked
523+
api.group.create(groupname="myservice_watchers")
524+
api.group.add_user(groupname="myservice_watchers", user=self.user)
525+
view = SigningUsersCsv(self.portal, self.request)
526+
users_data, _duplicates = view.get_users_data()
527+
self.assertEqual(
528+
users_data,
529+
[
530+
{
531+
"checked": True,
532+
"firstname": u"",
533+
"lastname": u"signer_user",
534+
"userid": "signer_user",
535+
"has_duplicate_email": True,
536+
"fullname": "",
537+
"email": "signer@test.com",
538+
},
539+
{
540+
"checked": False,
541+
"firstname": u"",
542+
"lastname": u"signer_user2",
543+
"userid": "signer_user2",
544+
"has_duplicate_email": True,
545+
"fullname": "",
546+
"email": "signer@test.com",
547+
},
548+
{
549+
"checked": False,
550+
"firstname": u"",
551+
"lastname": u"test_user_1_",
552+
"userid": "test_user_1_",
553+
"has_duplicate_email": False,
554+
"fullname": "",
555+
"email": "",
556+
},
557+
],
558+
)
559+
560+
# --- _get_selected_userids ---
561+
562+
def test_get_selected_userids_with_valid_json(self):
563+
"""Parses a JSON array of user IDs from the request form."""
564+
self.request.form["selected_users"] = json.dumps(["user1", "user2"])
565+
view = SigningUsersCsv(self.portal, self.request)
566+
self.assertEqual(view._get_selected_userids(), ["user1", "user2"])
567+
568+
def test_get_selected_userids_with_empty_param(self):
569+
"""Returns empty list when selected_users is absent."""
570+
view = SigningUsersCsv(self.portal, self.request)
571+
self.assertEqual(view._get_selected_userids(), [])
572+
573+
def test_get_selected_userids_with_invalid_json(self):
574+
"""Returns empty list for malformed JSON."""
575+
self.request.form["selected_users"] = "not-valid-json"
576+
view = SigningUsersCsv(self.portal, self.request)
577+
self.assertEqual(view._get_selected_userids(), [])
578+
579+
# --- _download_csv ---
580+
581+
def test_download_csv_no_selected_users(self):
582+
"""Shows warning and redirects when no users are selected."""
583+
view = SigningUsersCsv(self.portal, self.request)
584+
view._download_csv()
585+
messages = IStatusMessage(self.request).show()
586+
self.assertEqual(len(messages), 1)
587+
self.assertIn("No users selected", messages[0].message)
588+
self.assertEqual(messages[0].type, "warning")
589+
590+
def test_download_csv_generates_csv(self):
591+
"""Returns CSV content with headers and a row for each selected user."""
592+
self.request.form["selected_users"] = json.dumps([self.user.getId()])
593+
view = SigningUsersCsv(self.portal, self.request)
594+
result = view._download_csv()
595+
self.assertEqual(
596+
result, "userid,email,lastname,firstname,fullname\r\nsigner_user,signer@test.com,signer_user,,\r\n"
597+
)
598+
self.assertIn("text/csv", self.request.RESPONSE.getHeader("Content-Type"))
599+
600+
# --- _send_emails ---
601+
602+
def test_send_emails_no_selected_users(self):
603+
"""Shows warning and redirects when no users are selected."""
604+
view = SigningUsersCsv(self.portal, self.request)
605+
view._send_emails()
606+
messages = IStatusMessage(self.request).show()
607+
self.assertEqual(len(messages), 1)
608+
self.assertIn("No users selected", messages[0].message)
609+
self.assertEqual(messages[0].type, "warning")
610+
611+
def test_send_emails_no_email_content(self):
612+
"""Shows error and redirects when signing users email content is not configured."""
613+
set_registry_signing_users_email_content(u"")
614+
self.request.form["selected_users"] = json.dumps([self.user.getId()])
615+
view = SigningUsersCsv(self.portal, self.request)
616+
view._send_emails()
617+
messages = IStatusMessage(self.request).show()
618+
self.assertEqual(len(messages), 1)
619+
self.assertEqual(messages[0].message, u"Email content is not configured in the settings.")
620+
self.assertEqual(messages[0].type, "error")
621+
622+
def test_send_emails_no_portal_from_email(self):
623+
"""Shows error and redirects to mail-controlpanel when portal from email is not set."""
624+
self.request.form["selected_users"] = json.dumps([self.user.getId()])
625+
view = SigningUsersCsv(self.portal, self.request)
626+
view._send_emails()
627+
messages = IStatusMessage(self.request).show()
628+
self.assertEqual(len(messages), 1)
629+
self.assertEqual(messages[0].message, u"Portal from email is not configured.")
630+
self.assertEqual(messages[0].type, "error")
631+
632+
def test_send_emails_success(self):
633+
"""Sends emails to all selected users and shows a success message."""
634+
self.portal.manage_changeProperties({"email_from_address": "from@test.com"})
635+
set_registry_signing_users_email_content(u"<p>Hello</p>")
636+
self.request.form["selected_users"] = json.dumps([self.user.getId()])
637+
view = SigningUsersCsv(self.portal, self.request)
638+
with patch("imio.esign.browser.views.send_email", return_value=(True, None)):
639+
view._send_emails()
640+
messages = IStatusMessage(self.request).show()
641+
success_msgs = [m for m in messages if m.type == "info"]
642+
self.assertEqual(len(success_msgs), 1)
643+
self.assertIn("Emails sent successfully", success_msgs[0].message)
644+
645+
def test_send_emails_user_with_no_email(self):
646+
"""Shows per-user warning when a selected user has no email address."""
647+
self.portal.manage_changeProperties({"email_from_address": "from@test.com"})
648+
set_registry_signing_users_email_content(u"<p>Hello</p>")
649+
no_email_user = api.user.create(
650+
email="placeholder@test.com", username="no_email_user", password="password1" # noqa: S106
651+
)
652+
no_email_user.setMemberProperties({"email": ""})
653+
self.request.form["selected_users"] = json.dumps([no_email_user.getId()])
654+
view = SigningUsersCsv(self.portal, self.request)
655+
view._send_emails()
656+
messages = IStatusMessage(self.request).show()
657+
no_email_msgs = [m for m in messages if "no email address" in m.message]
658+
self.assertEqual(len(no_email_msgs), 1)
659+
self.assertEqual(no_email_msgs[0].type, "warning")
660+
661+
# --- _render_email_content ---
662+
663+
def test_render_email_content(self):
664+
"""Renders a TAL template substituting values from user_data."""
665+
template = u"<p tal:content=\"python: user_data['fullname']\">NAME</p>"
666+
user_data = {
667+
"userid": "testuser",
668+
"email": "test@test.com",
669+
"lastname": "Smith",
670+
"firstname": "John",
671+
"fullname": "John Smith",
672+
}
673+
view = SigningUsersCsv(self.portal, self.request)
674+
result = view._render_email_content(template, user_data)
675+
self.assertEqual(result, u"<p>John Smith</p>")

0 commit comments

Comments
 (0)