diff --git a/aleksis/apps/paweljong/forms.py b/aleksis/apps/paweljong/forms.py index 2d38289db8ce18dd09b8771e35cd9484458ffea2..46dade3deda9d17afca981ec628b68591465ef85 100644 --- a/aleksis/apps/paweljong/forms.py +++ b/aleksis/apps/paweljong/forms.py @@ -11,7 +11,7 @@ from phonenumber_field.formfields import PhoneNumberField from aleksis.core.mixins import ExtensibleForm from aleksis.core.models import Group, Person -from .models import Event, EventRegistration, Terms, Voucher +from .models import Event, EventRegistration, InfoMailing, Terms, Voucher COMMENT_CHOICES = [ ("first", _("Only first name")), @@ -49,6 +49,7 @@ class EditEventForm(ExtensibleForm): Fieldset(_("Date data"), Row("date_event", "date_registration", "date_retraction")), Fieldset(_("Event details"), Row("cost", "max_participants"), "information"), Fieldset(_("Terms"), "terms"), + Fieldset(_("Info mailings"), "info_mailings"), ), ) @@ -68,6 +69,7 @@ class EditEventForm(ExtensibleForm): "max_participants", "terms", "information", + "info_mailings", ] widgets = { "linked_group": ModelSelect2Widget( @@ -78,6 +80,10 @@ class EditEventForm(ExtensibleForm): search_fields=["aspect__icontains"], attrs={"data-minimum-input-length": 0, "class": "browser-default"}, ), + "info_mailings": ModelSelect2MultipleWidget( + search_fields=["subject__icontains"], + attrs={"data-minimum-input-length": 0, "class": "browser-default"}, + ), } @@ -456,3 +462,17 @@ class RegisterEventAccount(SignupForm, ExtensibleForm): "The username must only contain lower case letters and numbers, " "and must begin with a letter." ) + + +class EditInfoMailingForm(forms.ModelForm): + + layout = Layout( + Row("sender", "reply_to", "active"), + Row("send_to_person", "send_to_guardians"), + Row("subject"), + Row("text"), + ) + + class Meta: + model = InfoMailing + exclude = ["sent_to"] diff --git a/aleksis/apps/paweljong/menus.py b/aleksis/apps/paweljong/menus.py index 4655f1535de29effb42a1a143c93acf6b601d60a..afe3056df07a741f0bd94d2bb1db4a015c532907 100644 --- a/aleksis/apps/paweljong/menus.py +++ b/aleksis/apps/paweljong/menus.py @@ -44,6 +44,17 @@ MENUS = { ) ], }, + { + "name": _("Info mailings"), + "url": "info_mailings", + "icon": "info", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "paweljong.view_info_mailings_rule", + ) + ], + }, { "name": _("Generate participant list"), "url": "generate_lists", diff --git a/aleksis/apps/paweljong/migrations/0013_info_mailings.py b/aleksis/apps/paweljong/migrations/0013_info_mailings.py new file mode 100644 index 0000000000000000000000000000000000000000..f9bc02aa4ce93962f24dc134d7c1f21bf9e7bc94 --- /dev/null +++ b/aleksis/apps/paweljong/migrations/0013_info_mailings.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.12 on 2022-03-01 15:19 + +import ckeditor.fields +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0037_alter_personinvitation_id'), + ('sites', '0002_alter_domain_unique'), + ('paweljong', '0012_event_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='eventregistration', + name='event', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='paweljong.event', verbose_name='Event'), + ), + migrations.CreateModel( + name='InfoMailing', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('subject', models.CharField(max_length=255, verbose_name='subject')), + ('text', ckeditor.fields.RichTextField(verbose_name='Text')), + ('reply_to', models.EmailField(blank=True, max_length=254, verbose_name='Request replies to')), + ('active', models.BooleanField(default=False, verbose_name='Mailing is active')), + ('sender', models.EmailField(blank=True, max_length=254, verbose_name='Sender')), + ('send_to_person', models.BooleanField(default=True, verbose_name='Send to registered person')), + ('send_to_guardians', models.BooleanField(default=False, verbose_name='Send to guardians')), + ('sent_to', models.ManyToManyField(blank=True, editable=False, related_name='received_info_mailings', to='core.Person', verbose_name='Sent to persons')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ], + options={ + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + migrations.AddField( + model_name='event', + name='info_mailings', + field=models.ManyToManyField(blank=True, related_name='events', to='paweljong.InfoMailing', verbose_name='Info mailings'), + ), + ] diff --git a/aleksis/apps/paweljong/models.py b/aleksis/apps/paweljong/models.py index 74daa60b3d3b7fd9b88b5870d8a7c3e50f043a3d..55a067ad82e33466938c75670b2e2d1bd2955087 100644 --- a/aleksis/apps/paweljong/models.py +++ b/aleksis/apps/paweljong/models.py @@ -8,11 +8,11 @@ from django.utils.translation import gettext_lazy as _ from ckeditor.fields import RichTextField from django_iban.fields import IBANField -from templated_email import send_templated_mail from aleksis.core.mixins import ExtensibleModel from aleksis.core.models import Group, Person from aleksis.core.util.core_helpers import generate_random_code, get_site_preferences +from aleksis.core.util.email import send_email class Terms(ExtensibleModel): @@ -31,10 +31,17 @@ class InfoMailing(ExtensibleModel): active = models.BooleanField(verbose_name=_("Mailing is active"), default=False) + sender = models.EmailField(verbose_name=_("Sender"), blank=True) send_to_person = models.BooleanField(verbose_name=_("Send to registered person"), default=True) send_to_guardians = models.BooleanField(verbose_name=_("Send to guardians"), default=False) - sent_to = models.ManyToManyField(Person, verbose_name=_("Sent to persons"), related_name="received_info_mailings", editable=False, blank=True) + sent_to = models.ManyToManyField( + Person, + verbose_name=_("Sent to persons"), + related_name="received_info_mailings", + editable=False, + blank=True, + ) def __str__(self) -> str: return self.subject @@ -47,12 +54,16 @@ class InfoMailing(ExtensibleModel): sent_to = self.sent_to.all() for event in self.events.all(): - for registration in event.registrations: + for registration in event.registrations.all(): if registration.person in sent_to: continue - subject = self.subject.format(event=event, registration=registration, person=registration.person) - body = self.text.format(event=event, registration=registration, person=registration.person) + subject = self.subject.format( + event=event, registration=registration, person=registration.person + ) + body = self.text.format( + event=event, registration=registration, person=registration.person + ) if self.send_to_person: to = [registration.person.email] @@ -68,9 +79,18 @@ class InfoMailing(ExtensibleModel): reply_to = self.reply_to or sender context = {"subject": subject, "body": body} - send_templated_email(template_name="info_mailing", context=context, from_email=sender, recipient_list=to, cc=cc) + send_email( + template_name="info_mailing", + context=context, + from_email=sender, + recipient_list=to, + cc=cc, + headers={ + "Reply-To": reply_to, + }, + ) - self.sent_to.add(self.registration.person) + self.sent_to.add(registration.person) class Event(ExtensibleModel): @@ -94,7 +114,9 @@ class Event(ExtensibleModel): max_participants = models.PositiveSmallIntegerField(verbose_name=_("Maximum participants")) information = RichTextField(verbose_name=_("Information about the event")) terms = models.ManyToManyField(Terms, verbose_name=_("Terms"), related_name="event", blank=True) - info_mailings = models.ManyToManyField(InfoMailing, verbose_name=_("Info mailings"), related_name="events", blank=True) + info_mailings = models.ManyToManyField( + InfoMailing, verbose_name=_("Info mailings"), related_name="events", blank=True + ) def save(self, *args, **kwargs): if not self.slug: @@ -193,7 +215,9 @@ class Voucher(ExtensibleModel): class EventRegistration(ExtensibleModel): - event = models.ForeignKey(Event, on_delete=models.CASCADE, verbose_name=_("Event"), related_name="registrations") + event = models.ForeignKey( + Event, on_delete=models.CASCADE, verbose_name=_("Event"), related_name="registrations" + ) person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person")) date_registered = models.DateTimeField(auto_now_add=True, verbose_name=_("Registration date")) diff --git a/aleksis/apps/paweljong/rules.py b/aleksis/apps/paweljong/rules.py index 99254b87447353cbc6c437845bf3e7d61ebdcad5..056af3de4f17ea5c2fed2fce93b168a7c5fd8f74 100644 --- a/aleksis/apps/paweljong/rules.py +++ b/aleksis/apps/paweljong/rules.py @@ -109,3 +109,11 @@ view_terms_predicate = has_person & ( has_global_perm("paweljong.view_term") | has_any_object("paweljong.view_term", Terms) ) rules.add_perm("paweljong.view_terms_rule", view_terms_predicate) + + +# View info_mailings +view_info_mailings_predicate = has_person & ( + has_global_perm("paweljong.view_info_mailing") + | has_any_object("paweljong.view_info_mailing", Terms) +) +rules.add_perm("paweljong.view_info_mailings_rule", view_info_mailings_predicate) diff --git a/aleksis/apps/paweljong/tables.py b/aleksis/apps/paweljong/tables.py index 9dd0416c66601d415869104842983d8b0c7ad03a..2e7ac691d402d6c72ac1437eb5a7666146fe1e20 100644 --- a/aleksis/apps/paweljong/tables.py +++ b/aleksis/apps/paweljong/tables.py @@ -76,3 +76,23 @@ class TermsTable(tables.Table): verbose_name=_("Edit"), text=_("Edit"), ) + + +class InfoMailingsTable(tables.Table): + class Meta: + attrs = {"class": "responsive-table highlight"} + + subject = tables.Column() + + edit = tables.LinkColumn( + "edit_info_mailing_by_pk", + args=[A("id")], + verbose_name=_("Edit"), + text=_("Edit"), + ) + delete = tables.LinkColumn( + "delete_info_mailing_by_pk", + args=[A("id")], + verbose_name=_("Delete"), + text=_("Delete"), + ) diff --git a/aleksis/apps/paweljong/tasks.py b/aleksis/apps/paweljong/tasks.py index 0bbb807a572a749317cbec51dee40d3e9714d525..b1f34f10ec72dcc996aef7d85a9db5571a3a41bb 100644 --- a/aleksis/apps/paweljong/tasks.py +++ b/aleksis/apps/paweljong/tasks.py @@ -6,5 +6,6 @@ from aleksis.core.celery import app @app.task(run_every=timedelta(hours=1)) def send_info_mailings() -> None: from .models import InfoMailing # noqa + for mailing in InfoMailing.get_active_mailings(): mailing.send() diff --git a/aleksis/apps/paweljong/templates/paweljong/info_mailing/create.html b/aleksis/apps/paweljong/templates/paweljong/info_mailing/create.html new file mode 100644 index 0000000000000000000000000000000000000000..9c6952492f8a256e8e97bd5f17a7b56b6a00fbd8 --- /dev/null +++ b/aleksis/apps/paweljong/templates/paweljong/info_mailing/create.html @@ -0,0 +1,19 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block page_title %}{% blocktrans %}Create info mailing{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Create info mailing{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/paweljong/templates/paweljong/info_mailing/edit.html b/aleksis/apps/paweljong/templates/paweljong/info_mailing/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..d243a39ee8a5a9a0cbcd1a63a61ac5b5444a7157 --- /dev/null +++ b/aleksis/apps/paweljong/templates/paweljong/info_mailing/edit.html @@ -0,0 +1,18 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block page_title %}{% blocktrans %}Edit info mailing{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Edit info mailing{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/paweljong/templates/paweljong/info_mailing/list.html b/aleksis/apps/paweljong/templates/paweljong/info_mailing/list.html new file mode 100644 index 0000000000000000000000000000000000000000..28b1e1714120f774798cdb36851f83a12bed64ac --- /dev/null +++ b/aleksis/apps/paweljong/templates/paweljong/info_mailing/list.html @@ -0,0 +1,14 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% load render_table from django_tables2 %} + +{% block page_title %}{% blocktrans %}Info mailings{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Info mailings{% endblocktrans %}{% endblock %} + +{% block content %} + + <a class="btn colour-primary waves-effect waves-light" href="{% url 'create_info_mailing' %}">{% trans "Create info mailing" %}</a> + {% render_table table %} + +{% endblock %} diff --git a/aleksis/apps/paweljong/urls.py b/aleksis/apps/paweljong/urls.py index 0be09dfc82b789b40636088edae650f9308c4371..03b0341eab4f842b46d3b386e78efef791ec7241 100644 --- a/aleksis/apps/paweljong/urls.py +++ b/aleksis/apps/paweljong/urls.py @@ -102,4 +102,24 @@ urlpatterns = [ views.TermEditView.as_view(), name="edit_term_by_pk", ), + path( + "event/info_mailings/list", + views.InfoMailingListView.as_view(), + name="info_mailings", + ), + path( + "event/info_mailings/create", + views.InfoMailingCreateView.as_view(), + name="create_info_mailing", + ), + path( + "event/info_mailings/<int:pk>/edit", + views.InfoMailingEditView.as_view(), + name="edit_info_mailing_by_pk", + ), + path( + "event/info_mailings/<int:pk>/delete", + views.InfoMailingDeleteView.as_view(), + name="delete_info_mailing_by_pk", + ), ] diff --git a/aleksis/apps/paweljong/views.py b/aleksis/apps/paweljong/views.py index 972e2cabf863b1d6af8349708acf94a79675054f..efe2fee8e039f6022e85d02c56cad1ceaeb64477 100644 --- a/aleksis/apps/paweljong/views.py +++ b/aleksis/apps/paweljong/views.py @@ -32,12 +32,19 @@ from .filters import EventFilter, EventRegistrationFilter, VoucherFilter from .forms import ( EditEventForm, EditEventRegistrationForm, + EditInfoMailingForm, EditTermForm, EditVoucherForm, GenerateListForm, ) -from .models import Event, EventRegistration, Terms, Voucher -from .tables import EventRegistrationsTable, ManageEventsTable, TermsTable, VouchersTable +from .models import Event, EventRegistration, InfoMailing, Terms, Voucher +from .tables import ( + EventRegistrationsTable, + InfoMailingsTable, + ManageEventsTable, + TermsTable, + VouchersTable, +) User = get_user_model() @@ -769,3 +776,46 @@ class UpcomingEventsRSSFeed(Feed): class AccountRegisterStart(TemplateView): template_name = "paweljong/register_start.html" + + +class InfoMailingListView(PermissionRequiredMixin, SingleTableView): + """Table of all info mailings.""" + + model = InfoMailing + table_class = InfoMailingsTable + permission_required = "paweljong.view_info_mailing" + template_name = "paweljong/info_mailing/list.html" + + +@method_decorator(never_cache, name="dispatch") +class InfoMailingCreateView(PermissionRequiredMixin, AdvancedCreateView): + """Create view for info mailings.""" + + model = InfoMailing + form_class = EditInfoMailingForm + permission_required = "paweljong.add_info_mailing" + template_name = "paweljong/info_mailing/create.html" + success_url = reverse_lazy("info_mailings") + success_message = _("The info mailing has been created.") + + +@method_decorator(never_cache, name="dispatch") +class InfoMailingEditView(PermissionRequiredMixin, AdvancedEditView): + """Edit view for info mailings.""" + + model = InfoMailing + form_class = EditInfoMailingForm + permission_required = "paweljong.edit_info_mailing" + template_name = "paweljong/info_mailing/edit.html" + success_url = reverse_lazy("info_mailings") + success_message = _("The info mailing has been saved.") + + +class InfoMailingDeleteView(PermissionRequiredMixin, AdvancedDeleteView): + """Delete view for info mailings.""" + + model = InfoMailing + permission_required = "paweljong.delete_info_mailing" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("info_mailings") + success_message = _("The info mailing has been deleted.")