feat(members/excursion): automatically send crisis intervention list

pull/155/head
Christian Merten 8 months ago
parent d4137effa4
commit 1ada10fda4
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -247,3 +247,29 @@ Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBL
ADDRESS = get_text('address', default="""JDAV %(SEKTION)s ADDRESS = get_text('address', default="""JDAV %(SEKTION)s
%(STREET)s %(STREET)s
%(PLACE)s""" % { 'SEKTION': SEKTION, 'STREET': SEKTION_STREET, 'PLACE': SEKTION_TOWN }) %(PLACE)s""" % { 'SEKTION': SEKTION, 'STREET': SEKTION_STREET, 'PLACE': SEKTION_TOWN })
NOTIFY_EXCURSION_PARTICIPANT_LIST = get_text('notify_excursion_participant_list', default="""Hallo {name},
deine Ausfahrt {excursion} steht kurz bevor. Damit die Sektion dich im Notfall gut unterstützen kann, benötigt
die Geschäftsstelle eine aktuelle Kriseninterventionsliste, das heißt eine Teilnehmendenliste der Ausfahrt.
Das Verschicken der Liste passiert automatisch zum Zeitpunkt: {sending_time}.
Das sind die aktuell in der Ausfahrt eingetragenen Teilnehmenden:
{participants}
Falls diese Liste nicht mehr aktuell ist, gehe bitte umgehend auf {excursion_link} und trage die Daten nach.
Viele Grüße
Dein KOMPASS""")
SEND_EXCURSION_CRISIS_LIST = get_text('send_excursion_crisis_list', default="""Hallo zusammen,
vom {excursion_start} bis {excursion_end} findet die Ausfahrt {excursion} der Jugend statt. Die
Ausfahrt wird geleitet von {leaders}.
Im Anhang findet ihr die Kriseninterventionsliste.
Viele Grüße
Euer KOMPASS""")

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-27 23:00+0200\n" "POT-Creation-Date: 2025-05-03 18:06+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -469,6 +469,15 @@ msgstr ""
msgid "Finance overview" msgid "Finance overview"
msgstr "Kostenübersicht" msgstr "Kostenübersicht"
#: members/admin.py
msgid "Inform youth leaders about sending of crisis intervention list."
msgstr ""
"Informiere Jugendleiter:innen über Versand der Kriseninterventionsliste."
#: members/admin.py
msgid "Send crisis intervention list."
msgstr "Kriseninterventionsliste verschicken"
#: members/apps.py #: members/apps.py
msgid "member administration" msgid "member administration"
msgstr "Teilnehmer*innenverwaltung" msgstr "Teilnehmer*innenverwaltung"
@ -993,6 +1002,15 @@ msgstr "Ausfahrt"
msgid "Excursions" msgid "Excursions"
msgstr "Ausfahrten" msgstr "Ausfahrten"
#: members/models.py
msgid "Crisis intervention list for %(excursion)s from %(start)s to %(end)s"
msgstr "Kriseninterventionsliste für %(excursion)s vom %(start)s bis %(end)s"
#: members/models.py
#, python-format
msgid "Participant list for %(excursion)s from %(start)s to %(end)s"
msgstr "Teilnehmendenliste für %(excursion)s vom %(start)s bis %(end)s"
#: members/models.py #: members/models.py
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
@ -1392,9 +1410,9 @@ msgid ""
"%(total_org_fee_theoretical)s € is charged against the other transactions." "%(total_org_fee_theoretical)s € is charged against the other transactions."
msgstr "" msgstr ""
"Achtung: %(old_participant_count)s Teilnehmende der Ausfahrt sind 27 oder " "Achtung: %(old_participant_count)s Teilnehmende der Ausfahrt sind 27 oder "
"älter. Für diese Teilnehmende(n) ist ein Org-Beitrag von %(org_fee)s € pro Tag " "älter. Für diese Teilnehmende(n) ist ein Org-Beitrag von %(org_fee)s € pro "
"fällig. Durch die Länge der Ausfahrt von %(duration)s Tagen werden insgesamt " "Tag fällig. Durch die Länge der Ausfahrt von %(duration)s Tagen werden "
"%(total_org_fee_theoretical)s € mit den Zuschüssen und " "insgesamt %(total_org_fee_theoretical)s € mit den Zuschüssen und "
"Aufwandsentschädigungen verrechnet, sofern diese in Anspruch genommen werden." "Aufwandsentschädigungen verrechnet, sofern diese in Anspruch genommen werden."
#: members/templates/admin/freizeit_finance_overview.html #: members/templates/admin/freizeit_finance_overview.html

