Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 0 additions & 41 deletions ephios/core/forms/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
import re
from datetime import datetime, timedelta

from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Field, Layout, Submit
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
Expand All @@ -25,7 +22,6 @@
from ephios.core.signup.structure import enabled_shift_structures, shift_structure_from_slug
from ephios.core.widgets import MultiUserProfileWidget
from ephios.extra.colors import clear_eventtype_color_css_fragment_cache
from ephios.extra.crispy import AbortLink
from ephios.extra.permissions import get_groups_with_perms
from ephios.extra.widgets import CustomDateInput, CustomTimeInput, MarkdownTextarea, RecurrenceField
from ephios.modellogging.log import add_log_recorder, update_log
Expand Down Expand Up @@ -315,40 +311,3 @@ def is_function_active(self):
With the default template, if this is True, the collapse is expanded on page load.
"""
return False


class EventNotificationForm(forms.Form):
NEW_EVENT = "new"
REMINDER = "remind"
PARTICIPANTS = "participants"
action = forms.ChoiceField(
choices=[
(NEW_EVENT, _("Send notification about new event to everyone")),
(REMINDER, _("Send reminder to everyone that is not participating")),
(PARTICIPANTS, _("Send a message to all participants")),
],
widget=forms.RadioSelect,
label=False,
)
mail_content = forms.CharField(required=False, widget=forms.Textarea, label=_("Mail content"))

def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event")
super().__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.layout = Layout(
Field("action"),
Field("mail_content"),
FormActions(
Submit("submit", _("Send"), css_class="float-end"),
AbortLink(href=self.event.get_absolute_url()),
),
)

def clean(self):
if (
self.cleaned_data.get("action") == self.PARTICIPANTS
and not self.cleaned_data["mail_content"]
):
raise ValidationError(_("You cannot send an empty mail."))
return super().clean()
Empty file.
Empty file.
188 changes: 75 additions & 113 deletions ephios/core/services/notifications/types.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from typing import List
from typing import Collection, List
from urllib.parse import urlparse

from django.contrib.auth.tokens import default_token_generator
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.formats import date_format
from django.utils.http import urlsafe_base64_encode
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from dynamic_preferences.registries import global_preferences_registry
from guardian.shortcuts import get_users_with_perms
Expand All @@ -17,7 +15,7 @@
from ephios.core.models import AbstractParticipation, Event, LocalParticipation, UserProfile
from ephios.core.models.users import Consequence, Notification
from ephios.core.signals import register_notification_types
from ephios.core.signup.participants import LocalUserParticipant
from ephios.core.signup.participants import AbstractParticipant, LocalUserParticipant
from ephios.core.templatetags.settings_extras import make_absolute

NOTIFICATION_READ_PARAM_NAME = "fromNotification"
Expand Down Expand Up @@ -186,51 +184,6 @@ def _get_reset_url(cls, notification):
return make_absolute(reset_link)


class NewEventNotification(AbstractNotificationHandler):
slug = "ephios_new_event"
title = _("A new event has been added")
email_template_name = "core/mails/new_event.html"

@classmethod
def send(cls, event: Event, **kwargs):
notifications = []
for user in get_users_with_perms(event, only_with_perms_in=["view_event"]):
notifications.append(
Notification(slug=cls.slug, user=user, data={"event_id": event.id, **kwargs})
)
Notification.objects.bulk_create(notifications)

@classmethod
def get_subject(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return _("New {type}: {title}").format(type=event.type, title=event.title)

@classmethod
def get_body(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return _(
"A new {type} ({title}, {location}) has been added.\n"
"Further information: {description}"
).format(
type=event.type,
title=event.title,
location=event.location,
description=event.description,
)

@classmethod
def get_render_context(cls, notification):
context = super().get_render_context(notification)
event = Event.objects.get(pk=notification.data.get("event_id"))
context["event"] = event
return context

@classmethod
def get_actions(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return [(str(_("View event")), make_absolute(event.get_absolute_url()))]


class ParticipationMixin:
@classmethod
def is_obsolete(cls, notification):
Expand Down Expand Up @@ -514,101 +467,109 @@ def get_body(cls, notification):
return message


class EventReminderNotification(AbstractNotificationHandler):
slug = "ephios_event_reminder"
title = _("An event has vacant spots")
class SubjectBodyDataMixin:

@classmethod
def get_subject(cls, notification):
return notification.data.get("subject")

@classmethod
def get_body(cls, notification):
return notification.data.get("body")


class GenericMassNotification(SubjectBodyDataMixin, AbstractNotificationHandler):
slug = "ephios_custom_event_reminder"
title = _("Information on an event you are not participating in")
unsubscribe_allowed = False

@classmethod
def send(cls, event: Event):
users_not_participating = UserProfile.objects.exclude(
pk__in=AbstractParticipation.objects.filter(shift__event=event).values_list(
"localparticipation__user", flat=True
)
).filter(pk__in=get_users_with_perms(event, only_with_perms_in=["view_event"]))
def send(cls, users: Collection[UserProfile], subject, body):
notifications = []
for user in users_not_participating:
for user in users:
notifications.append(
Notification(slug=cls.slug, user=user, data={"event_id": event.id})
Notification(
slug=cls.slug,
user=user,
data={
"email": user.email,
"subject": subject,
"body": body,
},
)
)
Notification.objects.bulk_create(notifications)

@classmethod
def get_subject(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return _("Help needed for {title}").format(title=event.title)
def get_actions(cls, notification):
return [
(
str(_("View message")),
make_absolute(reverse("core:notification_detail", kwargs={"pk": notification.pk})),
)
]


class CustomEventReminderNotification(SubjectBodyDataMixin, AbstractNotificationHandler):
slug = "ephios_custom_event_reminder"
title = _("Information on an event you are not participating in")
unsubscribe_allowed = False

@classmethod
def get_body(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return _("Your support is needed for {title} ({start} - {end}).").format(
title=event.title,
start=date_format(localtime(event.get_start_time()), "SHORT_DATETIME_FORMAT"),
end=date_format(localtime(event.get_end_time()), "SHORT_DATETIME_FORMAT"),
)
def send(cls, event: Event, participants: Collection[LocalUserParticipant], subject, body):
notifications = []
for participant in participants:
notifications.append(
Notification(
slug=cls.slug,
user=getattr(participant, "user", None),
data={
"event_id": event.id,
"email": participant.email,
"subject": subject,
"body": body,
},
)
)
Notification.objects.bulk_create(notifications)

@classmethod
def get_actions(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return [(str(_("View event")), make_absolute(event.get_absolute_url()))]
return [
(
str(_("View message")),
make_absolute(reverse("core:notification_detail", kwargs={"pk": notification.pk})),
),
(str(_("View event")), make_absolute(event.get_absolute_url())),
]


class CustomEventParticipantNotification(AbstractNotificationHandler):
class CustomEventParticipantNotification(SubjectBodyDataMixin, AbstractNotificationHandler):
slug = "ephios_custom_event_participant"
title = _("Message to all participants")
unsubscribe_allowed = False

@classmethod
def send(cls, event: Event, content: str):
participants = set()
def send(
cls, event: Event, participants: Collection[AbstractParticipant], subject: str, body: str
):
notifications = []
responsible_users = get_users_with_perms(
event, with_superusers=False, only_with_perms_in=["change_event"]
)
for participation in AbstractParticipation.objects.filter(
shift__event=event, state=AbstractParticipation.States.CONFIRMED
):
participant = participation.participant
if participant not in participants:
participants.add(participant)
user = participant.user if isinstance(participant, LocalUserParticipant) else None
if user in responsible_users:
continue
notifications.append(
Notification(
slug=cls.slug,
user=user,
data={
"email": participant.email,
"participation_id": participation.id,
"event_id": event.id,
"content": content,
},
)
)
for responsible in responsible_users:
for participant in participants:
notifications.append(
Notification(
slug=cls.slug,
user=responsible,
user=getattr(participant, "user", None),
data={
"email": responsible.email,
"email": participant.email,
"event_id": event.id,
"content": content,
"subject": subject,
"body": body,
},
)
)
Notification.objects.bulk_create(notifications)

@classmethod
def get_subject(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
return _("Information regarding {title}").format(title=event.title)

@classmethod
def get_body(cls, notification):
return notification.data.get("content")

@classmethod
def get_actions(cls, notification):
event = Event.objects.get(pk=notification.data.get("event_id"))
Expand Down Expand Up @@ -680,8 +641,9 @@ def get_body(cls, notification):
ResponsibleParticipationStateChangeNotification,
ResponsibleConfirmedParticipationDeclinedNotification,
ResponsibleConfirmedParticipationCustomizedNotification,
NewEventNotification,
EventReminderNotification,
GenericMassNotification,
CustomEventParticipantNotification,
CustomEventReminderNotification,
CustomEventParticipantNotification,
ConsequenceApprovedNotification,
ConsequenceDeniedNotification,
Expand Down
18 changes: 18 additions & 0 deletions ephios/core/signup/participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ class AbstractParticipant:
date_of_birth: Optional[date]
email: Optional[str] # if set to None, no notifications are sent

@property
def identifier(self):
"""
Return a string identifying this participant. It should be unique to the ephios instance, and should not
change if changeable attributes like qualifications change.
The string must only contain alphanumeric characters and -_ special characters.
"""
raise NotImplementedError

def get_age(self, today: date = None):
if self.date_of_birth is None:
return None
Expand Down Expand Up @@ -80,6 +89,10 @@ def icon(self):
class LocalUserParticipant(AbstractParticipant):
user: get_user_model()

@property
def identifier(self):
return f"localuser-{self.user.pk}"

def new_participation(self, shift):
return LocalParticipation(shift=shift, user=self.user)

Expand All @@ -101,6 +114,11 @@ def reverse_event_detail(self, event):

@dataclasses.dataclass(frozen=True)
class PlaceholderParticipant(AbstractParticipant):

@property
def identifier(self):
return f"placeholder-{hash(self)}"

def new_participation(self, shift):
return PlaceholderParticipation(shift=shift, display_name=self.display_name)

Expand Down
2 changes: 1 addition & 1 deletion ephios/core/templates/core/event_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ <h1 class="card-title fw-bold fs-1">
{% translate "Download PDF" %}
</a></li>
<li><a class="dropdown-item"
href="{% url "core:event_notifications" event.pk %}">
href="{% url "core:notification_mass" %}{% querystring None event_id=event.id %}">
<span class="fas fa-envelope"></span>
{% translate "Send notifications" %}
</a></li>
Expand Down
Loading
Loading