diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19bcc736..e0ced08d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,5 @@ +image: "python:3.5" + services: - postgres:latest - redis:latest @@ -34,6 +36,7 @@ before_script: # Remove the old test database if it has not been done yet - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt + - python --version test: stage: test diff --git a/README.md b/README.md index 01f4ead2..a0dc5bc1 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,75 @@ # GestioCOF +![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg) + ## Installation +Il est possible d'installer GestioCOF sur votre machine de deux façons différentes : + +- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère +- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde + +### Installation manuelle + +Il est fortement conseillé d'utiliser un environnement virtuel pour Python. + +Il vous faudra installer pip, les librairies de développement de python ainsi +que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous +Debian et dérivées (Ubuntu, ...) : + + sudo apt-get install python3-pip python3-dev python3-venv sqlite3 + +Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; +fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF +(le dossier où se trouve ce README), et créez-le maintenant : + + python3 -m venv venv + +Pour l'activer, il faut taper + + . venv/bin/activate + +depuis le même dossier. + +Vous pouvez maintenant installer les dépendances Python depuis le fichier +`requirements-devel.txt` : + + pip install -U pip # parfois nécessaire la première fois + pip install -r requirements-devel.txt + +Pour terminer, copier le fichier `cof/settings/secret_example.py` vers +`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique +pour profiter de façon transparente des mises à jour du fichier: + + ln -s secret_example.py cof/settings/secret.py + + +#### Fin d'installation + +Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base +de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des +données bidons bien pratiques pour développer avec la commande suivante : + + bash provisioning/prepare_django.sh + +Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas +pour plus de détails. + +Vous êtes prêts à développer ! Lancer GestioCOF en faisant + + python manage.py runserver + + ### Vagrant -La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser +Une autre façon d'installer GestioCOF sur votre machine est d'utiliser [Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout le monde à la même configuration de développement (même sous Windows !), et l'installation se fait en une commande. Pour utiliser Vagrant, il faut le -[télécharger](https://www.vagrantup.com/downloads.html) et l'installer. +[télécharger](https://www.vagrantup.com/downloads.html) et l'installer. Si vous êtes sous Linux, votre distribution propose probablement des paquets Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui @@ -81,55 +139,6 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url code change, il faut relancer le worker avec `sudo systemctl restart worker.service` pour visualiser la dernière version du code. - -### Installation manuelle - -Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant, -il est fortement conseillé d'utiliser un environnement virtuel pour Python. - -Il vous faudra installer pip, les librairies de développement de python ainsi -que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous -Debian et dérivées (Ubuntu, ...) : - - sudo apt-get install python3-pip python3-dev sqlite3 - -Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; -fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF -(le dossier où se trouve ce README), et créez-le maintenant : - - python3 -m venv venv - -Pour l'activer, il faut faire - - . venv/bin/activate - -dans le même dossier. - -Vous pouvez maintenant installer les dépendances Python depuis le fichier -`requirements-devel.txt` : - - pip install -U pip - pip install -r requirements-devel.txt - -Pour terminer, copier le fichier `cof/settings/secret_example.py` vers -`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique -pour profiter de façon transparente des mises à jour du fichier: - - ln -s secret_example.py cof/settings/secret.py - - -#### Fin d'installation - -Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base -de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des -données bidons bien pratiques pour développer avec la commande suivante : - - bash provisioning/prepare_django.sh - -Vous êtes prêts à développer ! Lancer GestioCOF en faisant - - python manage.py runserver - ### Mise à jour Pour mettre à jour les paquets Python, utiliser la commande suivante : @@ -141,6 +150,32 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire : python manage.py migrate +## Outils pour développer + +### Base de donnée + +Quelle que soit la méthode d'installation choisie, la base de donnée locale est + peuplée avec des données artificielles pour faciliter le développement. + +- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez + vous sur ce compte pour accéder à tout GestioCOF. +- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques + spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA. +- À chaque compte est associé un trigramme K-Fêt +- Un certain nombre d'articles K-Fêt sont renseignés. + +### Tests unitaires + +On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab +à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante. + +Pour lancer les tests : + +``` +python manage.py test +``` + + ## Documentation utilisateur Une brève documentation utilisateur est accessible sur le diff --git a/TODO_PROD.md b/TODO_PROD.md new file mode 100644 index 00000000..1a7d0736 --- /dev/null +++ b/TODO_PROD.md @@ -0,0 +1 @@ +- Changer les urls dans les mails "bda-revente" et "bda-shotgun" diff --git a/bda/admin.py b/bda/admin.py index 6638ad45..485471da 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail @@ -166,7 +164,7 @@ class AttributionAdminForm(forms.ModelForm): ) def clean(self): - cleaned_data = super(AttributionAdminForm, self).clean() + cleaned_data = super().clean() participant = cleaned_data.get("participant") spectacle = cleaned_data.get("spectacle") if participant and spectacle: @@ -236,7 +234,7 @@ class SpectacleReventeAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['answered_mail'].queryset = ( + self.fields['confirmed_entry'].queryset = ( Participant.objects .select_related('user', 'tirage') ) @@ -299,13 +297,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin): count = queryset.count() for revente in queryset.filter( attribution__spectacle__date__gte=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() + revente.reset(new_date=timezone.now() - timedelta(hours=1)) self.message_user( request, "%d attribution%s %s été réinitialisée%s avec succès." % ( diff --git a/bda/algorithm.py b/bda/algorithm.py index 7f18ce18..f0f48ad9 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,9 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django.db.models import Max import random diff --git a/bda/forms.py b/bda/forms.py index c0417d1e..7e81587a 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- - from django import forms from django.forms.models import BaseInlineFormSet from django.utils import timezone -from bda.models import Attribution, Spectacle +from bda.models import Attribution, Spectacle, SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): @@ -43,7 +41,33 @@ class TokenForm(forms.Form): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.spectacle) + return str(obj.spectacle) + + +class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): + def __init__(self, *args, own=True, **kwargs): + super().__init__(*args, **kwargs) + self.own = own + + def label_from_instance(self, obj): + label = "{show}{suffix}" + suffix = "" + if self.own: + # C'est notre propre revente : pas besoin de spécifier le vendeur + if obj.soldTo is not None: + suffix = " -- Vendue à {firstname} {lastname}".format( + firstname=obj.soldTo.user.first_name, + lastname=obj.soldTo.user.last_name, + ) + else: + # Ce n'est pas à nous : on ne voit jamais l'acheteur + suffix = " -- Vendue par {firstname} {lastname}".format( + firstname=obj.seller.user.first_name, + lastname=obj.seller.user.last_name, + ) + + return label.format(show=str(obj.attribution.spectacle), + suffix=suffix) class ResellForm(forms.Form): @@ -54,7 +78,7 @@ class ResellForm(forms.Form): required=False) def __init__(self, participant, *args, **kwargs): - super(ResellForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['attributions'].queryset = ( participant.attribution_set .filter(spectacle__date__gte=timezone.now()) @@ -65,22 +89,22 @@ class ResellForm(forms.Form): class AnnulForm(forms.Form): - attributions = AttributionModelMultipleChoiceField( + reventes = ReventeModelMultipleChoiceField( + own=True, label='', queryset=Attribution.objects.none(), widget=forms.CheckboxSelectMultiple, required=False) def __init__(self, participant, *args, **kwargs): - super(AnnulForm, self).__init__(*args, **kwargs) - self.fields['attributions'].queryset = ( - participant.attribution_set - .filter(spectacle__date__gte=timezone.now(), - revente__isnull=False, - revente__notif_sent=False, - revente__soldTo__isnull=True) - .select_related('spectacle', 'spectacle__location', - 'participant__user') + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + participant.original_shows + .filter(attribution__spectacle__date__gte=timezone.now(), + notif_sent=False, + soldTo__isnull=True) + .select_related('attribution__spectacle', + 'attribution__spectacle__location') ) @@ -91,7 +115,7 @@ class InscriptionReventeForm(forms.Form): required=False) def __init__(self, tirage, *args, **kwargs): - super(InscriptionReventeForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['spectacles'].queryset = ( tirage.spectacle_set .select_related('location') @@ -99,19 +123,58 @@ class InscriptionReventeForm(forms.Form): ) +class ReventeTirageAnnulForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + own=False, + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + participant.entered.filter(soldTo__isnull=True) + .select_related('attribution__spectacle', + 'seller__user') + ) + + +class ReventeTirageForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + own=False, + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + SpectacleRevente.objects.filter( + notif_sent=True, + shotgun=False, + tirage_done=False + ).exclude(confirmed_entry=participant) + .select_related('attribution__spectacle') + ) + + class SoldForm(forms.Form): - attributions = AttributionModelMultipleChoiceField( + reventes = ReventeModelMultipleChoiceField( + own=True, label='', queryset=Attribution.objects.none(), widget=forms.CheckboxSelectMultiple) def __init__(self, participant, *args, **kwargs): - super(SoldForm, self).__init__(*args, **kwargs) - self.fields['attributions'].queryset = ( - participant.attribution_set - .filter(revente__isnull=False, - revente__soldTo__isnull=False) - .exclude(revente__soldTo=participant) - .select_related('spectacle', 'spectacle__location', - 'participant__user') + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + participant.original_shows + .filter(soldTo__isnull=False) + .exclude(soldTo=participant) + .select_related('attribution__spectacle', + 'attribution__spectacle__location') ) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 0302ec4b..5a06d40b 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -1,12 +1,7 @@ -# -*- coding: utf-8 -*- - """ Gestion en ligne de commande des reventes. """ -from __future__ import unicode_literals - -from datetime import timedelta from django.core.management import BaseCommand from django.utils import timezone from bda.models import SpectacleRevente @@ -21,23 +16,36 @@ class Command(BaseCommand): now = timezone.now() reventes = SpectacleRevente.objects.all() for revente in reventes: - # Check si < 24h - if (revente.attribution.spectacle.date <= - revente.date + timedelta(days=1)) and \ - now >= revente.date + timedelta(minutes=15) and \ - not revente.notif_sent: - self.stdout.write(str(now)) - revente.mail_shotgun() - self.stdout.write("Mail de disponibilité immédiate envoyé") - # Check si délai de retrait dépassé - elif (now >= revente.date + timedelta(hours=1) and - not revente.notif_sent): + # Le spectacle est bientôt et on a pas encore envoyé de mail : + # on met la place au shotgun et on prévient. + if revente.is_urgent and not revente.notif_sent: + if revente.can_notif: + self.stdout.write(str(now)) + revente.mail_shotgun() + self.stdout.write( + "Mails de disponibilité immédiate envoyés " + "pour la revente [%s]" % revente + ) + + # Le spectacle est dans plus longtemps : on prévient + elif (revente.can_notif and not revente.notif_sent): self.stdout.write(str(now)) revente.send_notif() - self.stdout.write("Mail d'inscription à une revente envoyé") - # Check si tirage à faire - elif (now >= revente.date_tirage and - not revente.tirage_done): + self.stdout.write( + "Mails d'inscription à la revente [%s] envoyés" + % revente + ) + + # On fait le tirage + elif (now >= revente.date_tirage and not revente.tirage_done): self.stdout.write(str(now)) - revente.tirage() - self.stdout.write("Tirage effectué, mails envoyés") + winner = revente.tirage() + self.stdout.write( + "Tirage effectué pour la revente [%s]" + % revente + ) + + if winner: + self.stdout.write("Gagnant : %s" % winner.user) + else: + self.stdout.write("Pas de gagnant ; place au shotgun") diff --git a/bda/management/commands/sendrappels.py b/bda/management/commands/sendrappels.py index 88cf9d5c..82889f80 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -1,11 +1,7 @@ -# -*- coding: utf-8 -*- - """ Gestion en ligne de commande des mails de rappel. """ -from __future__ import unicode_literals - from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py new file mode 100644 index 00000000..ee777e35 --- /dev/null +++ b/bda/migrations/0012_notif_time.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0011_tirage_appear_catalogue'), + ] + + operations = [ + migrations.RenameField( + model_name='spectaclerevente', + old_name='answered_mail', + new_name='confirmed_entry', + ), + migrations.AlterField( + model_name='spectaclerevente', + name='confirmed_entry', + field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), + ), + migrations.AddField( + model_name='spectaclerevente', + name='notif_time', + field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True), + ), + ] diff --git a/bda/migrations/0012_swap_double_choice.py b/bda/migrations/0012_swap_double_choice.py new file mode 100644 index 00000000..56f3e739 --- /dev/null +++ b/bda/migrations/0012_swap_double_choice.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def swap_double_choice(apps, schema_editor): + choices = apps.get_model("bda", "ChoixSpectacle").objects + + choices.filter(double_choice="double").update(double_choice="tmp") + choices.filter(double_choice="autoquit").update(double_choice="double") + choices.filter(double_choice="tmp").update(double_choice="autoquit") + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0011_tirage_appear_catalogue'), + ] + + operations = [ + # Temporarily allow an extra "tmp" value for the `double_choice` field + migrations.AlterField( + model_name='choixspectacle', + name='double_choice', + field=models.CharField( + verbose_name='Nombre de places', + max_length=10, + default='1', + choices=[ + ('tmp', 'tmp'), + ('1', '1 place'), + ('double', '2 places si possible, 1 sinon'), + ('autoquit', '2 places sinon rien') + ] + ), + ), + migrations.RunPython(swap_double_choice, migrations.RunPython.noop), + migrations.AlterField( + model_name='choixspectacle', + name='double_choice', + field=models.CharField( + verbose_name='Nombre de places', + max_length=10, + default='1', + choices=[ + ('1', '1 place'), + ('double', '2 places si possible, 1 sinon'), + ('autoquit', '2 places sinon rien') + ] + ), + ), + ] diff --git a/bda/migrations/0013_merge_20180524_2123.py b/bda/migrations/0013_merge_20180524_2123.py new file mode 100644 index 00000000..ae8b0630 --- /dev/null +++ b/bda/migrations/0013_merge_20180524_2123.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-05-24 19:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0012_notif_time'), + ('bda', '0012_swap_double_choice'), + ] + + operations = [ + ] diff --git a/bda/models.py b/bda/models.py index 73356038..63e01e31 100644 --- a/bda/models.py +++ b/bda/models.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import calendar import random from datetime import timedelta @@ -174,10 +172,11 @@ class Participant(models.Model): def __str__(self): return "%s - %s" % (self.user, self.tirage.title) + DOUBLE_CHOICES = ( ("1", "1 place"), - ("autoquit", "2 places si possible, 1 sinon"), - ("double", "2 places sinon rien"), + ("double", "2 places si possible, 1 sinon"), + ("autoquit", "2 places sinon rien"), ) @@ -232,9 +231,9 @@ class SpectacleRevente(models.Model): ) date = models.DateTimeField("Date de mise en vente", default=timezone.now) - answered_mail = models.ManyToManyField(Participant, - related_name="wanted", - blank=True) + confirmed_entry = models.ManyToManyField(Participant, + related_name="entered", + blank=True) seller = models.ForeignKey( Participant, on_delete=models.CASCADE, verbose_name="Vendeur", @@ -248,21 +247,61 @@ class SpectacleRevente(models.Model): notif_sent = models.BooleanField("Notification envoyée", default=False) + + notif_time = models.DateTimeField("Moment d'envoi de la notification", + blank=True, null=True) + tirage_done = models.BooleanField("Tirage effectué", default=False) + shotgun = models.BooleanField("Disponible immédiatement", default=False) + #### + # Some class attributes + ### + # TODO : settings ? + + # Temps minimum entre le tirage et le spectacle + min_margin = timedelta(days=5) + + # Temps entre la création d'une revente et l'envoi du mail + remorse_time = timedelta(hours=1) + + # Temps min/max d'attente avant le tirage + max_wait_time = timedelta(days=3) + min_wait_time = timedelta(days=1) + + @property + def real_notif_time(self): + if self.notif_time: + return self.notif_time + else: + return self.date + self.remorse_time @property def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - # L'acheteur doit être connu au plus 12h avant le spectacle + remaining_time = (self.attribution.spectacle.date - - self.date - timedelta(hours=13)) - # Au minimum, on attend 2 jours avant le tirage - delay = min(remaining_time, timedelta(days=2)) - # Le vendeur a aussi 1h pour changer d'avis - return self.date + delay + timedelta(hours=1) + - self.real_notif_time - self.min_margin) + + delay = min(remaining_time, self.max_wait_time) + + return self.real_notif_time + delay + + @property + def is_urgent(self): + """ + Renvoie True iff la revente doit être mise au shotgun directement. + Plus précisément, on doit avoir min_margin + min_wait_time de marge. + """ + spectacle_date = self.attribution.spectacle.date + return (spectacle_date <= timezone.now() + self.min_margin + + self.min_wait_time) + + @property + def can_notif(self): + return (timezone.now() >= self.date + self.remorse_time) def __str__(self): return "%s -- %s" % (self.seller, @@ -271,6 +310,18 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" + def reset(self, new_date=timezone.now()): + """Réinitialise la revente pour permettre une remise sur le marché""" + self.seller = self.attribution.participant + self.date = new_date + self.confirmed_entry.clear() + self.soldTo = None + self.notif_sent = False + self.notif_time = None + self.tirage_done = False + self.shotgun = False + self.save() + def send_notif(self): """ Envoie une notification pour indiquer la mise en vente d'une place sur @@ -291,6 +342,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() self.save() def mail_shotgun(self): @@ -312,76 +364,79 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() # Flag inutile, sauf si l'horloge interne merde self.tirage_done = True self.shotgun = True self.save() - def tirage(self): + def tirage(self, send_mails=True): """ Lance le tirage au sort associé à la revente. Un gagnant est choisi parmis les personnes intéressées par le spectacle. Les personnes sont ensuites prévenues par mail du résultat du tirage. """ - inscrits = list(self.answered_mail.all()) + inscrits = list(self.confirmed_entry.all()) spectacle = self.attribution.spectacle seller = self.seller + winner = None if inscrits: # Envoie un mail au gagnant et au vendeur winner = random.choice(inscrits) self.soldTo = winner + if send_mails: + mails = [] - mails = [] + context = { + 'acheteur': winner.user, + 'vendeur': seller.user, + 'show': spectacle, + } - context = { - 'acheteur': winner.user, - 'vendeur': seller.user, - 'show': spectacle, - } + c_mails_qs = CustomMail.objects.filter(shortname__in=[ + 'bda-revente-winner', 'bda-revente-loser', + 'bda-revente-seller', + ]) - c_mails_qs = CustomMail.objects.filter(shortname__in=[ - 'bda-revente-winner', 'bda-revente-loser', - 'bda-revente-seller', - ]) + c_mails = {cm.shortname: cm for cm in c_mails_qs} - c_mails = {cm.shortname: cm for cm in c_mails_qs} - - mails.append( - c_mails['bda-revente-winner'].get_message( - context, - from_email=settings.MAIL_DATA['revente']['FROM'], - to=[winner.user.email], - ) - ) - - mails.append( - c_mails['bda-revente-seller'].get_message( - context, - from_email=settings.MAIL_DATA['revente']['FROM'], - to=[seller.user.email], - reply_to=[winner.user.email], - ) - ) - - # Envoie un mail aux perdants - for inscrit in inscrits: - if inscrit != winner: - new_context = dict(context) - new_context['acheteur'] = inscrit.user - - mails.append( - c_mails['bda-revente-loser'].get_message( - new_context, - from_email=settings.MAIL_DATA['revente']['FROM'], - to=[inscrit.user.email], - ) + mails.append( + c_mails['bda-revente-winner'].get_message( + context, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[winner.user.email], ) + ) - mail_conn = mail.get_connection() - mail_conn.send_messages(mails) + mails.append( + c_mails['bda-revente-seller'].get_message( + context, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[seller.user.email], + reply_to=[winner.user.email], + ) + ) + + # Envoie un mail aux perdants + for inscrit in inscrits: + if inscrit != winner: + new_context = dict(context) + new_context['acheteur'] = inscrit.user + + mails.append( + c_mails['bda-revente-loser'].get_message( + new_context, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[inscrit.user.email], + ) + ) + + mail_conn = mail.get_connection() + mail_conn.send_messages(mails) # Si personne ne veut de la place, elle part au shotgun else: self.shotgun = True self.tirage_done = True self.save() + return winner diff --git a/bda/templates/bda/inscription-formset.html b/bda/templates/bda/inscription-formset.html index 65ef389b..88b65600 100644 --- a/bda/templates/bda/inscription-formset.html +++ b/bda/templates/bda/inscription-formset.html @@ -14,7 +14,7 @@ {% endif %} - + {% for field in form.visible_fields %} {% if field.name != "DELETE" and field.name != "priority" %} diff --git a/bda/templates/bda/inscription-tirage.html b/bda/templates/bda/inscription-tirage.html index d56b4229..3fd81378 100644 --- a/bda/templates/bda/inscription-tirage.html +++ b/bda/templates/bda/inscription-tirage.html @@ -27,6 +27,14 @@ var django = { var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-'); $(this).attr('for', newFor); }); + // Cloning - - - -{% endblock %} diff --git a/bda/templates/bda/resume_places.html b/bda/templates/bda/resume_places.html index 3785169b..7cbd06ea 100644 --- a/bda/templates/bda/resume_places.html +++ b/bda/templates/bda/resume_places.html @@ -16,7 +16,7 @@

