diff --git a/.gitignore b/.gitignore index 23b6a1b1..347d4b78 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,11 @@ media/ *.log .sass-cache/ *.sqlite3 +.coverage # PyCharm .idea .cache + +# VSCode +.vscode/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19bcc736..35637457 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,4 @@ -services: - - postgres:latest - - redis:latest +image: "python:3.5" variables: # GestioCOF settings @@ -10,7 +8,7 @@ variables: REDIS_PASSWD: "dummy" # Cached packages - PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" + PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip" # postgres service configuration POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" @@ -20,22 +18,44 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD -cache: - paths: - - vendor/python - - vendor/pip - - vendor/apt - -before_script: - - mkdir -p vendor/{python,pip,apt} - - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client - - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py - - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py - # 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 - test: stage: test + before_script: + - mkdir -p vendor/{pip,apt} + - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client + - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py + - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py + # 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 -r requirements.txt coverage + - python --version script: - - python manage.py test + - coverage run manage.py test + after_script: + - coverage report + services: + - postgres:9.6 + - redis:latest + cache: + key: test + paths: + - vendor/ + # For GitLab CI to get coverage from build. + # Keep this disabled for now, as it may kill GitLab... + # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' + +linters: + image: python:3.6 + stage: test + before_script: + - mkdir -p vendor/pip + - pip install --upgrade black isort flake8 + script: + - black --check . + - isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils + # Print errors only + - flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils + cache: + key: linters + paths: + - vendor/ diff --git a/.pre-commit.sh b/.pre-commit.sh new file mode 100755 index 00000000..0e0e3c1a --- /dev/null +++ b/.pre-commit.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# pre-commit hook for gestioCOF project. +# +# Run formatters first, then checkers. +# Formatters which changed a file must set the flag 'formatter_updated'. + +exit_code=0 +formatter_updated=0 +checker_dirty=0 + +# TODO(AD): We should check only staged changes. +# Working? -> Stash unstaged changes, run it, pop stash +STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$") + + +# Formatter: black + +printf "> black ... " + +if type black &>/dev/null; then + if [ -z "$STAGED_PYTHON_FILES" ]; then + printf "OK\n" + else + BLACK_OUTPUT="/tmp/gc-black-output.log" + touch $BLACK_OUTPUT + + if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black --check &>$BLACK_OUTPUT; then + echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black &>$BLACK_OUTPUT + tail -1 $BLACK_OUTPUT + formatter_updated=1 + else + printf "OK\n" + fi + fi +else + printf "SKIP: program not found\n" + printf "HINT: Install black with 'pip3 install black' (black requires Python>=3.6)\n" +fi + +# Formatter: isort + +printf "> isort ... " + +if type isort &>/dev/null; then + if [ -z "$STAGED_PYTHON_FILES" ]; then + printf "OK\n" + else + ISORT_OUTPUT="/tmp/gc-isort-output.log" + touch $ISORT_OUTPUT + + if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check-only &>$ISORT_OUTPUT; then + echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT + printf "Reformatted.\n" + formatter_updated=1 + else + printf "OK\n" + fi + fi +else + printf "SKIP: program not found\n" + printf "HINT: Install isort with 'pip install isort'\n" +fi + +# Checker: flake8 + +printf "> flake8 ... " + +if type flake8 &>/dev/null; then + if [ -z "$STAGED_PYTHON_FILES" ]; then + printf "OK\n" + else + FLAKE8_OUTPUT="/tmp/gc-flake8-output.log" + touch $FLAKE8_OUTPUT + + if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then + printf "FAIL\n" + cat $FLAKE8_OUTPUT + checker_dirty=1 + else + printf "OK\n" + fi + fi +else + printf "SKIP: program not found\n" + printf "HINT: Install flake8 with 'pip install flake8'\n" +fi + +# End + +if [ $checker_dirty -ne 0 ] +then + printf ">>> Checker(s) detect(s) issue(s)\n" + printf " You can still commit and push :)\n" + printf " Be warned that our CI may cause you more trouble.\n" +fi + +if [ $formatter_updated -ne 0 ] +then + printf ">>> Working tree updated by formatter(s)\n" + printf " Add changes to staging area and retry.\n" + exit_code=1 +fi + +printf "\n" + +exit $exit_code diff --git a/README.md b/README.md index 01f4ead2..2f08f3aa 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,86 @@ # GestioCOF +[![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) +[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) + ## 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 + +Nous avons un git hook de pre-commit pour formatter et vérifier que votre code +vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez +encore l'installation *via* un lien symbolique: + + ln -s ../../.pre-commit.sh .git/hooks/pre-commit + +Pour plus d'informations à ce sujet, consulter la +[page](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/coding-style) +du wiki gestioCOF liée aux conventions. + + +#### 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 +150,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 +161,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/__init__.py b/bda/__init__.py index 8b137891..e69de29b 100644 --- a/bda/__init__.py +++ b/bda/__init__.py @@ -1 +0,0 @@ - diff --git a/bda/admin.py b/bda/admin.py index 6638ad45..b32144f1 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,18 +1,24 @@ -# -*- coding: utf-8 -*- - from datetime import timedelta -from custommail.shortcuts import send_mass_custom_mail +from custommail.shortcuts import send_mass_custom_mail +from dal.autocomplete import ModelSelect2 +from django import forms from django.contrib import admin -from django.db.models import Sum, Count +from django.db.models import Count, Sum from django.template.defaultfilters import pluralize from django.utils import timezone -from django import forms -from dal.autocomplete import ModelSelect2 - -from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ - Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente +from bda.models import ( + Attribution, + CategorieSpectacle, + ChoixSpectacle, + Participant, + Quote, + Salle, + Spectacle, + SpectacleRevente, + Tirage, +) class ReadOnlyMixin(object): @@ -29,8 +35,8 @@ class ReadOnlyMixin(object): class ChoixSpectacleAdminForm(forms.ModelForm): class Meta: widgets = { - 'participant': ModelSelect2(url='bda-participant-autocomplete'), - 'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'), + "participant": ModelSelect2(url="bda-participant-autocomplete"), + "spectacle": ModelSelect2(url="bda-spectacle-autocomplete"), } @@ -45,10 +51,10 @@ class AttributionTabularAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - spectacles = Spectacle.objects.select_related('location') + spectacles = Spectacle.objects.select_related("location") if self.listing is not None: spectacles = spectacles.filter(listing=self.listing) - self.fields['spectacle'].queryset = spectacles + self.fields["spectacle"].queryset = spectacles class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm): @@ -72,7 +78,7 @@ class AttributionInline(admin.TabularInline): class WithListingAttributionInline(AttributionInline): - exclude = ('given', ) + exclude = ("given",) form = WithListingAttributionTabularAdminForm listing = True @@ -83,12 +89,10 @@ class WithoutListingAttributionInline(AttributionInline): class ParticipantAdminForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['choicesrevente'].queryset = ( - Spectacle.objects - .select_related('location') + self.fields["choicesrevente"].queryset = Spectacle.objects.select_related( + "location" ) @@ -96,11 +100,13 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): inlines = [WithListingAttributionInline, WithoutListingAttributionInline] def get_queryset(self, request): - return Participant.objects.annotate(nb_places=Count('attributions'), - total=Sum('attributions__price')) + return Participant.objects.annotate( + nb_places=Count("attributions"), total=Sum("attributions__price") + ) def nb_places(self, obj): return obj.nb_places + nb_places.admin_order_field = "nb_places" nb_places.short_description = "Nombre de places" @@ -110,33 +116,32 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): return "%.02f €" % tot else: return "0 €" + total.admin_order_field = "total" total.short_description = "Total à payer" - list_display = ("user", "nb_places", "total", "paid", "paymenttype", - "tirage") + list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage") list_filter = ("paid", "tirage") - search_fields = ('user__username', 'user__first_name', 'user__last_name') - actions = ['send_attribs', ] + search_fields = ("user__username", "user__first_name", "user__last_name") + actions = ["send_attribs"] actions_on_bottom = True list_per_page = 400 readonly_fields = ("total",) - readonly_fields_update = ('user', 'tirage') + readonly_fields_update = ("user", "tirage") form = ParticipantAdminForm def send_attribs(self, request, queryset): datatuple = [] for member in queryset.all(): attribs = member.attributions.all() - context = {'member': member.user} + context = {"member": member.user} shortname = "" if len(attribs) == 0: shortname = "bda-attributions-decus" else: shortname = "bda-attributions" - context['places'] = attribs + context["places"] = attribs print(context) - datatuple.append((shortname, context, "bda@ens.fr", - [member.user.email])) + datatuple.append((shortname, context, "bda@ens.fr", [member.user.email])) send_mass_custom_mail(datatuple) count = len(queryset.all()) if count == 1: @@ -145,49 +150,53 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): else: message_bit = "%d membres ont" % count plural = "s" - self.message_user(request, "%s été informé%s avec succès." - % (message_bit, plural)) + self.message_user( + request, "%s été informé%s avec succès." % (message_bit, plural) + ) + send_attribs.short_description = "Envoyer les résultats par mail" class AttributionAdminForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if 'spectacle' in self.fields: - self.fields['spectacle'].queryset = ( - Spectacle.objects - .select_related('location') + if "spectacle" in self.fields: + self.fields["spectacle"].queryset = Spectacle.objects.select_related( + "location" ) - if 'participant' in self.fields: - self.fields['participant'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + if "participant" in self.fields: + self.fields["participant"].queryset = Participant.objects.select_related( + "user", "tirage" ) 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: if participant.tirage != spectacle.tirage: raise forms.ValidationError( "Erreur : le participant et le spectacle n'appartiennent" - "pas au même tirage") + "pas au même tirage" + ) return cleaned_data class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): def paid(self, obj): return obj.participant.paid - paid.short_description = 'A payé' + + paid.short_description = "A payé" paid.boolean = True list_display = ("id", "spectacle", "participant", "given", "paid") - search_fields = ('spectacle__title', 'participant__user__username', - 'participant__user__first_name', - 'participant__user__last_name') + search_fields = ( + "spectacle__title", + "participant__user__username", + "participant__user__first_name", + "participant__user__last_name", + ) form = AttributionAdminForm - readonly_fields_update = ('spectacle', 'participant') + readonly_fields_update = ("spectacle", "participant") class ChoixSpectacleAdmin(admin.ModelAdmin): @@ -195,13 +204,15 @@ class ChoixSpectacleAdmin(admin.ModelAdmin): def tirage(self, obj): return obj.participant.tirage - list_display = ("participant", "tirage", "spectacle", "priority", - "double_choice") + + list_display = ("participant", "tirage", "spectacle", "priority", "double_choice") list_filter = ("double_choice", "participant__tirage") - search_fields = ('participant__user__username', - 'participant__user__first_name', - 'participant__user__last_name', - 'spectacle__title') + search_fields = ( + "participant__user__username", + "participant__user__first_name", + "participant__user__last_name", + "spectacle__title", + ) class QuoteInline(admin.TabularInline): @@ -211,42 +222,36 @@ class QuoteInline(admin.TabularInline): class SpectacleAdmin(admin.ModelAdmin): inlines = [QuoteInline] model = Spectacle - list_display = ("title", "date", "tirage", "location", "slots", "price", - "listing") - list_filter = ("location", "tirage",) + list_display = ("title", "date", "tirage", "location", "slots", "price", "listing") + list_filter = ("location", "tirage") search_fields = ("title", "location__name") - readonly_fields = ("rappel_sent", ) + readonly_fields = ("rappel_sent",) class TirageAdmin(admin.ModelAdmin): model = Tirage - list_display = ("title", "ouverture", "fermeture", "active", - "enable_do_tirage") - readonly_fields = ("tokens", ) - list_filter = ("active", ) - search_fields = ("title", ) + list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage") + readonly_fields = ("tokens",) + list_filter = ("active",) + search_fields = ("title",) class SalleAdmin(admin.ModelAdmin): model = Salle - search_fields = ('name', 'address') + search_fields = ("name", "address") class SpectacleReventeAdminForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['answered_mail'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + self.fields["confirmed_entry"].queryset = Participant.objects.select_related( + "user", "tirage" ) - self.fields['seller'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + self.fields["seller"].queryset = Participant.objects.select_related( + "user", "tirage" ) - self.fields['soldTo'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + self.fields["soldTo"].queryset = Participant.objects.select_related( + "user", "tirage" ) @@ -254,6 +259,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin): """ Administration des reventes de spectacles """ + model = SpectacleRevente def spectacle(self, obj): @@ -265,12 +271,14 @@ class SpectacleReventeAdmin(admin.ModelAdmin): list_display = ("spectacle", "seller", "date", "soldTo") raw_id_fields = ("attribution",) readonly_fields = ("date_tirage",) - search_fields = ['attribution__spectacle__title', - 'seller__user__username', - 'seller__user__first_name', - 'seller__user__last_name'] + search_fields = [ + "attribution__spectacle__title", + "seller__user__username", + "seller__user__first_name", + "seller__user__last_name", + ] - actions = ['transfer', 'reinit'] + actions = ["transfer", "reinit"] actions_on_bottom = True form = SpectacleReventeAdminForm @@ -286,10 +294,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin): 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)) - ) + "%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): @@ -298,20 +306,15 @@ 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() + attribution__spectacle__date__gte=timezone.now() + ): + 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." % ( - count, pluralize(count), - pluralize(count, "a,ont"), pluralize(count)) - ) + "%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" diff --git a/bda/algorithm.py b/bda/algorithm.py index 7f18ce18..add09335 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,11 +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 @@ -22,7 +14,7 @@ class Algorithm(object): show.requests - on crée des tables de demandes pour chaque personne, afin de pouvoir modifier les rankings""" - self.max_group = 2*max(choice.priority for choice in choices) + self.max_group = 2 * max(choice.priority for choice in choices) self.shows = [] showdict = {} for show in shows: @@ -60,16 +52,19 @@ class Algorithm(object): self.ranks[member][show] -= increment def appendResult(self, l, member, show): - l.append((member, - self.ranks[member][show], - self.origranks[member][show], - self.choices[member][show].double)) + l.append( + ( + member, + self.ranks[member][show], + self.origranks[member][show], + self.choices[member][show].double, + ) + ) def __call__(self, seed): random.seed(seed) results = [] - shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, - reverse=True) + shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True) for show in shows: # On regroupe tous les gens ayant le même rang groups = dict([(i, []) for i in range(1, self.max_group + 1)]) @@ -88,8 +83,10 @@ class Algorithm(object): if len(winners) + 1 < show.slots: self.appendResult(winners, member, show) self.appendResult(winners, member, show) - elif not self.choices[member][show].autoquit \ - and len(winners) < show.slots: + elif ( + not self.choices[member][show].autoquit + and len(winners) < show.slots + ): self.appendResult(winners, member, show) self.appendResult(losers, member, show) else: diff --git a/bda/forms.py b/bda/forms.py index c0417d1e..94a52128 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -1,14 +1,11 @@ -# -*- 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): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -18,9 +15,9 @@ class InscriptionInlineFormSet(BaseInlineFormSet): # set once for all "spectacle" field choices # - restrict choices to the spectacles of this tirage # - force_choices avoid many db requests - spectacles = tirage.spectacle_set.select_related('location') + spectacles = tirage.spectacle_set.select_related("location") choices = [(sp.pk, str(sp)) for sp in spectacles] - self.force_choices('spectacle', choices) + self.force_choices("spectacle", choices) def force_choices(self, name, choices): """Set choices of a field. @@ -32,7 +29,7 @@ class InscriptionInlineFormSet(BaseInlineFormSet): for form in self.forms: field = form.fields[name] if field.empty_label is not None: - field.choices = [('', field.empty_label)] + choices + field.choices = [("", field.empty_label)] + choices else: field.choices = choices @@ -43,75 +40,140 @@ 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 : informations sur le statut + if obj.soldTo is not None: + suffix = " -- Vendue à {firstname} {lastname}".format( + firstname=obj.soldTo.user.first_name, + lastname=obj.soldTo.user.last_name, + ) + elif obj.shotgun: + suffix = " -- Tirage infructueux" + elif obj.notif_sent: + suffix = " -- Inscriptions au tirage en cours" + 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): attributions = AttributionModelMultipleChoiceField( - label='', - queryset=Attribution.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False) + label="", + queryset=Attribution.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, participant, *args, **kwargs): - super(ResellForm, self).__init__(*args, **kwargs) - self.fields['attributions'].queryset = ( - participant.attribution_set - .filter(spectacle__date__gte=timezone.now()) + super().__init__(*args, **kwargs) + self.fields["attributions"].queryset = ( + participant.attribution_set.filter(spectacle__date__gte=timezone.now()) .exclude(revente__seller=participant) - .select_related('spectacle', 'spectacle__location', - 'participant__user') + .select_related("spectacle", "spectacle__location", "participant__user") ) class AnnulForm(forms.Form): - attributions = AttributionModelMultipleChoiceField( - label='', - queryset=Attribution.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False) + 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(), soldTo__isnull=True + ) + .select_related( + "attribution__spectacle", "attribution__spectacle__location" + ) + .order_by("-date") ) class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( - queryset=Spectacle.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False) + queryset=Spectacle.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, tirage, *args, **kwargs): - super(InscriptionReventeForm, self).__init__(*args, **kwargs) - self.fields['spectacles'].queryset = ( - tirage.spectacle_set - .select_related('location') - .filter(date__gte=timezone.now()) + super().__init__(*args, **kwargs) + self.fields["spectacles"].queryset = tirage.spectacle_set.select_related( + "location" + ).filter(date__gte=timezone.now()) + + +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( - label='', - queryset=Attribution.objects.none(), - widget=forms.CheckboxSelectMultiple) + 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/loadbdadevdata.py b/bda/management/commands/loadbdadevdata.py index a8e3f298..a608db6a 100644 --- a/bda/management/commands/loadbdadevdata.py +++ b/bda/management/commands/loadbdadevdata.py @@ -5,17 +5,15 @@ Crée deux tirages de test et y inscrit les utilisateurs import os import random -from django.utils import timezone from django.contrib.auth.models import User +from django.utils import timezone -from gestioncof.management.base import MyBaseCommand -from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle +from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage from bda.views import do_tirage - +from gestioncof.management.base import MyBaseCommand # Où sont stockés les fichiers json -DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), - 'data') +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") class Command(MyBaseCommand): @@ -27,27 +25,29 @@ class Command(MyBaseCommand): # --- Tirage.objects.all().delete() - Tirage.objects.bulk_create([ - Tirage( - title="Tirage de test 1", - ouverture=timezone.now()-timezone.timedelta(days=7), - fermeture=timezone.now(), - active=True - ), - Tirage( - title="Tirage de test 2", - ouverture=timezone.now(), - fermeture=timezone.now()+timezone.timedelta(days=60), - active=True - ) - ]) + Tirage.objects.bulk_create( + [ + Tirage( + title="Tirage de test 1", + ouverture=timezone.now() - timezone.timedelta(days=7), + fermeture=timezone.now(), + active=True, + ), + Tirage( + title="Tirage de test 2", + ouverture=timezone.now(), + fermeture=timezone.now() + timezone.timedelta(days=60), + active=True, + ), + ] + ) tirages = Tirage.objects.all() # --- # Salles # --- - locations = self.from_json('locations.json', DATA_DIR, Salle) + locations = self.from_json("locations.json", DATA_DIR, Salle) # --- # Spectacles @@ -60,15 +60,13 @@ class Command(MyBaseCommand): """ show.tirage = random.choice(tirages) show.listing = bool(random.randint(0, 1)) - show.date = ( - show.tirage.fermeture - + timezone.timedelta(days=random.randint(60, 90)) + show.date = show.tirage.fermeture + timezone.timedelta( + days=random.randint(60, 90) ) show.location = random.choice(locations) return show - shows = self.from_json( - 'shows.json', DATA_DIR, Spectacle, show_callback - ) + + shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback) # --- # Inscriptions @@ -79,23 +77,19 @@ class Command(MyBaseCommand): choices = [] for user in User.objects.filter(profile__is_cof=True): for tirage in tirages: - part, _ = Participant.objects.get_or_create( - user=user, - tirage=tirage - ) + part, _ = Participant.objects.get_or_create(user=user, tirage=tirage) shows = random.sample( - list(tirage.spectacle_set.all()), - tirage.spectacle_set.count() // 2 + list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2 ) for (rank, show) in enumerate(shows): - choices.append(ChoixSpectacle( - participant=part, - spectacle=show, - priority=rank + 1, - double_choice=random.choice( - ['1', 'double', 'autoquit'] + choices.append( + ChoixSpectacle( + participant=part, + spectacle=show, + priority=rank + 1, + double_choice=random.choice(["1", "double", "autoquit"]), ) - )) + ) ChoixSpectacle.objects.bulk_create(choices) self.stdout.write("- {:d} inscriptions générées".format(len(choices))) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 0302ec4b..bd25a28e 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -1,43 +1,49 @@ -# -*- 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 class Command(BaseCommand): - help = "Envoie les mails de notification et effectue " \ - "les tirages au sort des reventes" + help = ( + "Envoie les mails de notification et effectue les tirages au sort des reventes" + ) leave_locale_alone = True def handle(self, *args, **options): 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..65026736 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -1,33 +1,33 @@ -# -*- 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 + from bda.models import Spectacle class Command(BaseCommand): - help = 'Envoie les mails de rappel des spectacles dont la date ' \ - 'approche.\nNe renvoie pas les mails déjà envoyés.' + help = ( + "Envoie les mails de rappel des spectacles dont la date approche.\n" + "Ne renvoie pas les mails déjà envoyés." + ) leave_locale_alone = True def handle(self, *args, **options): now = timezone.now() delay = timedelta(days=4) - shows = Spectacle.objects \ - .filter(date__range=(now, now+delay)) \ - .filter(tirage__active=True) \ - .filter(rappel_sent__isnull=True) \ + shows = ( + Spectacle.objects.filter(date__range=(now, now + delay)) + .filter(tirage__active=True) + .filter(rappel_sent__isnull=True) .all() + ) for show in shows: show.send_rappel() - self.stdout.write( - 'Mails de rappels pour %s envoyés avec succès.' % show) + self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show) if not shows: - self.stdout.write('Aucun mail à envoyer.') + self.stdout.write("Aucun mail à envoyer.") diff --git a/bda/migrations/0001_initial.py b/bda/migrations/0001_initial.py index c4494413..077ddd4e 100644 --- a/bda/migrations/0001_initial.py +++ b/bda/migrations/0001_initial.py @@ -1,108 +1,206 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='Attribution', + name="Attribution", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('given', models.BooleanField(default=False, verbose_name='Donn\xe9e')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("given", models.BooleanField(default=False, verbose_name="Donn\xe9e")), ], ), migrations.CreateModel( - name='ChoixSpectacle', + name="ChoixSpectacle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('priority', models.PositiveIntegerField(verbose_name=b'Priorit\xc3\xa9')), - ('double_choice', models.CharField(default=b'1', max_length=10, verbose_name=b'Nombre de places', choices=[(b'1', b'1 place'), (b'autoquit', b'2 places si possible, 1 sinon'), (b'double', b'2 places sinon rien')])), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "priority", + models.PositiveIntegerField(verbose_name=b"Priorit\xc3\xa9"), + ), + ( + "double_choice", + models.CharField( + default=b"1", + max_length=10, + verbose_name=b"Nombre de places", + choices=[ + (b"1", b"1 place"), + (b"autoquit", b"2 places si possible, 1 sinon"), + (b"double", b"2 places sinon rien"), + ], + ), + ), ], options={ - 'ordering': ('priority',), - 'verbose_name': 'voeu', - 'verbose_name_plural': 'voeux', + "ordering": ("priority",), + "verbose_name": "voeu", + "verbose_name_plural": "voeux", }, ), migrations.CreateModel( - name='Participant', + name="Participant", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('paid', models.BooleanField(default=False, verbose_name='A pay\xe9')), - ('paymenttype', models.CharField(blank=True, max_length=6, verbose_name='Moyen de paiement', choices=[(b'cash', 'Cash'), (b'cb', b'CB'), (b'cheque', 'Ch\xe8que'), (b'autre', 'Autre')])), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("paid", models.BooleanField(default=False, verbose_name="A pay\xe9")), + ( + "paymenttype", + models.CharField( + blank=True, + max_length=6, + verbose_name="Moyen de paiement", + choices=[ + (b"cash", "Cash"), + (b"cb", b"CB"), + (b"cheque", "Ch\xe8que"), + (b"autre", "Autre"), + ], + ), + ), ], ), migrations.CreateModel( - name='Salle', + name="Salle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=300, verbose_name=b'Nom')), - ('address', models.TextField(verbose_name=b'Adresse')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=300, verbose_name=b"Nom")), + ("address", models.TextField(verbose_name=b"Adresse")), ], ), migrations.CreateModel( - name='Spectacle', + name="Spectacle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=300, verbose_name=b'Titre')), - ('date', models.DateTimeField(verbose_name=b'Date & heure')), - ('description', models.TextField(verbose_name=b'Description', blank=True)), - ('slots_description', models.TextField(verbose_name=b'Description des places', blank=True)), - ('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)), - ('slots', models.IntegerField(verbose_name=b'Places')), - ('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')), - ('location', models.ForeignKey(to='bda.Salle', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=300, verbose_name=b"Titre")), + ("date", models.DateTimeField(verbose_name=b"Date & heure")), + ( + "description", + models.TextField(verbose_name=b"Description", blank=True), + ), + ( + "slots_description", + models.TextField( + verbose_name=b"Description des places", blank=True + ), + ), + ( + "price", + models.FloatField(verbose_name=b"Prix d'une place", blank=True), + ), + ("slots", models.IntegerField(verbose_name=b"Places")), + ( + "priority", + models.IntegerField(default=1000, verbose_name=b"Priorit\xc3\xa9"), + ), + ( + "location", + models.ForeignKey(to="bda.Salle", on_delete=models.CASCADE), + ), ], options={ - 'ordering': ('priority', 'date', 'title'), - 'verbose_name': 'Spectacle', + "ordering": ("priority", "date", "title"), + "verbose_name": "Spectacle", }, ), migrations.AddField( - model_name='participant', - name='attributions', - field=models.ManyToManyField(related_name='attributed_to', through='bda.Attribution', to='bda.Spectacle'), + model_name="participant", + name="attributions", + field=models.ManyToManyField( + related_name="attributed_to", + through="bda.Attribution", + to="bda.Spectacle", + ), ), migrations.AddField( - model_name='participant', - name='choices', - field=models.ManyToManyField(related_name='chosen_by', through='bda.ChoixSpectacle', to='bda.Spectacle'), + model_name="participant", + name="choices", + field=models.ManyToManyField( + related_name="chosen_by", + through="bda.ChoixSpectacle", + to="bda.Spectacle", + ), ), migrations.AddField( - model_name='participant', - name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="participant", + name="user", + field=models.OneToOneField( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='choixspectacle', - name='participant', - field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), + model_name="choixspectacle", + name="participant", + field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE), ), migrations.AddField( - model_name='choixspectacle', - name='spectacle', - field=models.ForeignKey(related_name='participants', to='bda.Spectacle', on_delete=models.CASCADE), + model_name="choixspectacle", + name="spectacle", + field=models.ForeignKey( + related_name="participants", + to="bda.Spectacle", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='attribution', - name='participant', - field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), + model_name="attribution", + name="participant", + field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE), ), migrations.AddField( - model_name='attribution', - name='spectacle', - field=models.ForeignKey(related_name='attribues', to='bda.Spectacle', on_delete=models.CASCADE), + model_name="attribution", + name="spectacle", + field=models.ForeignKey( + related_name="attribues", to="bda.Spectacle", on_delete=models.CASCADE + ), ), migrations.AlterUniqueTogether( - name='choixspectacle', - unique_together=set([('participant', 'spectacle')]), + name="choixspectacle", unique_together=set([("participant", "spectacle")]) ), ] diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py index 79f79a57..f4b01ed2 100644 --- a/bda/migrations/0002_add_tirage.py +++ b/bda/migrations/0002_add_tirage.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models from django.utils import timezone @@ -36,49 +36,77 @@ def fill_tirage_fields(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('bda', '0001_initial'), - ] + dependencies = [("bda", "0001_initial")] operations = [ migrations.CreateModel( - name='Tirage', + name="Tirage", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=300, verbose_name=b'Titre')), - ('ouverture', models.DateTimeField(verbose_name=b"Date et heure d'ouverture du tirage")), - ('fermeture', models.DateTimeField(verbose_name=b'Date et heure de fermerture du tirage')), - ('token', models.TextField(verbose_name=b'Graine du tirage', blank=True)), - ('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=300, verbose_name=b"Titre")), + ( + "ouverture", + models.DateTimeField( + verbose_name=b"Date et heure d'ouverture du tirage" + ), + ), + ( + "fermeture", + models.DateTimeField( + verbose_name=b"Date et heure de fermerture du tirage" + ), + ), + ( + "token", + models.TextField(verbose_name=b"Graine du tirage", blank=True), + ), + ( + "active", + models.BooleanField(default=True, verbose_name=b"Tirage actif"), + ), ], ), migrations.AlterField( - model_name='participant', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="participant", + name="user", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), # Create fields `spectacle` for `Participant` and `Spectacle` models. # These fields are not nullable, but we first create them as nullable # to give a default value for existing instances of these models. migrations.AddField( - model_name='participant', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), + model_name="participant", + name="tirage", + field=models.ForeignKey( + to="bda.Tirage", null=True, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='spectacle', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), + model_name="spectacle", + name="tirage", + field=models.ForeignKey( + to="bda.Tirage", null=True, on_delete=models.CASCADE + ), ), migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop), migrations.AlterField( - model_name='participant', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), + model_name="participant", + name="tirage", + field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE), ), migrations.AlterField( - model_name='spectacle', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), + model_name="spectacle", + name="tirage", + field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0003_update_tirage_and_spectacle.py b/bda/migrations/0003_update_tirage_and_spectacle.py index f5ca671a..3548eb88 100644 --- a/bda/migrations/0003_update_tirage_and_spectacle.py +++ b/bda/migrations/0003_update_tirage_and_spectacle.py @@ -6,19 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0002_add_tirage'), - ] + dependencies = [("bda", "0002_add_tirage")] operations = [ migrations.AlterField( - model_name='spectacle', - name='price', + model_name="spectacle", + name="price", field=models.FloatField(verbose_name=b"Prix d'une place"), ), migrations.AlterField( - model_name='tirage', - name='active', - field=models.BooleanField(default=False, verbose_name=b'Tirage actif'), + model_name="tirage", + name="active", + field=models.BooleanField(default=False, verbose_name=b"Tirage actif"), ), ] diff --git a/bda/migrations/0004_mails-rappel.py b/bda/migrations/0004_mails-rappel.py index f17b711f..d331568a 100644 --- a/bda/migrations/0004_mails-rappel.py +++ b/bda/migrations/0004_mails-rappel.py @@ -6,20 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0003_update_tirage_and_spectacle'), - ] + dependencies = [("bda", "0003_update_tirage_and_spectacle")] operations = [ migrations.AddField( - model_name='spectacle', - name='listing', - field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'), + model_name="spectacle", + name="listing", + field=models.BooleanField( + default=False, verbose_name=b"Les places sont sur listing" + ), preserve_default=False, ), migrations.AddField( - model_name='spectacle', - name='rappel_sent', - field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True), + model_name="spectacle", + name="rappel_sent", + field=models.DateTimeField( + null=True, verbose_name=b"Mail de rappel envoy\xc3\xa9", blank=True + ), ), ] diff --git a/bda/migrations/0005_encoding.py b/bda/migrations/0005_encoding.py index b36113c2..eedfcee4 100644 --- a/bda/migrations/0005_encoding.py +++ b/bda/migrations/0005_encoding.py @@ -6,24 +6,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0004_mails-rappel'), - ] + dependencies = [("bda", "0004_mails-rappel")] operations = [ migrations.AlterField( - model_name='choixspectacle', - name='priority', - field=models.PositiveIntegerField(verbose_name='Priorit\xe9'), + model_name="choixspectacle", + name="priority", + field=models.PositiveIntegerField(verbose_name="Priorit\xe9"), ), migrations.AlterField( - model_name='spectacle', - name='priority', - field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'), + model_name="spectacle", + name="priority", + field=models.IntegerField(default=1000, verbose_name="Priorit\xe9"), ), migrations.AlterField( - model_name='spectacle', - name='rappel_sent', - field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True), + model_name="spectacle", + name="rappel_sent", + field=models.DateTimeField( + null=True, verbose_name="Mail de rappel envoy\xe9", blank=True + ), ), ] diff --git a/bda/migrations/0006_add_tirage_switch.py b/bda/migrations/0006_add_tirage_switch.py index fc923c9a..ccfe7505 100644 --- a/bda/migrations/0006_add_tirage_switch.py +++ b/bda/migrations/0006_add_tirage_switch.py @@ -10,26 +10,25 @@ def forwards_func(apps, schema_editor): db_alias = schema_editor.connection.alias for tirage in Tirage.objects.using(db_alias).all(): if tirage.tokens: - tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % ( - timezone.now().strftime("%y-%m-%d %H:%M:%S"), - tirage.tokens) + tirage.tokens = 'Before %s\n"""%s"""\n' % ( + timezone.now().strftime("%y-%m-%d %H:%M:%S"), + tirage.tokens, + ) tirage.save() class Migration(migrations.Migration): - dependencies = [ - ('bda', '0005_encoding'), - ] + dependencies = [("bda", "0005_encoding")] operations = [ - migrations.RenameField('tirage', 'token', 'tokens'), + migrations.RenameField("tirage", "token", "tokens"), migrations.AddField( - model_name='tirage', - name='enable_do_tirage', + model_name="tirage", + name="enable_do_tirage", field=models.BooleanField( - default=False, - verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'), + default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9" + ), ), migrations.RunPython(forwards_func, migrations.RunPython.noop), ] diff --git a/bda/migrations/0007_extends_spectacle.py b/bda/migrations/0007_extends_spectacle.py index 6ea11dc0..87182ff7 100644 --- a/bda/migrations/0007_extends_spectacle.py +++ b/bda/migrations/0007_extends_spectacle.py @@ -1,91 +1,100 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0006_add_tirage_switch'), - ] + dependencies = [("bda", "0006_add_tirage_switch")] operations = [ migrations.CreateModel( - name='CategorieSpectacle', + name="CategorieSpectacle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, - auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=100, verbose_name='Nom', - unique=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField(max_length=100, verbose_name="Nom", unique=True), + ), ], - options={ - 'verbose_name': 'Cat\xe9gorie', - }, + options={"verbose_name": "Cat\xe9gorie"}, ), migrations.CreateModel( - name='Quote', + name="Quote", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, - auto_created=True, primary_key=True)), - ('text', models.TextField(verbose_name='Citation')), - ('author', models.CharField(max_length=200, - verbose_name='Auteur')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("text", models.TextField(verbose_name="Citation")), + ("author", models.CharField(max_length=200, verbose_name="Auteur")), ], ), migrations.AlterModelOptions( - name='spectacle', - options={'ordering': ('date', 'title'), - 'verbose_name': 'Spectacle'}, - ), - migrations.RemoveField( - model_name='spectacle', - name='priority', + name="spectacle", + options={"ordering": ("date", "title"), "verbose_name": "Spectacle"}, ), + migrations.RemoveField(model_name="spectacle", name="priority"), migrations.AddField( - model_name='spectacle', - name='ext_link', + model_name="spectacle", + name="ext_link", field=models.CharField( max_length=500, - verbose_name='Lien vers le site du spectacle', - blank=True), + verbose_name="Lien vers le site du spectacle", + blank=True, + ), ), migrations.AddField( - model_name='spectacle', - name='image', - field=models.ImageField(upload_to='imgs/shows/', null=True, - verbose_name='Image', blank=True), + model_name="spectacle", + name="image", + field=models.ImageField( + upload_to="imgs/shows/", null=True, verbose_name="Image", blank=True + ), ), migrations.AlterField( - model_name='tirage', - name='enable_do_tirage', + model_name="tirage", + name="enable_do_tirage", field=models.BooleanField( - default=False, - verbose_name='Le tirage peut \xeatre lanc\xe9'), + default=False, verbose_name="Le tirage peut \xeatre lanc\xe9" + ), ), migrations.AlterField( - model_name='tirage', - name='tokens', - field=models.TextField(verbose_name='Graine(s) du tirage', - blank=True), + model_name="tirage", + name="tokens", + field=models.TextField(verbose_name="Graine(s) du tirage", blank=True), ), migrations.AddField( - model_name='spectacle', - name='category', - field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle', - on_delete=models.CASCADE, - null=True), + model_name="spectacle", + name="category", + field=models.ForeignKey( + blank=True, + to="bda.CategorieSpectacle", + on_delete=models.CASCADE, + null=True, + ), ), migrations.AddField( - model_name='spectacle', - name='vips', - field=models.TextField(verbose_name='Personnalit\xe9s', - blank=True), + model_name="spectacle", + name="vips", + field=models.TextField(verbose_name="Personnalit\xe9s", blank=True), ), migrations.AddField( - model_name='quote', - name='spectacle', - field=models.ForeignKey(to='bda.Spectacle', - on_delete=models.CASCADE), + model_name="quote", + name="spectacle", + field=models.ForeignKey(to="bda.Spectacle", on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0008_py3.py b/bda/migrations/0008_py3.py index fe6a8eaf..6aa69abd 100644 --- a/bda/migrations/0008_py3.py +++ b/bda/migrations/0008_py3.py @@ -1,103 +1,110 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0007_extends_spectacle'), - ] + dependencies = [("bda", "0007_extends_spectacle")] operations = [ migrations.AlterField( - model_name='choixspectacle', - name='double_choice', + model_name="choixspectacle", + name="double_choice", field=models.CharField( - verbose_name='Nombre de places', - choices=[('1', '1 place'), - ('autoquit', '2 places si possible, 1 sinon'), - ('double', '2 places sinon rien')], - max_length=10, default='1'), + verbose_name="Nombre de places", + choices=[ + ("1", "1 place"), + ("autoquit", "2 places si possible, 1 sinon"), + ("double", "2 places sinon rien"), + ], + max_length=10, + default="1", + ), ), migrations.AlterField( - model_name='participant', - name='paymenttype', + model_name="participant", + name="paymenttype", field=models.CharField( blank=True, - choices=[('cash', 'Cash'), ('cb', 'CB'), - ('cheque', 'Chèque'), ('autre', 'Autre')], - max_length=6, verbose_name='Moyen de paiement'), + choices=[ + ("cash", "Cash"), + ("cb", "CB"), + ("cheque", "Chèque"), + ("autre", "Autre"), + ], + max_length=6, + verbose_name="Moyen de paiement", + ), ), migrations.AlterField( - model_name='salle', - name='address', - field=models.TextField(verbose_name='Adresse'), + model_name="salle", + name="address", + field=models.TextField(verbose_name="Adresse"), ), migrations.AlterField( - model_name='salle', - name='name', - field=models.CharField(verbose_name='Nom', max_length=300), + model_name="salle", + name="name", + field=models.CharField(verbose_name="Nom", max_length=300), ), migrations.AlterField( - model_name='spectacle', - name='date', - field=models.DateTimeField(verbose_name='Date & heure'), + model_name="spectacle", + name="date", + field=models.DateTimeField(verbose_name="Date & heure"), ), migrations.AlterField( - model_name='spectacle', - name='description', - field=models.TextField(verbose_name='Description', blank=True), + model_name="spectacle", + name="description", + field=models.TextField(verbose_name="Description", blank=True), ), migrations.AlterField( - model_name='spectacle', - name='listing', - field=models.BooleanField( - verbose_name='Les places sont sur listing'), + model_name="spectacle", + name="listing", + field=models.BooleanField(verbose_name="Les places sont sur listing"), ), migrations.AlterField( - model_name='spectacle', - name='price', + model_name="spectacle", + name="price", field=models.FloatField(verbose_name="Prix d'une place"), ), migrations.AlterField( - model_name='spectacle', - name='slots', - field=models.IntegerField(verbose_name='Places'), + model_name="spectacle", + name="slots", + field=models.IntegerField(verbose_name="Places"), ), migrations.AlterField( - model_name='spectacle', - name='slots_description', - field=models.TextField(verbose_name='Description des places', - blank=True), + model_name="spectacle", + name="slots_description", + field=models.TextField(verbose_name="Description des places", blank=True), ), migrations.AlterField( - model_name='spectacle', - name='title', - field=models.CharField(verbose_name='Titre', max_length=300), + model_name="spectacle", + name="title", + field=models.CharField(verbose_name="Titre", max_length=300), ), migrations.AlterField( - model_name='tirage', - name='active', - field=models.BooleanField(verbose_name='Tirage actif', - default=False), + model_name="tirage", + name="active", + field=models.BooleanField(verbose_name="Tirage actif", default=False), ), migrations.AlterField( - model_name='tirage', - name='fermeture', + model_name="tirage", + name="fermeture", field=models.DateTimeField( - verbose_name='Date et heure de fermerture du tirage'), + verbose_name="Date et heure de fermerture du tirage" + ), ), migrations.AlterField( - model_name='tirage', - name='ouverture', + model_name="tirage", + name="ouverture", field=models.DateTimeField( - verbose_name="Date et heure d'ouverture du tirage"), + verbose_name="Date et heure d'ouverture du tirage" + ), ), migrations.AlterField( - model_name='tirage', - name='title', - field=models.CharField(verbose_name='Titre', max_length=300), + model_name="tirage", + name="title", + field=models.CharField(verbose_name="Titre", max_length=300), ), ] diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py index 70d6f338..d888140f 100644 --- a/bda/migrations/0009_revente.py +++ b/bda/migrations/0009_revente.py @@ -1,69 +1,87 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0008_py3'), - ] + dependencies = [("bda", "0008_py3")] operations = [ migrations.CreateModel( - name='SpectacleRevente', + name="SpectacleRevente", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, - auto_created=True, verbose_name='ID')), - ('date', models.DateTimeField( - verbose_name='Date de mise en vente', - default=django.utils.timezone.now)), - ('notif_sent', models.BooleanField( - verbose_name='Notification envoyée', default=False)), - ('tirage_done', models.BooleanField( - verbose_name='Tirage effectué', default=False)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ( + "date", + models.DateTimeField( + verbose_name="Date de mise en vente", + default=django.utils.timezone.now, + ), + ), + ( + "notif_sent", + models.BooleanField( + verbose_name="Notification envoyée", default=False + ), + ), + ( + "tirage_done", + models.BooleanField(verbose_name="Tirage effectué", default=False), + ), ], - options={ - 'verbose_name': 'Revente', - }, + options={"verbose_name": "Revente"}, ), migrations.AddField( - model_name='participant', - name='choicesrevente', - field=models.ManyToManyField(to='bda.Spectacle', - related_name='subscribed', - blank=True), + model_name="participant", + name="choicesrevente", + field=models.ManyToManyField( + to="bda.Spectacle", related_name="subscribed", blank=True + ), ), migrations.AddField( - model_name='spectaclerevente', - name='answered_mail', - field=models.ManyToManyField(to='bda.Participant', - related_name='wanted', - blank=True), + model_name="spectaclerevente", + name="answered_mail", + field=models.ManyToManyField( + to="bda.Participant", related_name="wanted", blank=True + ), ), migrations.AddField( - model_name='spectaclerevente', - name='attribution', - field=models.OneToOneField(to='bda.Attribution', - on_delete=models.CASCADE, - related_name='revente'), + model_name="spectaclerevente", + name="attribution", + field=models.OneToOneField( + to="bda.Attribution", on_delete=models.CASCADE, related_name="revente" + ), ), migrations.AddField( - model_name='spectaclerevente', - name='seller', - field=models.ForeignKey(to='bda.Participant', - on_delete=models.CASCADE, - verbose_name='Vendeur', - related_name='original_shows'), + model_name="spectaclerevente", + name="seller", + field=models.ForeignKey( + to="bda.Participant", + on_delete=models.CASCADE, + verbose_name="Vendeur", + related_name="original_shows", + ), ), migrations.AddField( - model_name='spectaclerevente', - name='soldTo', - field=models.ForeignKey(to='bda.Participant', - on_delete=models.CASCADE, - verbose_name='Vendue à', null=True, - blank=True), + model_name="spectaclerevente", + name="soldTo", + field=models.ForeignKey( + to="bda.Participant", + on_delete=models.CASCADE, + verbose_name="Vendue à", + null=True, + blank=True, + ), ), ] diff --git a/bda/migrations/0010_spectaclerevente_shotgun.py b/bda/migrations/0010_spectaclerevente_shotgun.py index 35b4da8a..da5c014c 100644 --- a/bda/migrations/0010_spectaclerevente_shotgun.py +++ b/bda/migrations/0010_spectaclerevente_shotgun.py @@ -1,33 +1,35 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations -from django.utils import timezone from datetime import timedelta +from django.db import migrations, models +from django.utils import timezone + def forwards_func(apps, schema_editor): SpectacleRevente = apps.get_model("bda", "SpectacleRevente") for revente in SpectacleRevente.objects.all(): is_expired = timezone.now() > revente.date_tirage() - is_direct = (revente.attribution.spectacle.date >= revente.date and - timezone.now() > revente.date + timedelta(minutes=15)) + is_direct = revente.attribution.spectacle.date >= revente.date and timezone.now() > revente.date + timedelta( + minutes=15 + ) revente.shotgun = is_expired or is_direct revente.save() class Migration(migrations.Migration): - dependencies = [ - ('bda', '0009_revente'), - ] + dependencies = [("bda", "0009_revente")] operations = [ migrations.AddField( - model_name='spectaclerevente', - name='shotgun', - field=models.BooleanField(default=False, verbose_name='Disponible imm\xe9diatement'), + model_name="spectaclerevente", + name="shotgun", + field=models.BooleanField( + default=False, verbose_name="Disponible imm\xe9diatement" + ), ), migrations.RunPython(forwards_func, migrations.RunPython.noop), ] diff --git a/bda/migrations/0011_tirage_appear_catalogue.py b/bda/migrations/0011_tirage_appear_catalogue.py index c2a2479d..446be392 100644 --- a/bda/migrations/0011_tirage_appear_catalogue.py +++ b/bda/migrations/0011_tirage_appear_catalogue.py @@ -6,17 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0010_spectaclerevente_shotgun'), - ] + dependencies = [("bda", "0010_spectaclerevente_shotgun")] operations = [ migrations.AddField( - model_name='tirage', - name='appear_catalogue', + model_name="tirage", + name="appear_catalogue", field=models.BooleanField( - default=False, - verbose_name='Tirage à afficher dans le catalogue' + default=False, verbose_name="Tirage à afficher dans le catalogue" ), - ), + ) ] diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py new file mode 100644 index 00000000..96853a24 --- /dev/null +++ b/bda/migrations/0012_notif_time.py @@ -0,0 +1,31 @@ +# -*- 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..e712f2ff --- /dev/null +++ b/bda/migrations/0012_swap_double_choice.py @@ -0,0 +1,51 @@ +# -*- 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..8f78b6a9 --- /dev/null +++ b/bda/migrations/0013_merge_20180524_2123.py @@ -0,0 +1,12 @@ +# -*- 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..9ac38a41 100644 --- a/bda/models.py +++ b/bda/models.py @@ -1,25 +1,22 @@ -# -*- coding: utf-8 -*- - import calendar import random from datetime import timedelta -from custommail.shortcuts import send_mass_custom_mail +from custommail.models import CustomMail +from custommail.shortcuts import send_mass_custom_mail +from django.conf import settings +from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core import mail from django.db import models from django.db.models import Count -from django.contrib.auth.models import User -from django.conf import settings -from django.utils import timezone, formats - -from custommail.models import CustomMail +from django.utils import formats, timezone def get_generic_user(): generic, _ = User.objects.get_or_create( username="bda_generic", - defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"} + defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"}, ) return generic @@ -31,15 +28,15 @@ class Tirage(models.Model): tokens = models.TextField("Graine(s) du tirage", blank=True) active = models.BooleanField("Tirage actif", default=False) appear_catalogue = models.BooleanField( - "Tirage à afficher dans le catalogue", - default=False + "Tirage à afficher dans le catalogue", default=False ) - enable_do_tirage = models.BooleanField("Le tirage peut être lancé", - default=False) + enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False) def __str__(self): - return "%s - %s" % (self.title, formats.localize( - timezone.template_localtime(self.fermeture))) + return "%s - %s" % ( + self.title, + formats.localize(timezone.template_localtime(self.fermeture)), + ) class Salle(models.Model): @@ -51,7 +48,7 @@ class Salle(models.Model): class CategorieSpectacle(models.Model): - name = models.CharField('Nom', max_length=100, unique=True) + name = models.CharField("Nom", max_length=100, unique=True) def __str__(self): return self.name @@ -63,28 +60,26 @@ class CategorieSpectacle(models.Model): class Spectacle(models.Model): title = models.CharField("Titre", max_length=300) category = models.ForeignKey( - CategorieSpectacle, on_delete=models.CASCADE, - blank=True, null=True, + CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True ) date = models.DateTimeField("Date & heure") location = models.ForeignKey(Salle, on_delete=models.CASCADE) - vips = models.TextField('Personnalités', blank=True) + vips = models.TextField("Personnalités", blank=True) description = models.TextField("Description", blank=True) slots_description = models.TextField("Description des places", blank=True) - image = models.ImageField('Image', blank=True, null=True, - upload_to='imgs/shows/') - ext_link = models.CharField('Lien vers le site du spectacle', blank=True, - max_length=500) + image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/") + ext_link = models.CharField( + "Lien vers le site du spectacle", blank=True, max_length=500 + ) price = models.FloatField("Prix d'une place") slots = models.IntegerField("Places") tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) listing = models.BooleanField("Les places sont sur listing") - rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, - null=True) + rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True) class Meta: verbose_name = "Spectacle" - ordering = ("date", "title",) + ordering = ("date", "title") def timestamp(self): return "%d" % calendar.timegm(self.date.utctimetuple()) @@ -94,7 +89,7 @@ class Spectacle(models.Model): self.title, formats.localize(timezone.template_localtime(self.date)), self.location, - self.price + self.price, ) def getImgUrl(self): @@ -103,7 +98,7 @@ class Spectacle(models.Model): """ try: return self.image.url - except: + except Exception: return None def send_rappel(self): @@ -113,19 +108,21 @@ class Spectacle(models.Model): """ # On récupère la liste des participants + le BdA members = list( - User.objects - .filter(participant__attributions=self) - .annotate(nb_attr=Count("id")).order_by() + User.objects.filter(participant__attributions=self) + .annotate(nb_attr=Count("id")) + .order_by() ) bda_generic = get_generic_user() bda_generic.nb_attr = 1 members.append(bda_generic) # On écrit un mail personnalisé à chaque participant - datatuple = [( - 'bda-rappel', - {'member': member, "nb_attr": member.nb_attr, 'show': self}, - settings.MAIL_DATA['rappels']['FROM'], - [member.email]) + datatuple = [ + ( + "bda-rappel", + {"member": member, "nb_attr": member.nb_attr, "show": self}, + settings.MAIL_DATA["rappels"]["FROM"], + [member.email], + ) for member in members ] send_mass_custom_mail(datatuple) @@ -142,8 +139,8 @@ class Spectacle(models.Model): class Quote(models.Model): spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE) - text = models.TextField('Citation') - author = models.CharField('Auteur', max_length=200) + text = models.TextField("Citation") + author = models.CharField("Auteur", max_length=200) PAYMENT_TYPES = ( @@ -156,58 +153,61 @@ PAYMENT_TYPES = ( class Participant(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - choices = models.ManyToManyField(Spectacle, - through="ChoixSpectacle", - related_name="chosen_by") - attributions = models.ManyToManyField(Spectacle, - through="Attribution", - related_name="attributed_to") + choices = models.ManyToManyField( + Spectacle, through="ChoixSpectacle", related_name="chosen_by" + ) + attributions = models.ManyToManyField( + Spectacle, through="Attribution", related_name="attributed_to" + ) paid = models.BooleanField("A payé", default=False) - paymenttype = models.CharField("Moyen de paiement", - max_length=6, choices=PAYMENT_TYPES, - blank=True) + paymenttype = models.CharField( + "Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True + ) tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) - choicesrevente = models.ManyToManyField(Spectacle, - related_name="subscribed", - blank=True) + choicesrevente = models.ManyToManyField( + Spectacle, related_name="subscribed", blank=True + ) 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"), ) class ChoixSpectacle(models.Model): participant = models.ForeignKey(Participant, on_delete=models.CASCADE) spectacle = models.ForeignKey( - Spectacle, on_delete=models.CASCADE, - related_name="participants", + Spectacle, on_delete=models.CASCADE, related_name="participants" ) priority = models.PositiveIntegerField("Priorité") - double_choice = models.CharField("Nombre de places", - default="1", choices=DOUBLE_CHOICES, - max_length=10) + double_choice = models.CharField( + "Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10 + ) def get_double(self): return self.double_choice != "1" + double = property(get_double) def get_autoquit(self): return self.double_choice == "autoquit" + autoquit = property(get_autoquit) def __str__(self): return "Vœux de %s pour %s" % ( - self.participant.user.get_full_name(), - self.spectacle.title) + self.participant.user.get_full_name(), + self.spectacle.title, + ) class Meta: ordering = ("priority",) - unique_together = (("participant", "spectacle",),) + unique_together = (("participant", "spectacle"),) verbose_name = "voeu" verbose_name_plural = "voeux" @@ -215,82 +215,137 @@ class ChoixSpectacle(models.Model): class Attribution(models.Model): participant = models.ForeignKey(Participant, on_delete=models.CASCADE) spectacle = models.ForeignKey( - Spectacle, on_delete=models.CASCADE, - related_name="attribues", + Spectacle, on_delete=models.CASCADE, related_name="attribues" ) given = models.BooleanField("Donnée", default=False) def __str__(self): - return "%s -- %s, %s" % (self.participant.user, self.spectacle.title, - self.spectacle.date) + return "%s -- %s, %s" % ( + self.participant.user, + self.spectacle.title, + self.spectacle.date, + ) class SpectacleRevente(models.Model): attribution = models.OneToOneField( - Attribution, on_delete=models.CASCADE, - related_name="revente", + Attribution, on_delete=models.CASCADE, related_name="revente" + ) + date = models.DateTimeField("Date de mise en vente", default=timezone.now) + confirmed_entry = models.ManyToManyField( + Participant, related_name="entered", blank=True ) - date = models.DateTimeField("Date de mise en vente", - default=timezone.now) - answered_mail = models.ManyToManyField(Participant, - related_name="wanted", - blank=True) seller = models.ForeignKey( - Participant, on_delete=models.CASCADE, + Participant, + on_delete=models.CASCADE, verbose_name="Vendeur", related_name="original_shows", ) soldTo = models.ForeignKey( - Participant, on_delete=models.CASCADE, + Participant, + on_delete=models.CASCADE, verbose_name="Vendue à", - blank=True, null=True, + blank=True, + null=True, ) - notif_sent = models.BooleanField("Notification envoyée", - default=False) - tirage_done = models.BooleanField("Tirage effectué", - default=False) - shotgun = models.BooleanField("Disponible immédiatement", - default=False) + 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) + + remaining_time = ( + self.attribution.spectacle.date - 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, - self.attribution.spectacle.title) + return "%s -- %s" % (self.seller, self.attribution.spectacle.title) 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 BdA-Revente à tous les intéressés. """ - inscrits = self.attribution.spectacle.subscribed.select_related('user') - datatuple = [( - 'bda-revente', - { - 'member': participant.user, - 'show': self.attribution.spectacle, - 'revente': self, - 'site': Site.objects.get_current() - }, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email]) + inscrits = self.attribution.spectacle.subscribed.select_related("user") + datatuple = [ + ( + "bda-revente", + { + "member": participant.user, + "show": self.attribution.spectacle, + "revente": self, + "site": Site.objects.get_current(), + }, + settings.MAIL_DATA["revente"]["FROM"], + [participant.user.email], + ) for participant in inscrits ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() self.save() def mail_shotgun(self): @@ -298,90 +353,98 @@ class SpectacleRevente(models.Model): Envoie un mail à toutes les personnes intéréssées par le spectacle pour leur indiquer qu'il est désormais disponible au shotgun. """ - inscrits = self.attribution.spectacle.subscribed.select_related('user') - datatuple = [( - 'bda-shotgun', - { - 'member': participant.user, - 'show': self.attribution.spectacle, - 'site': Site.objects.get_current(), - }, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email]) + inscrits = self.attribution.spectacle.subscribed.select_related("user") + datatuple = [ + ( + "bda-shotgun", + { + "member": participant.user, + "show": self.attribution.spectacle, + "site": Site.objects.get_current(), + }, + settings.MAIL_DATA["revente"]["FROM"], + [participant.user.email], + ) for participant in inscrits ] 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 = {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], + c_mails_qs = CustomMail.objects.filter( + shortname__in=[ + "bda-revente-winner", + "bda-revente-loser", + "bda-revente-seller", + ] ) - ) - 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], - ) - ) + c_mails = {cm.shortname: cm for cm in c_mails_qs} - # 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..9a39df6f 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..5147ff16 --- /dev/null +++ b/bda/templates/bda/revente/manage.html @@ -0,0 +1,80 @@ +{% 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 resell_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 %} +

Places en cours de revente

+
+
+ + Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se. +
+ {% csrf_token %} +
+
+
    + {% for revente in annul_reventes %} +
  • {{ revente.tag }} {{ revente.choice_label }}
  • + {% endfor %} +
+
+
+ +
+ +
+{% 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_reventes 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 %}