@ -0,0 +1,23 @@
# Generated by Django 4.2.20 on 2025-05-03 15:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0040_invitationtogroup_created_by'),
]
operations = [
migrations.AddField(
model_name='freizeit',
name='crisis_intervention_list_sent',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='freizeit',
name='notification_crisis_intervention_list_sent',
field=models.BooleanField(default=False),
),
]

@ -26,10 +26,12 @@ from django.conf import settings
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from .rules import may_view, may_change, may_delete, is_own_training, is_oneself, is_leader, is_leader_of_excursion from .rules import may_view, may_change, may_delete, is_own_training, is_oneself, is_leader, is_leader_of_excursion
from .pdf import render_tex
import rules import rules
from contrib.models import CommonModel from contrib.models import CommonModel
from contrib.media import media_path
from contrib.rules import memberize_user, has_global_perm from contrib.rules import memberize_user, has_global_perm
from utils import cvt_to_decimal from utils import cvt_to_decimal, coming_midnight
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from schwifty import IBAN from schwifty import IBAN
@ -1217,6 +1219,10 @@ class Freizeit(CommonModel):
approval_comments = models.TextField(verbose_name=_('Approval comments'), approval_comments = models.TextField(verbose_name=_('Approval comments'),
blank=True, default='') blank=True, default='')
# automatic sending of crisis intervention list
crisis_intervention_list_sent = models.BooleanField(default=False)
notification_crisis_intervention_list_sent = models.BooleanField(default=False)
def __str__(self): def __str__(self):
"""String represenation""" """String represenation"""
return self.name return self.name
@ -1609,6 +1615,61 @@ class Freizeit(CommonModel):
queryset = queryset.filter(Q(groups__in=groups) | Q(jugendleiter__pk=member.pk)).distinct() queryset = queryset.filter(Q(groups__in=groups) | Q(jugendleiter__pk=member.pk)).distinct()
return queryset return queryset
def send_crisis_intervention_list(self, sending_time=None):
"""
Send the crisis intervention list to the crisis invervention email, the
responsible and the youth leaders of this excursion.
"""
context = dict(memberlist=self, settings=settings)
start_date= timezone.localtime(self.date).strftime('%d.%m.%Y')
filename = render_tex(f"{self.code}_{self.name}_Krisenliste",
'members/crisis_intervention_list.tex', context,
date=self.date, save_only=True)
leaders = ", ".join([yl.name for yl in self.jugendleiter.all()])
start_date = timezone.localtime(self.date).strftime('%d.%m.%Y')
end_date = timezone.localtime(self.end).strftime('%d.%m.%Y')
# create email with attachment
send_mail(_('Crisis intervention list for %(excursion)s from %(start)s to %(end)s') %\
{ 'excursion': self.name,
'start': start_date,
'end': end_date },
settings.SEND_EXCURSION_CRISIS_LIST.format(excursion=self.name, leaders=leaders,
excursion_start=start_date,
excursion_end=end_date),
sender=settings.DEFAULT_SENDING_MAIL,
recipients=[settings.SEKTION_CRISIS_INTERVENTION_MAIL],
cc=[settings.RESPONSIBLE_MAIL] + [yl.email for yl in self.jugendleiter.all()],
attachments=[media_path(filename)])
self.crisis_intervention_list_sent = True
self.save()
def notify_leaders_crisis_intervention_list(self, sending_time=None):
"""
Send an email to the youth leaders of this excursion with a list of currently
registered participants and a heads-up that the crisis intervention list
will be automatically sent on the night of this day.
"""
participants = "\n".join([f"- {p.member.name}" for p in self.membersonlist.all()])
if not sending_time:
sending_time = coming_midnight().strftime("%d.%m.%y %H:%M")
elif not isinstance(sending_time, str):
sending_time = sending_time.strftime("%d.%m.%y %H:%M")
start_date = timezone.localtime(self.date).strftime('%d.%m.%Y')
end_date = timezone.localtime(self.end).strftime('%d.%m.%Y')
excursion_link = prepend_base_url(self.get_absolute_url())
for yl in self.jugendleiter.all():
yl.send_mail(_('Participant list for %(excursion)s from %(start)s to %(end)s') %\
{ 'excursion': self.name,
'start': start_date,
'end': end_date },
settings.NOTIFY_EXCURSION_PARTICIPANT_LIST.format(name=yl.prename,
excursion=self.name,
participants=participants,
sending_time=sending_time,
excursion_link=excursion_link))
self.notification_crisis_intervention_list_sent = True
self.save()
class MemberNoteList(models.Model): class MemberNoteList(models.Model):
""" """