Total à payer : {{ total|floatformat }}€


Ne manque pas un spectacle avec le - calendrier + calendrier automatique !

{% else %}

Vous n'avez aucune place :(

diff --git a/bda/templates/revente-confirm.html b/bda/templates/bda/revente/confirm-shotgun.html similarity index 100% rename from bda/templates/revente-confirm.html rename to bda/templates/bda/revente/confirm-shotgun.html diff --git a/bda/templates/bda-interested.html b/bda/templates/bda/revente/confirmed.html similarity index 100% rename from bda/templates/bda-interested.html rename to bda/templates/bda/revente/confirmed.html diff --git a/bda/templates/bda-success.html b/bda/templates/bda/revente/mail-success.html similarity index 100% rename from bda/templates/bda-success.html rename to bda/templates/bda/revente/mail-success.html diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html new file mode 100644 index 00000000..cf0ba80e --- /dev/null +++ b/bda/templates/bda/revente/manage.html @@ -0,0 +1,90 @@ +{% extends "base_title.html" %} +{% load bootstrap %} + +{% block realcontent %} + +

Gestion des places que je revends

+{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %} + +{% if resellform.attributions %} +
+ +

Places non revendues

+
+
+ + Cochez les places que vous souhaitez revendre, et validez. Vous aurez + ensuite 1h pour changer d'avis avant que la revente soit confirmée et + que les notifications soient envoyées aux intéressé·e·s. +
+
+ {% csrf_token %} + {{ resellform|bootstrap }} +
+
+ +
+
+ +
+{% endif %} + +{% if annul_reventes or overdue %} +

