714 lignes
25 KiB
Python
714 lignes
25 KiB
Python
import datetime
|
|
import logging
|
|
import os.path
|
|
import uuid
|
|
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
from django.db.models import Case, Count, ExpressionWrapper, F, When
|
|
from django.urls import reverse
|
|
from django.utils.timezone import now
|
|
|
|
from constance import config
|
|
from djchoices import ChoiceItem, DjangoChoices
|
|
|
|
from ..main import mail as b_mail
|
|
from ..main.models import Lieu
|
|
from ..main.utils import (
|
|
AccessHistoryMixin,
|
|
get_filename,
|
|
pourcentage,
|
|
resize_image_field,
|
|
toordweek,
|
|
transpose_dict,
|
|
)
|
|
from ..organisation.models import Fonction, Personne
|
|
from .pdf import refus_resa
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def upload_illustration_to(evenement, filename):
|
|
"""Génère le chemin vers l'illustration d'un événement."""
|
|
return 'evenements/{0}{1}'.format(
|
|
# génère un nom de fichier unique
|
|
uuid.uuid4().hex,
|
|
os.path.splitext(filename)[1],
|
|
)
|
|
|
|
|
|
def upload_vignette_to(evenement, filename):
|
|
"""Génère le chemin vers la vignette d'un événement."""
|
|
return 'evenements/thumbs/{0}{1}'.format(
|
|
# utilise le même nom de fichier que l'illustration
|
|
get_filename(evenement.illustration.name),
|
|
os.path.splitext(filename)[1],
|
|
)
|
|
|
|
|
|
class EvenementQuerySet(models.QuerySet):
|
|
"""Étends les méthodes et requêtes pour les `Evenement`."""
|
|
|
|
def a_venir(self):
|
|
"""Filtre avec une date de début à venir."""
|
|
return self.filter(
|
|
actif=True, date_debut__date__gte=datetime.date.today()
|
|
)
|
|
|
|
def a_venir_reservable(self):
|
|
"""Filtre avec une date de fermeture de resa début à venir."""
|
|
return self.filter(
|
|
actif=True, date_ferm_resa__gte=datetime.date.today()
|
|
)
|
|
|
|
def avec_reservations_pour(self, personne, **filters):
|
|
"""Associe les éventuelles réservations pour la `Personne`.
|
|
|
|
Sélectionne et attache pour chaque `Evenement` la `Reservation` pour
|
|
la `personne` donnée, s'il y en a une. Elle est accessible depuis
|
|
l'attribut ``ma_reservation``.
|
|
|
|
L'argument `personne` peut être soit un objet ou l'index (pk) d'un
|
|
objet `Personne`.
|
|
|
|
Les arguments restant seront ajoutés aux filtres sur les objets
|
|
`Reservation` liés.
|
|
"""
|
|
if isinstance(personne, Personne):
|
|
filters['personne'] = personne
|
|
else:
|
|
filters['personne__pk'] = personne
|
|
return self.prefetch_related(
|
|
models.Prefetch(
|
|
'reservations',
|
|
queryset=Reservation.objects.filter(**filters),
|
|
to_attr='ma_reservation',
|
|
)
|
|
)
|
|
|
|
def annotate_nb_resa(self, incl_attente=True):
|
|
"""annote la qs avec
|
|
- le nombre de reservation (accepte et en attente selon incl_attente)
|
|
- le nombre d'inscrits manquants (N<=0 si il y a assez d'inscrits)"""
|
|
statuts = [Reservation.Statut.ACCEPTE]
|
|
if incl_attente:
|
|
statuts.append(Reservation.Statut.ATTENTE)
|
|
return self.annotate(
|
|
reservation__count=Count(
|
|
Case(
|
|
When(reservation__statut__in=statuts, then=1),
|
|
output_field=models.PositiveIntegerField(),
|
|
)
|
|
)
|
|
).annotate(
|
|
inscr_manq=ExpressionWrapper(
|
|
F('nb_places_min') - F('reservation__count'),
|
|
output_field=models.IntegerField(),
|
|
)
|
|
)
|
|
|
|
def alerte_nb_place(self):
|
|
"""
|
|
Filtre les evenements dont le nombre d'inscrits est insuffisant
|
|
et dont la date d'alerte se trouve dans la semaine (entre
|
|
aujourd'hui et il y a 7 jours)
|
|
"""
|
|
today = datetime.date.today()
|
|
this_week = (today - datetime.timedelta(7), today)
|
|
return self.filter(
|
|
date_alert_nb_inscrit__range=this_week, actif=True
|
|
).annotate_nb_resa()
|
|
|
|
def annul_conf_automatique(self):
|
|
"""
|
|
utilise alerte_nb_place() (et donc annotate_nb_resa()),
|
|
ainsi que Evenement.alerte_annule_confirme() pour:
|
|
- recuperer les evenements candidats a l'annulation
|
|
- selon Evenement.annulation_auto_nb_inscrit:
|
|
- previent simplement l'organisateur
|
|
- ou en plus annule l'evenement et previent les participants
|
|
ne renvoit pas de queryset.
|
|
"""
|
|
logger.info("Appel de Evenement.objects.annul_conf_automatique()")
|
|
qs_evt = self.filter(verif_auto_faite=False).alerte_nb_place()
|
|
for evt in qs_evt:
|
|
annl = bool(evt.inscr_manq > 0)
|
|
alert = not evt.annulation_auto_nb_inscrit
|
|
evt.alerte_annule_confirme(annul=annl, p_resp=True, resp_rec=False)
|
|
logger.info(
|
|
"{0}ation auto {1} de '{2}'".format(
|
|
'Annul' if annl else 'Confirm',
|
|
'(alerte seule)' if (annl and alert) else '',
|
|
evt.nom,
|
|
)
|
|
)
|
|
# .update() ne fonctionne pas ici, il ne met a jour que le 1er elm
|
|
evt.verif_auto_faite = True
|
|
evt.save()
|
|
|
|
|
|
class Evenement(AccessHistoryMixin):
|
|
# Informations pratiques
|
|
organisateur = models.ForeignKey(Personne, blank=True, null=True)
|
|
nom = models.CharField("Nom", max_length=60)
|
|
description = models.TextField("Description", blank=True)
|
|
lieu = models.ForeignKey(
|
|
Lieu,
|
|
on_delete=models.CASCADE,
|
|
related_name='evenements',
|
|
related_query_name='evenement',
|
|
)
|
|
date_debut = models.DateTimeField("Date et heure")
|
|
ordweek = models.PositiveIntegerField(
|
|
"Ordinal de la semaine", editable=False, null=False
|
|
)
|
|
date_ouv_resa = models.DateField(
|
|
"Date d'ouverture des réservations",
|
|
blank=True,
|
|
null=True,
|
|
help_text="si non renseigné, utilise la date d'aujourd'hui",
|
|
)
|
|
date_ferm_resa = models.DateField(
|
|
"Date de fermeture des réservations",
|
|
blank=True,
|
|
null=True,
|
|
help_text="si non renseigné, utilise la valeur par défaut",
|
|
)
|
|
duree = models.DurationField("Durée", help_text="format: HH:MM:SS")
|
|
nb_places = models.PositiveIntegerField("Nombre total de places")
|
|
actif = models.BooleanField(
|
|
default=True, help_text="Permet d'annuler un évenement"
|
|
)
|
|
verif_auto_faite = models.BooleanField(default=False, editable=False)
|
|
annulation_auto_nb_inscrit = models.BooleanField(
|
|
"Annulation auto",
|
|
default=True,
|
|
help_text="annulation automatique si le nb d'inscrit est insuffisant "
|
|
"à la 'date d'alerte inscription'",
|
|
)
|
|
nb_places_min = models.PositiveIntegerField(
|
|
"Nb minimum d'inscrit",
|
|
blank=True,
|
|
null=True,
|
|
help_text="Alerter si le nb d'inscrit est inférieur à ce nombre",
|
|
)
|
|
date_alert_nb_inscrit = models.DateField(
|
|
"Date d'alerte inscription",
|
|
blank=True,
|
|
null=True,
|
|
editable=False,
|
|
help_text="Alerter a cette date si le nb d'inscrit est insuffisant",
|
|
)
|
|
# Illustration de l'événement
|
|
illustration = models.ImageField(
|
|
"Illustration",
|
|
null=True,
|
|
blank=True,
|
|
upload_to=upload_illustration_to,
|
|
)
|
|
vignette = models.ImageField(
|
|
"Vignette",
|
|
null=True,
|
|
blank=True,
|
|
editable=False,
|
|
upload_to=upload_vignette_to,
|
|
)
|
|
|
|
objects = EvenementQuerySet.as_manager()
|
|
|
|
class Meta:
|
|
verbose_name = "Événement"
|
|
verbose_name_plural = "Événements"
|
|
unique_together = ('nom', 'date_debut')
|
|
|
|
def __str__(self):
|
|
return "« {0} » le {1}".format(self.nom, self.date_debut)
|
|
|
|
def get_date_ouv_resa(self):
|
|
"getter qui calcul la bonne valeur en cas de val. null"
|
|
if self.date_ouv_resa is None:
|
|
return now().date()
|
|
return self.date_ouv_resa
|
|
|
|
def get_date_ferm_resa(self):
|
|
"getter qui calcul la bonne valeur en cas de val. null"
|
|
date_ferm = self.date_debut.date() - datetime.timedelta(
|
|
days=config.DEF_RESA_NB_J_AV_EVT
|
|
)
|
|
# calculer la date de fermeture des reservations:
|
|
if self.date_ferm_resa is None:
|
|
return date_ferm
|
|
else: # verif qu'on a bien la valeur min.
|
|
delta = self.date_debut.date() - self.date_ferm_resa
|
|
if delta.days < (
|
|
config.VAL_AUTO_NB_J_AV_EVT + config.TPS_REFLEXION
|
|
):
|
|
return date_ferm
|
|
return self.date_ferm_resa
|
|
|
|
def get_date_auto_val(self):
|
|
"getter qui renvoi la date d'autovalidation sur non reponse"
|
|
daa = self.date_debut.date() - datetime.timedelta(
|
|
days=config.ALERT_NB_J_AV_EVT_PART_INSUF
|
|
)
|
|
if self.date_alert_nb_inscrit is None:
|
|
return daa
|
|
else: # pas de date d'alerte apres la date par defaut
|
|
if self.date_alert_nb_inscrit > daa:
|
|
return daa
|
|
return self.date_alert_nb_inscrit
|
|
|
|
def clean(self):
|
|
"Verifie les contraintes sur l'évenement"
|
|
min_delta = datetime.timedelta(
|
|
config.TPS_REFLEXION + config.VAL_AUTO_NB_J_AV_EVT
|
|
)
|
|
date_debut_min = now().date() + min_delta
|
|
if self.date_debut and self.date_debut.date() < date_debut_min:
|
|
raise ValidationError(
|
|
{
|
|
'date_debut': "L'évenement ne peut pas être crée avant le {0}".format(
|
|
date_debut_min.strftime('%d/%m/%Y')
|
|
)
|
|
}
|
|
)
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""Sauvegarde l'événement, avec sa vignette si besoin."""
|
|
self.ordweek = toordweek(self.date_debut)
|
|
# TODO: supprimer l'illustration à sa suppression
|
|
if self.illustration and (
|
|
not self.vignette
|
|
or get_filename(self.illustration.name)
|
|
!= get_filename(self.vignette.name)
|
|
):
|
|
# génère une vignette si elle n'existe pas ou si
|
|
# elle ne correspond pas avec l'illustration
|
|
self.generate_vignette()
|
|
elif not self.illustration and self.vignette:
|
|
# supprime la vignette s'il n'y a plus d'illustration
|
|
self.vignette.delete(False)
|
|
# set date_ouv_resa
|
|
self.date_ouv_resa = self.get_date_ouv_resa()
|
|
# set date_ferm_resa
|
|
self.date_ferm_resa = self.get_date_ferm_resa()
|
|
# calculer la date d'alerte d'inscriptions insuffisantes
|
|
self.date_alert_nb_inscrit = self.get_date_auto_val()
|
|
# calculer le nb min. d'inscrit
|
|
if self.nb_places_min is None:
|
|
self.nb_places_min = pourcentage(
|
|
self.nb_places, config.PCENT_PART_EVT_MIN
|
|
)
|
|
return super().save(*args, **kwargs)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('evenements:voir', args=[str(self.id)])
|
|
|
|
@property
|
|
def date_fin(self):
|
|
return self.date_debut + self.duree
|
|
|
|
@property
|
|
def est_passe(self):
|
|
return now() > self.date_debut
|
|
|
|
@property
|
|
def nb_places_reservees(self):
|
|
return self.reservations.reservees().count()
|
|
|
|
@property
|
|
def nb_places_acceptees(self):
|
|
return self.reservations.acceptees().count()
|
|
|
|
@property
|
|
def nb_places_restantes(self):
|
|
return self.nb_places - self.nb_places_reservees
|
|
|
|
def generate_vignette(self):
|
|
"""Génère une nouvelle vignette depuis l'illustration."""
|
|
if self.vignette:
|
|
self.vignette.delete(False)
|
|
self.vignette = resize_image_field(
|
|
self.illustration,
|
|
settings.EVENEMENT_VIGNETTE_TAILLE,
|
|
)
|
|
|
|
@property
|
|
def est_reservable(self):
|
|
"""renvoie vrai si l'evenement est reservable"""
|
|
# verif date d'ouverture
|
|
today = now().date()
|
|
date_ouv_resa = self.get_date_ouv_resa()
|
|
date_ferm_resa = self.get_date_ferm_resa()
|
|
return date_ouv_resa <= today <= date_ferm_resa
|
|
|
|
def pose_reservation_personne(self, personne):
|
|
"""pose une reservation pour la personne"""
|
|
reservation = Reservation(evenement=self, personne=personne)
|
|
reservation.full_clean()
|
|
reservation.save()
|
|
logger.info(
|
|
"réservation faite par {0} pour '{1}'".format(personne, self.nom)
|
|
)
|
|
reservation.mail_resa_demande()
|
|
|
|
def inscrits_insuffisants(self):
|
|
"""renvoie vrai si l'evenement n'a pas assez d'inscrits"""
|
|
return self.reservations.all().reservees().count() < self.nb_places_min
|
|
|
|
def inscrits_insuffisants_date(self):
|
|
"""
|
|
renvoie vrai si l'evenement n'a pas assez d'inscrit et que
|
|
la date d'alerte est dépassée
|
|
"""
|
|
if now().date() >= self.date_alert_nb_inscrit:
|
|
return self.inscrits_insuffisants()
|
|
return False
|
|
|
|
def _get_sujet_alerte(self, action=True, annul=True):
|
|
"""
|
|
genere le sujet du message selon action/alerte et
|
|
annulation/confirmation de l'evt
|
|
annul - action -> repr binaire
|
|
0b00 -> annul==False et action==False
|
|
0b10 -> annul==True et action==False etc..
|
|
"""
|
|
MTX = {
|
|
0b00: "Nombre d'inscrits suffisants pour",
|
|
0b01: "Confirmation de",
|
|
0b10: "Alerte, inscrits insuffisants pour",
|
|
0b11: "Annulation de",
|
|
}
|
|
sujet = " '{evt.nom}' du {ddebut}".format(
|
|
evt=self, ddebut=self.date_debut.strftime('%-d/%-m/%Y à %H:%M')
|
|
)
|
|
return MTX[(annul * 0b10) + action] + sujet
|
|
|
|
def alerte_orga_nb_inscrit(
|
|
self, annul=True, action=True, p_resp=True, resp_rec=False
|
|
):
|
|
"""
|
|
alerte l'organisateur si l'evenement n'a pas assez d'inscrits
|
|
si action vaut False, fait une simple alerte, sinon annonce
|
|
une annulation ou une confirmation de l'evenement.
|
|
si annul vaut vrai, l'action est une annulation sinon une confirmation
|
|
"""
|
|
tmpl = 'evenements/mail/{0}_evt.html'.format(
|
|
'annule' if annul else 'confirme'
|
|
)
|
|
if self.organisateur and self.organisateur.email:
|
|
ctx = {
|
|
'evt': self,
|
|
'evt_action': action,
|
|
'qual': 'orga',
|
|
'pers': self.organisateur,
|
|
'sujet': self._get_sujet_alerte(action, annul),
|
|
}
|
|
logger.info(
|
|
"envoi courriel a l'organisateur: {0}".format(
|
|
self.organisateur.email
|
|
)
|
|
)
|
|
b_mail.send_tpl_mail(
|
|
ctx['sujet'], tmpl, ctx, [self.organisateur.email]
|
|
)
|
|
if self.organisateur and p_resp:
|
|
ctx['qual'] = 'resp_o'
|
|
resps = self.organisateur.mes_responsables(recursif=resp_rec)
|
|
logger.info(
|
|
"envoi courriel au resp. de l'org.: {0}".format(
|
|
",".join([elm.email for elm in resps])
|
|
)
|
|
)
|
|
for resp in resps:
|
|
ctx['pers'] = resp
|
|
ctx['agents'] = [self.organisateur]
|
|
b_mail.send_tpl_mail(ctx['sujet'], tmpl, ctx, [resp.email])
|
|
|
|
def annule_confirme_evt(
|
|
self, annul=True, prevenir=True, p_resp=True, resp_rec=False
|
|
):
|
|
"""
|
|
annule ou confirme l'evenement et si `prevenir`, prévient les
|
|
participants par courriel et leur responsables selon p_resp et
|
|
resp_rec.
|
|
ne previent pas l'organisateur (c'est alerte_orga_nb_inscrit()
|
|
qui s'en charge.
|
|
"""
|
|
tmpl = 'evenements/mail/{0}_evt.html'.format(
|
|
'annule' if annul else 'confirme'
|
|
)
|
|
statuts = [Reservation.Statut.ATTENTE, Reservation.Statut.ACCEPTE]
|
|
qs_resa = self.reservations.filter(statut__in=statuts)
|
|
if prevenir:
|
|
ctx = {
|
|
'evt': self,
|
|
'evt_action': True,
|
|
'qual': 'agent',
|
|
'sujet': self._get_sujet_alerte(True, annul),
|
|
}
|
|
qs_pers = qs_resa.personnes()
|
|
# les participants
|
|
logger.info(
|
|
"envoi courriel aux participants: {0}".format(
|
|
",".join([elm.email for elm in qs_pers if elm.email])
|
|
)
|
|
)
|
|
for pers in qs_pers:
|
|
if pers.email:
|
|
ctx['pers'] = pers
|
|
b_mail.send_tpl_mail(ctx["sujet"], tmpl, ctx, [pers.email])
|
|
# les responsables
|
|
if p_resp:
|
|
ctx['qual'] = 'resp'
|
|
pers_din = {}
|
|
for pers in qs_pers:
|
|
pers_din[pers] = pers.mes_responsables(recursif=resp_rec)
|
|
resp_dout = transpose_dict(pers_din)
|
|
logger.info(
|
|
"envoi courriel aux resp. des part.: {0}".format(
|
|
",".join([elm.email for elm in resp_dout])
|
|
)
|
|
)
|
|
for resp, agents in resp_dout.items():
|
|
if resp.email:
|
|
ctx['pers'] = resp
|
|
ctx['agents'] = agents
|
|
b_mail.send_tpl_mail(
|
|
ctx["sujet"], tmpl, ctx, [resp.email]
|
|
)
|
|
if annul:
|
|
# apres un update la qs est vide (a cause du filtre)
|
|
qs_resa.update(statut=Reservation.Statut.ANNULE)
|
|
self.actif = False
|
|
self.save()
|
|
|
|
def alerte_annule_confirme(self, annul=True, p_resp=True, resp_rec=False):
|
|
"""
|
|
si `self.annulation_auto_nb_inscrit` == False, alerte seulement
|
|
annule ou confirme selon `annul`
|
|
"""
|
|
act_auto = self.annulation_auto_nb_inscrit
|
|
self.alerte_orga_nb_inscrit(act_auto, annul, p_resp, resp_rec)
|
|
if act_auto:
|
|
self.annule_confirme_evt(annul, True, p_resp, resp_rec)
|
|
|
|
def force_annule(self, p_resp=True, resp_rec=False):
|
|
"""
|
|
force l'annulation qqsoit self.annulation_auto_nb_inscrit
|
|
permet de faire une annulation manuelle de l'evenement
|
|
"""
|
|
self.alerte_orga_nb_inscrit(True, p_resp, resp_rec)
|
|
self.annule_confirme_evt(True, True, p_resp, resp_rec)
|
|
logger.info("Annulation forcée de '{0}'".format(self.nom))
|
|
|
|
|
|
class ReservationQuerySet(models.QuerySet):
|
|
"""Étends les méthodes et requêtes pour les `Reservation`."""
|
|
|
|
def a_venir(self):
|
|
"""Filtre avec les événements à venir."""
|
|
return self.filter(
|
|
evenement__actif=True,
|
|
evenement__date_debut__date__gte=datetime.date.today(),
|
|
)
|
|
|
|
def reservees(self):
|
|
"""Exclue les réservations refusées ou annulées."""
|
|
return self.exclude(
|
|
statut__in=[Reservation.Statut.REFUSE, Reservation.Statut.ANNULE],
|
|
)
|
|
|
|
def acceptees(self):
|
|
"""Filtre avec les réservations acceptées."""
|
|
return self.filter(statut=Reservation.Statut.ACCEPTE)
|
|
|
|
def en_attentes(self):
|
|
"""Filtre avec les réservations en attentes."""
|
|
return self.filter(statut=Reservation.Statut.ATTENTE)
|
|
|
|
def val_auto(self, commit=False):
|
|
"""Filtre les evenements a venir en attente
|
|
dont la date correspond a ceux qui doivent etre
|
|
validé automatiquement.
|
|
si commit vaut True, valide ces reservations"""
|
|
logger.info("Appel de Reservation.objects.val_auto()")
|
|
date_auto_val = datetime.date.today() + datetime.timedelta(
|
|
config.VAL_AUTO_NB_J_AV_EVT
|
|
)
|
|
r_qs = self.filter(
|
|
statut=Reservation.Statut.ATTENTE,
|
|
evenement__date_debut__date__lte=date_auto_val,
|
|
)
|
|
if commit:
|
|
for elm in r_qs:
|
|
logger.info(
|
|
"val. auto resa de '{0}' pour '{1}'".format(
|
|
elm.personne, elm.evenement.nom
|
|
)
|
|
)
|
|
r_qs.update(statut=Reservation.Statut.ACCEPTE)
|
|
return r_qs
|
|
|
|
def personnes(self):
|
|
"""Retourne les objets `Personne` correspondant."""
|
|
return Personne.objects.filter(
|
|
pk__in=self.values_list('personne', flat=True)
|
|
)
|
|
|
|
|
|
class Reservation(AccessHistoryMixin):
|
|
"""Réservation d'une `Personne` pour un `Evenement`."""
|
|
|
|
class Statut(DjangoChoices):
|
|
ATTENTE = ChoiceItem('att', "En attente")
|
|
ACCEPTE = ChoiceItem('acc', "Accepté")
|
|
REFUSE = ChoiceItem('ref', "Refusé")
|
|
ANNULE = ChoiceItem('nul', "Annulé")
|
|
|
|
personne = models.ForeignKey(
|
|
Personne,
|
|
on_delete=models.CASCADE,
|
|
related_name='reservations',
|
|
related_query_name='reservation',
|
|
)
|
|
evenement = models.ForeignKey(
|
|
Evenement,
|
|
on_delete=models.CASCADE,
|
|
related_name='reservations',
|
|
related_query_name='reservation',
|
|
)
|
|
statut = models.CharField(
|
|
"Statut",
|
|
max_length=3,
|
|
choices=Statut.choices,
|
|
default=Statut.ATTENTE,
|
|
)
|
|
present = models.BooleanField(default=False, blank=True)
|
|
|
|
resp = models.ForeignKey(
|
|
Personne,
|
|
on_delete=models.CASCADE,
|
|
blank=True,
|
|
null=True,
|
|
editable=False,
|
|
related_name='resa_validees',
|
|
related_query_name='resa_validee',
|
|
)
|
|
date_val = models.DateTimeField(
|
|
"date de validation", blank=True, null=True, editable=False
|
|
)
|
|
message = models.TextField("Message de refus", blank=True)
|
|
|
|
objects = ReservationQuerySet.as_manager()
|
|
|
|
class Meta:
|
|
unique_together = ('personne', 'evenement')
|
|
verbose_name = "Réservation"
|
|
verbose_name_plural = "Réservations"
|
|
|
|
def __str__(self):
|
|
return "{0} pour {1}".format(
|
|
self.personne.get_full_name(),
|
|
self.evenement,
|
|
)
|
|
|
|
def clean(self):
|
|
if self.id is not None: # en cas de reservation existante
|
|
return
|
|
# en cas de nouvelle reservation
|
|
# valide automatiquement la réservation pour un responsable
|
|
if self.personne.a_la_fonction(Fonction.RESPONSABLE):
|
|
self.statut = Reservation.Statut.ACCEPTE
|
|
# il ne doit pas y avoir de reservation pour la meme
|
|
# personne durant la meme semaine (1 BETG par semaine)
|
|
rms = (
|
|
self.personne.reservations.a_venir()
|
|
.reservees()
|
|
.filter(evenement__ordweek=self.evenement.ordweek)
|
|
.exists()
|
|
)
|
|
if rms:
|
|
raise ValidationError(
|
|
"L'événement « {0} » n'est pas réservable, vous avez "
|
|
"déjà une réservation pour cette semaine".format(
|
|
self.evenement.nom
|
|
)
|
|
)
|
|
# est ce que l'evenement est reservable ?
|
|
if not self.evenement.est_reservable:
|
|
raise ValidationError(
|
|
"L'événement « {0} » n'est plus ou pas encore réservable".format(
|
|
self.evenement.nom
|
|
)
|
|
)
|
|
# si la personne n'est pas un responsable, il faut
|
|
# laisser TPS_REFLEXION + VAL_AUTO_NB_J_AV_EVT entre
|
|
# la date de la reservation et celle de l'evenement
|
|
# a la creation et a la modification (annulation de la reservation
|
|
# a l'initiative de l'utilisateur
|
|
if not self.personne.a_la_fonction(Fonction.RESPONSABLE):
|
|
date_debut_min = now().date() + datetime.timedelta(
|
|
config.TPS_REFLEXION + config.VAL_AUTO_NB_J_AV_EVT
|
|
)
|
|
if self.evenement.date_debut.date() < date_debut_min:
|
|
raise ValidationError(
|
|
"La date de l'évenement « {0} » est trop proche".format(
|
|
self.evenement.nom
|
|
)
|
|
)
|
|
|
|
def unique_error_message(self, *args, **kwargs):
|
|
# personnalise le message d'erreur en cas d'un doublon
|
|
error = super().unique_error_message(*args, **kwargs)
|
|
if error.code == 'unique_together':
|
|
error.message = "Une réservation est déjà faite pour l'événement « {0} »".format(
|
|
self.evenement.nom
|
|
)
|
|
return error
|
|
|
|
def mail_resa_demande(self):
|
|
"""envoi un courriel lorsqu'un agent fait une demande de reservation"""
|
|
resps = self.personne.mes_responsables()
|
|
logger.info(
|
|
"réservation faite par {0}, courriel(s) envoyé(s) à {1}".format(
|
|
self.personne, ','.join([elm.email for elm in resps])
|
|
)
|
|
)
|
|
for resp in resps:
|
|
if not resp.email:
|
|
continue
|
|
ctx = {
|
|
'resp': resp,
|
|
'agent': self.personne,
|
|
'reservation': self,
|
|
}
|
|
b_mail.send_tpl_mail(
|
|
"[BETG] reservation en attente pour {0}".format(self.personne),
|
|
'evenements/mail/a_resp_sur_resa.html',
|
|
ctx,
|
|
[resp.email],
|
|
)
|
|
|
|
def mail_reponse_resp(self):
|
|
"""envoi un courriel lorsqu'un responsable repond a une demande"""
|
|
refus = self.statut == Reservation.Statut.REFUSE
|
|
action = "refusée" if refus else "acceptée"
|
|
attachments = []
|
|
ctx = {
|
|
'refus': refus,
|
|
'pers': self.personne,
|
|
'evt': self.evenement,
|
|
'resa': self,
|
|
}
|
|
if refus:
|
|
attachments.append(refus_resa(self))
|
|
b_mail.send_tpl_mail(
|
|
"[BETG] votre réservation est {0}".format(action),
|
|
'evenements/mail/reponse_resa.html',
|
|
ctx,
|
|
[self.personne.email],
|
|
attachments=attachments,
|
|
)
|