|
| 1 | +# This file is part of Indico. |
| 2 | +# Copyright (C) 2002 - 2025 CERN |
| 3 | +# |
| 4 | +# Indico is free software; you can redistribute it and/or |
| 5 | +# modify it under the terms of the MIT License; see the |
| 6 | +# LICENSE file for more details. |
| 7 | + |
| 8 | +from copy import deepcopy |
| 9 | + |
| 10 | +from flask import flash, request, session |
| 11 | +from markupsafe import Markup |
| 12 | +from werkzeug.exceptions import Forbidden, NotFound |
| 13 | + |
| 14 | +from indico.core.db import db |
| 15 | +from indico.modules.categories import Category |
| 16 | +from indico.modules.categories.controllers.base import RHManageCategoryBase |
| 17 | +from indico.modules.designer.forms import CloneTemplateForm |
| 18 | +from indico.modules.email_templates.forms import CreateEmailTemplatesForm, EditEmailTemplatesForm |
| 19 | +from indico.modules.email_templates.models.email_templates import EmailTemplate |
| 20 | +from indico.modules.email_templates.util import get_inherited_templates |
| 21 | +from indico.modules.email_templates.views import WPCategoryManagementEmailTemplate, WPEventManagementEmailTemplate |
| 22 | +from indico.modules.events import Event |
| 23 | +from indico.modules.events.management.controllers import RHManageEventBase |
| 24 | +from indico.modules.events.util import check_event_locked |
| 25 | +from indico.util.i18n import _ |
| 26 | +from indico.web.flask.templating import get_template_module |
| 27 | +from indico.web.flask.util import url_for |
| 28 | +from indico.web.rh import RHProtected |
| 29 | +from indico.web.util import jsonify_data, jsonify_form |
| 30 | + |
| 31 | + |
| 32 | +def _render_template_list(target, event=None): |
| 33 | + tpl = get_template_module('email_templates/_list.html') |
| 34 | + return tpl.render_email_template_list(target.email_templates, target, event=event, |
| 35 | + inherited_templates=get_inherited_templates(target)) |
| 36 | + |
| 37 | + |
| 38 | +class EmailTemplateMixin: |
| 39 | + """Basic class for all template designer mixins. |
| 40 | +
|
| 41 | + It resolves the target object type from the blueprint URL. |
| 42 | + """ |
| 43 | + |
| 44 | + @property |
| 45 | + def object_type(self): |
| 46 | + """Figure out whether we're targeting an event or category, based on URL info.""" |
| 47 | + return request.view_args['object_type'] |
| 48 | + |
| 49 | + @property |
| 50 | + def event_or_none(self): |
| 51 | + return self.target if self.object_type == 'event' else None |
| 52 | + |
| 53 | + def _render_template(self, tpl_name, **kwargs): |
| 54 | + view_class = (WPEventManagementEmailTemplate |
| 55 | + if self.object_type == 'event' else WPCategoryManagementEmailTemplate) |
| 56 | + return view_class.render_template(tpl_name, self.target, 'email_templates', |
| 57 | + target=self.target, **kwargs) |
| 58 | + |
| 59 | + |
| 60 | +class SpecificEmailTemplateMixin(EmailTemplateMixin): |
| 61 | + """Mixin that accepts a target template passed in the URL. |
| 62 | +
|
| 63 | + The target category/event will be the owner of that template. |
| 64 | + """ |
| 65 | + |
| 66 | + normalize_url_spec = { |
| 67 | + 'locators': { |
| 68 | + lambda self: self.email_tpl |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + @property |
| 73 | + def target(self): |
| 74 | + return self.email_tpl.owner |
| 75 | + |
| 76 | + def _check_access(self): |
| 77 | + self._require_user() |
| 78 | + if not self.target.can_manage(session.user): |
| 79 | + raise Forbidden |
| 80 | + elif isinstance(self.target, Event): |
| 81 | + check_event_locked(self, self.target) |
| 82 | + |
| 83 | + def _process_args(self): |
| 84 | + self.email_tpl = EmailTemplate.get_or_404(request.view_args['email_template_id']) |
| 85 | + if self.target.is_deleted: |
| 86 | + raise NotFound |
| 87 | + |
| 88 | + |
| 89 | +class TargetFromURLMixin(EmailTemplateMixin): |
| 90 | + """Mixin that takes the target event/category from the URL that is passed.""" |
| 91 | + |
| 92 | + @property |
| 93 | + def target_dict(self): |
| 94 | + return {'event': self.event} if self.object_type == 'event' else {'category': self.category} |
| 95 | + |
| 96 | + @property |
| 97 | + def target(self): |
| 98 | + return self.event if self.object_type == 'event' else self.category |
| 99 | + |
| 100 | + |
| 101 | +class EmailTemplateListMixin(TargetFromURLMixin): |
| 102 | + def _process(self): |
| 103 | + inherited_templates = get_inherited_templates(self.target) |
| 104 | + return self._render_template('list.html', inherited_templates=inherited_templates) |
| 105 | + |
| 106 | + |
| 107 | +class AddEmailTemplateMixin(TargetFromURLMixin): |
| 108 | + def _check_access(self): |
| 109 | + if not self.target.can_manage(session.user): |
| 110 | + raise Forbidden |
| 111 | + |
| 112 | + def _process(self): |
| 113 | + form = CreateEmailTemplatesForm(target=self.target) |
| 114 | + if form.validate_on_submit(): |
| 115 | + new_email_tpl = EmailTemplate(**self.target_dict) |
| 116 | + new_email_tpl.rules = {'status': form.status.data} |
| 117 | + form.populate_obj(new_email_tpl) |
| 118 | + flash(_("Added a new email template '{}'").format(new_email_tpl.title), 'success') |
| 119 | + return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none)) |
| 120 | + return jsonify_form(form, disabled_until_change=False) |
| 121 | + |
| 122 | + |
| 123 | +class RHListEventEmailTemplates(EmailTemplateListMixin, RHManageEventBase): |
| 124 | + pass |
| 125 | + |
| 126 | + |
| 127 | +class RHListCategoryEmailTemplates(EmailTemplateListMixin, RHManageCategoryBase): |
| 128 | + pass |
| 129 | + |
| 130 | + |
| 131 | +class RHAddEventEmailTemplate(AddEmailTemplateMixin, RHManageEventBase): |
| 132 | + pass |
| 133 | + |
| 134 | + |
| 135 | +class RHAddCategoryEmailTemplate(AddEmailTemplateMixin, RHManageCategoryBase): |
| 136 | + pass |
| 137 | + |
| 138 | + |
| 139 | +class RHModifyEmailTemplateBase(SpecificEmailTemplateMixin, RHProtected): |
| 140 | + def _check_access(self): |
| 141 | + RHProtected._check_access(self) |
| 142 | + SpecificEmailTemplateMixin._check_access(self) |
| 143 | + |
| 144 | + |
| 145 | +class RHEditEmailTemplate(RHModifyEmailTemplateBase): |
| 146 | + def _process(self): |
| 147 | + form = EditEmailTemplatesForm(target=self.target, obj=self.email_tpl) |
| 148 | + if form.validate_on_submit(): |
| 149 | + self.email_tpl.rules = {'status': form.status.data} |
| 150 | + form.populate_obj(self.email_tpl) |
| 151 | + flash(_("Edited email template '{}'").format(self.email_tpl.title), 'success') |
| 152 | + return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none)) |
| 153 | + return jsonify_form(form, disabled_until_change=False) |
| 154 | + |
| 155 | + |
| 156 | +class RHDeleteEmailTemplate(RHModifyEmailTemplateBase): |
| 157 | + def _process(self): |
| 158 | + db.session.delete(self.email_tpl) |
| 159 | + db.session.flush() |
| 160 | + flash(_('The email template has been deleted'), 'success') |
| 161 | + return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none)) |
| 162 | + |
| 163 | + |
| 164 | +class CloneTemplateMixin(TargetFromURLMixin): |
| 165 | + cloneable_elsewhere = False |
| 166 | + |
| 167 | + def _check_access(self): |
| 168 | + if not self.target.can_manage(session.user): |
| 169 | + raise Forbidden |
| 170 | + elif isinstance(self.target, Event): |
| 171 | + check_event_locked(self, self.target) |
| 172 | + |
| 173 | + def _process_args(self): |
| 174 | + self.email_tpl = EmailTemplate.get_or_404(request.view_args['email_template_id']) |
| 175 | + if self.target.is_deleted: |
| 176 | + raise NotFound |
| 177 | + |
| 178 | + def is_active(self): |
| 179 | + email_templates = [email_tpl for email_tpl in self.target.email_templates |
| 180 | + if email_tpl.type == self.email_tpl.type] |
| 181 | + if status := self.email_tpl.rules.get('status'): |
| 182 | + email_templates = [email_tpl for email_tpl in email_templates |
| 183 | + if email_tpl.rules and email_tpl.rules.get('status') == status] |
| 184 | + if email_templates: |
| 185 | + return not any(tpl.is_active for tpl in email_templates) |
| 186 | + return self.email_tpl.is_active |
| 187 | + |
| 188 | + def clone_template(self, target=None): |
| 189 | + title = f'{self.email_tpl.title} (copy)' |
| 190 | + target_dict = target or self.target_dict |
| 191 | + new_email_tpl = EmailTemplate(title=title, type=self.email_tpl.type, **target_dict) |
| 192 | + new_email_tpl.subject = self.email_tpl.subject |
| 193 | + new_email_tpl.body = self.email_tpl.body |
| 194 | + new_email_tpl.is_system_template = self.email_tpl.is_system_template |
| 195 | + new_email_tpl.is_active = self.is_active() |
| 196 | + new_email_tpl.rules = deepcopy(self.email_tpl.rules) |
| 197 | + message = _("Created copy of template '{}'").format(self.email_tpl.title) |
| 198 | + if target_dict != self.target_dict: |
| 199 | + message += Markup(' (<a href="{}">{}</a>)').format(url_for('email_templates.email_template_list', |
| 200 | + target_dict['category']), _('Go to category')) |
| 201 | + flash(message, 'success') |
| 202 | + return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none)) |
| 203 | + |
| 204 | + def _process(self): |
| 205 | + if self.cloneable_elsewhere: |
| 206 | + category = (self.target if isinstance(self.target, Category) and |
| 207 | + self.target.can_manage(session.user) else None) |
| 208 | + form = CloneTemplateForm(category=category) |
| 209 | + if form.validate_on_submit(): |
| 210 | + return self.clone_template(target=form.data) |
| 211 | + return jsonify_form(form, submit=_('Clone'), disabled_until_change=False) |
| 212 | + if request.method == 'POST': |
| 213 | + return self.clone_template() |
| 214 | + |
| 215 | + |
| 216 | +class RHCloneEventEmailTemplate(CloneTemplateMixin, RHManageEventBase): |
| 217 | + def _process_args(self): |
| 218 | + RHManageEventBase._process_args(self) |
| 219 | + CloneTemplateMixin._process_args(self) |
| 220 | + |
| 221 | + |
| 222 | +class RHCloneCategoryEmailTemplate(CloneTemplateMixin, RHManageCategoryBase): |
| 223 | + cloneable_elsewhere = True |
| 224 | + |
| 225 | + def _process_args(self): |
| 226 | + RHManageCategoryBase._process_args(self) |
| 227 | + CloneTemplateMixin._process_args(self) |
0 commit comments