@ -1,7 +1,7 @@
from celery import shared_task from celery import shared_task
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from .models import MemberWaitingList from .models import MemberWaitingList, Freizeit
@shared_task @shared_task
def ask_for_waiting_confirmation(): def ask_for_waiting_confirmation():
@ -18,3 +18,31 @@ def ask_for_waiting_confirmation():
waiter.ask_for_wait_confirmation() waiter.ask_for_wait_confirmation()
no += 1 no += 1
return no return no
@shared_task
def send_crisis_intervention_list():
"""
Send crisis intervention lists for all excursions that start on the current day and
that have not been sent yet.
"""
no = 0
for excursion in Freizeit.objects.filter(date__date=timezone.now().date(),
crisis_intervention_list_sent=False):
excursion.send_crisis_intervention_list()
no += 1
return no
@shared_task
def send_notification_crisis_intervention_list():
"""
Send crisis intervention list notifiactions for all excursions that start on the next
day and that have not been sent yet.
"""
no = 0
for excursion in Freizeit.objects.filter(date__date=timezone.now().date() + timezone.timedelta(days=1),
notification_crisis_intervention_list_sent=False):
excursion.notify_leaders_crisis_intervention_list()
no += 1
return no

@ -631,11 +631,20 @@ class MemberAdminTestCase(AdminTestCase):
class FreizeitTestCase(BasicMemberTestCase): class FreizeitTestCase(BasicMemberTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
# this excursion is used for the counting tests
self.ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=120, self.ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR, tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE, tour_approach=MUSKELKRAFT_ANREISE,
difficulty=1, difficulty=1,
date=timezone.localtime()) date=timezone.localtime())
# this excursion is used in the other tests
self.ex2 = Freizeit.objects.create(name='Wild trip 2', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=1,
date=timezone.localtime())
self.ex2.jugendleiter.add(self.fritz)
self.ex2.save()
def _setup_test_sjr_application_numbers(self, n_yl, n_b27_local, n_b27_non_local): def _setup_test_sjr_application_numbers(self, n_yl, n_b27_local, n_b27_non_local):
for i in range(n_yl): for i in range(n_yl):
@ -749,6 +758,17 @@ class FreizeitTestCase(BasicMemberTestCase):
for i in range(10): for i in range(10):
self._test_sjr_application_numbers(10, 10 - i, i) self._test_sjr_application_numbers(10, 10 - i, i)
def test_notify_leaders_crisis_intervention_list(self):
self.ex2.notification_crisis_intervention_list_sent = False
self.ex2.notify_leaders_crisis_intervention_list()
self.assertTrue(self.ex2.notification_crisis_intervention_list_sent)
self.ex2.notify_leaders_crisis_intervention_list(sending_time=timezone.now())
def test_send_crisis_intervention_list(self):
self.ex2.crisis_intervention_list_sent = False
self.ex2.send_crisis_intervention_list()
self.assertTrue(self.ex2.crisis_intervention_list_sent)
class PDFActionMixin: class PDFActionMixin:
def _test_pdf(self, name, pk, model='freizeit', invalid=False, username='superuser', post_data=None): def _test_pdf(self, name, pk, model='freizeit', invalid=False, username='superuser', post_data=None):

@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from django.db import models from django.db import models
from django.utils import timezone
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from decimal import Decimal, ROUND_HALF_DOWN from decimal import Decimal, ROUND_HALF_DOWN
@ -80,3 +81,10 @@ def normalize_filename(filename, append_date=True, date=None):
filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') filename = filename.replace(' ', '_').replace('&', '').replace('/', '_')
# drop umlauts, accents etc. # drop umlauts, accents etc.
return unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode() return unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode()
def coming_midnight():
base = timezone.now() + timezone.timedelta(days=1)
return timezone.datetime(year=base.year, month=base.month, day=base.day,
hour=0, minute=0, second=0, microsecond=0,
tzinfo=base.tzinfo)

Loading…
Cancel
Save