kpsul/bda/views.py
Martin Pepin d6dd7b346c Merge branch 'Elarnon/mail_bda' into 'master'
Améliore les mails automatiques du BdA

Les mails du BdA sont maintenant tous chargés depuis des templates gérés par le système de templates de Django, et plus par de l'interpolation de chaîne de caractères. Ceci permet en particulier d'utiliser (et de configurer) la localisation de Django afin d'afficher les dates de façon uniforme (et sans "hack" à la `date_no_seconds`) dans un format comportant un "à" entre le jour et l'heure.

See merge request !113
2016-11-20 16:53:29 +01:00

589 lines
23 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import random
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.db import models, transaction
from django.db.models import Count, Q
from django.core import serializers, mail
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.conf import settings
import hashlib
from django.core.mail import send_mail
from django.template import loader
from django.utils import timezone
from django.views.generic.list import ListView
import time
from datetime import timedelta
from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
Tirage, SpectacleRevente
from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
InscriptionReventeForm
@cof_required
def etat_places(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles1 = ChoixSpectacle.objects \
.filter(spectacle__tirage=tirage) \
.filter(double_choice="1") \
.all() \
.values('spectacle', 'spectacle__title') \
.annotate(total=models.Count('spectacle'))
spectacles2 = ChoixSpectacle.objects \
.filter(spectacle__tirage=tirage) \
.exclude(double_choice="1") \
.all() \
.values('spectacle', 'spectacle__title') \
.annotate(total=models.Count('spectacle'))
spectacles = tirage.spectacle_set.all()
spectacles_dict = {}
total = 0
for spectacle in spectacles:
spectacle.total = 0
spectacle.ratio = 0.0
spectacles_dict[spectacle.id] = spectacle
for spectacle in spectacles1:
spectacles_dict[spectacle["spectacle"]].total += spectacle["total"]
spectacles_dict[spectacle["spectacle"]].ratio = \
spectacles_dict[spectacle["spectacle"]].total / \
spectacles_dict[spectacle["spectacle"]].slots
total += spectacle["total"]
for spectacle in spectacles2:
spectacles_dict[spectacle["spectacle"]].total += 2*spectacle["total"]
spectacles_dict[spectacle["spectacle"]].ratio = \
spectacles_dict[spectacle["spectacle"]].total / \
spectacles_dict[spectacle["spectacle"]].slots
total += spectacle["total"]
return render(request, "etat-places.html",
{"spectacles": spectacles, "total": total, 'tirage': tirage})
def _hash_queryset(queryset):
data = serializers.serialize("json", queryset).encode('utf-8')
hasher = hashlib.sha256()
hasher.update(data)
return hasher.hexdigest()
@cof_required
def places(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
places = participant.attribution_set.order_by(
"spectacle__date", "spectacle").all()
total = sum([place.spectacle.price for place in places])
filtered_places = []
places_dict = {}
spectacles = []
dates = []
warning = False
for place in places:
if place.spectacle in spectacles:
places_dict[place.spectacle].double = True
else:
place.double = False
places_dict[place.spectacle] = place
spectacles.append(place.spectacle)
filtered_places.append(place)
date = place.spectacle.date.date()
if date in dates:
warning = True
else:
dates.append(date)
return render(request, "resume_places.html",
{"participant": participant,
"places": filtered_places,
"tirage": tirage,
"total": total,
"warning": warning})
@cof_required
def inscription(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
if timezone.now() < tirage.ouverture:
error_desc = tirage.ouverture.strftime('Ouverture le %d %b %Y à %H:%M')
return render(request, 'resume_inscription.html',
{"error_title": "Le tirage n'est pas encore ouvert !",
"error_description": error_desc})
if timezone.now() > tirage.fermeture:
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
choices = participant.choixspectacle_set.order_by("priority").all()
return render(request, "resume_inscription.html",
{"error_title": "C'est fini !",
"error_description":
"Tirage au sort dans la journée !",
"choices": choices})
def formfield_callback(f, **kwargs):
if f.name == "spectacle":
kwargs['queryset'] = tirage.spectacle_set
return f.formfield(**kwargs)
BdaFormSet = inlineformset_factory(
Participant,
ChoixSpectacle,
fields=("spectacle", "double_choice", "priority"),
formset=BaseBdaFormSet,
formfield_callback=formfield_callback)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
success = False
stateerror = False
if request.method == "POST":
dbstate = _hash_queryset(participant.choixspectacle_set.all())
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
stateerror = True
formset = BdaFormSet(instance=participant)
else:
formset = BdaFormSet(request.POST, instance=participant)
if formset.is_valid():
formset.save()
success = True
formset = BdaFormSet(instance=participant)
else:
formset = BdaFormSet(instance=participant)
dbstate = _hash_queryset(participant.choixspectacle_set.all())
total_price = 0
for choice in participant.choixspectacle_set.all():
total_price += choice.spectacle.price
if choice.double:
total_price += choice.spectacle.price
return render(request, "inscription-bda.html",
{"formset": formset,
"success": success,
"total_price": total_price,
"dbstate": dbstate,
'tirage': tirage,
"stateerror": stateerror})
def do_tirage(request, tirage_id):
tirage_elt = get_object_or_404(Tirage, id=tirage_id)
form = TokenForm(request.POST)
if not form.is_valid():
return tirage(request, tirage_id)
start = time.time()
data = {}
shows = tirage_elt.spectacle_set.select_related().all()
members = tirage_elt.participant_set.all()
choices = ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt) \
.order_by('participant', 'priority').select_related().all()
algo = Algorithm(shows, members, choices)
results = algo(form.cleaned_data["token"])
total_slots = 0
total_losers = 0
for (_, members, losers) in results:
total_slots += len(members)
total_losers += len(losers)
data["total_slots"] = total_slots
data["total_losers"] = total_losers
data["shows"] = shows
data["token"] = form.cleaned_data["token"]
data["members"] = members
data["results"] = results
total_sold = 0
total_deficit = 0
opera_deficit = 0
for (show, members, _) in results:
deficit = (show.slots - len(members)) * show.price
total_sold += show.slots * show.price
if deficit >= 0:
if "Opéra" in show.location.name:
opera_deficit += deficit
total_deficit += deficit
data["total_sold"] = total_sold - total_deficit
data["total_deficit"] = total_deficit
data["opera_deficit"] = opera_deficit
data["duration"] = time.time() - start
if request.user.is_authenticated():
members2 = {}
# Participant objects are not shared accross spectacle results,
# so assign a single object for each Participant id
members_uniq = {}
for (show, members, _) in results:
for (member, _, _, _) in members:
if member.id not in members_uniq:
members_uniq[member.id] = member
members2[member] = []
member.total = 0
member = members_uniq[member.id]
members2[member].append(show)
member.total += show.price
members2 = members2.items()
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
# À partir d'ici, le tirage devient effectif
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
tirage_elt.tokens += "%s\n\"\"\"%s\"\"\"\n" % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
form.cleaned_data['token'])
tirage_elt.enable_do_tirage = False
tirage_elt.save()
Attribution.objects.bulk_create([
Attribution(spectacle=show, participant=member)
for show, members, _ in results
for member, _, _, _ in members])
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
for (show, _, losers) in results:
for (loser, _, _, _) in losers:
loser.choicesrevente.add(show)
loser.save()
return render(request, "bda-attrib-extra.html", data)
else:
return render(request, "bda-attrib.html", data)
@buro_required
def tirage(request, tirage_id):
tirage_elt = get_object_or_404(Tirage, id=tirage_id)
if not (tirage_elt.enable_do_tirage
and tirage_elt.fermeture < timezone.now()):
return render(request, "tirage-failed.html", {'tirage': tirage_elt})
if request.POST:
form = TokenForm(request.POST)
if form.is_valid():
return do_tirage(request, tirage_id)
else:
form = TokenForm()
return render(request, "bda-token.html", {"form": form})
@login_required
def revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
if not participant.paid:
return render(request, "bda-notpaid.html", {})
if request.method == 'POST':
if 'resell' in request.POST:
resellform = ResellForm(participant, request.POST, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
if resellform.is_valid():
mails = []
attributions = resellform.cleaned_data["attributions"]
with transaction.atomic():
for attribution in attributions:
revente, created = SpectacleRevente.objects.get_or_create(
attribution=attribution,
defaults={'seller': participant})
if not created:
revente.seller = participant
revente.date = timezone.now()
mail_subject = "BdA-Revente : {:s}".format(attribution.spectacle.title)
mail_body = loader.render_to_string('bda/mails/revente-new.txt', {
'vendeur': participant.user,
'spectacle': attribution.spectacle,
'revente': revente,
})
mails.append(mail.EmailMessage(
mail_subject, mail_body,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[participant.user.email],
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']],
))
revente.save()
mail.get_connection().send_messages(mails)
elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul')
resellform = ResellForm(participant, prefix='resell')
if annulform.is_valid():
attributions = annulform.cleaned_data["attributions"]
for attribution in attributions:
attribution.revente.delete()
elif 'transfer' in request.POST:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['transfer'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
attrib = revente.attribution
attrib.participant = revente.soldTo
attrib.save()
elif 'reinit' in request.POST:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['reinit'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
if revente.attribution.spectacle.date > timezone.now():
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
else:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
else:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
overdue = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__seller=participant,
revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
sold = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__soldTo__isnull=False).exclude(
revente__soldTo=participant)
return render(request, "bda-revente.html",
{'tirage': tirage, 'overdue': overdue, "sold": sold,
"annulform": annulform, "resellform": resellform})
@login_required
def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage)
if timezone.now() < revente.date + timedelta(hours=1) or revente.shotgun:
return render(request, "bda-wrongtime.html", {})
revente.answered_mail.add(participant)
return render(request, "bda-interested.html",
{"spectacle": revente.attribution.spectacle,
"date": revente.expiration_time})
@login_required
def list_revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
spectacles = tirage.spectacle_set.filter(
date__gte=timezone.now())
shotgun = []
deja_revente = False
success = False
for spectacle in spectacles:
revente_objects = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
revente_count = 0
for revente in revente_objects:
if revente.shotgun:
revente_count += 1
if revente_count:
spectacle.revente_count = revente_count
shotgun.append(spectacle)
if request.method == 'POST':
form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid():
choices = form.cleaned_data['spectacles']
participant.choicesrevente = choices
participant.save()
for spectacle in choices:
qset = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle)
if qset.exists():
# On l'inscrit à l'un des tirages au sort
for revente in qset.all():
if revente.shotgun and not revente.soldTo:
deja_revente = True
else:
revente.answered_mail.add(participant)
revente.save()
break
success = True
else:
form = InscriptionReventeForm(
tirage,
initial={'spectacles': participant.choicesrevente.all()})
return render(request, "liste-reventes.html",
{"form": form, 'shotgun': shotgun,
"deja_revente": deja_revente, "success": success})
@login_required
def buy_revente(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
if reventes.filter(seller=participant).exists():
revente = reventes.filter(seller=participant)[0]
revente.delete()
return HttpResponseRedirect(reverse("bda-shotgun",
args=[tirage.id]))
reventes_shotgun = []
for revente in reventes.all():
if revente.shotgun:
reventes_shotgun.append(revente)
if not reventes_shotgun:
return render(request, "bda-no-revente.html", {})
if request.POST:
revente = random.choice(reventes_shotgun)
revente.soldTo = participant
revente.save()
mail = loader.render_to_string('bda/mails/buy-shotgun.txt', {
'spectacle': spectacle,
'acheteur': request.user,
'vendeur': revente.seller.user,
})
send_mail("BdA-Revente : %s" % spectacle.title, mail,
request.user.email,
[revente.seller.user.email],
fail_silently=False)
return render(request, "bda-success.html",
{"seller": revente.attribution.participant.user,
"spectacle": spectacle})
return render(request, "revente-confirm.html",
{"spectacle": spectacle,
"user": request.user})
@login_required
def revente_shotgun(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = tirage.spectacle_set.filter(
date__gte=timezone.now())
shotgun = []
for spectacle in spectacles:
revente_objects = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
revente_count = 0
for revente in revente_objects:
if revente.shotgun:
revente_count += 1
if revente_count:
shotgun.append(spectacle)
return render(request, "bda-shotgun.html",
{"shotgun": shotgun})
@buro_required
def spectacle(request, tirage_id, spectacle_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
attributions = spectacle.attribues.all()
participants = {}
for attrib in attributions:
participant = attrib.participant
participant_info = {'lastname': participant.user.last_name,
'name': participant.user.get_full_name,
'username': participant.user.username,
'email': participant.user.email,
'given': int(attrib.given),
'paid': participant.paid,
'nb_places': 1}
if participant.id in participants:
participants[participant.id]['nb_places'] += 1
participants[participant.id]['given'] += attrib.given
else:
participants[participant.id] = participant_info
participants_info = sorted(participants.values(),
key=lambda part: part['lastname'])
return render(request, "bda-participants.html",
{"spectacle": spectacle, "participants": participants_info})
class SpectacleListView(ListView):
model = Spectacle
template_name = 'spectacle_list.html'
def get_queryset(self):
self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id'])
categories = self.tirage.spectacle_set.all()
return categories
def get_context_data(self, **kwargs):
context = super(SpectacleListView, self).get_context_data(**kwargs)
context['tirage_id'] = self.tirage.id
context['tirage_name'] = self.tirage.title
return context
@buro_required
def unpaid(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
unpaid = tirage.participant_set \
.annotate(nb_attributions=Count('attribution')) \
.filter(paid=False, nb_attributions__gt=0).all()
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
@buro_required
def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples
fake_member = request.user
fake_member.nb_attr = 1
exemple_mail_1place = loader.render_to_string('bda/mails/rappel.txt', {
'member': fake_member,
'show': show})
fake_member.nb_attr = 2
exemple_mail_2places = loader.render_to_string('bda/mails/rappel.txt', {
'member': fake_member,
'show': show})
# Contexte
ctxt = {'show': show,
'exemple_mail_1place': exemple_mail_1place,
'exemple_mail_2places': exemple_mail_2places}
# Envoi confirmé
if request.method == 'POST':
members = show.send_rappel()
ctxt['sent'] = True
ctxt['members'] = members
# Demande de confirmation
else:
ctxt['sent'] = False
return render(request, "mails-rappel.html", ctxt)
def descriptions_spectacles(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set
category_name = request.GET.get('category', '')
location_id = request.GET.get('location', '')
if category_name:
shows_qs = shows_qs.filter(category__name=category_name)
if location_id:
try:
shows_qs = shows_qs.filter(location__id=int(location_id))
except ValueError:
return HttpResponseBadRequest(
"La variable GET 'location' doit contenir un entier")
return render(request, 'descriptions.html', {'shows': shows_qs.all()})