From 56a1f8e634fbf29b80863e9e3fa559c6ab618048 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 27 Oct 2016 23:46:57 -0200 Subject: [PATCH 01/11] add transfer & reinit functions --- bda/admin.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bda/admin.py b/bda/admin.py index e07a5d49..d0483281 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -13,6 +13,7 @@ from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ from django import forms from datetime import timedelta +from django.utils import timezone import autocomplete_light @@ -224,6 +225,28 @@ class SpectacleReventeAdmin(admin.ModelAdmin): 'seller__user__first_name', 'seller__user__last_name'] + actions = ['transfer', 'reinit', ] + actions_on_bottom = True + + def transfer(self, request, queryset): + for revente in queryset.all(): + if revente.soldTo: + attrib = revente.attribution + attrib.participant = revente.soldTo + attrib.save() + transfer.short_description = "Transférer les reventes sélectionnées" + + def reinit(self, request, queryset): + 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() + reinit.short_description = "Réinitialiser les reventes sélectionnées" + admin.site.register(CategorieSpectacle) admin.site.register(Spectacle, SpectacleAdmin) From e408437ab158d6ae74654fa98be1f9303d07a350 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 27 Oct 2016 23:47:11 -0200 Subject: [PATCH 02/11] fix reinit --- bda/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bda/views.py b/bda/views.py index b9ce0825..756525f2 100644 --- a/bda/views.py +++ b/bda/views.py @@ -333,6 +333,8 @@ def revente(request, tirage_id): revente = rev.get() 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() From 8d1f599577d9044229f16cc3225ffdcd2dc252b9 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 28 Oct 2016 14:15:37 -0200 Subject: [PATCH 03/11] filter sold attributions --- bda/admin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bda/admin.py b/bda/admin.py index d0483281..668cc6dd 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -229,11 +229,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin): actions_on_bottom = True def transfer(self, request, queryset): - for revente in queryset.all(): - if revente.soldTo: - attrib = revente.attribution - attrib.participant = revente.soldTo - attrib.save() + for revente in queryset.exclude(soldTo__isnull=True).all(): + attrib = revente.attribution + attrib.participant = revente.soldTo + attrib.save() transfer.short_description = "Transférer les reventes sélectionnées" def reinit(self, request, queryset): From 249edb8d6826c98c29ec15b88c839405b7dbfdc1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 28 Oct 2016 17:52:16 -0200 Subject: [PATCH 04/11] =?UTF-8?q?guillemets=20=C3=A0=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bda/templates/bda-revente.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 %} From 33545b028e7f10f9e1b1481e5a2cdde970cc2537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 4 Nov 2016 08:35:17 +0100 Subject: [PATCH 05/11] Messages et docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout d'un message à l'aide de `message_user` lorsque les actions sont effectuées - Ajouts de docstrings là où c'est préconisé par pylint. --- bda/admin.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/bda/admin.py b/bda/admin.py index 668cc6dd..87a269d4 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -5,15 +5,15 @@ 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 -from django.utils import timezone import autocomplete_light @@ -212,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") @@ -225,17 +231,32 @@ class SpectacleReventeAdmin(admin.ModelAdmin): 'seller__user__first_name', 'seller__user__last_name'] - actions = ['transfer', 'reinit', ] + actions = ['transfer', 'reinit'] actions_on_bottom = True def transfer(self, request, queryset): - for revente in queryset.exclude(soldTo__isnull=True).all(): + """ + 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 @@ -244,6 +265,12 @@ class SpectacleReventeAdmin(admin.ModelAdmin): 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" From 6642f03720e1b195809e366bec23d3c8a30f1bed Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sat, 5 Nov 2016 18:04:54 +0100 Subject: [PATCH 06/11] Ajoute un UID aux VEVENTs du calendrier dynamique Fixes #102. --- gestioncof/views.py | 6 ++++++ 1 file changed, 6 insertions(+) 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" From 34032c9a00d17d42e234e71f341ac20bd7de3631 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sat, 5 Nov 2016 18:25:35 +0100 Subject: [PATCH 07/11] Utilise une version officielle de django-cas-ng MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le fix de Xapantu pour la double authentification a été intégré à la version 3.5.5 de django-cas-ng. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2d1109eb..aed65e6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ configparser==3.5.0 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 From 9846ed201b25659bac8508617bb1fd6f73572318 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sat, 5 Nov 2016 18:31:40 +0100 Subject: [PATCH 08/11] Normalise les logins clipper venant du CAS Le CAS de l'ENS autorise les logins avec des espaces/une casse variable, par exemple il considere equivalents les logins ' bCLeMeNt ' et 'bclement'. Ceci peut etre la cause de creation de doublons sur gestioCOF en cas de faute de frappe (ou utilisateur malicieux ;-) ) Ce patch normalise les logins a la sortie du CAS (strip + lowercase) pour eviter des desagrements. --- gestioncof/shared.py | 6 ++++++ 1 file changed, 6 insertions(+) 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] From ba2d90d906e7eb510764b4bac8d61f3d250f196b Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sat, 5 Nov 2016 20:04:25 +0100 Subject: [PATCH 09/11] [bda-revente] Envoie un mails aux perdants d'un tirage de revente --- bda/models.py | 64 +++++++++++++++++---------- bda/templates/mail-revente-loser.txt | 9 ++++ bda/templates/mail-revente-seller.txt | 7 +++ bda/templates/mail-revente-winner.txt | 7 +++ requirements.txt | 2 +- 5 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 bda/templates/mail-revente-loser.txt create mode 100644 bda/templates/mail-revente-seller.txt create mode 100644 bda/templates/mail-revente-winner.txt 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/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-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/requirements.txt b/requirements.txt index 2d1109eb..5fffab61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ 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 From b40fc6204f93152d4d195754e77d752fc23299ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 5 Nov 2016 21:59:49 +0100 Subject: [PATCH 10/11] Changements mineurs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Suppression de la variable `ALLOWED_HOSTS` de `cof/settings.dev` de sorte quand django utilise le default (qui est adapté à notre usage) - Correction d'indentation - Suppression d'un "-e" dans le fichier `requirements.txt` --- cof/settings_dev.py | 5 +---- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) 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/requirements.txt b/requirements.txt index 79a1d71c..23f4d618 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 981ff48e3d8e232816ac743d357704b71fd32605 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sat, 5 Nov 2016 22:35:46 +0100 Subject: [PATCH 11/11] Envoie un mail de confirmation lors d'une mise en vente Ce mail contient la date du tirage. --- bda/templates/mail-revente-new.txt | 13 +++++++++++ bda/views.py | 37 ++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 bda/templates/mail-revente-new.txt 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/views.py b/bda/views.py index 756525f2..8beeb84b 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')