From 8c84a4a3014f92a91a84ef401e60550066549421 Mon Sep 17 00:00:00 2001 From: Manuel Reinhardt Date: Thu, 11 Jan 2024 16:59:26 +0100 Subject: [PATCH 1/2] Mailing lists: Language specific lists for countries syslabcom/scrum#1621 --- docs/changes.rst | 4 +- src/osha/oira/client/__init__.py | 2 + src/osha/oira/client/browser/client.py | 82 +++++++++++--- src/osha/oira/client/subscribers.py | 44 ++++++++ .../oira/client/tests/test_mailing_lists.py | 101 ++++++++++++++++++ .../__init__.py | 0 .../upgrade.py | 49 +++++++++ 7 files changed, 268 insertions(+), 14 deletions(-) create mode 100644 src/osha/oira/client/subscribers.py create mode 100644 src/osha/oira/client/tests/test_mailing_lists.py create mode 100644 src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/__init__.py create mode 100644 src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/upgrade.py diff --git a/docs/changes.rst b/docs/changes.rst index 7417e971b..9b47e37a8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,7 +4,9 @@ Changelog 9.0.6 (unreleased) ------------------ -- Nothing changed yet. +- Mailing lists: Language specific lists for countries + (`#1621 `_) + [reinhardt] 9.0.5 (2024-01-08) diff --git a/src/osha/oira/client/__init__.py b/src/osha/oira/client/__init__.py index e69de29bb..99a86c7b3 100644 --- a/src/osha/oira/client/__init__.py +++ b/src/osha/oira/client/__init__.py @@ -0,0 +1,2 @@ +# Make sure sqlalchemy subscribers are initialized +from .subscribers import update_user_languages_subscriber # noqa: F401 diff --git a/src/osha/oira/client/browser/client.py b/src/osha/oira/client/browser/client.py index 44acd16ce..fe49161f5 100644 --- a/src/osha/oira/client/browser/client.py +++ b/src/osha/oira/client/browser/client.py @@ -7,8 +7,10 @@ from os import path from osha.oira import _ from osha.oira.client.interfaces import IOSHAClientSkinLayer +from osha.oira.client.model import NewsletterSetting from osha.oira.client.model import NewsletterSubscription from plone import api +from plone.memoize.view import memoize from plone.scale.scale import scaleImage from Products.Five import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile @@ -81,12 +83,51 @@ def __call__(self): class MailingListsJson(BaseJson): """Mailing lists (countries and tools, in the future also sectors)""" + @property + @memoize + def client_path(self): + return "/".join(self.context.getPhysicalPath()) + + def _get_mailing_lists_for(self, brain): + if brain.portal_type != "euphorie.clientcountry": + return [ + self._get_entry( + path.relpath(brain.getPath(), self.client_path), brain.Title + ) + ] + languages = set() + tools = api.content.find( + path=brain.getPath(), + portal_type="euphorie.survey", + review_state="published", + ) + # No filter for non-obsolete - + # if we get unexpected languages we can try adding that + for tool in tools: + language = tool.Language + if language: + if "-" in language: + language = language.split("-")[0] + if language and language != "None": + languages.add(language) + + return [ + self._get_entry( + "-".join((brain.getId, language)), f"{brain.Title} ({language})" + ) + for language in languages + ] + @property def results(self): """List of "mailing list" path/names. - The format fits pat-autosuggest. There is a special label for - the "all" list. + The format fits pat-autosuggest. + + The mailing list IDs are relative paths. + Countries have one mailing list per language. + The language is appended with a '-', e.g. "be-fr". + There is a special ID for the "all users" list. """ user_id = self.request.get("user_id") @@ -145,17 +186,12 @@ def filter_items(brain): filtered_brains = filter(filter_items, brains) - client_path = "/".join(self.context.getPhysicalPath()) cnt = len(results) for brain in filtered_brains: if cnt > 10: break cnt += 1 - results.append( - self._get_entry( - path.relpath(brain.getPath(), client_path), brain.Title - ) - ) + results.extend(self._get_mailing_lists_for(brain)) return results @@ -230,13 +266,32 @@ def get_token(self): return token def get_addresses_for_groups(self, group_paths): - subscribers = ( + subscribers = [] + other = [] + for group_id in group_paths: + if "/" in group_id or "-" not in group_id: + other.append(group_id) + continue + # Special handling for language specific mailing lists, e.g. "be-fr" + country, lang = group_id.split("-") + country_subscribers = ( + Session.query(Account.loginname) + .filter(Account.id == NewsletterSubscription.account_id) + .filter(NewsletterSubscription.zodb_path == (country)) + .filter(Account.id == NewsletterSetting.account_id) + .filter(NewsletterSetting.value == f"language:{lang}") + .group_by(Account.loginname) + ) + subscribers.extend([s.loginname for s in country_subscribers]) + + other_subscribers = ( Session.query(Account.loginname) .filter(Account.id == NewsletterSubscription.account_id) - .filter(NewsletterSubscription.zodb_path.in_(group_paths)) + .filter(NewsletterSubscription.zodb_path.in_(other)) .group_by(Account.loginname) ) - return [s.loginname for s in subscribers] + subscribers.extend([s.loginname for s in other_subscribers]) + return subscribers @property def results(self): @@ -249,8 +304,9 @@ def __call__(self): """Json list of email addresses subscribed to given group path (parameter `group`). - Group paths are relative to the client, i.e. only ids ("fr") for - countries. + Group paths are relative to the client, e.g. "be/agriculture/agriculture". + For countries the ID is combined with a language, e.g. "be-fr". + See also `MailingListsJson` """ token = self.request.get("token", "") if token != self.get_token(): diff --git a/src/osha/oira/client/subscribers.py b/src/osha/oira/client/subscribers.py new file mode 100644 index 000000000..9b0ecabd0 --- /dev/null +++ b/src/osha/oira/client/subscribers.py @@ -0,0 +1,44 @@ +from euphorie.client.model import get_current_account +from euphorie.client.model import SurveySession +from osha.oira.client.model import NewsletterSetting +from plone import api +from sqlalchemy import event +from z3c.saconfig import Session + +import logging + + +logger = logging.getLogger(__name__) + + +def update_user_languages(account_id, tool): + if not tool: + return + if not tool.language: + logger.info("No language for tool %s", "/".join(tool.getPhysicalPath())) + return + language = tool.language.split("-")[0] + if ( + Session.query(NewsletterSetting.value) + .filter(NewsletterSetting.account_id == account_id) + .filter(NewsletterSetting.value == f"language:{language}") + ).count() == 0: + Session.add( + NewsletterSetting( + account_id=account_id, + value=f"language:{language}", + ) + ) + + +@event.listens_for(SurveySession, "init") +def update_user_languages_subscriber(target, args, kwargs): + if "zodb_path" not in kwargs: + return + # kwargs["account_id"] can refer to the account of the session being cloned + account = get_current_account() + if not account: + return + client = api.portal.get().client + tool = client.restrictedTraverse(str(kwargs["zodb_path"]), None) + update_user_languages(account.id, tool) diff --git a/src/osha/oira/client/tests/test_mailing_lists.py b/src/osha/oira/client/tests/test_mailing_lists.py new file mode 100644 index 000000000..25349f8f4 --- /dev/null +++ b/src/osha/oira/client/tests/test_mailing_lists.py @@ -0,0 +1,101 @@ +from euphorie.client.interfaces import IClientSkinLayer +from euphorie.client.model import Account +from euphorie.client.model import SurveySession +from euphorie.client.tests.utils import addSurvey +from euphorie.content.tests.utils import BASIC_SURVEY +from euphorie.testing import EuphorieIntegrationTestCase +from osha.oira.client.browser.client import GroupToAddresses +from osha.oira.client.browser.client import MailingListsJson +from osha.oira.client.model import NewsletterSubscription +from plone import api +from z3c.saconfig import Session +from zope.interface import alsoProvides + + +class TestMailingLists(EuphorieIntegrationTestCase): + def setUp(self): + super().setUp() + survey = """ + Test + + Second Survey + fr + + """ + with api.env.adopt_user("admin"): + addSurvey(self.portal, BASIC_SURVEY) + addSurvey(self.portal, survey) + self.portal.client.nl.ict["software-development"].reindexObject() + self.portal.client.nl.test["second-survey"].reindexObject() + + def test_mailing_lists(self): + request = self.request.clone() + request.form = {"user_id": "admin"} + view = MailingListsJson(context=self.portal.client, request=request) + results = view.results + self.assertIn({"id": "general|QWxsIHVzZXJz", "text": "All users"}, results) + self.assertIn( + { + "id": "nl-nl|VGhlIE5ldGhlcmxhbmRzIChubCk=", + "text": "The Netherlands (nl)", + }, + results, + ) + self.assertIn( + { + "id": "nl-fr|VGhlIE5ldGhlcmxhbmRzIChmcik=", + "text": "The Netherlands (fr)", + }, + results, + ) + self.assertIn( + { + "id": "nl/ict/software-development|U29mdHdhcmUgZGV2ZWxvcG1lbnQ=", + "text": "Software development", + }, + results, + ) + + def test_group_to_addresses(self): + user = Account(loginname="leni@example.nl", password="secret") + Session.add(user) + alsoProvides(self.request, IClientSkinLayer) + with api.env.adopt_user("leni@example.nl"): + survey_session = SurveySession( + id=1, + title="Software", + zodb_path="nl/ict/software-development", + account=user, + ) + Session.add(survey_session) + Session.add( + NewsletterSubscription( + account_id=user.id, + zodb_path="nl", + ) + ) + Session.add( + NewsletterSubscription( + account_id=user.id, + zodb_path="nl/ict/software-development", + ) + ) + Session.flush() + + request = self.request.clone() + request.form = {"groups": "nl-nl"} + view = GroupToAddresses(context=self.portal.client, request=request) + results = view.results + self.assertIn("leni@example.nl", results) + + request = self.request.clone() + request.form = {"groups": "nl-fr"} + view = GroupToAddresses(context=self.portal.client, request=request) + results = view.results + self.assertNotIn("leni@example.nl", results) + + request = self.request.clone() + request.form = {"groups": "nl/ict/software-development"} + view = GroupToAddresses(context=self.portal.client, request=request) + results = view.results + self.assertIn("leni@example.nl", results) diff --git a/src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/__init__.py b/src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/upgrade.py b/src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/upgrade.py new file mode 100644 index 000000000..6373636fe --- /dev/null +++ b/src/osha/oira/upgrade/v1/20240111103145_set_newsletter_languages_per_user/upgrade.py @@ -0,0 +1,49 @@ +from collections import defaultdict +from euphorie.client.model import Account +from euphorie.client.model import SurveySession +from ftw.upgrade import UpgradeStep +from osha.oira.client.subscribers import update_user_languages +from plone import api +from plone.memoize.view import memoize +from z3c.saconfig import Session + +import logging + + +logger = logging.getLogger(__name__) + + +class SetNewsletterLanguagesPerUser(UpgradeStep): + """Set newsletter languages per user.""" + + @property + @memoize + def client(self): + return api.portal.get().client + + @memoize + def get_tool(self, zodb_path): + tool = self.client.restrictedTraverse(zodb_path, None) + if not tool: + return None + if not tool.language: + logger.info("No language for tool %s", zodb_path) + return None + return tool + + def __call__(self): + sessions = ( + Session.query(SurveySession, Account) + .filter(SurveySession.account_id == Account.id) + .filter(Account.account_type != "guest") + .filter(SurveySession.archived.is_(None)) + ) + done = defaultdict(list) + for session, account in sessions: + if session.zodb_path in done[account.id]: + continue + tool = self.get_tool(session.zodb_path) + if not tool: + continue + update_user_languages(account.id, tool) + done[account.id].append(session.zodb_path) From 5ef1b764bab08c214a205e64aa0fc95a3dc1e241 Mon Sep 17 00:00:00 2001 From: Manuel Reinhardt Date: Wed, 24 Jan 2024 16:55:52 +0100 Subject: [PATCH 2/2] Adapt test to path search syslabcom/scrum#1621 --- src/osha/oira/client/tests/test_mailing_lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/osha/oira/client/tests/test_mailing_lists.py b/src/osha/oira/client/tests/test_mailing_lists.py index 25349f8f4..72888b9c0 100644 --- a/src/osha/oira/client/tests/test_mailing_lists.py +++ b/src/osha/oira/client/tests/test_mailing_lists.py @@ -51,7 +51,7 @@ def test_mailing_lists(self): self.assertIn( { "id": "nl/ict/software-development|U29mdHdhcmUgZGV2ZWxvcG1lbnQ=", - "text": "Software development", + "text": "Software development (nl/ict/software-development)", }, results, )