Places en cours de revente

+
+ {% if annul_reventes %} +
+ + Vous pouvez annuler les places mises en vente il y a moins d'une heure. +
+ {% endif %} + {% csrf_token %} +
+
+
    + {% for revente in annul_reventes %} +
  • {{ revente.tag }} {{ revente.choice_label }}
  • + {% endfor %} + {% for attrib in overdue %} +
  • + + {{ attrib.spectacle }} +
  • + {% endfor %} +
+
+
+ {% if annul_reventes %} + + {% endif %} +
+ +
+{% endif %} + +{% if sold_reventes %} +

Places revendues

+
+
+ + Pour chaque revente, vous devez soit l'annuler soit la confirmer pour + transférer la place la place à la personne tirée au sort. + + L'annulation sert par exemple à pouvoir remettre la place en jeu si + vous ne parvenez pas à entrer en contact avec la personne tirée au + sort. +
+
+ {% csrf_token %} + {{ soldform|bootstrap }} +
+ + +
+{% endif %} +{% if not resell_attributions and not annul_attributions and not overdue and not sold_reventes %} +

Plus de reventes possibles !

+{% endif %} + +{% endwith %} +{% endblock %} diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda/revente/none.html similarity index 100% rename from bda/templates/bda-no-revente.html rename to bda/templates/bda/revente/none.html diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda/revente/notpaid.html similarity index 100% rename from bda/templates/bda-notpaid.html rename to bda/templates/bda/revente/notpaid.html diff --git a/bda/templates/bda-shotgun.html b/bda/templates/bda/revente/shotgun.html similarity index 83% rename from bda/templates/bda-shotgun.html rename to bda/templates/bda/revente/shotgun.html index e10fae00..fae36c04 100644 --- a/bda/templates/bda-shotgun.html +++ b/bda/templates/bda/revente/shotgun.html @@ -5,7 +5,7 @@ {% if shotgun %}