From bfa8d102c64088698dd1a19555b2c93aa2fd48b2 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 30 Apr 2024 15:21:03 +0200 Subject: [PATCH 1/8] Add i18n-demo plugin --- i18n_demo/MANIFEST.in | 3 + i18n_demo/README.md | 8 ++ i18n_demo/indico_i18n_demo/__init__.py | 0 i18n_demo/indico_i18n_demo/blueprint.py | 15 ++++ i18n_demo/indico_i18n_demo/controllers.py | 62 ++++++++++++++++ i18n_demo/indico_i18n_demo/plugin.py | 73 +++++++++++++++++++ .../templates/clone_button.html | 9 +++ i18n_demo/setup.cfg | 28 +++++++ i18n_demo/setup.py | 11 +++ 9 files changed, 209 insertions(+) create mode 100644 i18n_demo/MANIFEST.in create mode 100644 i18n_demo/README.md create mode 100644 i18n_demo/indico_i18n_demo/__init__.py create mode 100644 i18n_demo/indico_i18n_demo/blueprint.py create mode 100644 i18n_demo/indico_i18n_demo/controllers.py create mode 100644 i18n_demo/indico_i18n_demo/plugin.py create mode 100644 i18n_demo/indico_i18n_demo/templates/clone_button.html create mode 100644 i18n_demo/setup.cfg create mode 100644 i18n_demo/setup.py diff --git a/i18n_demo/MANIFEST.in b/i18n_demo/MANIFEST.in new file mode 100644 index 00000000..7a1cb894 --- /dev/null +++ b/i18n_demo/MANIFEST.in @@ -0,0 +1,3 @@ +graft indico_i18n_demo/templates + +global-exclude *.pyc __pycache__ .keep diff --git a/i18n_demo/README.md b/i18n_demo/README.md new file mode 100644 index 00000000..28b9e904 --- /dev/null +++ b/i18n_demo/README.md @@ -0,0 +1,8 @@ +# indico-plugin-i18n-demo + +This is a plugin intended for the [i18n demo instance](https://localization-demo.getindico.io). It does two things: + +- All emails are redirected to the user who sent them (avoids spamming random people and lets you check email translations) +- Adds a new **Clone** button to the example events. When clicked, the corresponding event is cloned into your own personal subcategory and you get management rights to it. This way, you can see the management area without accidentally modifying the example events. + +![](example.png) \ No newline at end of file diff --git a/i18n_demo/indico_i18n_demo/__init__.py b/i18n_demo/indico_i18n_demo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/i18n_demo/indico_i18n_demo/blueprint.py b/i18n_demo/indico_i18n_demo/blueprint.py new file mode 100644 index 00000000..3690d45c --- /dev/null +++ b/i18n_demo/indico_i18n_demo/blueprint.py @@ -0,0 +1,15 @@ +# This file is part of the CERN Indico plugins. +# Copyright (C) 2014 - 2024 CERN +# +# The CERN Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; see +# the LICENSE file for more details. + +from indico.core.plugins import IndicoPluginBlueprint + +from indico_i18n_demo.controllers import RHCloneEvent + + +blueprint = IndicoPluginBlueprint('i18n_demo', __name__, url_prefix='/i18n-demo') + +blueprint.add_url_rule('/event//clone', 'clone_event', RHCloneEvent, methods=('POST',)) diff --git a/i18n_demo/indico_i18n_demo/controllers.py b/i18n_demo/indico_i18n_demo/controllers.py new file mode 100644 index 00000000..e04ba851 --- /dev/null +++ b/i18n_demo/indico_i18n_demo/controllers.py @@ -0,0 +1,62 @@ +# This file is part of the CERN Indico plugins. +# Copyright (C) 2014 - 2024 CERN +# +# The CERN Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; see +# the LICENSE file for more details. + +from flask import flash, redirect, session +from flask_pluginengine import current_plugin +from werkzeug.exceptions import BadRequest + +from indico.core.db.sqlalchemy.protection import ProtectionMode +from indico.modules.categories.models.categories import Category +from indico.modules.categories.operations import create_category as _create_category +from indico.modules.events.cloning import get_event_cloners +from indico.modules.events.controllers.base import RHEventBase +from indico.modules.events.operations import clone_event +from indico.web.flask.util import url_for + + +class RHCloneEvent(RHEventBase): + """Clone an event to a user's personal category. + + If the category does not exist, it will be created. + The user has full management rights within the category. + """ + ALLOW_LOCKED = True + + def _process(self): + if not (category_id := current_plugin.settings.get('test_category_id')): + raise BadRequest('No test category ID configured') + + test_category = Category.get(int(category_id)) + user_category = get_user_category(test_category, session.user) + + cloners = {c for c in get_event_cloners().values() if not c.is_internal} + new_event = clone_event(self.event, n_occurrence=0, start_dt=self.event.start_dt, cloners=cloners, + category=user_category, refresh_users=False) + + flash('Event successfully cloned!', 'success') + return redirect(url_for('event_management.settings', new_event)) + + +def get_user_category(parent, user): + category = Category.query.filter(Category.title == get_category_title(user)).first() + if category: + if category.is_deleted: + category.is_deleted = False + return category + return create_category(parent, user) + + +def create_category(parent, user): + description = 'This is your own category where you have full management rights. Have fun!' + category = _create_category(parent, {'title': get_category_title(user), 'description': description}) + category.protection_mode = ProtectionMode.protected + category.update_principal(user, full_access=True) + return category + + +def get_category_title(user): + return f"{user.full_name}'s category ({user.id})" diff --git a/i18n_demo/indico_i18n_demo/plugin.py b/i18n_demo/indico_i18n_demo/plugin.py new file mode 100644 index 00000000..ffed78de --- /dev/null +++ b/i18n_demo/indico_i18n_demo/plugin.py @@ -0,0 +1,73 @@ +# This file is part of the CERN Indico plugins. +# Copyright (C) 2014 - 2024 CERN +# +# The CERN Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; see +# the LICENSE file for more details. + +from flask import session +from flask_pluginengine.plugin import render_plugin_template +from wtforms.fields import IntegerField +from wtforms.validators import NumberRange, Optional + +from indico.core import signals +from indico.core.config import config +from indico.core.notifications import make_email +from indico.core.plugins import IndicoPlugin +from indico.modules.categories.models.categories import Category +from indico.util.signals import interceptable_sender +from indico.web.forms.base import IndicoForm + +from indico_i18n_demo.blueprint import blueprint + + +class PluginSettingsForm(IndicoForm): + test_category_id = IntegerField('Test category ID', [Optional(), NumberRange(min=1)], + description='The ID of the category to clone events to') + + +class I18nDemoPlugin(IndicoPlugin): + """I18n Demo + + Provides utilities for the i18n-demo instance. + """ + + configurable = True + settings_form = PluginSettingsForm + default_settings = { + 'test_category_id': '' + } + + def init(self): + super().init() + self.template_hook('event-status-labels', self._inject_clone_button) + self.connect(signals.plugin.interceptable_function, self._intercept_make_email, + sender=interceptable_sender(make_email)) + + def get_blueprints(self): + return blueprint + + def _inject_clone_button(self, event, **kwargs): + if not (test_category_id := self.settings.get('test_category_id')): + return + + if not (test_category := Category.get(int(test_category_id))): + return + + if event.category != test_category and not event.category.is_descendant_of(test_category): + return render_plugin_template('clone_button.html', event=event) + + def _intercept_make_email(self, sender, func, args, **kwargs): + ret = func(**args.arguments) + + return { + 'to': {session.user.email}, + 'cc': set(), + 'bcc': set(), + 'from': config.NO_REPLY_EMAIL, + 'reply_to': set(), + 'attachments': ret['attachments'], + 'subject': ret['subject'], + 'body': ret['body'], + 'html': ret['html'], + } diff --git a/i18n_demo/indico_i18n_demo/templates/clone_button.html b/i18n_demo/indico_i18n_demo/templates/clone_button.html new file mode 100644 index 00000000..b4ebb64d --- /dev/null +++ b/i18n_demo/indico_i18n_demo/templates/clone_button.html @@ -0,0 +1,9 @@ + diff --git a/i18n_demo/setup.cfg b/i18n_demo/setup.cfg new file mode 100644 index 00000000..cda24ac2 --- /dev/null +++ b/i18n_demo/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = indico-plugin-i18n-demo +version = 3.3-dev +description = Indico plugin to sanitize a database for testing +url = https://github.com/indico/indico-plugins-cern/i18n-demo +license = MIT +author = Indico Team +author_email = indico-team@cern.ch +classifiers = + Environment :: Plugins + Environment :: Web Environment + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3.12 + +[options] +packages = find: +zip_safe = false +include_package_data = true +python_requires = >=3.12, <3.13 +install_requires = + indico>=3.3.2-dev0 + +[options.entry_points] +indico.plugins = + i18n_demo = indico_i18n_demo.plugin:I18nDemoPlugin + +[pydocstyle] +ignore = D100,D101,D102,D103,D104,D105,D107,D203,D213 diff --git a/i18n_demo/setup.py b/i18n_demo/setup.py new file mode 100644 index 00000000..a7f41746 --- /dev/null +++ b/i18n_demo/setup.py @@ -0,0 +1,11 @@ +# This file is part of the CERN Indico plugins. +# Copyright (C) 2014 - 2024 CERN +# +# The CERN Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; see +# the LICENSE file for more details. + +from setuptools import setup + + +setup() From 300a3d346ab5c044af35490716d653e503cb1d90 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 30 Apr 2024 15:56:20 +0200 Subject: [PATCH 2/8] Update setup.cfg --- i18n_demo/setup.cfg | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/i18n_demo/setup.cfg b/i18n_demo/setup.cfg index cda24ac2..6d4cb586 100644 --- a/i18n_demo/setup.cfg +++ b/i18n_demo/setup.cfg @@ -1,8 +1,7 @@ [metadata] name = indico-plugin-i18n-demo version = 3.3-dev -description = Indico plugin to sanitize a database for testing -url = https://github.com/indico/indico-plugins-cern/i18n-demo +url = https://github.com/indico/indico-plugins-cern license = MIT author = Indico Team author_email = indico-team@cern.ch @@ -18,7 +17,7 @@ zip_safe = false include_package_data = true python_requires = >=3.12, <3.13 install_requires = - indico>=3.3.2-dev0 + indico>=3.3 [options.entry_points] indico.plugins = From 1408af00e11419a9c70dab3650aa716e3c191435 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 30 Apr 2024 15:56:40 +0200 Subject: [PATCH 3/8] Simplify code --- i18n_demo/indico_i18n_demo/controllers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n_demo/indico_i18n_demo/controllers.py b/i18n_demo/indico_i18n_demo/controllers.py index e04ba851..e47e5208 100644 --- a/i18n_demo/indico_i18n_demo/controllers.py +++ b/i18n_demo/indico_i18n_demo/controllers.py @@ -42,7 +42,7 @@ def _process(self): def get_user_category(parent, user): - category = Category.query.filter(Category.title == get_category_title(user)).first() + category = Category.query.filter_by(title=get_category_title(user)).first() if category: if category.is_deleted: category.is_deleted = False From 94dfa45ad8b9dafc0f60a36a2027ac67c7887872 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 30 Apr 2024 15:57:50 +0200 Subject: [PATCH 4/8] Pass extra arguments in '_intercept_make_email' --- i18n_demo/indico_i18n_demo/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n_demo/indico_i18n_demo/plugin.py b/i18n_demo/indico_i18n_demo/plugin.py index ffed78de..99c1f9e9 100644 --- a/i18n_demo/indico_i18n_demo/plugin.py +++ b/i18n_demo/indico_i18n_demo/plugin.py @@ -60,7 +60,7 @@ def _inject_clone_button(self, event, **kwargs): def _intercept_make_email(self, sender, func, args, **kwargs): ret = func(**args.arguments) - return { + return ret | { 'to': {session.user.email}, 'cc': set(), 'bcc': set(), From 826143960e9339e51b70f5d19f2489639453d602 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Fri, 10 May 2024 12:32:25 +0200 Subject: [PATCH 5/8] Fix missing request context --- i18n_demo/indico_i18n_demo/plugin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/i18n_demo/indico_i18n_demo/plugin.py b/i18n_demo/indico_i18n_demo/plugin.py index 99c1f9e9..db89433e 100644 --- a/i18n_demo/indico_i18n_demo/plugin.py +++ b/i18n_demo/indico_i18n_demo/plugin.py @@ -5,7 +5,7 @@ # them and/or modify them under the terms of the MIT License; see # the LICENSE file for more details. -from flask import session +from flask import session, has_request_context from flask_pluginengine.plugin import render_plugin_template from wtforms.fields import IntegerField from wtforms.validators import NumberRange, Optional @@ -60,8 +60,7 @@ def _inject_clone_button(self, event, **kwargs): def _intercept_make_email(self, sender, func, args, **kwargs): ret = func(**args.arguments) - return ret | { - 'to': {session.user.email}, + overrides = { 'cc': set(), 'bcc': set(), 'from': config.NO_REPLY_EMAIL, @@ -71,3 +70,12 @@ def _intercept_make_email(self, sender, func, args, **kwargs): 'body': ret['body'], 'html': ret['html'], } + + if has_request_context(): + # If we're outside the request context (i.e. in a celery task), + # we can't access the session so we just keep the original address. + # This can happen for data export and event reminders (and maybe some other places?). + # In those cases, we trust the users not to spam random people. + overrides['to'] = session.user.email + + return ret | overrides From 1060846ab01c245f2f2c646d7a6af0ffe923bc74 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Fri, 17 May 2024 09:37:39 +0200 Subject: [PATCH 6/8] Fix import sorting --- i18n_demo/indico_i18n_demo/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n_demo/indico_i18n_demo/plugin.py b/i18n_demo/indico_i18n_demo/plugin.py index db89433e..75d5586c 100644 --- a/i18n_demo/indico_i18n_demo/plugin.py +++ b/i18n_demo/indico_i18n_demo/plugin.py @@ -5,7 +5,7 @@ # them and/or modify them under the terms of the MIT License; see # the LICENSE file for more details. -from flask import session, has_request_context +from flask import has_request_context, session from flask_pluginengine.plugin import render_plugin_template from wtforms.fields import IntegerField from wtforms.validators import NumberRange, Optional From a593e7ec22b337d2666d760d611d88dca93cfc23 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Fri, 17 May 2024 10:11:12 +0200 Subject: [PATCH 7/8] Wrap lines in readme --- i18n_demo/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/i18n_demo/README.md b/i18n_demo/README.md index 28b9e904..de3d4403 100644 --- a/i18n_demo/README.md +++ b/i18n_demo/README.md @@ -1,8 +1,13 @@ # indico-plugin-i18n-demo -This is a plugin intended for the [i18n demo instance](https://localization-demo.getindico.io). It does two things: +This is a plugin intended for the [i18n demo +instance](https://localization-demo.getindico.io). It does two things: -- All emails are redirected to the user who sent them (avoids spamming random people and lets you check email translations) -- Adds a new **Clone** button to the example events. When clicked, the corresponding event is cloned into your own personal subcategory and you get management rights to it. This way, you can see the management area without accidentally modifying the example events. +- All emails are redirected to the user who sent them (avoids spamming random + people and lets you check email translations) +- Adds a new **Clone** button to the example events. When clicked, the + corresponding event is cloned into your own personal subcategory and you get + management rights to it. This way, you can see the management area without + accidentally modifying the example events. ![](example.png) \ No newline at end of file From 2d89f4a9f5546ec9be13968c9509f72f73c85e8e Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 4 Jun 2024 13:41:27 +0200 Subject: [PATCH 8/8] Do not modify emails when request context is missing --- i18n_demo/indico_i18n_demo/plugin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/i18n_demo/indico_i18n_demo/plugin.py b/i18n_demo/indico_i18n_demo/plugin.py index 75d5586c..b68b2300 100644 --- a/i18n_demo/indico_i18n_demo/plugin.py +++ b/i18n_demo/indico_i18n_demo/plugin.py @@ -60,7 +60,15 @@ def _inject_clone_button(self, event, **kwargs): def _intercept_make_email(self, sender, func, args, **kwargs): ret = func(**args.arguments) + if not has_request_context(): + # If we're outside the request context (i.e. in a celery task), + # we can't access the session so we just return the original email unmodified. + # This can happen for data export and event reminders (and maybe some other places?). + # In those cases, we trust the users not to spam random people. + return ret + overrides = { + 'to': session.user.email, 'cc': set(), 'bcc': set(), 'from': config.NO_REPLY_EMAIL, @@ -70,12 +78,4 @@ def _intercept_make_email(self, sender, func, args, **kwargs): 'body': ret['body'], 'html': ret['html'], } - - if has_request_context(): - # If we're outside the request context (i.e. in a celery task), - # we can't access the session so we just keep the original address. - # This can happen for data export and event reminders (and maybe some other places?). - # In those cases, we trust the users not to spam random people. - overrides['to'] = session.user.email - return ret | overrides