diff --git a/src/backoffice/forms/__init__.py b/src/backoffice/forms/__init__.py index 35c2dca0744b004bba6d1713182e11ecb1925404..9a8d3824032fd0b2189b9737eca48994ed7c6aa4 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 0000000000000000000000000000000000000000..844b3ed56abf3dd529d39b5c5d8b9c6a8ac0f96c --- /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 3068f79693cb1a75c6c4effe74cb290f183345b5..7ca6719498f0c7ca307e9844aa98ca9955caca03 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,6 +753,12 @@ msgstr "(Noch?) Keine Daten zugeordnet." msgid "Assembly__registration_data" msgstr "Registrierungs-Daten" +msgid "german" +msgstr "Deutsch" + +msgid "english" +msgstr "Englisch" + msgid "assemblyedit_statebtn_registered" msgstr "registriert" @@ -826,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)" @@ -901,8 +916,8 @@ msgstr "Abmelden" msgid "nav_login" msgstr "Anmelden" -msgid "All" -msgstr "alle" +msgid "Overview" +msgstr "Übersicht" msgid "backoffice__not_a_conference_member" msgstr "Das Nutzerkonto, mit dem du gerade angemeldet bist, ist zur Zeit nicht mit einem Veranstaltungsticket verknüpft. Wenn Du gerade beim Aufbau hilfst, ist das kein Problem. Denk aber bitte daran, rechtzeitig ein Ticket zu kaufen und dies kurz vor Veranstaltungsbeginn mit deinem Nutzerkonto zu verknüpfen. Ansonsten kannst Du mit diesem Nutzerkonto nicht an der Veranstaltung teilnehmen." @@ -946,6 +961,12 @@ msgstr "Konferenz-Daten" msgid "Conference__submit" msgstr "Konferenz speichern" +msgid "AssemblyTeam__from_email" +msgstr "Assembly Team Absender E-Mail" + +msgid "AssemblyTeam__reply_to_email" +msgstr "Assembly Team Reply-To E-Mail" + msgid "Conferences__selection__header" msgstr "Konferenz auswählen" @@ -2124,3 +2145,6 @@ msgstr "Sessions" msgid "wa_textures" msgstr "Textures" + +#~ msgid "All" +#~ msgstr "alle" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 247640a564e8594abe9db712b68ce629e524f19b..992168f953e9e1a61cdf8c5b842ebdb36c4ba5f8 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,6 +751,12 @@ msgstr "No data assigned (yet?)." msgid "Assembly__registration_data" msgstr "registration data" +msgid "german" +msgstr "german" + +msgid "english" +msgstr "english" + msgid "assemblyedit_statebtn_registered" msgstr "registered" @@ -824,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)" @@ -899,8 +914,8 @@ msgstr "Logout" msgid "nav_login" msgstr "log in" -msgid "All" -msgstr "all" +msgid "Overview" +msgstr "overview" msgid "backoffice__not_a_conference_member" msgstr "The user account you are currently logged in with is not linked to an valid event ticket. If you are helping with the set-up, this is not a problem. But please remember to buy a ticket in time and link it to your user account. Otherwise you will not be able to participate in the event with this user account. Linking the ticket to the user account will be available directly before the event." @@ -944,6 +959,12 @@ msgstr "conference data" msgid "Conference__submit" msgstr "Recall conference" +msgid "AssemblyTeam__from_email" +msgstr "assembly team from email address" + +msgid "AssemblyTeam__reply_to_email" +msgstr "assembly team reply to email address" + msgid "Conferences__selection__header" msgstr "Select Conference" @@ -2127,3 +2148,9 @@ msgstr "sessions" msgid "wa_textures" msgstr "textures" + +#~ msgid "assemblyedit_message" +#~ msgstr "send a message to the assembly's organizing staff" + +#~ msgid "All" +#~ msgstr "all" diff --git a/src/backoffice/static/backoffice-no-script.css b/src/backoffice/static/backoffice-no-script.css new file mode 100644 index 0000000000000000000000000000000000000000..e4d99e23c8b945b3ebd1e99fc7fd4be393c098c8 --- /dev/null +++ b/src/backoffice/static/backoffice-no-script.css @@ -0,0 +1,3 @@ +div.collapse:not(.show) { + display: block !important; +} diff --git a/src/backoffice/static/backoffice.css b/src/backoffice/static/backoffice.css index 575f11386a398a5d8d755f622f4b28e19274daee..96469ab0eea74e0ce8f03c610352f1697b6f9d77 100644 --- a/src/backoffice/static/backoffice.css +++ b/src/backoffice/static/backoffice.css @@ -23,12 +23,11 @@ } #sidebar .list-group-item, -#sidebar .list-group-item > a { +#sidebar .list-group-item a { align-items: center; background: #7386d5; border: none; color: #fff; - display: flex; } #sidebar .list-group-item.active, diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html index 2598f5142e205a28626b1c068b3683e21f543441..cbede58d16260ff0a9269ae24ea52782ea4a36b1 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html +++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html @@ -3,6 +3,7 @@ {% load i18n %} {% load static %} {% load c3assemblies %} +{% load verbose_name %} {% block content %} <span class="float-end text-muted small"> @@ -64,7 +65,21 @@ </dl> <div class="row"> - <div class="col-md-6"></div> + <div class="col-md-12"> + <dl class="row"> + <dt class="col-md-2">{{ object|verbose_name:"description" }} {% trans "german" %}:</dt> + <dd class="col-sm-10"> + {{ object.description_de }} + </dd> + </dl> + + <dl class="row"> + <dt class="col-md-2">{{ object|verbose_name:"description" }} {% trans "english" %}</dt> + <dd class="col-sm-10"> + {{ object.description_en }} + </dd> + </dl> + </div> </div> </div> <div class="col-md-4"> diff --git a/src/backoffice/templates/backoffice/assemblyteam_message.html b/src/backoffice/templates/backoffice/assemblyteam_message.html index 9ce754e3180e2ba4293e098f332e99811e162df4..fcec41e08cb126a5a4f67f7fc50cdbae53de705c 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/templates/backoffice/base.html b/src/backoffice/templates/backoffice/base.html index fdcb86c8d82cbdee55ffaa39ac5298c332d4f343..da3d4539bc810801363a3dadf273e9db7e8e5510 100644 --- a/src/backoffice/templates/backoffice/base.html +++ b/src/backoffice/templates/backoffice/base.html @@ -17,6 +17,11 @@ type="text/css" href="{% static 'vendor/bootstrap-icons/bootstrap-icons.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'backoffice.css' %}"> + <noscript> + <link rel="stylesheet" + type="text/css" + href="{% static 'backoffice-no-script.css' %}"> + </noscript> {% if uses_map %} {% include "core/map_header.html" %} {% endif %} @@ -211,71 +216,61 @@ {% endif %} {% for item in sidebar.items %} - <div class="list-group-item list-group-action {% if item.active %}active{% endif %}" + <div class="list-group-item list-group-action d-inline-flex {% if item.active or item.child_active %}active{% endif %}" {% if item.active %}aria-current="true"{% endif %} - {% if item.children %} aria-expanded="{% if item.expanded %}true{% else %}false{% endif %} - " - data-bs-toggle="collapse" - data-bs-target=".sidebar{{ forloop.counter }}" + {% if item.children %}aria-expanded={% if item.expanded %}"true"{% else %}"false"{% endif %} {% endif %}> - {% if item.link %} - <a href="{{ item.link }}" - class="d-block {{ item.class|join:' ' }}" - onclick="event.stopPropagation()">{{ item.caption }}</a> - {% else %} - {{ item.caption }} - {% endif %} - {% if item.children or item.count != '' or item.add_link %} - {% if item.children %} - <a href="#" - class="sidebar me-auto {{ item.class|join:' ' }} dropdown-toggle" - aria-expanded="{% if item.expanded %}true{% else %}false{% endif %}" - data-bs-toggle="collapse" - data-bs-target=".sidebar{{ forloop.counter }}"> - {% if item.count != '' %} - <span class="badge text-bg-light ms-1">{{ item.count }}</span> - {% else %} - - {% endif %} - </a> + {% if item.link and not item.children %} + <a href="{{ item.link }}" class="flex-grow-1"> {% else %} - <span class="me-auto"> - {% if item.count != '' %} - <span class="badge text-bg-light ms-1">{{ item.count }}</span> - {% else %} - - {% endif %} - </span> - {% endif %} - {% if item.add_link %} - <a href="{{ item.add_link }}" - class="ms-1" - onclick="event.stopPropagation()"> - <i class="bi bi-plus-circle"></i> + <div class="flex-grow-1"> + {% endif %} + <div {% if item.children %}data-bs-toggle="collapse" data-bs-target=".sidebar{{ forloop.counter }}"{% endif %} + class="{{ item.class|join:' ' }}"> + {{ item.caption }} + + <span class="float-end"> + {% if item.children %} + <span class="dropdown-toggle dropdown-toggle-split" + data-bs-toggle="dropdown" + aria-expanded="false"> + <span class="visually-hidden">Toggle Dropdown</span> + </span> + {% endif %} + {% if item.count != '' %}<span class="badge text-bg-light ms-1">{{ item.count }}</span>{% endif %} + </span> + </div> + {% if item.link and not item.children %} </a> - {% endif %} + {% else %} + </div> + {% endif %} + {% if item.add_link %} + <a href="{{ item.add_link }}" class="ms-3 float-end"> + <i class="bi bi-plus-circle"></i> + </a> {% endif %} </div> {% if item.children %} - <div class="sidebar{{ forloop.counter }} collapse{% if item.expanded %} show{% endif %}"> + <div class="sidebar{{ forloop.counter }} collapse {% if item.expanded %}show{% endif %}"> {% if item.link %} - <a class="list-group-item list-group-action justify-content-between child {% if item.active %}active{% endif %}" - href="{{ item.link }}">{% trans "All" %}</a> + <div class="list-group-item list-group-action child d-inline-flex w-100 {% if item.active %}active{% endif %}"> + <a href="{{ item.link }}" class="flex-grow-1">{% trans "Overview" %}</a> + </div> {% endif %} {% for child in item.children %} - <a class="list-group-item list-group-action justify-content-between child {% if child.active %}active{% endif %}" - href="{{ child.link }}"> - <div>{{ child.caption }}</div> + <div class="list-group-item list-group-action child d-inline-flex w-100 {% if child.active %}active{% endif %}"> + <a href="{{ child.link }}" class="flex-grow-1 d-inline-flex"> + <span class="flex-grow-1">{{ child.caption }}</span> + {% if child.count %}<span class="badge text-bg-light ms-auto">{{ child.count }}</span>{% endif %} + </a> {% if child.add_link %} - <a href="{{ child.add_link }}" - class="ms-1" - onclick="event.stopPropagation()"> + <a href="{{ child.add_link }}" class="ms-2" onclick="event.stopPropagation()" nonce={{ request.csp_nonce }}> <i class="bi bi-plus-circle"></i> </a> {% endif %} - {% if child.count %}<span class="badge text-bg-light ms-1">{{ child.count }}</span>{% endif %} - </a> + </div> {% endfor %} </div> {% endif %} diff --git a/src/backoffice/templates/backoffice/conferences/registration_edit.html b/src/backoffice/templates/backoffice/conferences/registration_edit.html index 9b3c5aa49164246697b123f2c418bc40b85b79e2..5ff8dd8b6c0f7afc8b82400aaca578a868878128 100644 --- a/src/backoffice/templates/backoffice/conferences/registration_edit.html +++ b/src/backoffice/templates/backoffice/conferences/registration_edit.html @@ -71,6 +71,12 @@ <div class="col-md-6">{% bootstrap_field form.registration_start %}</div> <div class="col-md-6">{% bootstrap_field form.registration_deadline %}</div> </div> + <div class="row mb-3"> + <label for="from_email" class="col-md-2">{% trans "AssemblyTeam__from_email" %}</label> + <span class="col-md-4">{{ assembly_team_from_email }}</span> + <label for="from_email" class="col-md-2">{% trans "AssemblyTeam__reply_to_email" %}</label> + <span class="col-md-4">{{ assembly_team_reply_to }}</span> + </div> <div class="row mb-3"> <div class="col-md-12">{% bootstrap_field form.additional_fields_schema %}</div> </div> diff --git a/src/backoffice/templates/backoffice/map_poi_list.html b/src/backoffice/templates/backoffice/map_poi_list.html index f8560e6f19a613452cbdafa141a28d52ad1e54c5..443be84b979ee716cd59a06affca0bf03e760825 100644 --- a/src/backoffice/templates/backoffice/map_poi_list.html +++ b/src/backoffice/templates/backoffice/map_poi_list.html @@ -41,7 +41,7 @@ }); }); </script> -{% endblock script %} +{% endblock scripts %} {% block content %} diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py index 84bcd9020a4db4c12417b1eb61854c86914e6b02..c26d5a62af290c1f9c993cceb81def37b1b406b8 100644 --- a/src/backoffice/views/assemblyteam.py +++ b/src/backoffice/views/assemblyteam.py @@ -14,13 +14,15 @@ 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, ListView, View +from django.views.generic import DetailView, FormView, View from core.models import ActivityLogChange, ActivityLogEntry, Room from core.models.assemblies import Assembly, AssemblyMember 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 @@ -38,13 +40,13 @@ class AssemblyTeamMixin(ConferenceLoginRequiredMixin): status_field = 'state_assembly' MODES = { - 'all': (Q(), _('nav_assemblies_all')), - 'accepted': (Q(state_assembly__in=Assembly.PUBLIC_STATES), _('nav_assemblies_accepted')), - 'pending': (Q(state_assembly__in=[Assembly.State.REGISTERED]), _('nav_assemblies_pending')), - 'planned': (Q(state_assembly__in=[Assembly.State.PLANNED]), _('nav_assemblies_planned')), - 'rejected': (Q(state_assembly__in=[Assembly.State.REJECTED]), _('nav_assemblies_rejected')), - 'hidden': (Q(state_assembly__in=[Assembly.State.HIDDEN]), _('nav_assemblies_hidden')), - 'not_selected': (Q(state_assembly__in=[Assembly.State.NONE]), _('nav_assemblies_not_selected')), + 'all': (('XMode', Q(state_assembly__exact=Assembly.State.HIDDEN)), _('nav_assemblies_all')), + 'accepted': (('QMode', Q(state_assembly__in=Assembly.PUBLIC_STATES)), _('nav_assemblies_accepted')), + 'pending': (('QMode', Q(state_assembly__in=[Assembly.State.REGISTERED])), _('nav_assemblies_pending')), + 'planned': (('QMode', Q(state_assembly__in=[Assembly.State.PLANNED])), _('nav_assemblies_planned')), + 'rejected': (('QMode', Q(state_assembly__in=[Assembly.State.REJECTED])), _('nav_assemblies_rejected')), + 'hidden': (('QMode', Q(state_assembly__in=[Assembly.State.HIDDEN])), _('nav_assemblies_hidden')), + 'not_selected': (('QMode', Q(state_assembly__in=[Assembly.State.NONE])), _('nav_assemblies_not_selected')), } def get_context_data(self, **kwargs): @@ -75,7 +77,7 @@ class AssemblyTeamMixin(ConferenceLoginRequiredMixin): { 'mode': m, 'caption': t, - 'count': self.conference.assemblies.filter(q).count(), + 'count': self.conference.assemblies.filter(q[1]).count(), 'link': reverse(self.base_view_name) + '?mode=' + m, } ) @@ -116,32 +118,39 @@ class AssemblyTeamMixin(ConferenceLoginRequiredMixin): return context -class AssembliesListMixin(AssemblyTeamMixin): +class AssembliesListMixin(AssemblyTeamMixin, FilteredListView): default_assemblies_mode = 'all' + filters = { + 'tag': ('tags__tag__slug', 'list'), + } def get_queryset(self, **kwargs): # get the mode mode = (self.request.POST if self.request.method == 'POST' else self.request.GET).get('mode', self.default_assemblies_mode) + query_filters = {} - qs = Assembly.objects.associated_with_user(conference=self.conference, user=self.request.user, staff_can_see=True) if mode == 'not_selected': pass elif self.active_page == 'assemblies': - qs = qs.exclude(state_assembly=Assembly.State.NONE) + query_filters['!state_assembly'] = Assembly.State.NONE elif self.active_page == 'channels': - qs = qs.exclude(state_channel=Assembly.State.NONE) + query_filters['!state_channel'] = Assembly.State.NONE else: logging.warning('AssembliesListMixin: unexpected active_page="%s"', self.active_page) - qs = Assembly.objects.none() + return Assembly.objects.none() if mode in self.MODES: - qs = qs.filter(self.MODES[mode][0]) + query_filters[self.MODES[mode][0][0]] = self.MODES[mode][0][1] else: messages.warning(self.request, f'unknown mode "{mode}", using "all"') mode = 'all' + query_filters[self.MODES[mode][0][0]] = self.MODES[mode][0][1] self.assemblies_mode = mode - # done + qs = super().get_queryset( + Assembly.objects.associated_with_user(conference=self.conference, user=self.request.user, staff_can_see=True), query_filters=query_filters + ) + return qs @@ -200,13 +209,13 @@ class AssemblyView(SingleAssemblyTeamMixin, DetailView): return redirect('backoffice:assemblyteam-detail', self.assembly.id) -class AssembliesView(AssembliesListMixin, ListView): +class AssembliesView(AssembliesListMixin, AlphabeticalPaginatorView): template_name = 'backoffice/assembly_list.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['filter_tag'] = self.selected_tag + # context['filter_tag'] = self.selected_tag context['mode'] = self.assemblies_mode context['mode_display'] = self.MODES[self.assemblies_mode][1] @@ -222,19 +231,8 @@ class AssembliesView(AssembliesListMixin, ListView): return context - def get_queryset(self, *args, **kwargs): - qs = super().get_queryset(*args, **kwargs) - - # tag filtering - self.selected_tag = (self.request.POST if self.request.method == 'POST' else self.request.GET).get('tag') - if self.selected_tag is not None: - qs = qs.filter(tags__tag__slug=self.selected_tag, tags__tag__is_public=True) - - # return the assembled QuerySet - return qs - -class AssembliesListsView(AssembliesListMixin, View): +class AssembliesListsView(AssembliesListMixin): DEFAULT_DELIMITER_CHAR = ';' DEFAULT_QUOTE_CHAR = '"' DELIMITER_CHARS = ',;|\t=*/' @@ -364,6 +362,7 @@ class AssembliesListsView(AssembliesListMixin, View): def get(self, *args, **kwargs): qs, variant, variant_name, variant_fields = self.prepare(*args, **kwargs) + self.object_list = qs if qs is None: assert isinstance(variant, HttpResponse) return variant @@ -694,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) @@ -727,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/backoffice/views/conferences.py b/src/backoffice/views/conferences.py index 66515ce995aae3295dbf5504333b2989b32992b9..e44ef89750702e6d1e53113665a40ff697da8b93 100644 --- a/src/backoffice/views/conferences.py +++ b/src/backoffice/views/conferences.py @@ -1,6 +1,7 @@ import logging from typing import Any +from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied from django.forms.forms import BaseForm @@ -196,3 +197,10 @@ class ConferenceRegistrationView(ConferenceFormMixin, UpdateView): def get_success_url(self): return reverse('backoffice:conference-registration', kwargs={'pk': self.object.pk}) + + def get_context_data(self, *args, **kwargs): + return { + **super().get_context_data(*args, **kwargs), + 'assembly_team_from_email': settings.ASSEMBLY_TEAM_FROM_EMAIL, + 'assembly_team_reply_to': ', '.join(settings.ASSEMBLY_TEAM_REPLY_TO), + } diff --git a/src/backoffice/views/mixins.py b/src/backoffice/views/mixins.py index e6b187cf4b1b1b588744da4750112c060d419e70..c29a4c610e8f40ea899220f3b8f2767dc4b36379 100644 --- a/src/backoffice/views/mixins.py +++ b/src/backoffice/views/mixins.py @@ -426,24 +426,25 @@ def guess_active_sidebar_item(request: HttpRequest, sidebar_items: dict, with_qu query_string = '?' + qs request_url = request.META.get('PATH_INFO') + query_string - for x in sidebar_items: - if request_url == x.get('link'): - x['active'] = True - x['expanded'] = True + for sidebar_item in sidebar_items: + if request_url == sidebar_item.get('link'): + sidebar_item['active'] = True + sidebar_item['expanded'] = True continue - if request_url == x.get('add_link'): - x['active'] = True - x['expanded'] = False + if request_url == sidebar_item.get('add_link'): + sidebar_item['active'] = True + sidebar_item['expanded'] = False continue - if 'children' in x: - for y in x.get('children') or []: - if 'link' in y and y['link'] == request_url: - y['active'] = True - x['expanded'] = True + if 'children' in sidebar_item: + for sidebar_child in sidebar_item.get('children') or []: + if 'link' in sidebar_child and sidebar_child['link'] == request_url: + sidebar_child['active'] = True + sidebar_item['child_active'] = True + sidebar_item['expanded'] = True else: - x['children'] = None + sidebar_item['children'] = None class PasswordMixin: diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 2ace4f5ce1e5cb40b23ca92ce3ec22f4171af3db..8e2d4907c0aa41bceaf58c354c7e1430635c4f95 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -304,7 +304,7 @@ msgid "Assembly__description__help" msgstr "what happens in this assembly (will be shown publicly)" msgid "Assembly__description" -msgstr "description" +msgstr "public description" msgid "Assembly__registration_details__help" msgstr "Please tell us what you have planned for your assembly, in particular whether and what you want to design yourself and what you are bringing with you. (not public, only for assembly team)" diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index c0d6f7b60a83b4d5ba72da3c64f6613276f09405..7bb008b673b19119544fac5760fc8a91b0bd077a 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 052e0e1934dc967143472ca9695bc829a7d99f8b..4c8d660977ca2f9b7dd0deefd1b2777f4a298b56 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') diff --git a/src/core/views/list_views.py b/src/core/views/list_views.py index 2dccab5dd93f34856e7c2eef4196e22ff4a09c5b..da8d19277b70f8e67bb637675ce990a2565e6202 100644 --- a/src/core/views/list_views.py +++ b/src/core/views/list_views.py @@ -1,3 +1,4 @@ +import logging import string from typing import Any @@ -12,6 +13,8 @@ from django.views.generic import ListView from core.models.tags import TagItem from core.paginators import AlphabetPaginator +logger = logging.getLogger(__name__) + class FilteredListView(ListView): filters: dict[str, tuple[str, str]] | None = None @@ -30,6 +33,8 @@ class FilteredListView(ListView): """ try: paginate_by = self.request.GET.get('paginate_by', self.paginate_by) + if paginate_by is None: + return self.paginate_by paginate_by = int(paginate_by) except ValueError: if str(paginate_by).lower() == 'all': @@ -52,27 +57,40 @@ class FilteredListView(ListView): }, } - def get_queryset(self, queryset: QuerySet | None = None) -> QuerySet[Any]: + def get_queryset(self, queryset: QuerySet | None = None, query_filters: dict[str, str | list | bool] | None = None) -> QuerySet[Any]: queryset = super().get_queryset() if queryset is None else queryset ordering = self.get_ordering() if ordering: if isinstance(ordering, str): ordering = (ordering,) queryset = queryset.order_by(*ordering) - query_filters = {} + query_filters = query_filters or {} get_parameters = getattr(self.request, 'GET', {}) for parameter, (query_parameter, parameter_type) in self.filters.items(): value: str | bool | list[str] if value := get_parameters.get(parameter, self.kwargs.get(parameter)): self.params[parameter] = str(value) match parameter_type: - case 'bool': - value = value == 'true' case 'list': value = value.split(',') value = [x.strip() for x in value] - query_filters[query_parameter] = value - queryset = queryset.filter(**query_filters) + query_filters[f'{query_parameter}__in'] = value + case 'bool': + value = value == 'true' + query_filters[query_parameter] = value + case _: + query_filters[query_parameter] = value + logger.trace(f'Query Filters: {query_filters}') + for key, value in query_filters.items(): + match key[0]: + case '!': + queryset = queryset.exclude(**{key[1:]: value}) + case 'X': + queryset = queryset.exclude(value) + case 'Q': + queryset = queryset.filter(value) + case _: + queryset = queryset.filter(**{key: value}) return queryset