You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
25 KiB
Python
543 lines
25 KiB
Python
from datetime import datetime, timedelta
|
|
import math
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.urls import reverse
|
|
from django.conf import settings
|
|
from django.db.models import Q, F, When, Value, IntegerField, Case, Sum
|
|
from django.db.models.functions import Cast
|
|
from django.core.validators import MinValueValidator
|
|
from django.contrib.contenttypes.fields import GenericRelation
|
|
from contrib.models import CommonModel
|
|
from members.rules import is_leader
|
|
from contrib.rules import has_global_perm
|
|
from utils import cvt_to_decimal, coming_midnight
|
|
from mailer.mailutils import send as send_mail, prepend_base_url
|
|
from contrib.media import media_path
|
|
from members.pdf import render_tex
|
|
from .constants import (GEMEINSCHAFTS_TOUR, FUEHRUNGS_TOUR, AUSBILDUNGS_TOUR,
|
|
MUSKELKRAFT_ANREISE, OEFFENTLICHE_ANREISE, FAHRGEMEINSCHAFT_ANREISE)
|
|
from .group import Group
|
|
from .member_on_list import NewMemberOnList
|
|
from .activity import ActivityCategory
|
|
|
|
|
|
class Freizeit(CommonModel):
|
|
"""Lets the user create a 'Freizeit' and generate a members overview in pdf format. """
|
|
|
|
name = models.CharField(verbose_name=_('Activity'), default='',
|
|
max_length=50)
|
|
place = models.CharField(verbose_name=_('Place'), default='', max_length=50)
|
|
postcode = models.CharField(verbose_name=_('Postcode'), default='', max_length=30)
|
|
destination = models.CharField(verbose_name=_('Destination (optional)'),
|
|
default='', max_length=50, blank=True,
|
|
help_text=_('e.g. a peak'))
|
|
date = models.DateTimeField(default=timezone.now, verbose_name=_('Begin'))
|
|
end = models.DateTimeField(verbose_name=_('End (optional)'), default=timezone.now)
|
|
description = models.TextField(verbose_name=_('Description'), blank=True, default='')
|
|
# comment = models.TextField(_('Comments'), default='', blank=True)
|
|
groups = models.ManyToManyField(Group, verbose_name=_('Groups'))
|
|
jugendleiter = models.ManyToManyField('Member')
|
|
approved_extra_youth_leader_count = models.PositiveIntegerField(verbose_name=_('Number of additional approved youth leaders'),
|
|
default=0,
|
|
help_text=_('The number of approved youth leaders per excursion is determined by the number of participants. In special circumstances, e.g. in case of a technically demanding excursion, more youth leaders may be approved.'))
|
|
tour_type_choices = ((GEMEINSCHAFTS_TOUR, 'Gemeinschaftstour'),
|
|
(FUEHRUNGS_TOUR, 'Führungstour'),
|
|
(AUSBILDUNGS_TOUR, 'Ausbildung'))
|
|
# verbose_name is overriden by form, label is set in admin.py
|
|
tour_type = models.IntegerField(choices=tour_type_choices)
|
|
tour_approach_choices = ((MUSKELKRAFT_ANREISE, 'Muskelkraft'),
|
|
(OEFFENTLICHE_ANREISE, 'ÖPNV'),
|
|
(FAHRGEMEINSCHAFT_ANREISE, 'Fahrgemeinschaften'))
|
|
tour_approach = models.IntegerField(choices=tour_approach_choices,
|
|
default=MUSKELKRAFT_ANREISE,
|
|
verbose_name=_('Means of transportation'))
|
|
kilometers_traveled = models.IntegerField(verbose_name=_('Kilometers traveled'),
|
|
validators=[MinValueValidator(0)])
|
|
activity = models.ManyToManyField(ActivityCategory, default=None,
|
|
verbose_name=_('Categories'))
|
|
difficulty_choices = [(1, _('easy')), (2, _('medium')), (3, _('hard'))]
|
|
# verbose_name is overriden by form, label is set in admin.py
|
|
difficulty = models.IntegerField(choices=difficulty_choices)
|
|
membersonlist = GenericRelation(NewMemberOnList)
|
|
|
|
# approval: None means no decision taken, False means rejected
|
|
approved = models.BooleanField(verbose_name=_('Approved'),
|
|
null=True,
|
|
default=None,
|
|
help_text=_('Choose no in case of rejection or yes in case of approval. Leave empty, if not yet decided.'))
|
|
approval_comments = models.TextField(verbose_name=_('Approval comments'),
|
|
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):
|
|
"""String represenation"""
|
|
return self.name
|
|
|
|
class Meta(CommonModel.Meta):
|
|
verbose_name = _('Excursion')
|
|
verbose_name_plural = _('Excursions')
|
|
permissions = (
|
|
('manage_approval_excursion', 'Can edit the approval status of excursions.'),
|
|
('view_approval_excursion', 'Can view the approval status of excursions.'),
|
|
)
|
|
rules_permissions = {
|
|
'add_obj': has_global_perm('members.add_global_freizeit'),
|
|
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'),
|
|
'change_obj': is_leader | has_global_perm('members.change_global_freizeit'),
|
|
'delete_obj': is_leader | has_global_perm('members.delete_global_freizeit'),
|
|
}
|
|
|
|
@property
|
|
def code(self):
|
|
return f"B{self.date:%y}-{self.pk}"
|
|
|
|
@staticmethod
|
|
def filter_queryset_date_next_n_hours(hours, queryset=None):
|
|
if queryset is None:
|
|
queryset = Freizeit.objects.all()
|
|
return queryset.filter(date__lte=timezone.now() + timezone.timedelta(hours=hours),
|
|
date__gte=timezone.now())
|
|
|
|
@staticmethod
|
|
def to_notify_crisis_intervention_list():
|
|
qs = Freizeit.objects.filter(notification_crisis_intervention_list_sent=False)
|
|
return Freizeit.filter_queryset_date_next_n_hours(48, queryset=qs)
|
|
|
|
@staticmethod
|
|
def to_send_crisis_intervention_list():
|
|
qs = Freizeit.objects.filter(crisis_intervention_list_sent=False)
|
|
return Freizeit.filter_queryset_date_next_n_hours(24, queryset=qs)
|
|
|
|
def get_tour_type(self):
|
|
if self.tour_type == FUEHRUNGS_TOUR:
|
|
return "Führungstour"
|
|
elif self.tour_type == AUSBILDUNGS_TOUR:
|
|
return "Ausbildung"
|
|
return "Gemeinschaftstour"
|
|
|
|
def get_tour_approach(self):
|
|
if self.tour_approach == MUSKELKRAFT_ANREISE:
|
|
return "Muskelkraft"
|
|
elif self.tour_approach == OEFFENTLICHE_ANREISE:
|
|
return "ÖPNV"
|
|
return "Fahrgemeinschaften"
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('admin:members_freizeit_change', args=[str(self.id)])
|
|
|
|
@property
|
|
def night_count(self):
|
|
# convert to date first, since we might start at 11pm and end at 1am, which is one night
|
|
return (self.end.date() - self.date.date()).days
|
|
|
|
@property
|
|
def duration(self):
|
|
# number of nights is number of full days + 1
|
|
full_days = max(self.night_count - 1, 0)
|
|
extra_days = 0
|
|
|
|
if self.date.date() == self.end.date():
|
|
# excursion starts and ends on the same day
|
|
hours = max(self.end.hour - self.date.hour, 0)
|
|
# at least 6 hours counts as full day
|
|
extra_days = 1.0 if hours >= 6 else 0.5
|
|
else:
|
|
extra_days += 1.0 if self.date.hour <= 12 else 0.5
|
|
extra_days += 1.0 if self.end.hour >= 12 else 0.5
|
|
|
|
return full_days + extra_days
|
|
|
|
@property
|
|
def total_intervention_hours(self):
|
|
if hasattr(self, 'ljpproposal'):
|
|
return sum([i.duration for i in self.ljpproposal.intervention_set.all()])
|
|
else:
|
|
return 0
|
|
|
|
@property
|
|
def total_seminar_days(self):
|
|
"""calculate seminar days based on intervention hours in every day"""
|
|
# TODO: add tests for this
|
|
if hasattr(self, 'ljpproposal'):
|
|
hours_per_day = self.seminar_time_per_day
|
|
# Calculate the total number of seminar days
|
|
# Each day is counted as 1 if total_duration is >= 5 hours, as 0.5 if total_duration is >= 2.5
|
|
# otherwise 0
|
|
sum_days = sum([h['sum_days'] for h in hours_per_day])
|
|
|
|
return sum_days
|
|
else:
|
|
return 0
|
|
|
|
|
|
@property
|
|
def seminar_time_per_day(self):
|
|
if hasattr(self, 'ljpproposal'):
|
|
return (
|
|
self.ljpproposal.intervention_set
|
|
.annotate(day=Cast('date_start', output_field=models.DateField())) # Force it to date
|
|
.values('day') # Group by day
|
|
.annotate(total_duration=Sum('duration'))# Sum durations for each day
|
|
.annotate(
|
|
sum_days=Case(
|
|
When(total_duration__gte=5.0, then=Value(1.0)),
|
|
When(total_duration__gte=2.5, then=Value(0.5)),
|
|
default=Value(0.0),)
|
|
)
|
|
.order_by('day') # Sort results by date
|
|
)
|
|
else:
|
|
return []
|
|
|
|
@property
|
|
def ljp_duration(self):
|
|
"""calculate the duration in days for the LJP"""
|
|
return min(self.duration, self.total_seminar_days)
|
|
|
|
@property
|
|
def staff_count(self):
|
|
return self.jugendleiter.count()
|
|
|
|
@property
|
|
def staff_on_memberlist(self):
|
|
ps = set(map(lambda x: x.member, self.membersonlist.distinct()))
|
|
jls = set(self.jugendleiter.distinct())
|
|
return ps.intersection(jls)
|
|
|
|
@property
|
|
def staff_on_memberlist_count(self):
|
|
return len(self.staff_on_memberlist)
|
|
|
|
@property
|
|
def participant_count(self):
|
|
return len(self.participants)
|
|
|
|
@property
|
|
def participants(self):
|
|
ps = set(map(lambda x: x.member, self.membersonlist.distinct()))
|
|
jls = set(self.jugendleiter.distinct())
|
|
return list(ps - jls)
|
|
|
|
@property
|
|
def old_participant_count(self):
|
|
old_ps = [m for m in self.participants if m.age() >= 27]
|
|
return len(old_ps)
|
|
|
|
@property
|
|
def head_count(self):
|
|
return self.staff_on_memberlist_count + self.participant_count
|
|
|
|
@property
|
|
def approved_staff_count(self):
|
|
"""Number of approved youth leaders for this excursion. The base number is calculated
|
|
from the participant count. To this, the number of additional approved youth leaders is added."""
|
|
participant_count = self.participant_count
|
|
if participant_count < 4:
|
|
base_count = 0
|
|
elif 4 <= participant_count <= 7:
|
|
base_count = 2
|
|
else:
|
|
base_count = 2 + math.ceil((participant_count - 7) / 7)
|
|
return base_count + self.approved_extra_youth_leader_count
|
|
|
|
@property
|
|
def theoretic_ljp_participant_count(self):
|
|
"""
|
|
Calculate the participant count in the sense of the LJP regulations. This means
|
|
that all youth leaders are counted and all participants which are at least 6 years old and
|
|
strictly less than 27 years old. Additionally, up to 20% of the participants may violate the
|
|
age restrictions.
|
|
|
|
This is the theoretic value, ignoring the cutoff at 5 participants.
|
|
"""
|
|
# participants (possibly including youth leaders)
|
|
ps = {x.member for x in self.membersonlist.distinct()}
|
|
# youth leaders
|
|
jls = set(self.jugendleiter.distinct())
|
|
# non-youth leader participants
|
|
ps_only = ps - jls
|
|
# participants of the correct age
|
|
ps_correct_age = {m for m in ps_only if m.age_at(self.date) >= 6 and m.age_at(self.date) < 27}
|
|
# m = the official non-youth-leader participant count
|
|
# and, assuming there exist enough participants, unrounded m satisfies the equation
|
|
# len(ps_correct_age) + 1/5 * m = m
|
|
# if there are not enough participants,
|
|
# m = len(ps_only)
|
|
m = min(len(ps_only), math.floor(5/4 * len(ps_correct_age)))
|
|
return m + len(jls)
|
|
|
|
@property
|
|
def ljp_participant_count(self):
|
|
"""
|
|
The number of participants in the sense of LJP regulations. If the total
|
|
number of participants (including youth leaders and too old / young ones) is less
|
|
than 5, this is zero, otherwise it is `theoretic_ljp_participant_count`.
|
|
"""
|
|
# participants (possibly including youth leaders)
|
|
ps = {x.member for x in self.membersonlist.distinct()}
|
|
# youth leaders
|
|
jls = set(self.jugendleiter.distinct())
|
|
if len(ps.union(jls)) < 5:
|
|
return 0
|
|
return self.theoretic_ljp_participant_count
|
|
|
|
@property
|
|
def maximal_ljp_contributions(self):
|
|
"""This is the maximal amount of LJP contributions that can be requested given participants and length
|
|
This calculation if intended for the LJP application, not for the payout."""
|
|
return cvt_to_decimal(settings.LJP_CONTRIBUTION_PER_DAY * self.ljp_participant_count * self.duration)
|
|
|
|
@property
|
|
def potential_ljp_contributions(self):
|
|
"""The maximal amount can be reduced if the actual costs are lower than the maximal amount
|
|
This calculation if intended for the LJP application, not for the payout."""
|
|
if not hasattr(self, 'statement'):
|
|
return cvt_to_decimal(0)
|
|
return cvt_to_decimal(min(self.maximal_ljp_contributions,
|
|
0.9 * float(self.statement.total_bills_theoretic) + float(self.statement.total_staff)))
|
|
|
|
@property
|
|
def payable_ljp_contributions(self):
|
|
"""the payable contributions can differ from potential contributions if a tax is deducted for risk reduction.
|
|
the actual payout depends on more factors, e.g. the actual costs that had to be paid by the trip organisers."""
|
|
if hasattr(self, 'statement') and self.statement.ljp_to:
|
|
return self.statement.paid_ljp_contributions
|
|
return cvt_to_decimal(self.potential_ljp_contributions * cvt_to_decimal(1 - settings.LJP_TAX))
|
|
|
|
@property
|
|
def total_relative_costs(self):
|
|
if not hasattr(self, 'statement'):
|
|
return 0
|
|
total_costs = self.statement.total_bills_theoretic
|
|
total_contributions = self.statement.total_subsidies + self.payable_ljp_contributions
|
|
return total_costs - total_contributions
|
|
|
|
@property
|
|
def time_period_str(self):
|
|
time_period = self.date.strftime('%d.%m.%Y')
|
|
if self.end != self.date:
|
|
time_period += " - " + self.end.strftime('%d.%m.%Y')
|
|
return time_period
|
|
|
|
@property
|
|
def groups_str(self):
|
|
return ', '.join(g.name for g in self.groups.all())
|
|
|
|
@property
|
|
def staff_str(self):
|
|
return ', '.join(yl.name for yl in self.jugendleiter.all())
|
|
|
|
@property
|
|
def skill_summary(self):
|
|
activities = [a.name for a in self.activity.all()]
|
|
skills = {a: [] for a in activities}
|
|
people = []
|
|
for memberonlist in self.membersonlist.all():
|
|
m = memberonlist.member
|
|
qualities = []
|
|
for activity, value in m.get_skills().items():
|
|
if activity not in activities:
|
|
continue
|
|
skills[activity].append(value)
|
|
qualities.append("\\textit{%s:} %s" % (activity, value))
|
|
people.append(dict(name=m.name, qualities=", ".join(qualities), comments=memberonlist.comments_tex))
|
|
|
|
sks = []
|
|
for activity in activities:
|
|
skill_avg = 0 if len(skills[activity]) == 0 else\
|
|
sum(skills[activity]) / len(skills[activity])
|
|
skill_min = 0 if len(skills[activity]) == 0 else\
|
|
min(skills[activity])
|
|
skill_max = 0 if len(skills[activity]) == 0 else\
|
|
max(skills[activity])
|
|
sks.append(dict(name=activity, skill_avg=skill_avg, skill_min=skill_min, skill_max=skill_max))
|
|
return (people, sks)
|
|
|
|
def sjr_application_numbers(self):
|
|
members = set(map(lambda x: x.member, self.membersonlist.distinct()))
|
|
jls = set(self.jugendleiter.distinct())
|
|
participants = members - jls
|
|
b27_local = len([m for m in participants
|
|
if m.age_at(self.date) <= 27 and settings.SEKTION in m.town])
|
|
b27_non_local = len([m for m in participants
|
|
if m.age_at(self.date) <= 27 and not settings.SEKTION in m.town])
|
|
staff = len(jls)
|
|
total = b27_local + b27_non_local + len(jls)
|
|
relevant_b27 = min(b27_local + b27_non_local, math.floor(3/2 * b27_local))
|
|
subsidizable = relevant_b27 + min(math.ceil(relevant_b27 / 7), staff)
|
|
duration = self.night_count + 1
|
|
return {
|
|
'b27_local': b27_local,
|
|
'b27_non_local': b27_non_local,
|
|
'staff': staff,
|
|
'total': total,
|
|
'relevant_b27': relevant_b27,
|
|
'subsidizable': subsidizable,
|
|
'subsidized_days': duration * subsidizable,
|
|
'duration': duration
|
|
}
|
|
|
|
def sjr_application_fields(self):
|
|
members = set(map(lambda x: x.member, self.membersonlist.distinct()))
|
|
jls = set(self.jugendleiter.distinct())
|
|
numbers = self.sjr_application_numbers()
|
|
title = self.ljpproposal.title if hasattr(self, 'ljpproposal') else self.name
|
|
base = {'Haushaltsjahr': str(datetime.now().year),
|
|
'Art / Thema / Titel': title,
|
|
'Ort': self.place,
|
|
'Datum von': self.date.strftime('%d.%m.%Y'),
|
|
'Datum bis': self.end.strftime('%d.%m.%Y'),
|
|
'Dauer': str(numbers['duration']),
|
|
'Teilnehmenden gesamt': str(numbers['total']),
|
|
'bis 27 aus HD': str(numbers['b27_local']),
|
|
'bis 27 nicht aus HD': str(numbers['b27_non_local']),
|
|
'Verpflegungstage': str(numbers['subsidized_days']).replace('.', ','),
|
|
'Betreuer/in': str(numbers['staff']),
|
|
'Auswahl Veranstaltung': 'Auswahl2',
|
|
'Ort, Datum': '{p}, {d}'.format(p=settings.SEKTION, d=datetime.now().strftime('%d.%m.%Y'))}
|
|
for i, m in enumerate(members):
|
|
suffix = str(' {}'.format(i + 1))
|
|
# indexing starts at zero, but the listing in the pdf starts at 1
|
|
if i + 1 == 1:
|
|
suffix = ''
|
|
elif i + 1 >= 13:
|
|
suffix = str(i + 1)
|
|
base['Vor- und Nachname' + suffix] = m.name
|
|
base['Anschrift' + suffix] = m.address
|
|
base['Alter' + suffix] = str(m.age_at(self.date))
|
|
base['Status' + str(i+1)] = '2' if m in jls else 'Auswahl1' if settings.SEKTION in m.address else 'Auswahl2'
|
|
return base
|
|
|
|
def v32_fields(self):
|
|
title = self.ljpproposal.title if hasattr(self, 'ljpproposal') else self.name
|
|
base = {
|
|
# AntragstellerIn
|
|
'Textfeld 2': settings.ADDRESS,
|
|
# Dachorganisation
|
|
'Textfeld 3': settings.V32_HEAD_ORGANISATION,
|
|
# Datum der Maßnahme am/vom
|
|
'Textfeld 20': self.date.strftime('%d.%m.%Y'),
|
|
# bis
|
|
'Textfeld 28': self.end.strftime('%d.%m.%Y'),
|
|
# Thema der Maßnahme
|
|
'Textfeld 22': title,
|
|
# IBAN
|
|
'Textfeld 36': settings.SEKTION_IBAN,
|
|
# Kontoinhaber
|
|
'Textfeld 37': settings.SEKTION_ACCOUNT_HOLDER,
|
|
# Zahl der zuwendungsfähigen Teilnehemr
|
|
'Textfeld 43': str(self.ljp_participant_count),
|
|
# Teilnahmetage
|
|
'Textfeld 46': str(round(self.duration * self.ljp_participant_count, 1)).replace('.', ','),
|
|
# Euro Tagessatz
|
|
'Textfeld 48': str(settings.LJP_CONTRIBUTION_PER_DAY),
|
|
# Erbetener Zuschuss
|
|
'Textfeld 50': str(self.maximal_ljp_contributions).replace('.', ','),
|
|
# Stunden Bildungsprogramm
|
|
'Textfeld 52': '??',
|
|
# Tage
|
|
'Textfeld 53': str(round(self.duration, 1)).replace('.', ','),
|
|
# Haushaltsjahr
|
|
'Textfeld 54': str(datetime.now().year),
|
|
# nicht anrechenbare Teilnahmetage
|
|
'Textfeld 55': '0',
|
|
# Gesamt-Teilnahmetage
|
|
'Textfeld 56': str(round(self.duration * self.ljp_participant_count, 1)).replace('.', ','),
|
|
# Ort, Datum
|
|
'DatumOrt 2': '{place}, {date}'.format(place=settings.SEKTION,
|
|
date=datetime.now().strftime('%d.%m.%Y'))
|
|
}
|
|
if hasattr(self, 'statement'):
|
|
possible_contributions = self.maximal_ljp_contributions
|
|
total_contributions = min(self.statement.total_theoretic, possible_contributions)
|
|
self_participation = max(cvt_to_decimal(0), self.statement.total_theoretic - possible_contributions)
|
|
# Gesamtkosten von
|
|
base['Textfeld 62'] = str(self.statement.total_theoretic).replace('.', ',')
|
|
# Eigenmittel und Teilnahmebeiträge
|
|
base['Textfeld 59'] = str(self_participation).replace('.', ',')
|
|
# Drittmittel
|
|
base['Textfeld 60'] = '0,00'
|
|
# Erbetener Zuschuss
|
|
base['Textfeld 61'] = str(total_contributions).replace('.', ',')
|
|
# Ergibt wieder
|
|
base['Textfeld 58'] = base['Textfeld 62']
|
|
return base
|
|
|
|
def get_ljp_activity_category(self):
|
|
"""
|
|
The official LJP activity category associated with this excursion. This is deduced
|
|
from the `activity` field.
|
|
"""
|
|
return ", ".join([a.ljp_category for a in self.activity.all()])
|
|
|
|
@staticmethod
|
|
def filter_queryset_by_permissions(member, queryset=None):
|
|
if queryset is None:
|
|
queryset = Freizeit.objects.all()
|
|
|
|
groups = member.leited_groups.all()
|
|
# one may view all leited groups and oneself
|
|
queryset = queryset.filter(Q(groups__in=groups) | Q(jugendleiter__pk=member.pk)).distinct()
|
|
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()
|
|
|