diff --git a/bda/admin.py b/bda/admin.py index e07a5d49..87a269d4 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -5,12 +5,13 @@ from __future__ import print_function from __future__ import unicode_literals from django.core.mail import send_mail - from django.contrib import admin from django.db.models import Sum, Count +from django.template.defaultfilters import pluralize +from django.utils import timezone +from django import forms from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente -from django import forms from datetime import timedelta @@ -211,9 +212,15 @@ class SalleAdmin(admin.ModelAdmin): class SpectacleReventeAdmin(admin.ModelAdmin): + """ + Administration des reventes de spectacles + """ model = SpectacleRevente def spectacle(self, obj): + """ + Raccourci vers le spectacle associé à la revente. + """ return obj.attribution.spectacle list_display = ("spectacle", "seller", "date", "soldTo") @@ -224,6 +231,48 @@ class SpectacleReventeAdmin(admin.ModelAdmin): 'seller__user__first_name', 'seller__user__last_name'] + actions = ['transfer', 'reinit'] + actions_on_bottom = True + + def transfer(self, request, queryset): + """ + Effectue le transfert des reventes pour lesquels on connaît l'acheteur. + """ + reventes = queryset.exclude(soldTo__isnull=True).all() + count = reventes.count() + for revente in reventes: + attrib = revente.attribution + attrib.participant = revente.soldTo + attrib.save() + self.message_user( + request, + "%d attribution%s %s été transférée%s avec succès." % ( + count, pluralize(count), + pluralize(count, "a,ont"), pluralize(count)) + ) + transfer.short_description = "Transférer les reventes sélectionnées" + + def reinit(self, request, queryset): + """ + Réinitialise les reventes. + """ + count = queryset.count() + for revente in queryset.all(): + 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() + self.message_user( + request, + "%d attribution%s %s été réinitialisée%s avec succès." % ( + count, pluralize(count), + pluralize(count, "a,ont"), pluralize(count)) + ) + reinit.short_description = "Réinitialiser les reventes sélectionnées" + admin.site.register(CategorieSpectacle) admin.site.register(Spectacle, SpectacleAdmin) diff --git a/bda/models.py b/bda/models.py index 08f17bbe..6fb22189 100644 --- a/bda/models.py +++ b/bda/models.py @@ -320,33 +320,51 @@ class SpectacleRevente(models.Model): self.save() def tirage(self): - inscrits = self.answered_mail + inscrits = list(self.answered_mail.all()) spectacle = self.attribution.spectacle seller = self.seller - if inscrits.exists(): - winner = random.choice(inscrits.all()) + + if inscrits: + mails = [] + mail_subject = "BdA-Revente : {:s}".format(spectacle.title) + + # Envoie un mail au gagnant et au vendeur + winner = random.choice(inscrits) self.soldTo = winner - mail_buyer = """Bonjour, + context = { + 'acheteur': winner.user, + 'vendeur': seller.user, + 'spectacle': spectacle, + } + mails.append(mail.EmailMessage( + mail_subject, loader.render_to_string('mail-revente-winner.txt', context), + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[winner.user.email], + reply_to=[seller.user.email], + )) + mails.append(mail.EmailMessage( + mail_subject, loader.render_to_string('mail-revente-seller.txt', context), + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[seller.user.email], + reply_to=[winner.user.email], + )) -Tu as été tiré-e au sort pour racheter une place pour %s le %s (%s) à %0.02f€. -Tu peux contacter le/la vendeur-se à l'adresse %s. + # Envoie un mail aux perdants + for inscrit in inscrits: + if inscrit == winner: + continue -Chaleureusement, -Le BdA""" % (spectacle.title, spectacle.date_no_seconds(), - spectacle.location, spectacle.price, seller.user.email) - - mail.send_mail("BdA-Revente : %s" % spectacle.title, - mail_buyer, "bda@ens.fr", [winner.user.email], - fail_silently=False) - mail_seller = """Bonjour, -La personne tirée au sort pour racheter ta place pour %s est %s. -Tu peux le/la contacter à l'adresse %s. - -Chaleureusement, -Le BdA""" % (spectacle.title, winner.user.get_full_name(), winner.user.email) - - mail.send_mail("BdA-Revente : %s" % spectacle.title, - mail_seller, "bda@ens.fr", [seller.user.email], - fail_silently=False) + mail_body = loader.render_to_string('mail-revente-loser.txt', { + 'acheteur': inscrit.user, + 'vendeur': seller.user, + 'spectacle': spectacle, + }) + mails.append(mail.EmailMessage( + mail_subject, mail_body, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[inscrit.user.email], + reply_to=[settings.MAIL_DATA['revente']['REPLYTO']], + )) + mail.get_connection().send_messages(mails) self.tirage_done = True self.save() diff --git a/bda/templates/bda-revente.html b/bda/templates/bda-revente.html index a5f64678..569a3f3a 100644 --- a/bda/templates/bda-revente.html +++ b/bda/templates/bda-revente.html @@ -62,9 +62,9 @@ {{attrib.spectacle}} {{attrib.revente.soldTo.user.get_full_name}} + value="{{attrib.revente.id}}">Transférer + value="{{attrib.revente.id}}">Réinitialiser {% endfor %} diff --git a/bda/templates/mail-revente-loser.txt b/bda/templates/mail-revente-loser.txt new file mode 100644 index 00000000..6b50944d --- /dev/null +++ b/bda/templates/mail-revente-loser.txt @@ -0,0 +1,9 @@ +Bonjour {{ acheteur.first_name }}, + +Tu t'étais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }} +pour {{ spectacle.title }}. +Malheureusement, une autre personne a été tirée au sort pour racheter la place. +Tu pourras certainement retenter ta chance pour une autre revente ! + +À très bientôt, +Le Bureau des Arts diff --git a/bda/templates/mail-revente-new.txt b/bda/templates/mail-revente-new.txt new file mode 100644 index 00000000..ffba3083 --- /dev/null +++ b/bda/templates/mail-revente-new.txt @@ -0,0 +1,13 @@ +Bonjour {{ vendeur.first_name }}, + +Tu t’es bien inscrit-e pour la revente de {{ spectacle.title }}. + +{% with revente.expiration_time as time %} +Le tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu +le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}). +Si personne ne s’est inscrit pour racheter la place, celle-ci apparaitra parmi +les « Places disponibles immédiatement à la revente » sur GestioCOF. +{% endwith %} + +Bonne revente ! +Le Bureau des Arts diff --git a/bda/templates/mail-revente-seller.txt b/bda/templates/mail-revente-seller.txt new file mode 100644 index 00000000..ec99b98b --- /dev/null +++ b/bda/templates/mail-revente-seller.txt @@ -0,0 +1,7 @@ +Bonjour {{ vendeur.first_name }}, + +La personne tirée au sort pour racheter ta place pour {{ spectacle.title }} est {{ acheteur.get_full_name }}. +Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail. + +Chaleureusement, +Le BdA diff --git a/bda/templates/mail-revente-winner.txt b/bda/templates/mail-revente-winner.txt new file mode 100644 index 00000000..8ca7236a --- /dev/null +++ b/bda/templates/mail-revente-winner.txt @@ -0,0 +1,7 @@ +Bonjour {{ acheteur.first_name }}, + +Tu as été tiré-e au sort pour racheter une place pour {{ spectacle.title }} le {{ spectacle.date_no_seconds }} ({{ spectacle.location }}) à {{ spectacle.price|floatformat:2 }}€. +Tu peux contacter le/la vendeur-se à l'adresse {{ vendeur.email }}, ou en répondant à ce mail. + +Chaleureusement, +Le BdA diff --git a/bda/views.py b/bda/views.py index 9b667cc2..43ee816c 100644 --- a/bda/views.py +++ b/bda/views.py @@ -8,15 +8,17 @@ import random from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required -from django.db import models +from django.db import models, transaction from django.db.models import Count, Q -from django.core import serializers +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 @@ -292,15 +294,30 @@ def revente(request, tirage_id): resellform = ResellForm(participant, request.POST, prefix='resell') annulform = AnnulForm(participant, prefix='annul') if resellform.is_valid(): + mails = [] attributions = resellform.cleaned_data["attributions"] - 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() - revente.save() + 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('mail-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') @@ -334,6 +351,8 @@ def revente(request, tirage_id): 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() diff --git a/cof/settings_dev.py b/cof/settings_dev.py index b2bda35d..6747963b 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -29,9 +29,6 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['127.0.0.1'] - - # Application definition INSTALLED_APPS = ( 'gestioncof', @@ -126,7 +123,7 @@ STATIC_URL = '/static/' STATIC_ROOT = '/var/www/static/' STATICFILES_DIRS = ( - os.path.join(BASE_DIR, 'static/'), + os.path.join(BASE_DIR, 'static/'), ) # Media upload (through ImageField, SiteField) diff --git a/gestioncof/shared.py b/gestioncof/shared.py index 87021a00..8fe17d43 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -29,6 +29,12 @@ class COFCASBackend(CASBackend): request.session['attributes'] = attributes if not username: return None + + # Le CAS de l'ENS accepte les logins avec des espaces au début + # et à la fin, ainsi qu’avec une casse variable. On normalise pour + # éviter les doublons. + username = username.strip().lower() + profiles = CofProfile.objects.filter(login_clipper=username) if len(profiles) > 0: profile = profiles.order_by('-is_cof')[0] diff --git a/gestioncof/views.py b/gestioncof/views.py index 80f528f7..4f1e5009 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -14,6 +14,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden from django.contrib.auth.decorators import login_required from django.contrib.auth.views import login as django_login_view from django.contrib.auth.models import User +from django.contrib.sites.models import Site from django.utils import timezone import django.utils.six as six @@ -710,12 +711,15 @@ def calendar_ics(request, token): tirage__active=True) shows = shows.distinct() vcal = Calendar() + site = Site.objects.get_current() for show in shows: vevent = Vevent() vevent.add('dtstart', show.date) vevent.add('dtend', show.date + timedelta(seconds=7200)) vevent.add('summary', show.title) vevent.add('location', show.location.name) + vevent.add('uid', 'show-{:d}-{:d}@{:s}'.format( + show.pk, show.tirage_id, site.domain)) vcal.add_component(vevent) if subscription.subscribe_to_events: for event in Event.objects.filter(old=False).all(): @@ -725,6 +729,8 @@ def calendar_ics(request, token): vevent.add('summary', event.title) vevent.add('location', event.location) vevent.add('description', event.description) + vevent.add('uid', 'event-{:d}@{:s}'.format( + event.pk, site.domain)) vcal.add_component(vevent) response = HttpResponse(content=vcal.to_ical()) response['Content-Type'] = "text/calendar" diff --git a/requirements.txt b/requirements.txt index 2d1109eb..23f4d618 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ configparser==3.5.0 -Django==1.8 +Django==1.8.* django-autocomplete-light==2.3.3 django-autoslug==1.9.3 -git+https://github.com/xapantu/django-cas-ng.git#egg=django-cas-ng +django-cas-ng==3.5.5 django-grappelli==2.8.1 django-recaptcha==1.0.5 mysqlclient==1.3.7 @@ -15,7 +15,7 @@ django-bootstrap-form==3.2.1 asgiref==0.14.0 daphne==0.14.3 asgi-redis==0.14.0 --e git+https://github.com/Aureplop/channels.git#egg=channel +git+https://github.com/Aureplop/channels.git#egg=channel statistics==1.0.3.5 future==0.15.2 django-widget-tweaks==1.4.1