From 455ef34a9d00742c96dfbb1d13feddaf9cf2dd67 Mon Sep 17 00:00:00 2001 From: Lucas Brandstaetter <lucas@brandstaetter.tech> Date: Thu, 28 Nov 2024 04:52:21 +0100 Subject: [PATCH] Update assembly team message form - Add a new form for sending messages to assembly - Use FormView instead of View for the message view - Add option to include assembly name in the subject - Update translations - Update the template to use the new form and bootstrap5 - Add a recipients list to the preview Fixes #616 --- src/backoffice/forms/__init__.py | 4 + src/backoffice/forms/assemblies_team.py | 13 ++ .../locale/de/LC_MESSAGES/django.po | 35 ++-- .../locale/en/LC_MESSAGES/django.po | 35 ++-- .../backoffice/assemblyteam_message.html | 180 +++++++++--------- src/backoffice/views/assemblyteam.py | 65 ++++--- src/core/models/assemblies.py | 7 +- src/core/tests/assemblies.py | 4 +- 8 files changed, 198 insertions(+), 145 deletions(-) create mode 100644 src/backoffice/forms/assemblies_team.py diff --git a/src/backoffice/forms/__init__.py b/src/backoffice/forms/__init__.py index 35c2dca07..9a8d38240 100644 --- a/src/backoffice/forms/__init__.py +++ b/src/backoffice/forms/__init__.py @@ -20,6 +20,9 @@ from backoffice.forms.assemblies import ( AssemblyMemberAddForm, AssemblyMemberEditForm, ) +from backoffice.forms.assemblies_team import ( + AssemblyTeamMessageForm, +) from backoffice.forms.badges import ( BadgeAssignForm, BadgeForm, @@ -154,6 +157,7 @@ __all__ = [ 'AssemblyRoomEditHangarForm', 'AssemblyRoomEditWorkAdventureForm', 'AssemblyRoomLinkCreateForm', + 'AssemblyTeamMessageForm', 'BadgeAssignForm', 'BadgeForm', 'BadgeTokenForm', diff --git a/src/backoffice/forms/assemblies_team.py b/src/backoffice/forms/assemblies_team.py new file mode 100644 index 000000000..844b3ed56 --- /dev/null +++ b/src/backoffice/forms/assemblies_team.py @@ -0,0 +1,13 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class AssemblyTeamMessageForm(forms.Form): + subject = forms.CharField(max_length=200, label=_('AssemblyTeam__message__subject')) + include_assembly_name_in_subject = forms.BooleanField(initial=True, label=_('AssemblyTeam__message__include_assembly_name_in_subject'), required=False) + message = forms.CharField(widget=forms.Textarea, label=_('AssemblyTeam__message__message')) + + +__all__ = [ + 'AssemblyTeamMessageForm', +] diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 532e75823..7ca671949 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -44,6 +44,15 @@ msgstr "Mehrere Tags bitte mit einem Komma trennen." msgid "AssemblyMember__needamanager" msgstr "Mindestens ein Mitglied muss die Assembly verwalten können!" +msgid "AssemblyTeam__message__subject" +msgstr "Betreff" + +msgid "AssemblyTeam__message__include_assembly_name_in_subject" +msgstr "Assembly Namen im Betreff einfügen?" + +msgid "AssemblyTeam__message__message" +msgstr "Nachrichtentext" + msgid "BadgeTokenTimeConstraint__date_time_range__label" msgstr "Gültigkeitszeitraum" @@ -744,10 +753,6 @@ msgstr "(Noch?) Keine Daten zugeordnet." msgid "Assembly__registration_data" msgstr "Registrierungs-Daten" -# use translation from core -msgid "Assembly__description" -msgstr "" - msgid "german" msgstr "Deutsch" @@ -836,23 +841,23 @@ msgstr "Position für nicht akzeptierte Assembly speichern" msgid "assemblyedit_state" msgstr "Anmelde-Status der Assembly ändern" -msgid "assemblyedit_message" -msgstr "Nachricht an Verwaltende der Assembly senden" +msgid "AssemblyTeam__message__title" +msgstr "Nachricht an ein Assembly erstellen" -msgid "assemblyedit_message_intro" +msgid "AssemblyTeam__message__intro" msgstr "Schreibe einen Text welcher an die Verwaltenden der Assembly gesendet werden soll. Markdown ist erlaubt." -msgid "subject" -msgstr "Betreff" +msgid "AssemblyTeam__message__preview_button" +msgstr "Vorschau generieren" -msgid "message" -msgstr "Nachricht" +msgid "AssemblyTeam__message__preview_title" +msgstr "Nachrichten Vorschau" -msgid "preview" -msgstr "Vorschau" +msgid "AssemblyTeam__message__recipients" +msgstr "Nachrichten Empfänger" -msgid "assemblyedit_message_preview" -msgstr "Vorschau" +msgid "AssemblyTeam__message__send_button" +msgstr "Nachricht versenden" msgid "nav_activetab_srmarker" msgstr "(ausgewählt)" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 792b67f1a..992168f95 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -44,6 +44,15 @@ msgstr "Split multiple tags by comma." msgid "AssemblyMember__needamanager" msgstr "At least one member must be able to manage the assembly!" +msgid "AssemblyTeam__message__subject" +msgstr "subject" + +msgid "AssemblyTeam__message__include_assembly_name_in_subject" +msgstr "Include assembly name in subject?" + +msgid "AssemblyTeam__message__message" +msgstr "message text" + msgid "BadgeTokenTimeConstraint__date_time_range__label" msgstr "validity range" @@ -742,10 +751,6 @@ msgstr "No data assigned (yet?)." msgid "Assembly__registration_data" msgstr "registration data" -# (use translation from core) -msgid "Assembly__description" -msgstr "" - msgid "german" msgstr "german" @@ -834,23 +839,23 @@ msgstr "save location for yet-unaccepted assembly" msgid "assemblyedit_state" msgstr "change assembly's registration state" -msgid "assemblyedit_message" -msgstr "send a message to the assembly's organizing staff" +msgid "AssemblyTeam__message__title" +msgstr "Create a message to an assembly" -msgid "assemblyedit_message_intro" +msgid "AssemblyTeam__message__intro" msgstr "Write the message to the assembly. Markdown is allowed." -msgid "subject" -msgstr "" +msgid "AssemblyTeam__message__preview_button" +msgstr "preview the message" -msgid "message" -msgstr "" +msgid "AssemblyTeam__message__preview_title" +msgstr "Message preview" -msgid "preview" -msgstr "" +msgid "AssemblyTeam__message__recipients" +msgstr "message recipients" -msgid "assemblyedit_message_preview" -msgstr "preview" +msgid "AssemblyTeam__message__send_button" +msgstr "Send the message" msgid "nav_activetab_srmarker" msgstr "(current)" diff --git a/src/backoffice/templates/backoffice/assemblyteam_message.html b/src/backoffice/templates/backoffice/assemblyteam_message.html index 9ce754e31..fcec41e08 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_message.html +++ b/src/backoffice/templates/backoffice/assemblyteam_message.html @@ -1,7 +1,8 @@ {% extends "backoffice/base.html" %} {% load i18n %} {% load static %} -{% load widget_tweaks %} +{% load verbose_name %} +{% load django_bootstrap5 %} {% block content %} {% if form.errors %} @@ -12,104 +13,111 @@ </div> {% endif %} - <div class="row"> - <div class="col-md-12"> - <div class="card mb-3"> - <div class="card-header">{% trans "assemblyedit_message" %}</div> - <div class="card-body"> - <form action="{% url 'backoffice:assemblyteam-message' pk=assembly.id %}" - method="post"> + <form action="{% url 'backoffice:assemblyteam-message' pk=assembly.id %}" + method="post"> + <div class="row"> + <div class="col-md-12"> + <div class="card mb-3"> + <div class="card-header">{% trans "AssemblyTeam__message__title" %}</div> + <div class="card-body"> {% csrf_token %} - <p>{% trans "Assembly__slug" %}: {{ assembly.slug }}</p> - - <p>{% trans "assemblyedit_message_intro" %}</p> - - <div class="mb-3"> - <label for="asm_subj" class="form-label">{% trans "subject" %}</label> - <input type="text" - class="form-control" - name="subject" - id="asm_subj" - value="{{ subject }}"> - </div> - <div class="mb-3"> - <label for="asm_msg" class="form-label">{% trans "message" %}</label> - <textarea class="form-control" name="message" id="asm_msg">{{ message }}</textarea> + <div class="row"> + <div class="col-md-6"> + <p> + <label class="form-label" for="assembly_name">{{ assembly|verbose_name:"name" }}</label> + : <span id="assembly_name">{{ assembly.name }}</span> + </p> + </div> + <div class="col-md-6"> + <p> + <label class="form-label" for="assembly_slug">{{ assembly|verbose_name:"slug" }}</label> + : <span id="assembly_slug">{{ assembly.slug }}</span> + </p> + </div> </div> - <button type="submit" class="btn btn-sm btn-primary"> - <i class="bi bi-eye"></i> {% trans "preview" %} + <p>{% trans "AssemblyTeam__message__intro" %}</p> + {% bootstrap_form form %} + </div> + <div class="card-footer text-end"> + <button type="submit" class="btn btn-primary" name="action" value="preview"> + <i class="bi bi-eye"></i> {% trans "AssemblyTeam__message__preview_button" %} </button> - </form> + </div> </div> </div> </div> - </div> - - {% if preview %} - <div class="row"> - <div class="col-md-12"> - <div class="card mb-3"> - <div class="card-header">{% trans "assemblyedit_message_preview" %}</div> - <div class="card-body"> - {% trans "subject" %}: <strong>{{ preview.subject }}</strong> - <ul class="nav nav-tabs" id="myTab" role="tablist"> - <li class="nav-item" role="presentation"> - <button class="nav-link active" - id="html-preview-tab" - data-bs-toggle="tab" - data-bs-target="#html-tab-pane" - type="button" - role="tab" - aria-controls="html-tab-pane" - aria-selected="true">HTML</button> - </li> - <li class="nav-item" role="presentation"> - <button class="nav-link" - id="text-preview-tab" - data-bs-toggle="tab" - data-bs-target="#text-tab-pane" - type="button" - role="tab" - aria-controls="text-tab-pane" - aria-selected="false">Text</button> - </li> - </ul> - <div class="tab-content" id="myTabContent"> - <div class="tab-pane fade show active" - id="html-tab-pane" - role="tabpanel" - aria-labelledby="html-preview-tab" - tabindex="0"> - <div class="border border-top-0 p-3">{{ preview.html }}</div> - </div> - <div class="tab-pane fade" - id="text-tab-pane" - role="tabpanel" - aria-labelledby="text-preview-tab" - tabindex="0"> - <pre class="border border-top-0 p-3">{{ preview.text }}</pre> - </div> + {% if preview %} + <div class="row"> + <div class="col-md-12"> + <div class="card mb-3"> + <div class="card-header">{% trans "AssemblyTeam__message__preview_title" %}</div> + <div class="card-body"> - </div> - </div> - <div class="card-footer"> - <form action="{% url 'backoffice:assemblyteam-message' pk=assembly.id %}" - method="post"> - {% csrf_token %} - <input type="hidden" name="previewed" value="1"> - <input type="hidden" name="subject" value="{{ subject }}"> - <textarea class="d-none" readonly name="message">{{ message }}</textarea> - <button type="submit" class="btn btn-primary"> - <i class="bi bi-envelope-paper"></i> send it - </button> - </form> + <div class="row"> + <col-md-12> + {% trans "AssemblyTeam__message__subject" %}: <strong>{{ preview.subject }} + </col-md-12> + </strong> </div> + <div class="row mb-3"> + <col-md-12> + {% trans "AssemblyTeam__message__recipients" %}: <strong>{{ recipients }} + </col-md-12> + </strong> + </div> + + <ul class="nav nav-tabs" id="preview" role="tablist"> + <li class="nav-item" role="presentation"> + <button class="nav-link active" + id="html-preview-tab" + data-bs-toggle="tab" + data-bs-target="#html-tab-pane" + type="button" + role="tab" + aria-controls="html-tab-pane" + aria-selected="true">HTML</button> + </li> + <li class="nav-item" role="presentation"> + <button class="nav-link" + id="text-preview-tab" + data-bs-toggle="tab" + data-bs-target="#text-tab-pane" + type="button" + role="tab" + aria-controls="text-tab-pane" + aria-selected="false">Text</button> + </li> + </ul> + <div class="tab-content" id="previewContent"> + <div class="tab-pane fade show active" + id="html-tab-pane" + role="tabpanel" + aria-labelledby="html-preview-tab" + tabindex="0"> + <div class="border border-top-0 p-3">{{ preview.html }}</div> </div> + <div class="tab-pane fade" + id="text-tab-pane" + role="tabpanel" + aria-labelledby="text-preview-tab" + tabindex="0"> + <pre class="border border-top-0 p-3">{{ preview.text }}</pre> + </div> + </div> </div> - {% endif %} + <div class="card-footer text-end"> + <button type="submit" class="btn btn-primary" name="action" value="send"> + <i class="bi bi-envelope-paper"></i> {% trans "AssemblyTeam__message__send_button" %} + </button> + </div> + </div> +</div> +</div> +{% endif %} +</form> {% endblock content %} diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py index f7178da4c..c26d5a62a 100644 --- a/src/backoffice/views/assemblyteam.py +++ b/src/backoffice/views/assemblyteam.py @@ -14,7 +14,7 @@ from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, View +from django.views.generic import DetailView, FormView, View from core.models import ActivityLogChange, ActivityLogEntry, Room from core.models.assemblies import Assembly, AssemblyMember @@ -22,6 +22,7 @@ from core.models.conference import ConferenceExportCache from core.models.users import UserCommunicationChannel from core.views.list_views import AlphabeticalPaginatorView, FilteredListView +from backoffice.forms import AssemblyTeamMessageForm from backoffice.templatetags.c3assemblies import get_language_item from .mixins import AssemblyMixin, ConferenceLoginRequiredMixin @@ -692,23 +693,20 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View): return render(self.request, 'backoffice/assemblyteam_editposition.html', context) -class AssemblyMessageView(SingleAssemblyTeamMixin, View): - def get(self, *args, **kwargs): - context = self.get_context_data() - return render(self.request, 'backoffice/assemblyteam_message.html', context) +class AssemblyMessageView(SingleAssemblyTeamMixin, FormView): + template_name = 'backoffice/assemblyteam_message.html' + form_class = AssemblyTeamMessageForm - def post(self, *args, **kwargs): - request = self.request - assembly = self.assembly - previewed = request.POST.get('previewed') == '1' - subject = request.POST.get('subject', '') - message = request.POST.get('message', '') + preview: bool = False - context = self.get_context_data() - context['subject'] = subject - context['message'] = message + def form_valid(self, form): + action = self.request.POST.get('action', 'preview') + assembly = self.assembly + subject = form.cleaned_data['subject'] + message = form.cleaned_data['message'] + include_assembly = form.cleaned_data['include_assembly_name_in_subject'] - if previewed: + if action == 'send': # preview confirmed, send it try: recipients = assembly.send_mail_to_managers(subject=subject, message=message) @@ -725,15 +723,32 @@ class AssemblyMessageView(SingleAssemblyTeamMixin, View): except Exception as err: messages.error(self.request, f'MESSAGE FAILED: {err}') - return render(self.request, 'backoffice/assemblyteam_message.html', context) - # generate preview of mail - p_subj, p_html, p_text = assembly.prepare_mail_to_managers(subject=subject, message=message) - context['preview'] = { - 'subject': p_subj, - 'html': p_html, - 'text': p_text, - } - + p_subj, p_html, p_text = assembly.prepare_mail_to_managers(subject=subject, message=message, include_assembly_name=include_assembly) # render page w/ preview - return render(self.request, 'backoffice/assemblyteam_message.html', context) + return render( + self.request, + self.template_name, + { + **self.get_context_data(), + 'preview': { + 'subject': p_subj, + 'html': p_html, + 'text': p_text, + }, + }, + ) + + def get_context_data(self, *args, **kwargs): + return { + **super().get_context_data(*args, **kwargs), + 'recipients': '; '.join( + list( + self.assembly.members.filter( + can_manage_assembly=True, + member__communication_channels__is_verified=True, + member__communication_channels__channel=UserCommunicationChannel.Channel.MAIL, + ).values_list('member__communication_channels__address', flat=True) + ) + ), + } diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index c0d6f7b60..7bb008b67 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -474,7 +474,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): return super().save(*args, update_fields=update_fields, **kwargs) - def prepare_mail_to_managers(self, subject, message) -> tuple[str, str | None, str]: + def prepare_mail_to_managers(self, subject: str, message: str, *, include_assembly_name: bool = True) -> tuple[str, str | None, str]: from core.templatetags.hub_absolute import hub_absolute # pylint: disable=import-outside-toplevel ctx = { @@ -494,7 +494,10 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel): else: body_html = None - msg_subj = f'[{self.conference.slug}] {subject}' + msg_subj = f'[{self.conference.slug}]' + if include_assembly_name: + msg_subj += f' {{{self.name}}}' + msg_subj += f' {subject}' return msg_subj, body_html, body_text diff --git a/src/core/tests/assemblies.py b/src/core/tests/assemblies.py index 052e0e193..4c8d66097 100644 --- a/src/core/tests/assemblies.py +++ b/src/core/tests/assemblies.py @@ -76,7 +76,7 @@ class AssembliesTests(AssembliesTestsMixin): self.assertEqual(len(mail.outbox), 1) the_mail = mail.outbox[0] # type: mail.EmailMultiAlternatives - self.assertEqual(the_mail.subject, f'[{self.conference.slug}] Hello World') + self.assertEqual(the_mail.subject, f'[{self.conference.slug}] {{{self.assembly2.name}}} Hello World') self.assertListEqual(the_mail.to, ['notifications2@unittest.local']) self.assertEqual(the_mail.from_email, 'assembly_team+mail_from_not_configured@localhost') self.assertListEqual(the_mail.reply_to, ['assembly_team+mail_from_not_configured@localhost']) @@ -100,7 +100,7 @@ class AssembliesTests(AssembliesTestsMixin): self.assertEqual(len(mail.outbox), 2) the_mail = mail.outbox[0] # type: mail.EmailMultiAlternatives - self.assertEqual(the_mail.subject, f'[{self.conference.slug}] Hello World') + self.assertEqual(the_mail.subject, f'[{self.conference.slug}] {{{self.assembly1.name}}} Hello World') # check that plaintext + html parts are present and contain the link self.assertEqual(the_mail.content_subtype, 'plain') -- GitLab