forked from DGNum/gestioCOF
Compare commits
1 commit
master
...
aureplop/h
Author | SHA1 | Date | |
---|---|---|---|
|
83faf417c9 |
591 changed files with 62261 additions and 62662 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use nix
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -5,19 +5,13 @@ cof/settings.py
|
||||||
settings.py
|
settings.py
|
||||||
*~
|
*~
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
|
||||||
.vagrant
|
.vagrant
|
||||||
/src
|
/src
|
||||||
media/
|
media/
|
||||||
*.log
|
*.log
|
||||||
.sass-cache/
|
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.coverage
|
.coverage
|
||||||
|
|
||||||
# PyCharm
|
# PyCharm
|
||||||
.idea
|
.idea
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
# VSCode
|
|
||||||
.vscode/
|
|
||||||
.direnv
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
image: "python:3.7"
|
image: "python:3.5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
- postgres:latest
|
||||||
|
- redis:latest
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# GestioCOF settings
|
# GestioCOF settings
|
||||||
|
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
|
||||||
DBHOST: "postgres"
|
DBHOST: "postgres"
|
||||||
REDIS_HOST: "redis"
|
REDIS_HOST: "redis"
|
||||||
REDIS_PASSWD: "dummy"
|
REDIS_PASSWD: "dummy"
|
||||||
|
@ -17,24 +22,21 @@ variables:
|
||||||
# psql password authentication
|
# psql password authentication
|
||||||
PGPASSWORD: $POSTGRES_PASSWORD
|
PGPASSWORD: $POSTGRES_PASSWORD
|
||||||
|
|
||||||
# apps to check migrations for
|
test:
|
||||||
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
|
stage: test
|
||||||
|
|
||||||
.test_template:
|
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p vendor/{pip,apt}
|
- mkdir -p vendor/{pip,apt}
|
||||||
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
|
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
|
||||||
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py
|
- 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 = "";' gestioasso/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
|
# 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"
|
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
||||||
- pip install --upgrade -r requirements-prod.txt coverage tblib
|
- pip install --upgrade -r requirements.txt coverage
|
||||||
- python --version
|
- python --version
|
||||||
|
script:
|
||||||
|
- coverage run manage.py test
|
||||||
after_script:
|
after_script:
|
||||||
- coverage report
|
- coverage report
|
||||||
services:
|
|
||||||
- postgres:11.7
|
|
||||||
- redis:latest
|
|
||||||
cache:
|
cache:
|
||||||
key: test
|
key: test
|
||||||
paths:
|
paths:
|
||||||
|
@ -43,53 +45,18 @@ variables:
|
||||||
# Keep this disabled for now, as it may kill GitLab...
|
# Keep this disabled for now, as it may kill GitLab...
|
||||||
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
|
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
|
||||||
|
|
||||||
coftest:
|
|
||||||
stage: test
|
|
||||||
extends: .test_template
|
|
||||||
variables:
|
|
||||||
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
|
|
||||||
script:
|
|
||||||
- coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
|
|
||||||
|
|
||||||
bdstest:
|
|
||||||
stage: test
|
|
||||||
extends: .test_template
|
|
||||||
variables:
|
|
||||||
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod"
|
|
||||||
script:
|
|
||||||
- coverage run manage.py test bds clubs events --parallel
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
|
image: python:3.6
|
||||||
stage: test
|
stage: test
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p vendor/pip
|
- mkdir -p vendor/pip
|
||||||
- pip install --upgrade black isort flake8
|
- pip install --upgrade black isort flake8
|
||||||
script:
|
script:
|
||||||
- black --check .
|
- black --check .
|
||||||
- isort --check --diff .
|
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
|
||||||
# Print errors only
|
# Print errors only
|
||||||
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared
|
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
|
||||||
cache:
|
cache:
|
||||||
key: linters
|
key: linters
|
||||||
paths:
|
paths:
|
||||||
- vendor/
|
- vendor/
|
||||||
|
|
||||||
# Check whether there are some missing migrations.
|
|
||||||
migration_checks:
|
|
||||||
stage: test
|
|
||||||
variables:
|
|
||||||
DJANGO_SETTINGS_MODULE: "gestioasso.settings.local"
|
|
||||||
before_script:
|
|
||||||
- mkdir -p vendor/{pip,apt}
|
|
||||||
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
|
|
||||||
- cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py
|
|
||||||
- pip install --upgrade -r requirements-devel.txt
|
|
||||||
- python --version
|
|
||||||
script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS
|
|
||||||
services:
|
|
||||||
# this should not be necessary…
|
|
||||||
- postgres:11.7
|
|
||||||
cache:
|
|
||||||
key: migration_checks
|
|
||||||
paths:
|
|
||||||
- vendor/
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ checker_dirty=0
|
||||||
# Working? -> Stash unstaged changes, run it, pop stash
|
# Working? -> Stash unstaged changes, run it, pop stash
|
||||||
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$")
|
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$")
|
||||||
|
|
||||||
|
|
||||||
# Formatter: black
|
# Formatter: black
|
||||||
|
|
||||||
printf "> black ... "
|
printf "> black ... "
|
||||||
|
@ -24,8 +23,8 @@ if type black &>/dev/null; then
|
||||||
BLACK_OUTPUT="/tmp/gc-black-output.log"
|
BLACK_OUTPUT="/tmp/gc-black-output.log"
|
||||||
touch $BLACK_OUTPUT
|
touch $BLACK_OUTPUT
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black --check &>$BLACK_OUTPUT; then
|
if ! black --check "$STAGED_PYTHON_FILES" &>$BLACK_OUTPUT; then
|
||||||
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black &>$BLACK_OUTPUT
|
black $STAGED_PYTHON_FILES &>$BLACK_OUTPUT
|
||||||
tail -1 $BLACK_OUTPUT
|
tail -1 $BLACK_OUTPUT
|
||||||
formatter_updated=1
|
formatter_updated=1
|
||||||
else
|
else
|
||||||
|
@ -48,8 +47,8 @@ if type isort &>/dev/null; then
|
||||||
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
||||||
touch $ISORT_OUTPUT
|
touch $ISORT_OUTPUT
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then
|
if ! isort --check-only "$STAGED_PYTHON_FILES" &>$ISORT_OUTPUT; then
|
||||||
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
|
isort $STAGED_PYTHON_FILES &>$ISORT_OUTPUT
|
||||||
printf "Reformatted.\n"
|
printf "Reformatted.\n"
|
||||||
formatter_updated=1
|
formatter_updated=1
|
||||||
else
|
else
|
||||||
|
@ -72,7 +71,7 @@ if type flake8 &>/dev/null; then
|
||||||
FLAKE8_OUTPUT="/tmp/gc-flake8-output.log"
|
FLAKE8_OUTPUT="/tmp/gc-flake8-output.log"
|
||||||
touch $FLAKE8_OUTPUT
|
touch $FLAKE8_OUTPUT
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then
|
if ! flake8 $STAGED_PYTHON_FILES &>$FLAKE8_OUTPUT; then
|
||||||
printf "FAIL\n"
|
printf "FAIL\n"
|
||||||
cat $FLAKE8_OUTPUT
|
cat $FLAKE8_OUTPUT
|
||||||
checker_dirty=1
|
checker_dirty=1
|
||||||
|
|
298
CHANGELOG.md
298
CHANGELOG.md
|
@ -1,298 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
|
|
||||||
2018).
|
|
||||||
|
|
||||||
## Le FUTUR ! (pas prêt pour la prod)
|
|
||||||
|
|
||||||
### Nouveau module de gestion des événements
|
|
||||||
|
|
||||||
- Désormais complet niveau modèles
|
|
||||||
- Export des participants implémenté
|
|
||||||
|
|
||||||
#### TODO
|
|
||||||
|
|
||||||
- Vue de création d'événements ergonomique
|
|
||||||
- Vue d'inscription à un événement **ou** intégration propre dans la vue
|
|
||||||
"inscription d'un nouveau membre"
|
|
||||||
|
|
||||||
### Nouveau module de gestion des clubs
|
|
||||||
|
|
||||||
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
|
|
||||||
adhérents ni des cotisations.
|
|
||||||
|
|
||||||
## TODO Prod
|
|
||||||
|
|
||||||
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
|
|
||||||
|
|
||||||
## Version ??? - ??/??/????
|
|
||||||
|
|
||||||
## Version 0.15.1 - 15/06/2023
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Rattrape les erreurs d'envoi de mail de négatif
|
|
||||||
- Utilise l'adresse chefs pour les envois de négatifs
|
|
||||||
|
|
||||||
## Version 0.15 - 22/05/2023
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Rajoute un formulaire de contact
|
|
||||||
- Rajoute un formulaire de demande de soirée
|
|
||||||
- Désactive les mails d'envoi de négatifs sur les comptes gelés
|
|
||||||
|
|
||||||
## Version 0.14 - 19/05/2023
|
|
||||||
|
|
||||||
- Répare les dépendances en spécifiant toutes les versions
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Répare la gestion des changement d'heure via moment.js
|
|
||||||
|
|
||||||
## Version 0.13 - 19/02/2023
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Rajoute la valeur des inventaires
|
|
||||||
- Résout les problèmes de négatif ne disparaissant pas
|
|
||||||
- Affiche son surnom s'il y en a un
|
|
||||||
- Bugfixes
|
|
||||||
|
|
||||||
## Version 0.12.1 - 03/10/2022
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Fixe un problème de rendu causé par l'agrandissement du menu
|
|
||||||
|
|
||||||
## Version 0.12 - 17/06/2022
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13`
|
|
||||||
- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt
|
|
||||||
|
|
||||||
## Version 0.11 - 26/10/2021
|
|
||||||
|
|
||||||
### COF
|
|
||||||
|
|
||||||
- Répare un problème de rendu sur le wagtail du COF
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Ajoute de mails de rappels pour les comptes en négatif
|
|
||||||
- La recherche de comptes sur K-Psul remarche normalement
|
|
||||||
- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion
|
|
||||||
- Modification du gel de compte:
|
|
||||||
- on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt")
|
|
||||||
- on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul
|
|
||||||
- les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement
|
|
||||||
- Modification du fonctionnement des négatifs
|
|
||||||
- impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount`
|
|
||||||
- il n'y a plus de limite de temps sur les négatifs
|
|
||||||
- supression des autorisations de négatif
|
|
||||||
- il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge
|
|
||||||
- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites
|
|
||||||
- La supression d'opérations anciennes est réparée
|
|
||||||
|
|
||||||
## Version 0.10 - 18/04/2021
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à
|
|
||||||
la fois.
|
|
||||||
- L'interface indique plus clairement quand on fait une erreur en modifiant un
|
|
||||||
compte.
|
|
||||||
- On supprime la fonction "décalage de balance".
|
|
||||||
- L'accès à l'historique est maintenant limité à 7 jours pour raison de
|
|
||||||
confidentialité. Les chefs/trez peuvent disposer d'une permission
|
|
||||||
supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta.
|
|
||||||
L'accès à son historique personnel n'est pas limité. Les durées sont
|
|
||||||
configurables dans `settings/cof_prod.py`.
|
|
||||||
|
|
||||||
### COF
|
|
||||||
|
|
||||||
- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha
|
|
||||||
au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices
|
|
||||||
|
|
||||||
## Version 0.9 - 06/02/2020
|
|
||||||
|
|
||||||
### COF / BdA
|
|
||||||
|
|
||||||
- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par
|
|
||||||
KDE).
|
|
||||||
- La page d'accueil affiche la date de fermeture des tirages BdA.
|
|
||||||
- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes
|
|
||||||
ses places pour pouvoir revendre.
|
|
||||||
- On s'assure que l'email fourni lors d'une demande de petit cours est valide.
|
|
||||||
|
|
||||||
### BDS
|
|
||||||
|
|
||||||
- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô
|
|
||||||
en modifiant le profil d'un membre du BDS.
|
|
||||||
- Le burô peut exporter la liste de ses membres avec email au format CSV depuis
|
|
||||||
la page d'accueil.
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- On affiche les articles actuellement en vente en premier lors des inventaires
|
|
||||||
et des commandes.
|
|
||||||
- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier
|
|
||||||
inventaire sont affectés.
|
|
||||||
|
|
||||||
## Version 0.8 - 03/12/2020
|
|
||||||
|
|
||||||
### COF
|
|
||||||
|
|
||||||
- La page "Mes places" dans la section BdA indique quelles places sont sur
|
|
||||||
listing.
|
|
||||||
- ergonomie de l'interface admin du BdA : moins d'options inutiles lors de
|
|
||||||
la sélection de participants.
|
|
||||||
- les tirages sont maintenant archivables pour éviter d'avoir encore d'autres
|
|
||||||
options inutiles.
|
|
||||||
- l'autocomplétion dans l'admin BdA est réparée.
|
|
||||||
- Les icones de la page de gestion des petits cours sont (à nouveau) réparées.
|
|
||||||
- On a supprimé la possibilité de modifier les mails automatiques depuis
|
|
||||||
l'interface admin car trop problématique. Faute de mieux, envoyer un mail à
|
|
||||||
KDE pour modifier ces mails.
|
|
||||||
- corrige un crash sporadique sur la page d'inscription au système de petits
|
|
||||||
cours
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- (fix partiel) Empêche la K-Fêt de modifier des données COF (e.g. nom, prénom,
|
|
||||||
username) lors de la création d'un nouveau compte.
|
|
||||||
- Les statistiques de conso globales montrent deux courbes COF / non-COF au
|
|
||||||
lieu de LIQ / sur compte.
|
|
||||||
- Un bug empêchait de fermer manuellement la K-Fêt depuis un compte non
|
|
||||||
privilégié en tapant un mot de passe. C'est corrigé.
|
|
||||||
|
|
||||||
## Version 0.7.2 - 08/09/2020
|
|
||||||
|
|
||||||
- Nouvelle page 404
|
|
||||||
- Correction de bug en K-Fêt : le lien pour créer un nouveau compte exté apparaît
|
|
||||||
à nouveau dans l'autocomplétion
|
|
||||||
|
|
||||||
## Version 0.7.1 - 05/09/2020
|
|
||||||
|
|
||||||
Petits ajustements sur le site du COF :
|
|
||||||
|
|
||||||
- Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et
|
|
||||||
de la page web dans les annuaires (clubs et partenaires).
|
|
||||||
- Corrige un bug d'affichage des adresses emails de clubs
|
|
||||||
|
|
||||||
## Version 0.7 - 29/08/2020
|
|
||||||
|
|
||||||
### GestioBDS
|
|
||||||
|
|
||||||
- Ajout d'un bouton pour supprimer un compte
|
|
||||||
- Le nombre d'adhérent⋅es est affiché sur la page d'accueil
|
|
||||||
- le groupe BDS a les bonnes permissions
|
|
||||||
|
|
||||||
### Site du COF
|
|
||||||
|
|
||||||
- Captcha fonctionnel pour les mailing-listes
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- L'autocomplétion pour la création de compte K-Fêt se lance à 3 caractères seulement,
|
|
||||||
donc est plus rapide.
|
|
||||||
|
|
||||||
## Version 0.6 - 27/07/2020
|
|
||||||
|
|
||||||
Arrivée du BDS !
|
|
||||||
GestioCOF et GestioBDS ont du code en commun mais tournent de façon séparée, les
|
|
||||||
deux bases de données sont distinctes.
|
|
||||||
|
|
||||||
## Version 0.5 - 11/07/2020
|
|
||||||
|
|
||||||
### Problèmes corrigés
|
|
||||||
|
|
||||||
- La recherche d'utilisateurices (COF + K-Fêt) fonctionne de nouveau
|
|
||||||
- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur
|
|
||||||
la page des clubs (nouveau site du COF)
|
|
||||||
- Version mobile plus ergonimique sur le nouveau site du COF
|
|
||||||
- Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus
|
|
||||||
d'erreurs 500 (nouveau site du COF)
|
|
||||||
- L'historique des ventes des articles K-Fêt fonctionne à nouveau
|
|
||||||
- Les montants en K-Fêt sont à nouveau affichés en UKF (et non en €).
|
|
||||||
- Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un
|
|
||||||
spectacle BdA fonctionnent à nouveau.
|
|
||||||
- on ne peut plus compter de consos sur ☠☠☠, ni éditer les comptes spéciaux
|
|
||||||
(LIQ, GNR, ☠☠☠, #13).
|
|
||||||
|
|
||||||
### Nouvelles fonctionnalités
|
|
||||||
|
|
||||||
- On n'affiche que 4 articles sur la pages "nouveautés" (nouveau site du COF)
|
|
||||||
- Plus de traductions sur le nouveau site du COF
|
|
||||||
- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique
|
|
||||||
personnel.
|
|
||||||
- les statistiques K-Fêt remontent à plus d'un an (et le code est simplifié)
|
|
||||||
|
|
||||||
## Version 0.4.1 - 17/01/2020
|
|
||||||
|
|
||||||
- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés
|
|
||||||
aux urls (\#, /...)
|
|
||||||
|
|
||||||
## Version 0.4 - 15/01/2020
|
|
||||||
|
|
||||||
- Corrige un bug d'affichage d'images sur l'interface des petits cours
|
|
||||||
- La page des transferts permet de créer un nombre illimité de transferts en
|
|
||||||
une fois.
|
|
||||||
- Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs
|
|
||||||
- Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil
|
|
||||||
- Certaines opérations sont à nouveau accessibles depuis la session partagée
|
|
||||||
K-Fêt.
|
|
||||||
- Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper
|
|
||||||
- Corrige un crash sur la page des reventes pour les nouveaux participants.
|
|
||||||
- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux
|
|
||||||
|
|
||||||
## Version 0.3.3 - 30/11/2019
|
|
||||||
|
|
||||||
- Corrige un problème de redirection lors de la déconnexion (CAS seulement)
|
|
||||||
- Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF
|
|
||||||
- Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ
|
|
||||||
- Corrige une privilege escalation liée aux sessions partagées en K-Fêt
|
|
||||||
https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240
|
|
||||||
|
|
||||||
## Version 0.3.2 - 04/11/2019
|
|
||||||
|
|
||||||
- Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom
|
|
||||||
|
|
||||||
## Version 0.3.1 - 19/10/2019
|
|
||||||
|
|
||||||
- Bugfix: l'historique des utilisateurices s'affiche à nouveau
|
|
||||||
|
|
||||||
## Version 0.3 - 16/10/2019
|
|
||||||
|
|
||||||
- Comptes extés: lien pour changer son mot de passe sur la page d'accueil
|
|
||||||
- Les utilisateurices non-COF peuvent éditer leur profil
|
|
||||||
- Un peu de pub pour KDEns sur la page d'accueil
|
|
||||||
- Fix erreur 500 sur /bda/revente/<tirage_id>/manage
|
|
||||||
- Si on essaie d'accéder au compte que qqn d'autre on a une 404 (et plus une 403)
|
|
||||||
- On ne peut plus modifier des comptes COF depuis l'interface K-Fêt
|
|
||||||
- Le champ de paiement BdA se fait au niveau des attributions
|
|
||||||
- Affiche un message d'erreur plutôt que de crasher si échec de l'envoi du mail
|
|
||||||
de bienvenue aux nouveaux membres
|
|
||||||
- On peut supprimer des comptes et des articles K-Fêt
|
|
||||||
- Passage à Django2
|
|
||||||
- Dev : on peut désactiver la barre de debug avec une variable shell
|
|
||||||
- Remplace les CSS de Google par des polices de proximité
|
|
||||||
- Passage du site du COF et de la K-Fêt en Wagtail 2.3 et Wagtail-modeltranslation 0.9
|
|
||||||
- Ajoute un lien vers l'administration générale depuis les petits cours
|
|
||||||
- Abandon de l'ancien catalogue BdA (déjà plus utilisé depuis longtemps)
|
|
||||||
- Force l'unicité des logins clipper
|
|
||||||
- Nouveau site du COF en wagtail
|
|
||||||
- Meilleurs affichage des longues listes de spectacles à cocher dans BdA-Revente
|
|
||||||
- Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du
|
|
||||||
COF
|
|
||||||
|
|
||||||
## Version 0.2 - 07/11/2018
|
|
||||||
|
|
||||||
- Corrections de bugs d'interface dans l'inscription aux tirages BdA
|
|
||||||
- On peut annuler une revente à tout moment
|
|
||||||
- Pleiiiiin de tests
|
|
||||||
|
|
||||||
## Version 0.1 - 09/09/2018
|
|
||||||
|
|
||||||
Début de la numérotation des versions, début du changelog
|
|
20
README.md
20
README.md
|
@ -1,4 +1,4 @@
|
||||||
# GestioCOF / GestioBDS
|
# 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)
|
[![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)
|
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
|
||||||
|
@ -38,11 +38,11 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||||
pip install -U pip # parfois nécessaire la première fois
|
pip install -U pip # parfois nécessaire la première fois
|
||||||
pip install -r requirements-devel.txt
|
pip install -r requirements-devel.txt
|
||||||
|
|
||||||
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers
|
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
|
||||||
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
|
`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:
|
pour profiter de façon transparente des mises à jour du fichier:
|
||||||
|
|
||||||
ln -s secret_example.py gestioasso/settings/secret.py
|
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
|
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
|
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
|
||||||
|
@ -186,18 +186,6 @@ Pour lancer les tests :
|
||||||
python manage.py test
|
python manage.py test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Astuces
|
|
||||||
|
|
||||||
- En développement on utilise la django debug toolbar parce que c'est utile pour
|
|
||||||
débuguer les templates ou les requêtes SQL mais des fois c'est pénible parce
|
|
||||||
ça fait ramer GestioCOF (surtout dans wagtail).
|
|
||||||
Vous pouvez la désactiver temporairement en définissant la variable
|
|
||||||
d'environnement `DJANGO_NO_DDT` dans votre shell : par exemple dans
|
|
||||||
bash/zsh/…:
|
|
||||||
```
|
|
||||||
$ export DJANGO_NO_DDT=1
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Documentation utilisateur
|
## Documentation utilisateur
|
||||||
|
|
||||||
|
|
1
TODO_PROD.md
Normal file
1
TODO_PROD.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
- Changer les urls dans les mails "bda-revente" et "bda-shotgun"
|
42
Vagrantfile
vendored
42
Vagrantfile
vendored
|
@ -1,19 +1,47 @@
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
# Configuration de base pour GestioCOF.
|
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||||
# Voir https://docs.vagrantup.com pour plus d'informations.
|
# configures the configuration version (we support older styles for
|
||||||
|
# backwards compatibility). Please don't change it unless you know what
|
||||||
|
# you're doing.
|
||||||
Vagrant.configure(2) do |config|
|
Vagrant.configure(2) do |config|
|
||||||
# On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en
|
# The most common configuration options are documented and commented below.
|
||||||
# production.
|
# For a complete reference, please see the online documentation at
|
||||||
config.vm.box = "debian/contrib-buster64"
|
# https://docs.vagrantup.com.
|
||||||
|
|
||||||
|
config.vm.box = "ubuntu/xenial64"
|
||||||
|
|
||||||
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
|
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
|
||||||
# ordinateur, et le port 8000 avec le port 8000.
|
# ordinateur, et le port 8000 avec le port 8000.
|
||||||
config.vm.network :forwarded_port, guest: 80, host: 8080
|
config.vm.network :forwarded_port, guest: 80, host: 8080
|
||||||
config.vm.network :forwarded_port, guest: 8000, host: 8000
|
config.vm.network :forwarded_port, guest: 8000, host: 8000
|
||||||
|
|
||||||
# Le restes de la configuration (installation de paquets, etc) est géré un
|
# Create a private network, which allows host-only access to the machine
|
||||||
# script shell.
|
# using a specific IP.
|
||||||
|
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||||
|
|
||||||
|
# Provider-specific configuration so you can fine-tune various
|
||||||
|
# backing providers for Vagrant. These expose provider-specific options.
|
||||||
|
# Example for VirtualBox:
|
||||||
|
#
|
||||||
|
# config.vm.provider "virtualbox" do |vb|
|
||||||
|
# # Display the VirtualBox GUI when booting the machine
|
||||||
|
# vb.gui = true
|
||||||
|
#
|
||||||
|
# # Customize the amount of memory on the VM:
|
||||||
|
# vb.memory = "1024"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# View the documentation for the provider you are using for more
|
||||||
|
# information on available options.
|
||||||
|
|
||||||
|
# Enable provisioning with a shell script. Additional provisioners such as
|
||||||
|
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
|
||||||
|
# documentation for more information about their specific syntax and use.
|
||||||
|
# config.vm.provision "shell", inline: <<-SHELL
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y apache2
|
||||||
|
# SHELL
|
||||||
config.vm.provision :shell, path: "provisioning/bootstrap.sh"
|
config.vm.provision :shell, path: "provisioning/bootstrap.sh"
|
||||||
end
|
end
|
||||||
|
|
328
bda/admin.py
Normal file
328
bda/admin.py
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
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 Count, Sum
|
||||||
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bda.models import (
|
||||||
|
Attribution,
|
||||||
|
CategorieSpectacle,
|
||||||
|
ChoixSpectacle,
|
||||||
|
Participant,
|
||||||
|
Quote,
|
||||||
|
Salle,
|
||||||
|
Spectacle,
|
||||||
|
SpectacleRevente,
|
||||||
|
Tirage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyMixin(object):
|
||||||
|
readonly_fields_update = ()
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
readonly_fields = super().get_readonly_fields(request, obj)
|
||||||
|
if obj is None:
|
||||||
|
return readonly_fields
|
||||||
|
else:
|
||||||
|
return readonly_fields + self.readonly_fields_update
|
||||||
|
|
||||||
|
|
||||||
|
class ChoixSpectacleAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"participant": ModelSelect2(url="bda-participant-autocomplete"),
|
||||||
|
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChoixSpectacleInline(admin.TabularInline):
|
||||||
|
model = ChoixSpectacle
|
||||||
|
form = ChoixSpectacleAdminForm
|
||||||
|
sortable_field_name = "priority"
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionTabularAdminForm(forms.ModelForm):
|
||||||
|
listing = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
spectacles = Spectacle.objects.select_related("location")
|
||||||
|
if self.listing is not None:
|
||||||
|
spectacles = spectacles.filter(listing=self.listing)
|
||||||
|
self.fields["spectacle"].queryset = spectacles
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
|
||||||
|
listing = False
|
||||||
|
|
||||||
|
|
||||||
|
class WithListingAttributionTabularAdminForm(AttributionTabularAdminForm):
|
||||||
|
listing = True
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionInline(admin.TabularInline):
|
||||||
|
model = Attribution
|
||||||
|
extra = 0
|
||||||
|
listing = None
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
if self.listing is not None:
|
||||||
|
qs = qs.filter(spectacle__listing=self.listing)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class WithListingAttributionInline(AttributionInline):
|
||||||
|
exclude = ("given",)
|
||||||
|
form = WithListingAttributionTabularAdminForm
|
||||||
|
listing = True
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutListingAttributionInline(AttributionInline):
|
||||||
|
form = WithoutListingAttributionTabularAdminForm
|
||||||
|
listing = False
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantAdminForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
|
||||||
|
"location"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
def nb_places(self, obj):
|
||||||
|
return obj.nb_places
|
||||||
|
|
||||||
|
nb_places.admin_order_field = "nb_places"
|
||||||
|
nb_places.short_description = "Nombre de places"
|
||||||
|
|
||||||
|
def total(self, obj):
|
||||||
|
tot = obj.total
|
||||||
|
if tot:
|
||||||
|
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_filter = ("paid", "tirage")
|
||||||
|
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")
|
||||||
|
form = ParticipantAdminForm
|
||||||
|
|
||||||
|
def send_attribs(self, request, queryset):
|
||||||
|
datatuple = []
|
||||||
|
for member in queryset.all():
|
||||||
|
attribs = member.attributions.all()
|
||||||
|
context = {"member": member.user}
|
||||||
|
shortname = ""
|
||||||
|
if len(attribs) == 0:
|
||||||
|
shortname = "bda-attributions-decus"
|
||||||
|
else:
|
||||||
|
shortname = "bda-attributions"
|
||||||
|
context["places"] = attribs
|
||||||
|
print(context)
|
||||||
|
datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
count = len(queryset.all())
|
||||||
|
if count == 1:
|
||||||
|
message_bit = "1 membre a"
|
||||||
|
plural = ""
|
||||||
|
else:
|
||||||
|
message_bit = "%d membres ont" % count
|
||||||
|
plural = "s"
|
||||||
|
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 "participant" in self.fields:
|
||||||
|
self.fields["participant"].queryset = Participant.objects.select_related(
|
||||||
|
"user", "tirage"
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
|
def paid(self, obj):
|
||||||
|
return obj.participant.paid
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
form = AttributionAdminForm
|
||||||
|
readonly_fields_update = ("spectacle", "participant")
|
||||||
|
|
||||||
|
|
||||||
|
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
||||||
|
form = ChoixSpectacleAdminForm
|
||||||
|
|
||||||
|
def tirage(self, obj):
|
||||||
|
return obj.participant.tirage
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteInline(admin.TabularInline):
|
||||||
|
model = Quote
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleAdmin(admin.ModelAdmin):
|
||||||
|
inlines = [QuoteInline]
|
||||||
|
model = Spectacle
|
||||||
|
list_display = ("title", "date", "tirage", "location", "slots", "price", "listing")
|
||||||
|
list_filter = ("location", "tirage")
|
||||||
|
search_fields = ("title", "location__name")
|
||||||
|
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",)
|
||||||
|
|
||||||
|
|
||||||
|
class SalleAdmin(admin.ModelAdmin):
|
||||||
|
model = Salle
|
||||||
|
search_fields = ("name", "address")
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleReventeAdminForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["confirmed_entry"].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"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Administration des reventes de spectacles
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = SpectacleRevente
|
||||||
|
|
||||||
|
def spectacle(self, obj):
|
||||||
|
"""
|
||||||
|
Raccourci vers le spectacle associé à la revente.
|
||||||
|
"""
|
||||||
|
return obj.attribution.spectacle
|
||||||
|
|
||||||
|
list_display = ("spectacle", "seller", "date", "soldTo")
|
||||||
|
raw_id_fields = ("attribution",)
|
||||||
|
readonly_fields = ("date_tirage",)
|
||||||
|
search_fields = [
|
||||||
|
"attribution__spectacle__title",
|
||||||
|
"seller__user__username",
|
||||||
|
"seller__user__first_name",
|
||||||
|
"seller__user__last_name",
|
||||||
|
]
|
||||||
|
|
||||||
|
actions = ["transfer", "reinit"]
|
||||||
|
actions_on_bottom = True
|
||||||
|
form = SpectacleReventeAdminForm
|
||||||
|
|
||||||
|
def transfer(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Effectue le transfert des reventes pour lesquels on connaît l'acheteur.
|
||||||
|
"""
|
||||||
|
reventes = queryset.exclude(soldTo__isnull=True).all()
|
||||||
|
count = reventes.count()
|
||||||
|
for revente in reventes:
|
||||||
|
attrib = revente.attribution
|
||||||
|
attrib.participant = revente.soldTo
|
||||||
|
attrib.save()
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"%d attribution%s %s été transférée%s avec succès."
|
||||||
|
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer.short_description = "Transférer les reventes sélectionnées"
|
||||||
|
|
||||||
|
def reinit(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Réinitialise les reventes.
|
||||||
|
"""
|
||||||
|
count = queryset.count()
|
||||||
|
for revente in queryset.filter(
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
|
||||||
|
reinit.short_description = "Réinitialiser les reventes sélectionnées"
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CategorieSpectacle)
|
||||||
|
admin.site.register(Spectacle, SpectacleAdmin)
|
||||||
|
admin.site.register(Salle, SalleAdmin)
|
||||||
|
admin.site.register(Participant, ParticipantAdmin)
|
||||||
|
admin.site.register(Attribution, AttributionAdmin)
|
||||||
|
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
|
||||||
|
admin.site.register(Tirage, TirageAdmin)
|
||||||
|
admin.site.register(SpectacleRevente, SpectacleReventeAdmin)
|
103
bda/algorithm.py
Normal file
103
bda/algorithm.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Algorithm(object):
|
||||||
|
|
||||||
|
shows = None
|
||||||
|
ranks = None
|
||||||
|
origranks = None
|
||||||
|
double = None
|
||||||
|
|
||||||
|
def __init__(self, shows, members, choices):
|
||||||
|
"""Initialisation :
|
||||||
|
- on aggrège toutes les demandes pour chaque spectacle dans
|
||||||
|
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.shows = []
|
||||||
|
showdict = {}
|
||||||
|
for show in shows:
|
||||||
|
show.nrequests = 0
|
||||||
|
showdict[show] = show
|
||||||
|
show.requests = []
|
||||||
|
self.shows.append(show)
|
||||||
|
self.ranks = {}
|
||||||
|
self.origranks = {}
|
||||||
|
self.choices = {}
|
||||||
|
next_rank = {}
|
||||||
|
member_shows = {}
|
||||||
|
for member in members:
|
||||||
|
self.ranks[member] = {}
|
||||||
|
self.choices[member] = {}
|
||||||
|
next_rank[member] = 1
|
||||||
|
member_shows[member] = {}
|
||||||
|
for choice in choices:
|
||||||
|
member = choice.participant
|
||||||
|
if choice.spectacle in member_shows[member]:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
member_shows[member][choice.spectacle] = True
|
||||||
|
showdict[choice.spectacle].requests.append(member)
|
||||||
|
showdict[choice.spectacle].nrequests += 2 if choice.double else 1
|
||||||
|
self.ranks[member][choice.spectacle] = next_rank[member]
|
||||||
|
next_rank[member] += 2 if choice.double else 1
|
||||||
|
self.choices[member][choice.spectacle] = choice
|
||||||
|
for member in members:
|
||||||
|
self.origranks[member] = dict(self.ranks[member])
|
||||||
|
|
||||||
|
def IncrementRanks(self, member, currank, increment=1):
|
||||||
|
for show in self.ranks[member]:
|
||||||
|
if self.ranks[member][show] > currank:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __call__(self, seed):
|
||||||
|
random.seed(seed)
|
||||||
|
results = []
|
||||||
|
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)])
|
||||||
|
for member in show.requests:
|
||||||
|
if self.ranks[member][show] == 0:
|
||||||
|
raise RuntimeError(member, show.title)
|
||||||
|
groups[self.ranks[member][show]].append(member)
|
||||||
|
# On passe à l'attribution
|
||||||
|
winners = []
|
||||||
|
losers = []
|
||||||
|
for i in range(1, self.max_group + 1):
|
||||||
|
group = list(groups[i])
|
||||||
|
random.shuffle(group)
|
||||||
|
for member in group:
|
||||||
|
if self.choices[member][show].double: # double
|
||||||
|
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
|
||||||
|
):
|
||||||
|
self.appendResult(winners, member, show)
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
else:
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
self.IncrementRanks(member, i, 2)
|
||||||
|
else: # simple
|
||||||
|
if len(winners) < show.slots:
|
||||||
|
self.appendResult(winners, member, show)
|
||||||
|
else:
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
self.IncrementRanks(member, i)
|
||||||
|
results.append((show, winners, losers))
|
||||||
|
return results
|
171
bda/forms.py
Normal file
171
bda/forms.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
from django import forms
|
||||||
|
from django.forms.models import BaseInlineFormSet
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bda.models import Attribution, Spectacle, SpectacleRevente
|
||||||
|
|
||||||
|
|
||||||
|
class InscriptionInlineFormSet(BaseInlineFormSet):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# self.instance is a Participant object
|
||||||
|
tirage = self.instance.tirage
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
choices = [(sp.pk, str(sp)) for sp in spectacles]
|
||||||
|
self.force_choices("spectacle", choices)
|
||||||
|
|
||||||
|
def force_choices(self, name, choices):
|
||||||
|
"""Set choices of a field.
|
||||||
|
|
||||||
|
As ModelChoiceIterator (default use to get choices of a
|
||||||
|
ModelChoiceField), it appends an empty selection if requested.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for form in self.forms:
|
||||||
|
field = form.fields[name]
|
||||||
|
if field.empty_label is not None:
|
||||||
|
field.choices = [("", field.empty_label)] + choices
|
||||||
|
else:
|
||||||
|
field.choices = choices
|
||||||
|
|
||||||
|
|
||||||
|
class TokenForm(forms.Form):
|
||||||
|
token = forms.CharField(widget=forms.widgets.Textarea())
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
return str(obj.spectacle)
|
||||||
|
|
||||||
|
|
||||||
|
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
def __init__(self, *args, own=True, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.own = own
|
||||||
|
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
label = "{show}{suffix}"
|
||||||
|
suffix = ""
|
||||||
|
if self.own:
|
||||||
|
# C'est notre propre revente : pas besoin de spécifier le vendeur
|
||||||
|
if obj.soldTo is not None:
|
||||||
|
suffix = " -- Vendue à {firstname} {lastname}".format(
|
||||||
|
firstname=obj.soldTo.user.first_name,
|
||||||
|
lastname=obj.soldTo.user.last_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Ce n'est pas à nous : on ne voit jamais l'acheteur
|
||||||
|
suffix = " -- Vendue par {firstname} {lastname}".format(
|
||||||
|
firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name
|
||||||
|
)
|
||||||
|
|
||||||
|
return label.format(show=str(obj.attribution.spectacle), suffix=suffix)
|
||||||
|
|
||||||
|
|
||||||
|
class ResellForm(forms.Form):
|
||||||
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label="",
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnulForm(forms.Form):
|
||||||
|
reventes = ReventeModelMultipleChoiceField(
|
||||||
|
own=True,
|
||||||
|
label="",
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["reventes"].queryset = participant.original_shows.filter(
|
||||||
|
attribution__spectacle__date__gte=timezone.now(),
|
||||||
|
notif_sent=False,
|
||||||
|
soldTo__isnull=True,
|
||||||
|
).select_related("attribution__spectacle", "attribution__spectacle__location")
|
||||||
|
|
||||||
|
|
||||||
|
class InscriptionReventeForm(forms.Form):
|
||||||
|
spectacles = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Spectacle.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, tirage, *args, **kwargs):
|
||||||
|
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):
|
||||||
|
reventes = ReventeModelMultipleChoiceField(
|
||||||
|
own=True,
|
||||||
|
label="",
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
)
|
0
bda/management/commands/__init__.py
Normal file
0
bda/management/commands/__init__.py
Normal file
101
bda/management/commands/loadbdadevdata.py
Normal file
101
bda/management/commands/loadbdadevdata.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
"""
|
||||||
|
Crée deux tirages de test et y inscrit les utilisateurs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
class Command(MyBaseCommand):
|
||||||
|
help = "Crée deux tirages de test et y inscrit les utilisateurs."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# ---
|
||||||
|
# Tirages
|
||||||
|
# ---
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
tirages = Tirage.objects.all()
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Salles
|
||||||
|
# ---
|
||||||
|
|
||||||
|
locations = self.from_json("locations.json", DATA_DIR, Salle)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Spectacles
|
||||||
|
# ---
|
||||||
|
|
||||||
|
def show_callback(show):
|
||||||
|
"""
|
||||||
|
Assigne un tirage, une date et un lieu à un spectacle et décide si
|
||||||
|
les places sont sur listing.
|
||||||
|
"""
|
||||||
|
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.location = random.choice(locations)
|
||||||
|
return show
|
||||||
|
|
||||||
|
shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Inscriptions
|
||||||
|
# ---
|
||||||
|
|
||||||
|
self.stdout.write("Inscription des utilisateurs aux tirages")
|
||||||
|
ChoixSpectacle.objects.all().delete()
|
||||||
|
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)
|
||||||
|
shows = random.sample(
|
||||||
|
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"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ChoixSpectacle.objects.bulk_create(choices)
|
||||||
|
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# On lance le premier tirage
|
||||||
|
# ---
|
||||||
|
|
||||||
|
self.stdout.write("Lancement du premier tirage")
|
||||||
|
do_tirage(tirages[0], "dummy_token")
|
49
bda/management/commands/manage_reventes.py
Normal file
49
bda/management/commands/manage_reventes.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
"""
|
||||||
|
Gestion en ligne de commande des reventes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
leave_locale_alone = True
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
now = timezone.now()
|
||||||
|
reventes = SpectacleRevente.objects.all()
|
||||||
|
for revente in reventes:
|
||||||
|
# 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(
|
||||||
|
"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))
|
||||||
|
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")
|
33
bda/management/commands/sendrappels.py
Normal file
33
bda/management/commands/sendrappels.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""
|
||||||
|
Gestion en ligne de commande des mails de rappel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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.\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)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for show in shows:
|
||||||
|
show.send_rappel()
|
||||||
|
self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show)
|
||||||
|
if not shows:
|
||||||
|
self.stdout.write("Aucun mail à envoyer.")
|
26
bda/management/data/locations.json
Normal file
26
bda/management/data/locations.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Cour\u00f4",
|
||||||
|
"address": "45 rue d'Ulm, cour\u00f4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "K-F\u00eat",
|
||||||
|
"address": "45 rue d'Ulm, escalier C, niveau -1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Th\u00e9\u00e2tre",
|
||||||
|
"address": "45 rue d'Ulm, escalier C, niveau -1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cours Pasteur",
|
||||||
|
"address": "45 rue d'Ulm, cours pasteur"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Salle des actes",
|
||||||
|
"address": "45 rue d'Ulm, escalier A, niveau 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Amphi Rataud",
|
||||||
|
"address": "45 rue d'Ulm, NIR, niveau PB"
|
||||||
|
}
|
||||||
|
]
|
100
bda/management/data/shows.json
Normal file
100
bda/management/data/shows.json
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "Jazz / Funk",
|
||||||
|
"title": "Un super concert",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Debout",
|
||||||
|
"slots": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Homemade",
|
||||||
|
"title": "Une super pi\u00e8ce",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Assises",
|
||||||
|
"slots": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Plein air, soleil, bonne musique",
|
||||||
|
"title": "Concert pour la f\u00eate de la musique",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "Debout, attention \u00e0 la fontaine",
|
||||||
|
"slots": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Sous le regard s\u00e9v\u00e8re de Louis Pasteur",
|
||||||
|
"title": "Op\u00e9ra sans d\u00e9cors",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "Assis sur l'herbe",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Buffet \u00e0 la fin",
|
||||||
|
"title": "Concert Trouv\u00e8re",
|
||||||
|
"price": 20.0,
|
||||||
|
"slots_description": "Assises",
|
||||||
|
"slots": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Vive les maths",
|
||||||
|
"title": "Dessin \u00e0 la craie sur tableau noir",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Assises, tablette pour prendre des notes",
|
||||||
|
"slots": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Une pi\u00e8ce \u00e0 un personnage",
|
||||||
|
"title": "D\u00e9cors, d\u00e9montage en musique",
|
||||||
|
"price": 0.0,
|
||||||
|
"slots_description": "Assises",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Annulera, annulera pas\u00a0?",
|
||||||
|
"title": "La Nuit",
|
||||||
|
"price": 27.0,
|
||||||
|
"slots_description": "",
|
||||||
|
"slots": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Le boum fait sa carte blanche",
|
||||||
|
"title": "Turbomix",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Debout les mains en l'air",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Unique repr\u00e9sentation",
|
||||||
|
"title": "Carinettes et trombone",
|
||||||
|
"price": 15.0,
|
||||||
|
"slots_description": "Chaises ikea",
|
||||||
|
"slots": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Suivi d'une jam session",
|
||||||
|
"title": "Percussion sur rondins",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "B\u00fbches",
|
||||||
|
"slots": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "\u00c9preuve sportive et artistique",
|
||||||
|
"title": "Bassin aux ernests, nage libre",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "Humides",
|
||||||
|
"slots": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Sonore",
|
||||||
|
"title": "Chant du barde",
|
||||||
|
"price": 13.0,
|
||||||
|
"slots_description": "Ne venez pas",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Cocorico",
|
||||||
|
"title": "Chant du coq",
|
||||||
|
"price": 4.0,
|
||||||
|
"slots_description": "bancs",
|
||||||
|
"slots": 15
|
||||||
|
}
|
||||||
|
]
|
206
bda/migrations/0001_initial.py
Normal file
206
bda/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
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")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("priority",),
|
||||||
|
"verbose_name": "voeu",
|
||||||
|
"verbose_name_plural": "voeux",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
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")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="choixspectacle", unique_together=set([("participant", "spectacle")])
|
||||||
|
),
|
||||||
|
]
|
112
bda/migrations/0002_add_tirage.py
Normal file
112
bda/migrations/0002_add_tirage.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
def fill_tirage_fields(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Create a `Tirage` to fill new field `tirage` of `Participant`
|
||||||
|
and `Spectacle` already existing.
|
||||||
|
"""
|
||||||
|
Participant = apps.get_model("bda", "Participant")
|
||||||
|
Spectacle = apps.get_model("bda", "Spectacle")
|
||||||
|
Tirage = apps.get_model("bda", "Tirage")
|
||||||
|
|
||||||
|
# These querysets only contains instances not linked to any `Tirage`.
|
||||||
|
participants = Participant.objects.filter(tirage=None)
|
||||||
|
spectacles = Spectacle.objects.filter(tirage=None)
|
||||||
|
|
||||||
|
if not participants.count() and not spectacles.count():
|
||||||
|
# No need to create a "trash" tirage.
|
||||||
|
return
|
||||||
|
|
||||||
|
tirage = Tirage.objects.create(
|
||||||
|
title="Tirage de test (migration)",
|
||||||
|
active=False,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
participants.update(tirage=tirage)
|
||||||
|
spectacles.update(tirage=tirage)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0001_initial")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="spectacle",
|
||||||
|
name="tirage",
|
||||||
|
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
|
||||||
|
),
|
||||||
|
]
|
22
bda/migrations/0003_update_tirage_and_spectacle.py
Normal file
22
bda/migrations/0003_update_tirage_and_spectacle.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0002_add_tirage")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
]
|
27
bda/migrations/0004_mails-rappel.py
Normal file
27
bda/migrations/0004_mails-rappel.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
29
bda/migrations/0005_encoding.py
Normal file
29
bda/migrations/0005_encoding.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0004_mails-rappel")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="spectacle",
|
||||||
|
name="rappel_sent",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
null=True, verbose_name="Mail de rappel envoy\xe9", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
34
bda/migrations/0006_add_tirage_switch.py
Normal file
34
bda/migrations/0006_add_tirage_switch.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
|
Tirage = apps.get_model("bda", "Tirage")
|
||||||
|
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.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0005_encoding")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField("tirage", "token", "tokens"),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="enable_do_tirage",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
|
]
|
100
bda/migrations/0007_extends_spectacle.py
Normal file
100
bda/migrations/0007_extends_spectacle.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0006_add_tirage_switch")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Cat\xe9gorie"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
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")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="spectacle",
|
||||||
|
options={"ordering": ("date", "title"), "verbose_name": "Spectacle"},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(model_name="spectacle", name="priority"),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="spectacle",
|
||||||
|
name="ext_link",
|
||||||
|
field=models.CharField(
|
||||||
|
max_length=500,
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="enable_do_tirage",
|
||||||
|
field=models.BooleanField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="quote",
|
||||||
|
name="spectacle",
|
||||||
|
field=models.ForeignKey(to="bda.Spectacle", on_delete=models.CASCADE),
|
||||||
|
),
|
||||||
|
]
|
110
bda/migrations/0008_py3.py
Normal file
110
bda/migrations/0008_py3.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0007_extends_spectacle")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="spectacle",
|
||||||
|
name="listing",
|
||||||
|
field=models.BooleanField(verbose_name="Les places sont sur listing"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="active",
|
||||||
|
field=models.BooleanField(verbose_name="Tirage actif", default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="fermeture",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
verbose_name="Date et heure de fermerture du tirage"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="ouverture",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
87
bda/migrations/0009_revente.py
Normal file
87
bda/migrations/0009_revente.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0008_py3")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Revente"},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="spectaclerevente",
|
||||||
|
name="soldTo",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
to="bda.Participant",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="Vendue à",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
35
bda/migrations/0010_spectaclerevente_shotgun.py
Normal file
35
bda/migrations/0010_spectaclerevente_shotgun.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
revente.shotgun = is_expired or is_direct
|
||||||
|
revente.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0009_revente")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="spectaclerevente",
|
||||||
|
name="shotgun",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name="Disponible imm\xe9diatement"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
|
]
|
19
bda/migrations/0011_tirage_appear_catalogue.py
Normal file
19
bda/migrations/0011_tirage_appear_catalogue.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0010_spectaclerevente_shotgun")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="appear_catalogue",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name="Tirage à afficher dans le catalogue"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
31
bda/migrations/0012_notif_time.py
Normal file
31
bda/migrations/0012_notif_time.py
Normal file
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
51
bda/migrations/0012_swap_double_choice.py
Normal file
51
bda/migrations/0012_swap_double_choice.py
Normal file
|
@ -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"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
12
bda/migrations/0013_merge_20180524_2123.py
Normal file
12
bda/migrations/0013_merge_20180524_2123.py
Normal file
|
@ -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 = []
|
0
bda/migrations/__init__.py
Normal file
0
bda/migrations/__init__.py
Normal file
450
bda/models.py
Normal file
450
bda/models.py
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
import calendar
|
||||||
|
import random
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
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.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"},
|
||||||
|
)
|
||||||
|
return generic
|
||||||
|
|
||||||
|
|
||||||
|
class Tirage(models.Model):
|
||||||
|
title = models.CharField("Titre", max_length=300)
|
||||||
|
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
|
||||||
|
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Salle(models.Model):
|
||||||
|
name = models.CharField("Nom", max_length=300)
|
||||||
|
address = models.TextField("Adresse")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class CategorieSpectacle(models.Model):
|
||||||
|
name = models.CharField("Nom", max_length=100, unique=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Catégorie"
|
||||||
|
|
||||||
|
|
||||||
|
class Spectacle(models.Model):
|
||||||
|
title = models.CharField("Titre", max_length=300)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Spectacle"
|
||||||
|
ordering = ("date", "title")
|
||||||
|
|
||||||
|
def timestamp(self):
|
||||||
|
return "%d" % calendar.timegm(self.date.utctimetuple())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s, %s, %.02f€" % (
|
||||||
|
self.title,
|
||||||
|
formats.localize(timezone.template_localtime(self.date)),
|
||||||
|
self.location,
|
||||||
|
self.price,
|
||||||
|
)
|
||||||
|
|
||||||
|
def getImgUrl(self):
|
||||||
|
"""
|
||||||
|
Cette fonction permet d'obtenir l'URL de l'image, si elle existe
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.image.url
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_rappel(self):
|
||||||
|
"""
|
||||||
|
Envoie un mail de rappel à toutes les personnes qui ont une place pour
|
||||||
|
ce spectacle.
|
||||||
|
"""
|
||||||
|
# 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()
|
||||||
|
)
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
for member in members
|
||||||
|
]
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
# On enregistre le fait que l'envoi a bien eu lieu
|
||||||
|
self.rappel_sent = timezone.now()
|
||||||
|
self.save()
|
||||||
|
# On renvoie la liste des destinataires
|
||||||
|
return members
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_past(self):
|
||||||
|
return self.date < timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
class Quote(models.Model):
|
||||||
|
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
|
||||||
|
text = models.TextField("Citation")
|
||||||
|
author = models.CharField("Auteur", max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
PAYMENT_TYPES = (
|
||||||
|
("cash", "Cash"),
|
||||||
|
("cb", "CB"),
|
||||||
|
("cheque", "Chèque"),
|
||||||
|
("autre", "Autre"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
paid = models.BooleanField("A payé", default=False)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s" % (self.user, self.tirage.title)
|
||||||
|
|
||||||
|
|
||||||
|
DOUBLE_CHOICES = (
|
||||||
|
("1", "1 place"),
|
||||||
|
("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"
|
||||||
|
)
|
||||||
|
priority = models.PositiveIntegerField("Priorité")
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("priority",)
|
||||||
|
unique_together = (("participant", "spectacle"),)
|
||||||
|
verbose_name = "voeu"
|
||||||
|
verbose_name_plural = "voeux"
|
||||||
|
|
||||||
|
|
||||||
|
class Attribution(models.Model):
|
||||||
|
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
|
||||||
|
spectacle = models.ForeignKey(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleRevente(models.Model):
|
||||||
|
attribution = models.OneToOneField(
|
||||||
|
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
|
||||||
|
)
|
||||||
|
seller = models.ForeignKey(
|
||||||
|
Participant,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="Vendeur",
|
||||||
|
related_name="original_shows",
|
||||||
|
)
|
||||||
|
soldTo = models.ForeignKey(
|
||||||
|
Participant,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="Vendue à",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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."""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
for participant in inscrits
|
||||||
|
]
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
self.notif_sent = True
|
||||||
|
self.notif_time = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def mail_shotgun(self):
|
||||||
|
"""
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
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, 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.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 = []
|
||||||
|
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
48
bda/static/css/bda.css
Normal file
48
bda/static/css/bda.css
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
form#tokenform {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
form#tokenform textarea {
|
||||||
|
font-size: 2em;
|
||||||
|
width: 350px;
|
||||||
|
height: 200px;
|
||||||
|
font-family: 'Droif Serif', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wft ?
|
||||||
|
input {
|
||||||
|
width: 400px;
|
||||||
|
font-size: 2em;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
ul.losers {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.losers li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.details {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 0px solid black;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.attribresult {
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectacle-passe {
|
||||||
|
opacity:0.5;
|
||||||
|
}
|
BIN
bda/static/fonts/josefinsans.ttf
Normal file
BIN
bda/static/fonts/josefinsans.ttf
Normal file
Binary file not shown.
28
bda/templates/bda-attrib-extra.html
Normal file
28
bda/templates/bda-attrib-extra.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "bda-attrib.html" %}
|
||||||
|
|
||||||
|
{% block extracontent %}
|
||||||
|
|
||||||
|
<h2>Attributions (détails)</h2>
|
||||||
|
<h3 class="horizontal-title">Token :</h3>
|
||||||
|
<pre>{{ token }}</pre>
|
||||||
|
<h3 class="horizontal-title">Placés : {{ total_slots }} ; Déçus : {{ total_losers }}</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{% for member, shows in members2 %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ member.user.get_full_name }}</td>
|
||||||
|
<td>{{ member.user.email }}</td>
|
||||||
|
<td>Total: {{ member.total }}€</td>
|
||||||
|
<td style="width: 120px;"></td>
|
||||||
|
</tr>
|
||||||
|
{% for show in shows %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ show }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
49
bda/templates/bda-attrib.html
Normal file
49
bda/templates/bda-attrib.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Attributions</h2>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<p class="success">Pour raison de sécurité, le lancement du tirage
|
||||||
|
a été désactivé. Vous pouvez le réactiver dans
|
||||||
|
l'<a href="{% url "admin:index" %}">interface admin</a></p>
|
||||||
|
|
||||||
|
<h3 class="horizontal-title">Token :</h3>
|
||||||
|
<pre>{{ token }}</pre>
|
||||||
|
<h3 class="horizontal-title">Placés : {{ total_slots }} ; Déçus : {{ total_losers }}</h3>
|
||||||
|
{% if user.profile.is_buro %}<h3 class="horizontal-title">Déficit total: {{ total_deficit }} €, Opéra: {{ opera_deficit }} €, Attribué: {{ total_sold }} €</h3>{% endif %}
|
||||||
|
<h3 class="horizontal-title">Temps de calcul : {{ duration|floatformat }}s</h3>
|
||||||
|
|
||||||
|
{% for show, members, losers in results %}
|
||||||
|
<div class="attribresult">
|
||||||
|
<h3 class="horizontal-title">{{ show.title }} - {{ show.date }} @ {{ show.location }}</h3>
|
||||||
|
<p>
|
||||||
|
<strong>{{ show.nrequests }} demandes pour {{ show.slots }} places</strong>
|
||||||
|
{{ show.price }}€ par place{% if user.profile.is_buro and show.nrequests < show.slots %}, {{ show.deficit }}€ de déficit{% endif %}
|
||||||
|
</p>
|
||||||
|
Places :
|
||||||
|
<ul>
|
||||||
|
{% for member, rank, origrank, double in members %}
|
||||||
|
<li>{{ member.user.get_full_name }} <span class="details">(souhait {{ origrank }} — rang {{ rank }})</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
Déçus :
|
||||||
|
{% if not losers %}/{% else %}
|
||||||
|
<ul class="losers">
|
||||||
|
{% for member, rank, origrank, double in losers %}
|
||||||
|
{% if not forloop.first %} ; {% endif %}
|
||||||
|
<li>{{ member.user.get_full_name }} <span class="details">(souhait {{ origrank }} — rang {{ rank }})</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% block extracontent %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
7
bda/templates/bda-emails.html
Normal file
7
bda/templates/bda-emails.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>{{ spectacle }}</h2>
|
||||||
|
<textarea style="width: 100%; height: 100px; margin-top: 10px;">
|
||||||
|
{% for attrib in spectacle.attribues.all %}{{ attrib.participant.user.email }}, {% endfor %}</textarea>
|
||||||
|
{% endblock %}
|
13
bda/templates/bda-token.html
Normal file
13
bda/templates/bda-token.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Tirage au sort du BdA</h2>
|
||||||
|
<form action="" method="post" id="tokenform">
|
||||||
|
{% csrf_token %}
|
||||||
|
<strong>La graine :</strong>
|
||||||
|
<div>
|
||||||
|
{{ form.token }}
|
||||||
|
</div>
|
||||||
|
<input type="submit" onsubmit="return confirm('Voulez vous lancer le Tirage maintenant ?\n\nCECI REMETTRA À ZÉRO TOUTES LES DONNÉES si le tirage a déjà été lancé.')" value="Go" />
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
8
bda/templates/bda-unpaid.html
Normal file
8
bda/templates/bda-unpaid.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Impayés</h2>
|
||||||
|
<textarea style="width: 100%; height: 100px; margin-top: 10px;">
|
||||||
|
{% for participant in unpaid %}{{ participant.user.email }}, {% endfor %}</textarea>
|
||||||
|
<h3>Total : {{ unpaid|length }}</h3>
|
||||||
|
{% endblock %}
|
53
bda/templates/bda/etat-places.html
Normal file
53
bda/templates/bda/etat-places.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>État des inscriptions BdA</h2>
|
||||||
|
<table class="table table-striped etat-bda">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Places</th>
|
||||||
|
<th data-sort="int">Demandes</th>
|
||||||
|
<th data-sort="float">Ratio</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for spectacle in spectacles %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ spectacle.title }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.slots }}">{{ spectacle.slots }} places</td>
|
||||||
|
<td data-sort-value="{{ spectacle.total }}">{{ spectacle.total }} demandes</td>
|
||||||
|
<td data-sort-value="{{ spectacle.ratio |stringformat:".3f" }}"
|
||||||
|
class={% if spectacle.ratio < 1.0 %}
|
||||||
|
"greenratio"
|
||||||
|
{% else %}
|
||||||
|
{% if spectacle.ratio < 2.5 %}
|
||||||
|
"orangeratio"
|
||||||
|
{% else %}
|
||||||
|
"redratio"
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}>
|
||||||
|
{{ spectacle.ratio |floatformat }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<span class="bda-prix">
|
||||||
|
Total : {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
|
||||||
|
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
|
||||||
|
</span>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("table.etat-bda").stupidtable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
41
bda/templates/bda/inscription-formset.html
Normal file
41
bda/templates/bda/inscription-formset.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% load bootstrap %}
|
||||||
|
{{ formset.non_form_errors.as_ul }}
|
||||||
|
<table id="bda_formset" class="form table">
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% for form in formset.forms %}
|
||||||
|
{% if forloop.first %}
|
||||||
|
<thead><tr>
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||||
|
<th class="bda-field-{{ field.name }}">{{ field.label|safe|capfirst }}</th>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<th><sup>1</sup></th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody class="bda_formset_content">
|
||||||
|
{% endif %}
|
||||||
|
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||||
|
<td class="bda-field-{{ field.name }}">
|
||||||
|
{% if forloop.first %}
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{{ field.errors.as_ul }}
|
||||||
|
{{ field | bootstrap }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<td class="tools-cell"><div class="tools">
|
||||||
|
<a href="javascript://" class="glyphicon glyphicon-sort drag-btn" title="Déplacer"></a>
|
||||||
|
<input type="checkbox" name="{{ form.DELETE.html_name }}" style="display: none;" />
|
||||||
|
<input type="hidden" name="{{ form.priority.html_name }}" style="{{ form.priority.value }}" />
|
||||||
|
<a href="javascript://" class="glyphicon glyphicon-remove remove-btn" title="Supprimer"></a>
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
123
bda/templates/bda/inscription-tirage.html
Normal file
123
bda/templates/bda/inscription-tirage.html
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
|
||||||
|
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
var django = {
|
||||||
|
"jQuery": jQuery.noConflict(true)
|
||||||
|
};
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
cloneMore = function(selector, type) {
|
||||||
|
var newElement = $(selector).clone(true);
|
||||||
|
var total = $('#id_' + type + '-TOTAL_FORMS').val();
|
||||||
|
newElement.find(':input').each(function() {
|
||||||
|
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
|
||||||
|
var id = 'id_' + name;
|
||||||
|
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
|
||||||
|
});
|
||||||
|
newElement.find('label').each(function() {
|
||||||
|
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
|
||||||
|
$(this).attr('for', newFor);
|
||||||
|
});
|
||||||
|
// Cloning <select> element doesn't properly propagate the default
|
||||||
|
// selected <option>, so we set it manually.
|
||||||
|
newElement.find('select').each(function (index, select) {
|
||||||
|
var defaultValue = $(select).find('option[selected]').val();
|
||||||
|
if (typeof defaultValue !== 'undefined') {
|
||||||
|
$(select).val(defaultValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
total++;
|
||||||
|
$('#id_' + type + '-TOTAL_FORMS').val(total);
|
||||||
|
$(selector).after(newElement);
|
||||||
|
}
|
||||||
|
deleteButtonHandler = function(elem) {
|
||||||
|
elem.bind("click", function() {
|
||||||
|
var deleteInput = $(this).prev().prev(),
|
||||||
|
form = $(this).parents(".dynamic-form").first();
|
||||||
|
// callback
|
||||||
|
// toggle options.predeleteCssClass and toggle checkbox
|
||||||
|
if (form.hasClass("has_original")) {
|
||||||
|
form.toggleClass("predelete");
|
||||||
|
if (deleteInput.attr("checked")) {
|
||||||
|
deleteInput.attr("checked", false);
|
||||||
|
} else {
|
||||||
|
deleteInput.attr("checked", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// callback
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$(document).ready(function($) {
|
||||||
|
deleteButtonHandler($("table#bda_formset tbody.bda_formset_content").find("a.remove-btn"));
|
||||||
|
$("table#bda_formset tbody.bda_formset_content").sortable({
|
||||||
|
handle: "a.drag-btn",
|
||||||
|
items: "tr",
|
||||||
|
axis: "y",
|
||||||
|
appendTo: 'body',
|
||||||
|
forceHelperSize: true,
|
||||||
|
placeholder: 'ui-sortable-placeholder',
|
||||||
|
forcePlaceholderSize: true,
|
||||||
|
containment: 'form#bda_form',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
start: function(evt, ui) {
|
||||||
|
var template = "",
|
||||||
|
len = ui.item.children("td").length;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
template += "<td style='height:" + (ui.item.outerHeight() + 12 ) + "px' class='placeholder-cell'> </td>"
|
||||||
|
}
|
||||||
|
template += "";
|
||||||
|
ui.placeholder.html(template);
|
||||||
|
},
|
||||||
|
stop: function(evt, ui) {
|
||||||
|
// Toggle div.table twice to remove webkits border-spacing bug
|
||||||
|
$("table#bda_formset").toggle().toggle();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$("#bda_form").bind("submit", function(){
|
||||||
|
var sortable_field_name = "priority";
|
||||||
|
var i = 1;
|
||||||
|
$(".bda_formset_content").find("tr").each(function(){
|
||||||
|
var fields = $(this).find("td :input[value]"),
|
||||||
|
select = $(this).find("td select");
|
||||||
|
if (select.val() && fields.serialize()) {
|
||||||
|
$(this).find("input[name$='"+sortable_field_name+"']").val(i);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
})(django.jQuery);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2 class="no-bottom-margin">Inscription au tirage au sort du BdA</h2>
|
||||||
|
<form class="form-horizontal" id="bda_form" method="post" action="{% url 'bda-tirage-inscription' tirage.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% include "bda/inscription-formset.html" %}
|
||||||
|
<div class="inscription-bottom">
|
||||||
|
<span class="bda-prix">Prix total actuel : {{ total_price }}€</span>
|
||||||
|
<div class="pull-right">
|
||||||
|
<input type="button" class="btn btn-default" value="Ajouter un autre vœu" id="add_more">
|
||||||
|
<script>
|
||||||
|
django.jQuery('#add_more').click(function() {
|
||||||
|
cloneMore('tbody.bda_formset_content tr:last-child', 'choixspectacle_set');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
|
||||||
|
<input type="submit" class="btn btn-primary" value="Enregistrer" />
|
||||||
|
</div>
|
||||||
|
<p class="footnotes">
|
||||||
|
<sup>1</sup>: cette liste de vœux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque vœu.<br />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
48
bda/templates/bda/mails-rappel.html
Normal file
48
bda/templates/bda/mails-rappel.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Mails de rappels</h2>
|
||||||
|
{% if sent %}
|
||||||
|
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
|
||||||
|
<ul>
|
||||||
|
{% for member in members %}
|
||||||
|
<li>{{ member.get_full_name }} ({{ member.email }})</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<h3>Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }} ?</h3>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="empty-form">
|
||||||
|
{% if not sent %}
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="pull-right">
|
||||||
|
<input class="btn btn-primary" type="submit" value="Envoyer" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr \>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<em>Note :</em> le template de ce mail peut être modifié à
|
||||||
|
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr \>
|
||||||
|
|
||||||
|
<h3>Forme des mails</h3>
|
||||||
|
|
||||||
|
<h4>Une seule place</h4>
|
||||||
|
{% for part in exemple_mail_1place %}
|
||||||
|
<pre>{{ part }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<h4>Deux places</h4>
|
||||||
|
{% for part in exemple_mail_2places %}
|
||||||
|
<pre>{{ part }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
73
bda/templates/bda/participants.html
Normal file
73
bda/templates/bda/participants.html
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>{{ spectacle }}</h2>
|
||||||
|
<table class='table table-striped etat-bda'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="string">Nom</th>
|
||||||
|
<th data-sort="int">Places</th>
|
||||||
|
<th data-sort="string">Adresse Mail</th>
|
||||||
|
<th data-sort="string">Payé</th>
|
||||||
|
<th data-sort="string">Donné</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for participant in participants %}
|
||||||
|
<tr>
|
||||||
|
<td data-sort-value="{{ participan.name}}">{{participant.name}}</td>
|
||||||
|
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
|
||||||
|
<td data-sort-value="{{participant.email}}">{{participant.email}}</td>
|
||||||
|
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
|
||||||
|
{% if participant.paid %}Oui{% else %}Non{%endif%}
|
||||||
|
</td>
|
||||||
|
<td data-sort-value="{{participant.given}}" class={%if participant.given == participant.nb_places %}"greenratio"
|
||||||
|
{%elif participant.given == 0%}"redratio"
|
||||||
|
{%else%}"orangeratio"
|
||||||
|
{%endif%}>
|
||||||
|
{% if participant.given == participant.nb_places %}Oui
|
||||||
|
{% elif participant.given == 0 %}Non
|
||||||
|
{% else %}{{participant.given}}/{{participant.nb_places}}
|
||||||
|
{%endif%}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
|
||||||
|
<pre id="export-mails" style="display:none">{% spaceless %}
|
||||||
|
{% for participant in participants %}{{ participant.email }}, {% endfor %}
|
||||||
|
{% endspaceless %}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
|
||||||
|
<pre id="export-salle" style="display:none">{% spaceless %}
|
||||||
|
{% for participant in participants %}{{ participant.name }} : {{ participant.nb_places }} place{{ participant.nb_places|pluralize }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endspaceless %}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
|
||||||
|
<script>
|
||||||
|
function toggle(id) {
|
||||||
|
var pre = document.getElementById(id) ;
|
||||||
|
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("table.etat-bda").stupidtable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
14
bda/templates/bda/resume-inscription-tirage.html
Normal file
14
bda/templates/bda/resume-inscription-tirage.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
{% if choices %}
|
||||||
|
<h3>Vos vœux:</h3>
|
||||||
|
<ol>
|
||||||
|
{% for choice in choices %}
|
||||||
|
<li>{{ choice.spectacle }}{% if choice.double %} (deux places{% if autoquit %}, abandon automatique{% endif %}){% endif %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% else %}
|
||||||
|
<h3>Vous n'avez enregistré aucun vœu pour le tirage au sort</h3>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
24
bda/templates/bda/resume_places.html
Normal file
24
bda/templates/bda/resume_places.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2><strong>Places attribuées</strong></h3>
|
||||||
|
{% if places %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
{% for place in places %}
|
||||||
|
<tr>
|
||||||
|
<td>{{place.spectacle.title}}</td>
|
||||||
|
<td>{{place.spectacle.location}}</td>
|
||||||
|
<td>{{place.spectacle.date}}</td>
|
||||||
|
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
|
||||||
|
<br/>
|
||||||
|
<p>Ne manque pas un spectacle avec le
|
||||||
|
<a href="{% url "calendar" %}">calendrier
|
||||||
|
automatique !</a></p>
|
||||||
|
{% else %}
|
||||||
|
<h3>Vous n'avez aucune place :(</h3>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
20
bda/templates/bda/revente/confirm-shotgun.html
Normal file
20
bda/templates/bda/revente/confirm-shotgun.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
|
||||||
|
{%block realcontent %}
|
||||||
|
<h2>Rachat d'une place</h2>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<pre>
|
||||||
|
Bonjour !
|
||||||
|
|
||||||
|
Je souhaiterais racheter ta place pour {{spectacle.title}} le {{spectacle.date}} ({{spectacle.location}}) à {{spectacle.price}}€.
|
||||||
|
Contacte-moi si tu es toujours intéressé-e !
|
||||||
|
|
||||||
|
{{user.get_full_name}} ({{user.email}})
|
||||||
|
</pre>
|
||||||
|
<input type="submit" class="btn btn-primary pull-right" value="Envoyer">
|
||||||
|
</form>
|
||||||
|
<p class="bda-prix">Note : ce mail sera envoyé à une personne au hasard revendant sa place.</p>
|
||||||
|
{%endblock%}
|
9
bda/templates/bda/revente/confirmed.html
Normal file
9
bda/templates/bda/revente/confirmed.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Inscription à une revente</h2>
|
||||||
|
<p class="success"> Votre inscription a bien été enregistrée !</p>
|
||||||
|
<p>Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}.
|
||||||
|
|
||||||
|
{% endblock %}
|
8
bda/templates/bda/revente/mail-success.html
Normal file
8
bda/templates/bda/revente/mail-success.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Revente de place</h2>
|
||||||
|
<p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
|
||||||
|
{% endblock %}
|
90
bda/templates/bda/revente/manage.html
Normal file
90
bda/templates/bda/revente/manage.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Gestion des places que je revends</h2>
|
||||||
|
{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %}
|
||||||
|
|
||||||
|
{% if resellform.attributions %}
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h3>Places non revendues</h3>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<div class="bootstrap-form-reduce">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ resellform|bootstrap }}
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if annul_reventes or overdue %}
|
||||||
|
<h3>Places en cours de revente</h3>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% if annul_reventes %}
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
Vous pouvez annuler les places mises en vente il y a moins d'une heure.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class='form-group'>
|
||||||
|
<div class='multiple-checkbox'>
|
||||||
|
<ul>
|
||||||
|
{% for revente in annul_reventes %}
|
||||||
|
<li>{{ revente.tag }} {{ revente.choice_label }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for attrib in overdue %}
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" style="visibility:hidden">
|
||||||
|
{{ attrib.spectacle }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if annul_reventes %}
|
||||||
|
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if sold_reventes %}
|
||||||
|
<h3>Places revendues</h3>
|
||||||
|
<form action="" method="post">
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<div class="bootstrap-form-reduce">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ soldform|bootstrap }}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
|
||||||
|
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if not resell_attributions and not annul_attributions and not overdue and not sold_reventes %}
|
||||||
|
<p>Plus de reventes possibles !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
6
bda/templates/bda/revente/none.html
Normal file
6
bda/templates/bda/revente/none.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>BdA-Revente</h2>
|
||||||
|
<p>Il n'y a plus de places en revente pour ce spectacle, désolé !</p>
|
||||||
|
{% endblock %}
|
6
bda/templates/bda/revente/notpaid.html
Normal file
6
bda/templates/bda/revente/notpaid.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2><strong>Nope</strong></h2>
|
||||||
|
<p>Avant de revendre des places, il faut aller les payer !</p>
|
||||||
|
{% endblock %}
|
14
bda/templates/bda/revente/shotgun.html
Normal file
14
bda/templates/bda/revente/shotgun.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Places disponibles immédiatement</h2>
|
||||||
|
{% if shotgun %}
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
{% for spectacle in shotgun %}
|
||||||
|
<li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p> Pas de places disponibles immédiatement, désolé !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
46
bda/templates/bda/revente/subscribe.html
Normal file
46
bda/templates/bda/revente/subscribe.html
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Inscriptions pour BdA-Revente</h2>
|
||||||
|
<form action="" class="form-horizontal" method="post">
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
Cochez les spectacles pour lesquels vous souhaitez recevoir un
|
||||||
|
notification quand une place est disponible en revente. <br />
|
||||||
|
Lorsque vous validez vos choix, si un tirage au sort est en cours pour
|
||||||
|
un des spectacles que vous avez sélectionné, vous serez automatiquement
|
||||||
|
inscrit à ce tirage.
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
onClick="select(true)">Tout sélectionner</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
onClick="select(false)">Tout désélectionner</button>
|
||||||
|
|
||||||
|
<div class="multiple-checkbox">
|
||||||
|
<ul>
|
||||||
|
{% for checkbox in form.spectacles %}
|
||||||
|
<li>{{ checkbox }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
value="S'inscrire pour les places sélectionnées">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script language="JavaScript">
|
||||||
|
function select(check) {
|
||||||
|
checkboxes = document.getElementsByName("spectacles");
|
||||||
|
for(var i=0, n=checkboxes.length; i < n; i++) {
|
||||||
|
checkboxes[i].checked = check;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
52
bda/templates/bda/revente/tirages.html
Normal file
52
bda/templates/bda/revente/tirages.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Tirages au sort de reventes</h2>
|
||||||
|
|
||||||
|
{% if annulform.reventes %}
|
||||||
|
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
Vous pouvez vous désinscrire des reventes suivantes tant que le tirage n'a
|
||||||
|
pas eu lieu.
|
||||||
|
</div>
|
||||||
|
<div class="bootstrap-form-reduce">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ annulform|bootstrap }}
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
name="annul"
|
||||||
|
value="Se désinscrire des tirages sélectionnés">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if subform.reventes %}
|
||||||
|
|
||||||
|
<h3>Tirages en cours</h3>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
Vous pouvez vous inscrire aux tirage en cours suivants.
|
||||||
|
</div>
|
||||||
|
<div class="bootstrap-form-reduce">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ subform|bootstrap }}
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
name="subscribe"
|
||||||
|
value="S'inscrire aux tirages sélectionnés">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
13
bda/templates/bda/revente/wrongtime.html
Normal file
13
bda/templates/bda/revente/wrongtime.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Nope</h2>
|
||||||
|
{% if revente.shotgun %}
|
||||||
|
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
|
||||||
|
|
||||||
|
<p>Si personne n'était intéressé, elle est maintenant disponible
|
||||||
|
<a href="{% url "bda-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
|
||||||
|
{% else %}
|
||||||
|
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
113
bda/templates/descriptions.html
Normal file
113
bda/templates/descriptions.html
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_parent"/>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: josefinsans;
|
||||||
|
src: url({% static "fonts/josefinsans.ttf" %});
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-moz-selection {
|
||||||
|
background: #B0B0B0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::selection {
|
||||||
|
background: #B0B0B0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descTable{
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 1em;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 2;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: 'josefinsans', 'Arial';
|
||||||
|
font-weight: 700;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
img{
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<meta charset="utf8" />
|
||||||
|
</head>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
|
||||||
|
<body>
|
||||||
|
{% for show in shows %}
|
||||||
|
<table class="descTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2"><p style="text-align:center;font-size:22px;">{{ show.title }}</p></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><p style="text-align: left;">{{ show.location }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.category }}</p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><p style="text-align: left;">{{ show.date|date:"l j F Y - H\hi" }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %} - {{ show.price }} euro{{ show.price|pluralize}}</p></td>
|
||||||
|
</tr>
|
||||||
|
{% if show.vips %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><p style="text-align: justify;">{{ show.vips }}</p></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<p style="text-align: justify;">{{ show.description }}</p>
|
||||||
|
{% for quote in show.quote_set.all %}
|
||||||
|
<p style="text-align:center; font-style: italic;">«{{ quote.text }}»{% if quote.author %} - {{ quote.author }}{% endif %}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if show.image %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><p style="text-align:center;"><a href="{{ show.ext_link }}"><img class="imgDesc" style="display: inline;" src="{{ MEDIA_URL }}{{ show.image }}" alt="{{ show.title }}"></a></p></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
<script>
|
||||||
|
// Correction de la taille des images
|
||||||
|
|
||||||
|
/*$(document).ready(function() {
|
||||||
|
$(".descTable").each(function() {
|
||||||
|
$(this).width($("body").width());
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".imgDesc").on("load", function() {
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
origHeight = 500; // Hauteur souhaitée
|
||||||
|
|
||||||
|
w = $(this).width();
|
||||||
|
h = $(this).height();
|
||||||
|
r = w/h; // Ratio de l'image
|
||||||
|
maxWidth = $("body").width();
|
||||||
|
|
||||||
|
if (r * origHeight > maxWidth)
|
||||||
|
{
|
||||||
|
$(this).width(maxWidth);
|
||||||
|
$(this).height(maxWidth/r);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$(this).width(r * origHeight);
|
||||||
|
$(this).height(origHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});*/
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
56
bda/templates/spectacle_list.html
Normal file
56
bda/templates/spectacle_list.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2><strong>{{tirage_name}}</strong></h2>
|
||||||
|
<h3>Liste des spectacles</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover etat-bda">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="float">Prix</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for spectacle in object_list %}
|
||||||
|
<tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
|
||||||
|
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
|
||||||
|
<td data-sort-value="{{ spectacle.timestamp }}"">{{ spectacle.date }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
|
||||||
|
{{ spectacle.price |floatformat }}€
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("table.etat-bda").stupidtable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$(".clickable-row").click(function() {
|
||||||
|
window.document.location = $(this).data("href");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3> Exports </h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
|
||||||
|
<li><a href="{% url 'bda-descriptions' tirage_id %}">Lien vers les descriptions des spectacles, à utiliser dans une page wordpress</a>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
10
bda/templates/tirage-failed.html
Normal file
10
bda/templates/tirage-failed.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Raté, le tirage ne peut pas être lancé !</h2>
|
||||||
|
|
||||||
|
<p>Soit les inscriptions ne sont en pas encore fermées, soit le lancement du
|
||||||
|
tirage est désactivé. Si vous savez ce que vous faites, vous pouvez autoriser
|
||||||
|
le lancement du tirage dans
|
||||||
|
l'<a href="{% url "admin:index" %}">interface admin</a>.</p>
|
||||||
|
{% endblock %}
|
0
bda/tests/__init__.py
Normal file
0
bda/tests/__init__.py
Normal file
102
bda/tests/test_models.py
Normal file
102
bda/tests/test_models.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bda.models import (
|
||||||
|
Attribution,
|
||||||
|
Participant,
|
||||||
|
Salle,
|
||||||
|
Spectacle,
|
||||||
|
SpectacleRevente,
|
||||||
|
Tirage,
|
||||||
|
)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleReventeTests(TestCase):
|
||||||
|
fixtures = ["gestioncof/management/data/custommail.json"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
self.t = Tirage.objects.create(
|
||||||
|
title="Tirage",
|
||||||
|
ouverture=now - timedelta(days=7),
|
||||||
|
fermeture=now - timedelta(days=3),
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
self.s = Spectacle.objects.create(
|
||||||
|
title="Spectacle",
|
||||||
|
date=now + timedelta(days=20),
|
||||||
|
location=Salle.objects.create(name="Salle", address="Address"),
|
||||||
|
price=10.5,
|
||||||
|
slots=5,
|
||||||
|
tirage=self.t,
|
||||||
|
listing=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.seller = Participant.objects.create(
|
||||||
|
user=User.objects.create(username="seller", email="seller@mail.net"),
|
||||||
|
tirage=self.t,
|
||||||
|
)
|
||||||
|
self.p1 = Participant.objects.create(
|
||||||
|
user=User.objects.create(username="part1", email="part1@mail.net"),
|
||||||
|
tirage=self.t,
|
||||||
|
)
|
||||||
|
self.p2 = Participant.objects.create(
|
||||||
|
user=User.objects.create(username="part2", email="part2@mail.net"),
|
||||||
|
tirage=self.t,
|
||||||
|
)
|
||||||
|
self.p3 = Participant.objects.create(
|
||||||
|
user=User.objects.create(username="part3", email="part3@mail.net"),
|
||||||
|
tirage=self.t,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.attr = Attribution.objects.create(
|
||||||
|
participant=self.seller, spectacle=self.s
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rev = SpectacleRevente.objects.create(
|
||||||
|
attribution=self.attr, seller=self.seller
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tirage(self):
|
||||||
|
revente = self.rev
|
||||||
|
|
||||||
|
wanted_by = [self.p1, self.p2, self.p3]
|
||||||
|
revente.confirmed_entry = wanted_by
|
||||||
|
|
||||||
|
with mock.patch("bda.models.random.choice") as mc:
|
||||||
|
# Set winner to self.p1.
|
||||||
|
mc.return_value = self.p1
|
||||||
|
|
||||||
|
revente.tirage()
|
||||||
|
|
||||||
|
# Call to random.choice used participants in wanted_by.
|
||||||
|
mc_args, _ = mc.call_args
|
||||||
|
|
||||||
|
self.assertEqual(set(mc_args[0]), set(wanted_by))
|
||||||
|
|
||||||
|
self.assertEqual(revente.soldTo, self.p1)
|
||||||
|
self.assertTrue(revente.tirage_done)
|
||||||
|
|
||||||
|
mails = {m.to[0]: m for m in mail.outbox}
|
||||||
|
|
||||||
|
self.assertEqual(len(mails), 4)
|
||||||
|
|
||||||
|
m_seller = mails["seller@mail.net"]
|
||||||
|
self.assertListEqual(m_seller.to, ["seller@mail.net"])
|
||||||
|
self.assertListEqual(m_seller.reply_to, ["part1@mail.net"])
|
||||||
|
|
||||||
|
m_winner = mails["part1@mail.net"]
|
||||||
|
self.assertListEqual(m_winner.to, ["part1@mail.net"])
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
[mails["part2@mail.net"].to, mails["part3@mail.net"].to],
|
||||||
|
[["part2@mail.net"], ["part3@mail.net"]],
|
||||||
|
)
|
79
bda/tests/test_revente.py
Normal file
79
bda/tests/test_revente.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bda.models import (
|
||||||
|
Attribution,
|
||||||
|
CategorieSpectacle,
|
||||||
|
Participant,
|
||||||
|
Salle,
|
||||||
|
Spectacle,
|
||||||
|
SpectacleRevente,
|
||||||
|
Tirage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestModels(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tirage = Tirage.objects.create(
|
||||||
|
title="Tirage test",
|
||||||
|
appear_catalogue=True,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
self.category = CategorieSpectacle.objects.create(name="Category")
|
||||||
|
self.location = Salle.objects.create(name="here")
|
||||||
|
self.spectacle_soon = Spectacle.objects.create(
|
||||||
|
title="foo",
|
||||||
|
date=timezone.now() + timedelta(days=1),
|
||||||
|
location=self.location,
|
||||||
|
price=0,
|
||||||
|
slots=42,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
)
|
||||||
|
self.spectacle_later = Spectacle.objects.create(
|
||||||
|
title="bar",
|
||||||
|
date=timezone.now() + timedelta(days=30),
|
||||||
|
location=self.location,
|
||||||
|
price=0,
|
||||||
|
slots=42,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_buyer = User.objects.create_user(
|
||||||
|
username="bda_buyer", password="testbuyer"
|
||||||
|
)
|
||||||
|
user_seller = User.objects.create_user(
|
||||||
|
username="bda_seller", password="testseller"
|
||||||
|
)
|
||||||
|
self.buyer = Participant.objects.create(user=user_buyer, tirage=self.tirage)
|
||||||
|
self.seller = Participant.objects.create(user=user_seller, tirage=self.tirage)
|
||||||
|
|
||||||
|
self.attr_soon = Attribution.objects.create(
|
||||||
|
participant=self.seller, spectacle=self.spectacle_soon
|
||||||
|
)
|
||||||
|
self.attr_later = Attribution.objects.create(
|
||||||
|
participant=self.seller, spectacle=self.spectacle_later
|
||||||
|
)
|
||||||
|
self.revente_soon = SpectacleRevente.objects.create(
|
||||||
|
seller=self.seller, attribution=self.attr_soon
|
||||||
|
)
|
||||||
|
self.revente_later = SpectacleRevente.objects.create(
|
||||||
|
seller=self.seller, attribution=self.attr_later
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_urgent(self):
|
||||||
|
self.assertTrue(self.revente_soon.is_urgent)
|
||||||
|
self.assertFalse(self.revente_later.is_urgent)
|
||||||
|
|
||||||
|
def test_tirage(self):
|
||||||
|
self.revente_soon.confirmed_entry.add(self.buyer)
|
||||||
|
|
||||||
|
self.assertEqual(self.revente_soon.tirage(send_mails=False), self.buyer)
|
||||||
|
self.assertIsNone(self.revente_later.tirage(send_mails=False))
|
243
bda/tests/test_views.py
Normal file
243
bda/tests/test_views.py
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest import mock
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username, is_cof=False, is_buro=False):
|
||||||
|
user = User.objects.create_user(username=username, password=username)
|
||||||
|
user.profile.is_cof = is_cof
|
||||||
|
user.profile.is_buro = is_buro
|
||||||
|
user.profile.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def user_is_cof(user):
|
||||||
|
return (user is not None) and user.profile.is_cof
|
||||||
|
|
||||||
|
|
||||||
|
def user_is_staff(user):
|
||||||
|
return (user is not None) and user.profile.is_buro
|
||||||
|
|
||||||
|
|
||||||
|
class BdATestHelpers:
|
||||||
|
def setUp(self):
|
||||||
|
# Some user with different access privileges
|
||||||
|
staff = create_user(username="bda_staff", is_cof=True, is_buro=True)
|
||||||
|
staff_c = Client()
|
||||||
|
staff_c.force_login(staff)
|
||||||
|
|
||||||
|
member = create_user(username="bda_member", is_cof=True)
|
||||||
|
member_c = Client()
|
||||||
|
member_c.force_login(member)
|
||||||
|
|
||||||
|
other = create_user(username="bda_other")
|
||||||
|
other_c = Client()
|
||||||
|
other_c.force_login(other)
|
||||||
|
|
||||||
|
self.client_matrix = [
|
||||||
|
(staff, staff_c),
|
||||||
|
(member, member_c),
|
||||||
|
(other, other_c),
|
||||||
|
(None, Client()),
|
||||||
|
]
|
||||||
|
|
||||||
|
def require_custommails(self):
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
call_command("syncmails", verbosity=0)
|
||||||
|
|
||||||
|
def check_restricted_access(
|
||||||
|
self, url, validate_user=user_is_cof, redirect_url=None
|
||||||
|
):
|
||||||
|
def craft_redirect_url(user):
|
||||||
|
if redirect_url:
|
||||||
|
return redirect_url
|
||||||
|
elif user is None:
|
||||||
|
# client is not logged in
|
||||||
|
login_url = "/login"
|
||||||
|
if url:
|
||||||
|
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
|
||||||
|
return login_url
|
||||||
|
else:
|
||||||
|
return "/"
|
||||||
|
|
||||||
|
for (user, client) in self.client_matrix:
|
||||||
|
resp = client.get(url, follow=True)
|
||||||
|
if validate_user(user):
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
else:
|
||||||
|
self.assertRedirects(resp, craft_redirect_url(user))
|
||||||
|
|
||||||
|
|
||||||
|
class TestBdAViews(BdATestHelpers, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Signals handlers on login/logout send messages.
|
||||||
|
# Due to the way the Django' test Client performs login, this raise an
|
||||||
|
# error. As workaround, we mock the Django' messages module.
|
||||||
|
patcher_messages = mock.patch("gestioncof.signals.messages")
|
||||||
|
patcher_messages.start()
|
||||||
|
self.addCleanup(patcher_messages.stop)
|
||||||
|
# Set up the helpers
|
||||||
|
super().setUp()
|
||||||
|
# Some BdA stuff
|
||||||
|
self.tirage = Tirage.objects.create(
|
||||||
|
title="Test tirage",
|
||||||
|
appear_catalogue=True,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
self.category = CategorieSpectacle.objects.create(name="Category")
|
||||||
|
self.location = Salle.objects.create(name="here")
|
||||||
|
Spectacle.objects.bulk_create(
|
||||||
|
[
|
||||||
|
Spectacle(
|
||||||
|
title="foo",
|
||||||
|
date=timezone.now(),
|
||||||
|
location=self.location,
|
||||||
|
price=0,
|
||||||
|
slots=42,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="bar",
|
||||||
|
date=timezone.now(),
|
||||||
|
location=self.location,
|
||||||
|
price=1,
|
||||||
|
slots=142,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="baz",
|
||||||
|
date=timezone.now(),
|
||||||
|
location=self.location,
|
||||||
|
price=2,
|
||||||
|
slots=242,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_bda_inscriptions(self):
|
||||||
|
# TODO: test the form
|
||||||
|
url = "/bda/inscription/{}".format(self.tirage.id)
|
||||||
|
self.check_restricted_access(url)
|
||||||
|
|
||||||
|
def test_bda_places(self):
|
||||||
|
url = "/bda/places/{}".format(self.tirage.id)
|
||||||
|
self.check_restricted_access(url)
|
||||||
|
|
||||||
|
def test_etat_places(self):
|
||||||
|
url = "/bda/etat-places/{}".format(self.tirage.id)
|
||||||
|
self.check_restricted_access(url)
|
||||||
|
|
||||||
|
def test_perform_tirage(self):
|
||||||
|
# Only staff member can perform a tirage
|
||||||
|
url = "/bda/tirage/{}".format(self.tirage.id)
|
||||||
|
self.check_restricted_access(url, validate_user=user_is_staff)
|
||||||
|
|
||||||
|
_, staff_c = self.client_matrix[0]
|
||||||
|
# Cannot be performed if disabled
|
||||||
|
self.tirage.enable_do_tirage = False
|
||||||
|
self.tirage.save()
|
||||||
|
resp = staff_c.get(url)
|
||||||
|
self.assertTemplateUsed(resp, "tirage-failed.html")
|
||||||
|
# Cannot be performed if registrations are still open
|
||||||
|
self.tirage.enable_do_tirage = True
|
||||||
|
self.tirage.fermeture = timezone.now() + timedelta(seconds=3600)
|
||||||
|
self.tirage.save()
|
||||||
|
resp = staff_c.get(url)
|
||||||
|
self.assertTemplateUsed(resp, "tirage-failed.html")
|
||||||
|
# Otherwise, perform the tirage
|
||||||
|
self.tirage.fermeture = timezone.now()
|
||||||
|
self.tirage.save()
|
||||||
|
resp = staff_c.get(url)
|
||||||
|
self.assertTemplateNotUsed(resp, "tirage-failed.html")
|
||||||
|
|
||||||
|
def test_spectacles_list(self):
|
||||||
|
url = "/bda/spectacles/{}".format(self.tirage.id)
|
||||||
|
self.check_restricted_access(url, validate_user=user_is_staff)
|
||||||
|
|
||||||
|
def test_spectacle_detail(self):
|
||||||
|
show = self.tirage.spectacle_set.first()
|
||||||
|
url = "/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
|
||||||
|
self.check_restricted_access(url, validate_user=user_is_staff)
|
||||||
|
|
||||||
|
def test_tirage_unpaid(self):
|
||||||
|
url = "/bda/spectacles/unpaid/{}".format(self.tirage.id)
|
||||||
|
self.check_restricted_access(url, validate_user=user_is_staff)
|
||||||
|
|
||||||
|
def test_send_reminders(self):
|
||||||
|
self.require_custommails()
|
||||||
|
# Just get the page
|
||||||
|
show = self.tirage.spectacle_set.first()
|
||||||
|
url = "/bda/mails-rappel/{}".format(show.id)
|
||||||
|
self.check_restricted_access(url, validate_user=user_is_staff)
|
||||||
|
# Actually send the reminder emails
|
||||||
|
_, staff_c = self.client_matrix[0]
|
||||||
|
resp = staff_c.post(url)
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
# TODO: check that emails are sent
|
||||||
|
|
||||||
|
def test_catalogue_api(self):
|
||||||
|
url_list = "/bda/catalogue/list"
|
||||||
|
url_details = "/bda/catalogue/details?id={}".format(self.tirage.id)
|
||||||
|
url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id)
|
||||||
|
|
||||||
|
# Anyone can get
|
||||||
|
def anyone_can_get(url):
|
||||||
|
self.check_restricted_access(url, validate_user=lambda user: True)
|
||||||
|
|
||||||
|
anyone_can_get(url_list)
|
||||||
|
anyone_can_get(url_details)
|
||||||
|
anyone_can_get(url_descriptions)
|
||||||
|
|
||||||
|
# The resulting JSON contains the information
|
||||||
|
_, client = self.client_matrix[0]
|
||||||
|
|
||||||
|
# List
|
||||||
|
resp = client.get(url_list)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
resp.content.decode("utf-8"),
|
||||||
|
[{"id": self.tirage.id, "title": self.tirage.title}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Details
|
||||||
|
resp = client.get(url_details)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
resp.content.decode("utf-8"),
|
||||||
|
{
|
||||||
|
"categories": [{"id": self.category.id, "name": self.category.name}],
|
||||||
|
"locations": [{"id": self.location.id, "name": self.location.name}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Descriptions
|
||||||
|
resp = client.get(url_descriptions)
|
||||||
|
raw = resp.content.decode("utf-8")
|
||||||
|
try:
|
||||||
|
results = json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Not valid JSON: {}".format(raw))
|
||||||
|
self.assertEqual(len(results), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
{(s["title"], s["price"], s["slots"]) for s in results},
|
||||||
|
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBdaRevente:
|
||||||
|
pass
|
||||||
|
# TODO
|
75
bda/urls.py
Normal file
75
bda/urls.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from bda import views
|
||||||
|
from bda.views import SpectacleListView
|
||||||
|
from gestioncof.decorators import buro_required
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(
|
||||||
|
r"^inscription/(?P<tirage_id>\d+)$",
|
||||||
|
views.inscription,
|
||||||
|
name="bda-tirage-inscription",
|
||||||
|
),
|
||||||
|
url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
|
||||||
|
url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
|
||||||
|
url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage),
|
||||||
|
url(
|
||||||
|
r"^spectacles/(?P<tirage_id>\d+)$",
|
||||||
|
buro_required(SpectacleListView.as_view()),
|
||||||
|
name="bda-liste-spectacles",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
|
||||||
|
views.spectacle,
|
||||||
|
name="bda-spectacle",
|
||||||
|
),
|
||||||
|
url(r"^spectacles/unpaid/(?P<tirage_id>\d+)$", views.unpaid, name="bda-unpaid"),
|
||||||
|
url(
|
||||||
|
r"^spectacles/autocomplete$",
|
||||||
|
views.spectacle_autocomplete,
|
||||||
|
name="bda-spectacle-autocomplete",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^participants/autocomplete$",
|
||||||
|
views.participant_autocomplete,
|
||||||
|
name="bda-participant-autocomplete",
|
||||||
|
),
|
||||||
|
# Urls BdA-Revente
|
||||||
|
url(
|
||||||
|
r"^revente/(?P<tirage_id>\d+)/manage$",
|
||||||
|
views.revente_manage,
|
||||||
|
name="bda-revente-manage",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^revente/(?P<tirage_id>\d+)/subscribe$",
|
||||||
|
views.revente_subscribe,
|
||||||
|
name="bda-revente-subscribe",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^revente/(?P<tirage_id>\d+)/tirages$",
|
||||||
|
views.revente_tirages,
|
||||||
|
name="bda-revente-tirages",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^revente/(?P<spectacle_id>\d+)/buy$",
|
||||||
|
views.revente_buy,
|
||||||
|
name="bda-revente-buy",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^revente/(?P<revente_id>\d+)/confirm$",
|
||||||
|
views.revente_confirm,
|
||||||
|
name="bda-revente-confirm",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^revente/(?P<tirage_id>\d+)/shotgun$",
|
||||||
|
views.revente_shotgun,
|
||||||
|
name="bda-revente-shotgun",
|
||||||
|
),
|
||||||
|
url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
|
||||||
|
url(
|
||||||
|
r"^descriptions/(?P<tirage_id>\d+)$",
|
||||||
|
views.descriptions_spectacles,
|
||||||
|
name="bda-descriptions",
|
||||||
|
),
|
||||||
|
url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"),
|
||||||
|
]
|
910
bda/views.py
Normal file
910
bda/views.py
Normal file
|
@ -0,0 +1,910 @@
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from custommail.models import CustomMail
|
||||||
|
from custommail.shortcuts import send_custom_mail, send_mass_custom_mail
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core import serializers
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Count, Prefetch, Q
|
||||||
|
from django.forms.models import inlineformset_factory
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.utils import formats, timezone
|
||||||
|
from django.views.generic.list import ListView
|
||||||
|
|
||||||
|
from bda.algorithm import Algorithm
|
||||||
|
from bda.forms import (
|
||||||
|
AnnulForm,
|
||||||
|
InscriptionInlineFormSet,
|
||||||
|
InscriptionReventeForm,
|
||||||
|
ResellForm,
|
||||||
|
ReventeTirageAnnulForm,
|
||||||
|
ReventeTirageForm,
|
||||||
|
SoldForm,
|
||||||
|
TokenForm,
|
||||||
|
)
|
||||||
|
from bda.models import (
|
||||||
|
Attribution,
|
||||||
|
CategorieSpectacle,
|
||||||
|
ChoixSpectacle,
|
||||||
|
Participant,
|
||||||
|
Salle,
|
||||||
|
Spectacle,
|
||||||
|
SpectacleRevente,
|
||||||
|
Tirage,
|
||||||
|
)
|
||||||
|
from gestioncof.decorators import buro_required, cof_required
|
||||||
|
from utils.views.autocomplete import Select2QuerySetView
|
||||||
|
|
||||||
|
|
||||||
|
@cof_required
|
||||||
|
def etat_places(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Résumé des spectacles d'un tirage avec pour chaque spectacle :
|
||||||
|
- Le nombre de places en jeu
|
||||||
|
- Le nombre de demandes
|
||||||
|
- Le ratio demandes/places
|
||||||
|
Et le total de toutes les demandes
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
|
||||||
|
spectacles = tirage.spectacle_set.select_related("location")
|
||||||
|
spectacles_dict = {} # index of spectacle by id
|
||||||
|
|
||||||
|
for spectacle in spectacles:
|
||||||
|
spectacle.total = 0 # init total requests
|
||||||
|
spectacles_dict[spectacle.id] = spectacle
|
||||||
|
|
||||||
|
choices = (
|
||||||
|
ChoixSpectacle.objects.filter(spectacle__in=spectacles)
|
||||||
|
.values("spectacle")
|
||||||
|
.annotate(total=Count("spectacle"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# choices *by spectacles* whose only 1 place is requested
|
||||||
|
choices1 = choices.filter(double_choice="1")
|
||||||
|
# choices *by spectacles* whose 2 places is requested
|
||||||
|
choices2 = choices.exclude(double_choice="1")
|
||||||
|
|
||||||
|
for spectacle in choices1:
|
||||||
|
pk = spectacle["spectacle"]
|
||||||
|
spectacles_dict[pk].total += spectacle["total"]
|
||||||
|
for spectacle in choices2:
|
||||||
|
pk = spectacle["spectacle"]
|
||||||
|
spectacles_dict[pk].total += 2 * spectacle["total"]
|
||||||
|
|
||||||
|
# here, each spectacle.total contains the number of requests
|
||||||
|
|
||||||
|
slots = 0 # proposed slots
|
||||||
|
total = 0 # requests
|
||||||
|
for spectacle in spectacles:
|
||||||
|
slots += spectacle.slots
|
||||||
|
total += spectacle.total
|
||||||
|
spectacle.ratio = spectacle.total / spectacle.slots
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"proposed": slots,
|
||||||
|
"spectacles": spectacles,
|
||||||
|
"total": total,
|
||||||
|
"tirage": tirage,
|
||||||
|
}
|
||||||
|
return render(request, "bda/etat-places.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_queryset(queryset):
|
||||||
|
data = serializers.serialize("json", queryset).encode("utf-8")
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
hasher.update(data)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@cof_required
|
||||||
|
def places(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
places = participant.attribution_set.order_by(
|
||||||
|
"spectacle__date", "spectacle"
|
||||||
|
).select_related("spectacle", "spectacle__location")
|
||||||
|
total = sum(place.spectacle.price for place in places)
|
||||||
|
filtered_places = []
|
||||||
|
places_dict = {}
|
||||||
|
spectacles = []
|
||||||
|
dates = []
|
||||||
|
warning = False
|
||||||
|
for place in places:
|
||||||
|
if place.spectacle in spectacles:
|
||||||
|
places_dict[place.spectacle].double = True
|
||||||
|
else:
|
||||||
|
place.double = False
|
||||||
|
places_dict[place.spectacle] = place
|
||||||
|
spectacles.append(place.spectacle)
|
||||||
|
filtered_places.append(place)
|
||||||
|
date = place.spectacle.date.date()
|
||||||
|
if date in dates:
|
||||||
|
warning = True
|
||||||
|
else:
|
||||||
|
dates.append(date)
|
||||||
|
# On prévient l'utilisateur s'il a deux places à la même date
|
||||||
|
if warning:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"Attention, vous avez reçu des places pour "
|
||||||
|
"des spectacles différents à la même date.",
|
||||||
|
)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/resume_places.html",
|
||||||
|
{
|
||||||
|
"participant": participant,
|
||||||
|
"places": filtered_places,
|
||||||
|
"tirage": tirage,
|
||||||
|
"total": total,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cof_required
|
||||||
|
def inscription(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Vue d'inscription à un tirage BdA.
|
||||||
|
- On vérifie qu'on se situe bien entre la date d'ouverture et la date de
|
||||||
|
fermeture des inscriptions.
|
||||||
|
- On vérifie que l'inscription n'a pas été modifiée entre le moment où le
|
||||||
|
client demande le formulaire et le moment où il soumet son inscription
|
||||||
|
(autre session par exemple).
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
if timezone.now() < tirage.ouverture:
|
||||||
|
# Le tirage n'est pas encore ouvert.
|
||||||
|
opening = formats.localize(timezone.template_localtime(tirage.ouverture))
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Le tirage n'est pas encore ouvert : " "ouverture le {:s}".format(opening),
|
||||||
|
)
|
||||||
|
return render(request, "bda/resume-inscription-tirage.html", {})
|
||||||
|
|
||||||
|
participant, _ = Participant.objects.select_related("tirage").get_or_create(
|
||||||
|
user=request.user, tirage=tirage
|
||||||
|
)
|
||||||
|
|
||||||
|
if timezone.now() > tirage.fermeture:
|
||||||
|
# Le tirage est fermé.
|
||||||
|
choices = participant.choixspectacle_set.order_by("priority")
|
||||||
|
messages.error(request, " C'est fini : tirage au sort dans la journée !")
|
||||||
|
return render(
|
||||||
|
request, "bda/resume-inscription-tirage.html", {"choices": choices}
|
||||||
|
)
|
||||||
|
|
||||||
|
BdaFormSet = inlineformset_factory(
|
||||||
|
Participant,
|
||||||
|
ChoixSpectacle,
|
||||||
|
fields=("spectacle", "double_choice", "priority"),
|
||||||
|
formset=InscriptionInlineFormSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
success = False
|
||||||
|
stateerror = False
|
||||||
|
if request.method == "POST":
|
||||||
|
# use *this* queryset
|
||||||
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
|
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
||||||
|
stateerror = True
|
||||||
|
formset = BdaFormSet(instance=participant)
|
||||||
|
else:
|
||||||
|
formset = BdaFormSet(request.POST, instance=participant)
|
||||||
|
if formset.is_valid():
|
||||||
|
formset.save()
|
||||||
|
success = True
|
||||||
|
formset = BdaFormSet(instance=participant)
|
||||||
|
else:
|
||||||
|
formset = BdaFormSet(instance=participant)
|
||||||
|
# use *this* queryset
|
||||||
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
|
total_price = 0
|
||||||
|
choices = participant.choixspectacle_set.select_related("spectacle")
|
||||||
|
for choice in choices:
|
||||||
|
total_price += choice.spectacle.price
|
||||||
|
if choice.double:
|
||||||
|
total_price += choice.spectacle.price
|
||||||
|
# Messages
|
||||||
|
if success:
|
||||||
|
messages.success(
|
||||||
|
request, "Votre inscription a été mise à jour avec " "succès !"
|
||||||
|
)
|
||||||
|
if stateerror:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Impossible d'enregistrer vos modifications "
|
||||||
|
": vous avez apporté d'autres modifications "
|
||||||
|
"entre temps.",
|
||||||
|
)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/inscription-tirage.html",
|
||||||
|
{
|
||||||
|
"formset": formset,
|
||||||
|
"total_price": total_price,
|
||||||
|
"dbstate": dbstate,
|
||||||
|
"tirage": tirage,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def do_tirage(tirage_elt, token):
|
||||||
|
"""
|
||||||
|
Fonction auxiliaire à la vue ``tirage`` qui lance effectivement le tirage
|
||||||
|
après qu'on a vérifié que c'est légitime et que le token donné en argument
|
||||||
|
est correct.
|
||||||
|
Rend les résultats
|
||||||
|
"""
|
||||||
|
# Initialisation du dictionnaire data qui va contenir les résultats
|
||||||
|
start = time.time()
|
||||||
|
data = {
|
||||||
|
"shows": tirage_elt.spectacle_set.select_related("location"),
|
||||||
|
"token": token,
|
||||||
|
"members": tirage_elt.participant_set.select_related("user"),
|
||||||
|
"total_slots": 0,
|
||||||
|
"total_losers": 0,
|
||||||
|
"total_sold": 0,
|
||||||
|
"total_deficit": 0,
|
||||||
|
"opera_deficit": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# On lance le tirage
|
||||||
|
choices = (
|
||||||
|
ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt)
|
||||||
|
.order_by("participant", "priority")
|
||||||
|
.select_related("participant", "participant__user", "spectacle")
|
||||||
|
)
|
||||||
|
results = Algorithm(data["shows"], data["members"], choices)(token)
|
||||||
|
|
||||||
|
# On compte les places attribuées et les déçus
|
||||||
|
for (_, members, losers) in results:
|
||||||
|
data["total_slots"] += len(members)
|
||||||
|
data["total_losers"] += len(losers)
|
||||||
|
|
||||||
|
# On calcule le déficit et les bénéfices pour le BdA
|
||||||
|
# FIXME: le traitement de l'opéra est sale
|
||||||
|
for (show, members, _) in results:
|
||||||
|
deficit = (show.slots - len(members)) * show.price
|
||||||
|
data["total_sold"] += show.slots * show.price
|
||||||
|
if deficit >= 0:
|
||||||
|
if "Opéra" in show.location.name:
|
||||||
|
data["opera_deficit"] += deficit
|
||||||
|
data["total_deficit"] += deficit
|
||||||
|
data["total_sold"] -= data["total_deficit"]
|
||||||
|
|
||||||
|
# Participant objects are not shared accross spectacle results,
|
||||||
|
# so assign a single object for each Participant id
|
||||||
|
members_uniq = {}
|
||||||
|
members2 = {}
|
||||||
|
for (show, members, _) in results:
|
||||||
|
for (member, _, _, _) in members:
|
||||||
|
if member.id not in members_uniq:
|
||||||
|
members_uniq[member.id] = member
|
||||||
|
members2[member] = []
|
||||||
|
member.total = 0
|
||||||
|
member = members_uniq[member.id]
|
||||||
|
members2[member].append(show)
|
||||||
|
member.total += show.price
|
||||||
|
members2 = members2.items()
|
||||||
|
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# À partir d'ici, le tirage devient effectif
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# On suppression les vieilles attributions, on sauvegarde le token et on
|
||||||
|
# désactive le tirage
|
||||||
|
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
|
||||||
|
tirage_elt.tokens += '{:s}\n"""{:s}"""\n'.format(
|
||||||
|
timezone.now().strftime("%y-%m-%d %H:%M:%S"), token
|
||||||
|
)
|
||||||
|
tirage_elt.enable_do_tirage = False
|
||||||
|
tirage_elt.save()
|
||||||
|
|
||||||
|
# On enregistre les nouvelles attributions
|
||||||
|
Attribution.objects.bulk_create(
|
||||||
|
[
|
||||||
|
Attribution(spectacle=show, participant=member)
|
||||||
|
for show, members, _ in results
|
||||||
|
for member, _, _, _ in members
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
|
||||||
|
ChoixRevente = Participant.choicesrevente.through
|
||||||
|
|
||||||
|
# Suppression des reventes demandées/enregistrées
|
||||||
|
# (si le tirage est relancé)
|
||||||
|
(ChoixRevente.objects.filter(spectacle__tirage=tirage_elt).delete())
|
||||||
|
(
|
||||||
|
SpectacleRevente.objects.filter(
|
||||||
|
attribution__spectacle__tirage=tirage_elt
|
||||||
|
).delete()
|
||||||
|
)
|
||||||
|
|
||||||
|
lost_by = defaultdict(set)
|
||||||
|
for show, _, losers in results:
|
||||||
|
for loser, _, _, _ in losers:
|
||||||
|
lost_by[loser].add(show)
|
||||||
|
|
||||||
|
ChoixRevente.objects.bulk_create(
|
||||||
|
ChoixRevente(participant=member, spectacle=show)
|
||||||
|
for member, shows in lost_by.items()
|
||||||
|
for show in shows
|
||||||
|
)
|
||||||
|
|
||||||
|
data["duration"] = time.time() - start
|
||||||
|
data["results"] = results
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def tirage(request, tirage_id):
|
||||||
|
tirage_elt = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
if not (tirage_elt.enable_do_tirage and tirage_elt.fermeture < timezone.now()):
|
||||||
|
return render(request, "tirage-failed.html", {"tirage": tirage_elt})
|
||||||
|
if request.POST:
|
||||||
|
form = TokenForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
results = do_tirage(tirage_elt, form.cleaned_data["token"])
|
||||||
|
return render(request, "bda-attrib-extra.html", results)
|
||||||
|
else:
|
||||||
|
form = TokenForm()
|
||||||
|
return render(request, "bda-token.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_manage(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Gestion de ses propres reventes :
|
||||||
|
- Création d'une revente
|
||||||
|
- Annulation d'une revente
|
||||||
|
- Confirmation d'une revente = transfert de la place à la personne qui
|
||||||
|
rachète
|
||||||
|
- Annulation d'une revente après que le tirage a eu lieu
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, created = Participant.objects.get_or_create(
|
||||||
|
user=request.user, tirage=tirage
|
||||||
|
)
|
||||||
|
|
||||||
|
if not participant.paid:
|
||||||
|
return render(request, "bda/revente/notpaid.html", {})
|
||||||
|
|
||||||
|
resellform = ResellForm(participant, prefix="resell")
|
||||||
|
annulform = AnnulForm(participant, prefix="annul")
|
||||||
|
soldform = SoldForm(participant, prefix="sold")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
# On met en vente une place
|
||||||
|
if "resell" in request.POST:
|
||||||
|
resellform = ResellForm(participant, request.POST, prefix="resell")
|
||||||
|
if resellform.is_valid():
|
||||||
|
datatuple = []
|
||||||
|
attributions = resellform.cleaned_data["attributions"]
|
||||||
|
with transaction.atomic():
|
||||||
|
for attribution in attributions:
|
||||||
|
revente, created = SpectacleRevente.objects.get_or_create(
|
||||||
|
attribution=attribution, defaults={"seller": participant}
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
revente.reset()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"vendeur": participant.user,
|
||||||
|
"show": attribution.spectacle,
|
||||||
|
"revente": revente,
|
||||||
|
}
|
||||||
|
datatuple.append(
|
||||||
|
(
|
||||||
|
"bda-revente-new",
|
||||||
|
context,
|
||||||
|
settings.MAIL_DATA["revente"]["FROM"],
|
||||||
|
[participant.user.email],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
revente.save()
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
# On annule une revente
|
||||||
|
elif "annul" in request.POST:
|
||||||
|
annulform = AnnulForm(participant, request.POST, prefix="annul")
|
||||||
|
if annulform.is_valid():
|
||||||
|
reventes = annulform.cleaned_data["reventes"]
|
||||||
|
for revente in reventes:
|
||||||
|
revente.delete()
|
||||||
|
# On confirme une vente en transférant la place à la personne qui a
|
||||||
|
# gagné le tirage
|
||||||
|
elif "transfer" in request.POST:
|
||||||
|
soldform = SoldForm(participant, request.POST, prefix="sold")
|
||||||
|
if soldform.is_valid():
|
||||||
|
reventes = soldform.cleaned_data["reventes"]
|
||||||
|
for revente in reventes:
|
||||||
|
revente.attribution.participant = revente.soldTo
|
||||||
|
revente.attribution.save()
|
||||||
|
|
||||||
|
# On annule la revente après le tirage au sort (par exemple si
|
||||||
|
# la personne qui a gagné le tirage ne se manifeste pas). La place est
|
||||||
|
# alors remise en vente
|
||||||
|
elif "reinit" in request.POST:
|
||||||
|
soldform = SoldForm(participant, request.POST, prefix="sold")
|
||||||
|
if soldform.is_valid():
|
||||||
|
reventes = soldform.cleaned_data["reventes"]
|
||||||
|
for revente in reventes:
|
||||||
|
if revente.attribution.spectacle.date > timezone.now():
|
||||||
|
# On antidate pour envoyer le mail plus vite
|
||||||
|
new_date = timezone.now() - SpectacleRevente.remorse_time
|
||||||
|
revente.reset(new_date=new_date)
|
||||||
|
|
||||||
|
overdue = participant.attribution_set.filter(
|
||||||
|
spectacle__date__gte=timezone.now(),
|
||||||
|
revente__isnull=False,
|
||||||
|
revente__seller=participant,
|
||||||
|
revente__notif_sent=True,
|
||||||
|
).filter(Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/revente/manage.html",
|
||||||
|
{
|
||||||
|
"tirage": tirage,
|
||||||
|
"overdue": overdue,
|
||||||
|
"soldform": soldform,
|
||||||
|
"annulform": annulform,
|
||||||
|
"resellform": resellform,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_tirages(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Affiche à un participant la liste de toutes les reventes en cours (pour un
|
||||||
|
tirage donné) et lui permet de s'inscrire et se désinscrire à ces reventes.
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
subform = ReventeTirageForm(participant, prefix="subscribe")
|
||||||
|
annulform = ReventeTirageAnnulForm(participant, prefix="annul")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if "subscribe" in request.POST:
|
||||||
|
subform = ReventeTirageForm(participant, request.POST, prefix="subscribe")
|
||||||
|
if subform.is_valid():
|
||||||
|
reventes = subform.cleaned_data["reventes"]
|
||||||
|
count = reventes.count()
|
||||||
|
for revente in reventes:
|
||||||
|
revente.confirmed_entry.add(participant)
|
||||||
|
if count > 0:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
"Tu as bien été inscrit à {} revente{}".format(
|
||||||
|
count, pluralize(count)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif "annul" in request.POST:
|
||||||
|
annulform = ReventeTirageAnnulForm(
|
||||||
|
participant, request.POST, prefix="annul"
|
||||||
|
)
|
||||||
|
if annulform.is_valid():
|
||||||
|
reventes = annulform.cleaned_data["reventes"]
|
||||||
|
count = reventes.count()
|
||||||
|
for revente in reventes:
|
||||||
|
revente.confirmed_entry.remove(participant)
|
||||||
|
if count > 0:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
"Tu as bien été désinscrit de {} revente{}".format(
|
||||||
|
count, pluralize(count)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/revente/tirages.html",
|
||||||
|
{"annulform": annulform, "subform": subform},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_confirm(request, revente_id):
|
||||||
|
revente = get_object_or_404(SpectacleRevente, id=revente_id)
|
||||||
|
participant, _ = Participant.objects.get_or_create(
|
||||||
|
user=request.user, tirage=revente.attribution.spectacle.tirage
|
||||||
|
)
|
||||||
|
if not revente.notif_sent or revente.shotgun:
|
||||||
|
return render(request, "bda/revente/wrongtime.html", {"revente": revente})
|
||||||
|
|
||||||
|
revente.confirmed_entry.add(participant)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/revente/confirmed.html",
|
||||||
|
{"spectacle": revente.attribution.spectacle, "date": revente.date_tirage},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_subscribe(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Permet à un participant de sélectionner ses préférences pour les reventes.
|
||||||
|
Il recevra des notifications pour les spectacles qui l'intéressent et il
|
||||||
|
est automatiquement inscrit aux reventes en cours au moment où il ajoute un
|
||||||
|
spectacle à la liste des spectacles qui l'intéressent.
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
deja_revente = False
|
||||||
|
success = False
|
||||||
|
inscrit_revente = []
|
||||||
|
if request.method == "POST":
|
||||||
|
form = InscriptionReventeForm(tirage, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
choices = form.cleaned_data["spectacles"]
|
||||||
|
participant.choicesrevente = choices
|
||||||
|
participant.save()
|
||||||
|
for spectacle in choices:
|
||||||
|
qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle)
|
||||||
|
if qset.filter(shotgun=True, soldTo__isnull=True).exists():
|
||||||
|
# Une place est disponible au shotgun, on suggère à
|
||||||
|
# l'utilisateur d'aller la récupérer
|
||||||
|
deja_revente = True
|
||||||
|
else:
|
||||||
|
# La place n'est pas disponible au shotgun, si des reventes
|
||||||
|
# pour ce spectacle existent déjà, on inscrit la personne à
|
||||||
|
# la revente ayant le moins d'inscrits
|
||||||
|
min_resell = (
|
||||||
|
qset.filter(shotgun=False)
|
||||||
|
.annotate(nb_subscribers=Count("confirmed_entry"))
|
||||||
|
.order_by("nb_subscribers")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if min_resell is not None:
|
||||||
|
min_resell.confirmed_entry.add(participant)
|
||||||
|
inscrit_revente.append(spectacle)
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
form = InscriptionReventeForm(
|
||||||
|
tirage, initial={"spectacles": participant.choicesrevente.all()}
|
||||||
|
)
|
||||||
|
# Messages
|
||||||
|
if success:
|
||||||
|
messages.success(request, "Ton inscription a bien été prise en compte")
|
||||||
|
if deja_revente:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
"Des reventes existent déjà pour certains de "
|
||||||
|
"ces spectacles, vérifie les places "
|
||||||
|
"disponibles sans tirage !",
|
||||||
|
)
|
||||||
|
if inscrit_revente:
|
||||||
|
shows = map("<li>{!s}</li>".format, inscrit_revente)
|
||||||
|
msg = (
|
||||||
|
"Tu as été inscrit à des reventes en cours pour les spectacles "
|
||||||
|
"<ul>{:s}</ul>".format("\n".join(shows))
|
||||||
|
)
|
||||||
|
messages.info(request, msg, extra_tags="safe")
|
||||||
|
|
||||||
|
return render(request, "bda/revente/subscribe.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_buy(request, spectacle_id):
|
||||||
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
||||||
|
tirage = spectacle.tirage
|
||||||
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
reventes = SpectacleRevente.objects.filter(
|
||||||
|
attribution__spectacle=spectacle, soldTo__isnull=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
|
||||||
|
# on supprime la revente en question.
|
||||||
|
own_reventes = reventes.filter(seller=participant)
|
||||||
|
if len(own_reventes) > 0:
|
||||||
|
own_reventes[0].delete()
|
||||||
|
return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id]))
|
||||||
|
|
||||||
|
reventes_shotgun = reventes.filter(shotgun=True)
|
||||||
|
|
||||||
|
if not reventes_shotgun:
|
||||||
|
return render(request, "bda/revente/none.html", {})
|
||||||
|
|
||||||
|
if request.POST:
|
||||||
|
revente = random.choice(reventes_shotgun)
|
||||||
|
revente.soldTo = participant
|
||||||
|
revente.save()
|
||||||
|
context = {
|
||||||
|
"show": spectacle,
|
||||||
|
"acheteur": request.user,
|
||||||
|
"vendeur": revente.seller.user,
|
||||||
|
}
|
||||||
|
send_custom_mail(
|
||||||
|
"bda-buy-shotgun",
|
||||||
|
"bda@ens.fr",
|
||||||
|
[revente.seller.user.email],
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/revente/mail-success.html",
|
||||||
|
{"seller": revente.attribution.participant.user, "spectacle": spectacle},
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/revente/confirm-shotgun.html",
|
||||||
|
{"spectacle": spectacle, "user": request.user},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_shotgun(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
spectacles = (
|
||||||
|
tirage.spectacle_set.filter(date__gte=timezone.now())
|
||||||
|
.select_related("location")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"attribues",
|
||||||
|
queryset=(
|
||||||
|
Attribution.objects.filter(
|
||||||
|
revente__shotgun=True, revente__soldTo__isnull=True
|
||||||
|
)
|
||||||
|
),
|
||||||
|
to_attr="shotguns",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
||||||
|
|
||||||
|
return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun})
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def spectacle(request, tirage_id, spectacle_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
|
||||||
|
attributions = spectacle.attribues.select_related(
|
||||||
|
"participant", "participant__user"
|
||||||
|
)
|
||||||
|
participants = {}
|
||||||
|
for attrib in attributions:
|
||||||
|
participant = attrib.participant
|
||||||
|
participant_info = {
|
||||||
|
"lastname": participant.user.last_name,
|
||||||
|
"name": participant.user.get_full_name,
|
||||||
|
"username": participant.user.username,
|
||||||
|
"email": participant.user.email,
|
||||||
|
"given": int(attrib.given),
|
||||||
|
"paid": participant.paid,
|
||||||
|
"nb_places": 1,
|
||||||
|
}
|
||||||
|
if participant.id in participants:
|
||||||
|
participants[participant.id]["nb_places"] += 1
|
||||||
|
participants[participant.id]["given"] += attrib.given
|
||||||
|
else:
|
||||||
|
participants[participant.id] = participant_info
|
||||||
|
|
||||||
|
participants_info = sorted(participants.values(), key=lambda part: part["lastname"])
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bda/participants.html",
|
||||||
|
{"spectacle": spectacle, "participants": participants_info},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleListView(ListView):
|
||||||
|
model = Spectacle
|
||||||
|
template_name = "spectacle_list.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
self.tirage = get_object_or_404(Tirage, id=self.kwargs["tirage_id"])
|
||||||
|
categories = self.tirage.spectacle_set.select_related("location")
|
||||||
|
return categories
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["tirage_id"] = self.tirage.id
|
||||||
|
context["tirage_name"] = self.tirage.title
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def unpaid(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
unpaid = (
|
||||||
|
tirage.participant_set.annotate(nb_attributions=Count("attribution"))
|
||||||
|
.filter(paid=False, nb_attributions__gt=0)
|
||||||
|
.select_related("user")
|
||||||
|
)
|
||||||
|
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def send_rappel(request, spectacle_id):
|
||||||
|
show = get_object_or_404(Spectacle, id=spectacle_id)
|
||||||
|
# Mails d'exemples
|
||||||
|
custommail = CustomMail.objects.get(shortname="bda-rappel")
|
||||||
|
exemple_mail_1place = custommail.render(
|
||||||
|
{"member": request.user, "show": show, "nb_attr": 1}
|
||||||
|
)
|
||||||
|
exemple_mail_2places = custommail.render(
|
||||||
|
{"member": request.user, "show": show, "nb_attr": 2}
|
||||||
|
)
|
||||||
|
# Contexte
|
||||||
|
ctxt = {
|
||||||
|
"show": show,
|
||||||
|
"exemple_mail_1place": exemple_mail_1place,
|
||||||
|
"exemple_mail_2places": exemple_mail_2places,
|
||||||
|
"custommail": custommail,
|
||||||
|
}
|
||||||
|
# Envoi confirmé
|
||||||
|
if request.method == "POST":
|
||||||
|
members = show.send_rappel()
|
||||||
|
ctxt["sent"] = True
|
||||||
|
ctxt["members"] = members
|
||||||
|
# Demande de confirmation
|
||||||
|
else:
|
||||||
|
ctxt["sent"] = False
|
||||||
|
if show.rappel_sent:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"Attention, un mail de rappel pour ce spectale a déjà été "
|
||||||
|
"envoyé le {}".format(
|
||||||
|
formats.localize(timezone.template_localtime(show.rappel_sent))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return render(request, "bda/mails-rappel.html", ctxt)
|
||||||
|
|
||||||
|
|
||||||
|
def descriptions_spectacles(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
|
||||||
|
"quote_set"
|
||||||
|
)
|
||||||
|
category_name = request.GET.get("category", "")
|
||||||
|
location_id = request.GET.get("location", "")
|
||||||
|
if category_name:
|
||||||
|
shows_qs = shows_qs.filter(category__name=category_name)
|
||||||
|
if location_id:
|
||||||
|
try:
|
||||||
|
shows_qs = shows_qs.filter(location__id=int(location_id))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"La variable GET 'location' doit contenir un entier"
|
||||||
|
)
|
||||||
|
return render(request, "descriptions.html", {"shows": shows_qs})
|
||||||
|
|
||||||
|
|
||||||
|
def catalogue(request, request_type):
|
||||||
|
"""
|
||||||
|
Vue destinée à communiquer avec un client AJAX, fournissant soit :
|
||||||
|
- la liste des tirages
|
||||||
|
- les catégories et salles d'un tirage
|
||||||
|
- les descriptions d'un tirage (filtrées selon la catégorie et la salle)
|
||||||
|
"""
|
||||||
|
if request_type == "list":
|
||||||
|
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
|
||||||
|
data_return = list(
|
||||||
|
Tirage.objects.filter(appear_catalogue=True).values("id", "title")
|
||||||
|
)
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
if request_type == "details":
|
||||||
|
# Dans ce cas on retourne une liste des catégories et des salles
|
||||||
|
tirage_id = request.GET.get("id", None)
|
||||||
|
if tirage_id is None:
|
||||||
|
return HttpResponseBadRequest("Missing GET parameter: id <int>")
|
||||||
|
try:
|
||||||
|
tirage = get_object_or_404(Tirage, id=int(tirage_id))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest("Bad format: int expected for `id`")
|
||||||
|
shows = tirage.spectacle_set.values_list("id", flat=True)
|
||||||
|
categories = list(
|
||||||
|
CategorieSpectacle.objects.filter(spectacle__in=shows)
|
||||||
|
.distinct()
|
||||||
|
.values("id", "name")
|
||||||
|
)
|
||||||
|
locations = list(
|
||||||
|
Salle.objects.filter(spectacle__in=shows).distinct().values("id", "name")
|
||||||
|
)
|
||||||
|
data_return = {"categories": categories, "locations": locations}
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
if request_type == "descriptions":
|
||||||
|
# Ici on retourne les descriptions correspondant à la catégorie et
|
||||||
|
# à la salle spécifiées
|
||||||
|
|
||||||
|
tirage_id = request.GET.get("id", "")
|
||||||
|
categories = request.GET.get("category", "[]")
|
||||||
|
locations = request.GET.get("location", "[]")
|
||||||
|
try:
|
||||||
|
tirage_id = int(tirage_id)
|
||||||
|
categories_id = json.loads(categories)
|
||||||
|
locations_id = json.loads(locations)
|
||||||
|
# Integers expected
|
||||||
|
if not all(isinstance(id, int) for id in categories_id):
|
||||||
|
raise ValueError
|
||||||
|
if not all(isinstance(id, int) for id in locations_id):
|
||||||
|
raise ValueError
|
||||||
|
except ValueError: # Contient JSONDecodeError
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Parse error, please ensure the GET parameters have the "
|
||||||
|
"following types:\n"
|
||||||
|
"id: int, category: [int], location: [int]\n"
|
||||||
|
"Data received:\n"
|
||||||
|
"id = {}, category = {}, locations = {}".format(
|
||||||
|
request.GET.get("id", ""),
|
||||||
|
request.GET.get("category", "[]"),
|
||||||
|
request.GET.get("location", "[]"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
|
||||||
|
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
|
||||||
|
"quote_set"
|
||||||
|
)
|
||||||
|
if categories_id and 0 not in categories_id:
|
||||||
|
shows_qs = shows_qs.filter(category__id__in=categories_id)
|
||||||
|
if locations_id and 0 not in locations_id:
|
||||||
|
shows_qs = shows_qs.filter(location__id__in=locations_id)
|
||||||
|
|
||||||
|
# On convertit les descriptions à envoyer en une liste facilement
|
||||||
|
# JSONifiable (il devrait y avoir un moyen plus efficace en
|
||||||
|
# redéfinissant le serializer de JSON)
|
||||||
|
data_return = [
|
||||||
|
{
|
||||||
|
"title": spectacle.title,
|
||||||
|
"category": str(spectacle.category),
|
||||||
|
"date": str(
|
||||||
|
formats.date_format(
|
||||||
|
timezone.localtime(spectacle.date), "SHORT_DATETIME_FORMAT"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"location": str(spectacle.location),
|
||||||
|
"vips": spectacle.vips,
|
||||||
|
"description": spectacle.description,
|
||||||
|
"slots_description": spectacle.slots_description,
|
||||||
|
"quotes": [
|
||||||
|
dict(author=quote.author, text=quote.text)
|
||||||
|
for quote in spectacle.quote_set.all()
|
||||||
|
],
|
||||||
|
"image": spectacle.getImgUrl(),
|
||||||
|
"ext_link": spectacle.ext_link,
|
||||||
|
"price": spectacle.price,
|
||||||
|
"slots": spectacle.slots,
|
||||||
|
}
|
||||||
|
for spectacle in shows_qs
|
||||||
|
]
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Autocomplete views
|
||||||
|
#
|
||||||
|
# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantAutocomplete(Select2QuerySetView):
|
||||||
|
model = Participant
|
||||||
|
search_fields = ("user__username", "user__first_name", "user__last_name")
|
||||||
|
|
||||||
|
|
||||||
|
participant_autocomplete = buro_required(ParticipantAutocomplete.as_view())
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleAutocomplete(Select2QuerySetView):
|
||||||
|
model = Spectacle
|
||||||
|
search_fields = ("title",)
|
||||||
|
|
||||||
|
|
||||||
|
spectacle_autocomplete = buro_required(SpectacleAutocomplete.as_view())
|
0
cof/__init__.py
Normal file
0
cof/__init__.py
Normal file
8
cof/asgi.py
Normal file
8
cof/asgi.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from channels.asgi import get_channel_layer
|
||||||
|
|
||||||
|
if "DJANGO_SETTINGS_MODULE" not in os.environ:
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
|
||||||
|
|
||||||
|
channel_layer = get_channel_layer()
|
0
cof/locale/__init__.py
Normal file
0
cof/locale/__init__.py
Normal file
0
cof/locale/fr/__init__.py
Normal file
0
cof/locale/fr/__init__.py
Normal file
5
cof/locale/fr/formats.py
Normal file
5
cof/locale/fr/formats.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
"""
|
||||||
|
Formats français.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DATETIME_FORMAT = r"l j F Y \à H:i"
|
3
cof/routing.py
Normal file
3
cof/routing.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from channels.routing import include
|
||||||
|
|
||||||
|
routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")]
|
1
cof/settings/.gitignore
vendored
Normal file
1
cof/settings/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
secret.py
|
0
cof/settings/__init__.py
Normal file
0
cof/settings/__init__.py
Normal file
252
cof/settings/common.py
Normal file
252
cof/settings/common.py
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
"""
|
||||||
|
Django common settings for cof project.
|
||||||
|
|
||||||
|
Everything which is supposed to be identical between the production server and
|
||||||
|
the local development server should be here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import secret
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"The secret.py file is missing.\n"
|
||||||
|
"For a development environment, simply copy secret_example.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def import_secret(name):
|
||||||
|
"""
|
||||||
|
Shorthand for importing a value from the secret module and raising an
|
||||||
|
informative exception if a secret is missing.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return getattr(secret, name)
|
||||||
|
except AttributeError:
|
||||||
|
raise RuntimeError("Secret missing: {}".format(name))
|
||||||
|
|
||||||
|
|
||||||
|
SECRET_KEY = import_secret("SECRET_KEY")
|
||||||
|
ADMINS = import_secret("ADMINS")
|
||||||
|
SERVER_EMAIL = import_secret("SERVER_EMAIL")
|
||||||
|
EMAIL_HOST = import_secret("EMAIL_HOST")
|
||||||
|
|
||||||
|
DBNAME = import_secret("DBNAME")
|
||||||
|
DBUSER = import_secret("DBUSER")
|
||||||
|
DBPASSWD = import_secret("DBPASSWD")
|
||||||
|
|
||||||
|
REDIS_PASSWD = import_secret("REDIS_PASSWD")
|
||||||
|
REDIS_DB = import_secret("REDIS_DB")
|
||||||
|
REDIS_HOST = import_secret("REDIS_HOST")
|
||||||
|
REDIS_PORT = import_secret("REDIS_PORT")
|
||||||
|
|
||||||
|
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
|
||||||
|
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
TESTING = sys.argv[1] == "test"
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"shared",
|
||||||
|
"gestioncof",
|
||||||
|
# Must be before 'django.contrib.admin'.
|
||||||
|
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
|
||||||
|
"dal",
|
||||||
|
"dal_select2",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.sites",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.admindocs",
|
||||||
|
"bda",
|
||||||
|
"captcha",
|
||||||
|
"django_cas_ng",
|
||||||
|
"bootstrapform",
|
||||||
|
"kfet",
|
||||||
|
"kfet.open",
|
||||||
|
"channels",
|
||||||
|
"widget_tweaks",
|
||||||
|
"custommail",
|
||||||
|
"djconfig",
|
||||||
|
"wagtail.wagtailforms",
|
||||||
|
"wagtail.wagtailredirects",
|
||||||
|
"wagtail.wagtailembeds",
|
||||||
|
"wagtail.wagtailsites",
|
||||||
|
"wagtail.wagtailusers",
|
||||||
|
"wagtail.wagtailsnippets",
|
||||||
|
"wagtail.wagtaildocs",
|
||||||
|
"wagtail.wagtailimages",
|
||||||
|
"wagtail.wagtailsearch",
|
||||||
|
"wagtail.wagtailadmin",
|
||||||
|
"wagtail.wagtailcore",
|
||||||
|
"wagtail.contrib.modeladmin",
|
||||||
|
"wagtailmenus",
|
||||||
|
"modelcluster",
|
||||||
|
"taggit",
|
||||||
|
"kfet.auth",
|
||||||
|
"kfet.cms",
|
||||||
|
"corsheaders",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.auth.middleware.SessionAuthenticationMiddleware",
|
||||||
|
"kfet.auth.middleware.TemporaryAuthMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"djconfig.middleware.DjConfigMiddleware",
|
||||||
|
"wagtail.wagtailcore.middleware.SiteMiddleware",
|
||||||
|
"wagtail.wagtailredirects.middleware.RedirectMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "cof.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
"django.template.context_processors.i18n",
|
||||||
|
"django.template.context_processors.media",
|
||||||
|
"django.template.context_processors.static",
|
||||||
|
"wagtailmenus.context_processors.wagtailmenus",
|
||||||
|
"djconfig.context_processors.config",
|
||||||
|
"gestioncof.shared.context_processor",
|
||||||
|
"kfet.auth.context_processors.temporary_auth",
|
||||||
|
"kfet.context_processors.config",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||||
|
"NAME": DBNAME,
|
||||||
|
"USER": DBUSER,
|
||||||
|
"PASSWORD": DBPASSWD,
|
||||||
|
"HOST": os.environ.get("DBHOST", "localhost"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "fr-fr"
|
||||||
|
|
||||||
|
TIME_ZONE = "Europe/Paris"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Various additional settings
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
|
||||||
|
GRAPPELLI_ADMIN_TITLE = '<a href="/">GestioCOF</a>'
|
||||||
|
|
||||||
|
MAIL_DATA = {
|
||||||
|
"petits_cours": {
|
||||||
|
"FROM": "Le COF <cof@ens.fr>",
|
||||||
|
"BCC": "archivescof@gmail.com",
|
||||||
|
"REPLYTO": "cof@ens.fr",
|
||||||
|
},
|
||||||
|
"rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"},
|
||||||
|
"revente": {
|
||||||
|
"FROM": "BdA-Revente <bda-revente@ens.fr>",
|
||||||
|
"REPLYTO": "BdA-Revente <bda-revente@ens.fr>",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGIN_URL = "cof-login"
|
||||||
|
LOGIN_REDIRECT_URL = "home"
|
||||||
|
|
||||||
|
CAS_SERVER_URL = "https://cas.eleves.ens.fr/"
|
||||||
|
CAS_VERSION = "3"
|
||||||
|
CAS_LOGIN_MSG = None
|
||||||
|
CAS_IGNORE_REFERER = True
|
||||||
|
CAS_REDIRECT_URL = "/"
|
||||||
|
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
|
"gestioncof.shared.COFCASBackend",
|
||||||
|
"kfet.auth.backends.GenericBackend",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# reCAPTCHA settings
|
||||||
|
# https://github.com/praekelt/django-recaptcha
|
||||||
|
#
|
||||||
|
# Default settings authorize reCAPTCHA usage for local developement.
|
||||||
|
# Public and private keys are appended in the 'prod' module settings.
|
||||||
|
|
||||||
|
NOCAPTCHA = True
|
||||||
|
RECAPTCHA_USE_SSL = True
|
||||||
|
|
||||||
|
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
|
||||||
|
|
||||||
|
# Cache settings
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "redis_cache.RedisCache",
|
||||||
|
"LOCATION": "redis://:{passwd}@{host}:{port}/db".format(
|
||||||
|
passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Channels settings
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [
|
||||||
|
(
|
||||||
|
"redis://:{passwd}@{host}:{port}/{db}".format(
|
||||||
|
passwd=REDIS_PASSWD,
|
||||||
|
host=REDIS_HOST,
|
||||||
|
port=REDIS_PORT,
|
||||||
|
db=REDIS_DB,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ROUTING": "cof.routing.routing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FORMAT_MODULE_PATH = "cof.locale"
|
||||||
|
|
||||||
|
# Wagtail settings
|
||||||
|
|
||||||
|
WAGTAIL_SITE_NAME = "GestioCOF"
|
||||||
|
WAGTAIL_ENABLE_UPDATE_CHECK = False
|
||||||
|
TAGGIT_CASE_INSENSITIVE = True
|
48
cof/settings/dev.py
Normal file
48
cof/settings/dev.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
"""
|
||||||
|
Django development settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
from .common import INSTALLED_APPS, MIDDLEWARE, TESTING
|
||||||
|
|
||||||
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
if TESTING:
|
||||||
|
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Apache static/media config
|
||||||
|
# ---
|
||||||
|
|
||||||
|
STATIC_URL = "/static/"
|
||||||
|
STATIC_ROOT = "/srv/gestiocof/static/"
|
||||||
|
|
||||||
|
MEDIA_ROOT = "/srv/gestiocof/media/"
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Debug tool bar
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
def show_toolbar(request):
|
||||||
|
"""
|
||||||
|
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
|
||||||
|
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
|
||||||
|
machine physique n'est pas forcément connue, et peut difficilement être
|
||||||
|
mise dans les INTERNAL_IPS.
|
||||||
|
"""
|
||||||
|
return DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
if not TESTING:
|
||||||
|
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||||
|
|
||||||
|
MIDDLEWARE = ["debug_panel.middleware.DebugPanelMiddleware"] + MIDDLEWARE
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}
|
31
cof/settings/local.py
Normal file
31
cof/settings/local.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"""
|
||||||
|
Django local settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .dev import * # NOQA
|
||||||
|
from .dev import BASE_DIR
|
||||||
|
|
||||||
|
# Use sqlite for local development
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the default cache backend for local development
|
||||||
|
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
|
||||||
|
|
||||||
|
# Use the default in memory asgi backend for local development
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgiref.inmemory.ChannelLayer",
|
||||||
|
"ROUTING": "cof.routing.routing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# No need to run collectstatic -> unset STATIC_ROOT
|
||||||
|
STATIC_ROOT = None
|
26
cof/settings/prod.py
Normal file
26
cof/settings/prod.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"""
|
||||||
|
Django development settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
from .common import BASE_DIR, import_secret
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["cof.ens.fr", "www.cof.ens.fr", "dev.cof.ens.fr"]
|
||||||
|
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static"
|
||||||
|
)
|
||||||
|
|
||||||
|
STATIC_URL = "/gestion/static/"
|
||||||
|
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
|
||||||
|
MEDIA_URL = "/gestion/media/"
|
||||||
|
|
||||||
|
|
||||||
|
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
|
||||||
|
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
|
21
cof/settings/secret_example.py
Normal file
21
cof/settings/secret_example.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah"
|
||||||
|
ADMINS = None
|
||||||
|
SERVER_EMAIL = "root@vagrant"
|
||||||
|
EMAIL_HOST = "localhost"
|
||||||
|
|
||||||
|
DBUSER = "cof_gestion"
|
||||||
|
DBNAME = "cof_gestion"
|
||||||
|
DBPASSWD = "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
||||||
|
|
||||||
|
REDIS_PASSWD = "dummy"
|
||||||
|
REDIS_PORT = 6379
|
||||||
|
REDIS_DB = 0
|
||||||
|
REDIS_HOST = "127.0.0.1"
|
||||||
|
|
||||||
|
RECAPTCHA_PUBLIC_KEY = "DUMMY"
|
||||||
|
RECAPTCHA_PRIVATE_KEY = "DUMMY"
|
||||||
|
|
||||||
|
EMAIL_HOST = None
|
||||||
|
|
||||||
|
KFETOPEN_TOKEN = "plop"
|
||||||
|
LDAP_SERVER_URL = None
|
135
cof/urls.py
Normal file
135
cof/urls.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
"""
|
||||||
|
Fichier principal de configuration des urls du projet GestioCOF
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import views as django_views
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
from django_cas_ng import views as django_cas_views
|
||||||
|
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
||||||
|
from wagtail.wagtailcore import urls as wagtail_urls
|
||||||
|
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
||||||
|
|
||||||
|
from gestioncof import csv_views, views as gestioncof_views
|
||||||
|
from gestioncof.autocomplete import autocomplete
|
||||||
|
from gestioncof.urls import (
|
||||||
|
calendar_patterns,
|
||||||
|
clubs_patterns,
|
||||||
|
events_patterns,
|
||||||
|
export_patterns,
|
||||||
|
petitcours_patterns,
|
||||||
|
surveys_patterns,
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.autodiscover()
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Page d'accueil
|
||||||
|
url(r"^$", gestioncof_views.home, name="home"),
|
||||||
|
# Le BdA
|
||||||
|
url(r"^bda/", include("bda.urls")),
|
||||||
|
# Les exports
|
||||||
|
url(r"^export/", include(export_patterns)),
|
||||||
|
# Les petits cours
|
||||||
|
url(r"^petitcours/", include(petitcours_patterns)),
|
||||||
|
# Les sondages
|
||||||
|
url(r"^survey/", include(surveys_patterns)),
|
||||||
|
# Evenements
|
||||||
|
url(r"^event/", include(events_patterns)),
|
||||||
|
# Calendrier
|
||||||
|
url(r"^calendar/", include(calendar_patterns)),
|
||||||
|
# Clubs
|
||||||
|
url(r"^clubs/", include(clubs_patterns)),
|
||||||
|
# Authentification
|
||||||
|
url(
|
||||||
|
r"^cof/denied$",
|
||||||
|
TemplateView.as_view(template_name="cof-denied.html"),
|
||||||
|
name="cof-denied",
|
||||||
|
),
|
||||||
|
url(r"^cas/login$", django_cas_views.login, name="cas_login_view"),
|
||||||
|
url(r"^cas/logout$", django_cas_views.logout),
|
||||||
|
url(r"^outsider/login$", gestioncof_views.login_ext, name="ext_login_view"),
|
||||||
|
url(r"^outsider/logout$", django_views.logout, {"next_page": "home"}),
|
||||||
|
url(r"^login$", gestioncof_views.login, name="cof-login"),
|
||||||
|
url(r"^logout$", gestioncof_views.logout, name="cof-logout"),
|
||||||
|
# Infos persos
|
||||||
|
url(r"^profile$", gestioncof_views.profile, name="profile"),
|
||||||
|
url(
|
||||||
|
r"^outsider/password-change$",
|
||||||
|
django_views.password_change,
|
||||||
|
name="password_change",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^outsider/password-change-done$",
|
||||||
|
django_views.password_change_done,
|
||||||
|
name="password_change_done",
|
||||||
|
),
|
||||||
|
# Inscription d'un nouveau membre
|
||||||
|
url(r"^registration$", gestioncof_views.registration, name="registration"),
|
||||||
|
url(
|
||||||
|
r"^registration/clipper/(?P<login_clipper>[\w-]+)/" r"(?P<fullname>.*)$",
|
||||||
|
gestioncof_views.registration_form2,
|
||||||
|
name="clipper-registration",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^registration/user/(?P<username>.+)$",
|
||||||
|
gestioncof_views.registration_form2,
|
||||||
|
name="user-registration",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^registration/empty$",
|
||||||
|
gestioncof_views.registration_form2,
|
||||||
|
name="empty-registration",
|
||||||
|
),
|
||||||
|
# Autocompletion
|
||||||
|
url(
|
||||||
|
r"^autocomplete/registration$",
|
||||||
|
autocomplete,
|
||||||
|
name="cof.registration.autocomplete",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^user/autocomplete$",
|
||||||
|
gestioncof_views.user_autocomplete,
|
||||||
|
name="cof-user-autocomplete",
|
||||||
|
),
|
||||||
|
# Interface admin
|
||||||
|
url(r"^admin/logout/", gestioncof_views.logout),
|
||||||
|
url(r"^admin/doc/", include("django.contrib.admindocs.urls")),
|
||||||
|
url(
|
||||||
|
r"^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/",
|
||||||
|
csv_views.admin_list_export,
|
||||||
|
{"fields": ["username"]},
|
||||||
|
),
|
||||||
|
url(r"^admin/", include(admin.site.urls)),
|
||||||
|
# Liens utiles du COF et du BdA
|
||||||
|
url(r"^utile_cof$", gestioncof_views.utile_cof, name="utile_cof"),
|
||||||
|
url(r"^utile_bda$", gestioncof_views.utile_bda, name="utile_bda"),
|
||||||
|
url(r"^utile_bda/bda_diff$", gestioncof_views.liste_bdadiff, name="ml_diffbda"),
|
||||||
|
url(r"^utile_cof/diff_cof$", gestioncof_views.liste_diffcof, name="ml_diffcof"),
|
||||||
|
url(
|
||||||
|
r"^utile_bda/bda_revente$",
|
||||||
|
gestioncof_views.liste_bdarevente,
|
||||||
|
name="ml_bda_revente",
|
||||||
|
),
|
||||||
|
url(r"^k-fet/", include("kfet.urls")),
|
||||||
|
url(r"^cms/", include(wagtailadmin_urls)),
|
||||||
|
url(r"^documents/", include(wagtaildocs_urls)),
|
||||||
|
# djconfig
|
||||||
|
url(r"^config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if "debug_toolbar" in settings.INSTALLED_APPS:
|
||||||
|
import debug_toolbar
|
||||||
|
|
||||||
|
urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
# Si on est en production, MEDIA_ROOT est servi par Apache.
|
||||||
|
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
# Wagtail for uncatched
|
||||||
|
urlpatterns += [url(r"", include(wagtail_urls))]
|
1
gestioncof/__init__.py
Normal file
1
gestioncof/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = "gestioncof.apps.GestioncofConfig"
|
333
gestioncof/admin.py
Normal file
333
gestioncof/admin.py
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
from dal.autocomplete import ModelSelect2
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.contrib.auth.models import Group, Permission, User
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from gestioncof.models import (
|
||||||
|
Club,
|
||||||
|
CofProfile,
|
||||||
|
Event,
|
||||||
|
EventCommentField,
|
||||||
|
EventOption,
|
||||||
|
EventOptionChoice,
|
||||||
|
EventRegistration,
|
||||||
|
Survey,
|
||||||
|
SurveyQuestion,
|
||||||
|
SurveyQuestionAnswer,
|
||||||
|
)
|
||||||
|
from gestioncof.petits_cours_models import (
|
||||||
|
PetitCoursAbility,
|
||||||
|
PetitCoursAttribution,
|
||||||
|
PetitCoursAttributionCounter,
|
||||||
|
PetitCoursDemande,
|
||||||
|
PetitCoursSubject,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_link_field(target_model="", field="", link_text=str, desc_text=str):
|
||||||
|
def add_link(cls):
|
||||||
|
reverse_name = target_model or cls.model.__name__.lower()
|
||||||
|
|
||||||
|
def link(self, instance):
|
||||||
|
app_name = instance._meta.app_label
|
||||||
|
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
|
||||||
|
link_obj = getattr(instance, field, None) or instance
|
||||||
|
if not link_obj.id:
|
||||||
|
return ""
|
||||||
|
url = reverse(reverse_path, args=(link_obj.id,))
|
||||||
|
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
|
||||||
|
|
||||||
|
link.allow_tags = True
|
||||||
|
link.short_description = desc_text(reverse_name + " link")
|
||||||
|
cls.link = link
|
||||||
|
cls.readonly_fields = list(getattr(cls, "readonly_fields", [])) + ["link"]
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return add_link
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyQuestionAnswerInline(admin.TabularInline):
|
||||||
|
model = SurveyQuestionAnswer
|
||||||
|
|
||||||
|
|
||||||
|
@add_link_field(
|
||||||
|
desc_text=lambda x: "Réponses", link_text=lambda x: "Éditer les réponses"
|
||||||
|
)
|
||||||
|
class SurveyQuestionInline(admin.TabularInline):
|
||||||
|
model = SurveyQuestion
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyQuestionAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ("survey__title", "answer")
|
||||||
|
inlines = [SurveyQuestionAnswerInline]
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ("title", "details")
|
||||||
|
inlines = [SurveyQuestionInline]
|
||||||
|
|
||||||
|
|
||||||
|
class EventOptionChoiceInline(admin.TabularInline):
|
||||||
|
model = EventOptionChoice
|
||||||
|
|
||||||
|
|
||||||
|
@add_link_field(desc_text=lambda x: "Choix", link_text=lambda x: "Éditer les choix")
|
||||||
|
class EventOptionInline(admin.TabularInline):
|
||||||
|
model = EventOption
|
||||||
|
|
||||||
|
|
||||||
|
class EventCommentFieldInline(admin.TabularInline):
|
||||||
|
model = EventCommentField
|
||||||
|
|
||||||
|
|
||||||
|
class EventOptionAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ("event__title", "name")
|
||||||
|
inlines = [EventOptionChoiceInline]
|
||||||
|
|
||||||
|
|
||||||
|
class EventAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ("title", "location", "description")
|
||||||
|
inlines = [EventOptionInline, EventCommentFieldInline]
|
||||||
|
|
||||||
|
|
||||||
|
class CofProfileInline(admin.StackedInline):
|
||||||
|
model = CofProfile
|
||||||
|
inline_classes = ("collapse open",)
|
||||||
|
|
||||||
|
|
||||||
|
class FkeyLookup(object):
|
||||||
|
def __init__(self, fkeydecl, short_description=None, admin_order_field=None):
|
||||||
|
self.fk, fkattrs = fkeydecl.split("__", 1)
|
||||||
|
self.fkattrs = fkattrs.split("__")
|
||||||
|
|
||||||
|
self.short_description = short_description or self.fkattrs[-1]
|
||||||
|
self.admin_order_field = admin_order_field or fkeydecl
|
||||||
|
|
||||||
|
def __get__(self, obj, klass):
|
||||||
|
if obj is None:
|
||||||
|
"""
|
||||||
|
hack required to make Django validate (if obj is
|
||||||
|
None, then we're a class, and classes are callable
|
||||||
|
<wink>)
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
item = getattr(obj, self.fk)
|
||||||
|
for attr in self.fkattrs:
|
||||||
|
item = getattr(item, attr)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def ProfileInfo(field, short_description, boolean=False):
|
||||||
|
def getter(self):
|
||||||
|
try:
|
||||||
|
return getattr(self.profile, field)
|
||||||
|
except CofProfile.DoesNotExist:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
getter.short_description = short_description
|
||||||
|
getter.boolean = boolean
|
||||||
|
return getter
|
||||||
|
|
||||||
|
|
||||||
|
User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper")
|
||||||
|
User.profile_phone = ProfileInfo("phone", "Téléphone")
|
||||||
|
User.profile_occupation = ProfileInfo("occupation", "Occupation")
|
||||||
|
User.profile_departement = ProfileInfo("departement", "Departement")
|
||||||
|
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True)
|
||||||
|
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True)
|
||||||
|
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente", "ML BdA-R", True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileAdmin(UserAdmin):
|
||||||
|
def is_buro(self, obj):
|
||||||
|
try:
|
||||||
|
return obj.profile.is_buro
|
||||||
|
except CofProfile.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_buro.short_description = "Membre du Buro"
|
||||||
|
is_buro.boolean = True
|
||||||
|
|
||||||
|
def is_cof(self, obj):
|
||||||
|
try:
|
||||||
|
return obj.profile.is_cof
|
||||||
|
except CofProfile.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_cof.short_description = "Membre du COF"
|
||||||
|
is_cof.boolean = True
|
||||||
|
|
||||||
|
list_display = UserAdmin.list_display + (
|
||||||
|
"profile_login_clipper",
|
||||||
|
"profile_phone",
|
||||||
|
"profile_occupation",
|
||||||
|
"profile_mailing_cof",
|
||||||
|
"profile_mailing_bda",
|
||||||
|
"profile_mailing_bda_revente",
|
||||||
|
"is_cof",
|
||||||
|
"is_buro",
|
||||||
|
)
|
||||||
|
list_display_links = ("username", "email", "first_name", "last_name")
|
||||||
|
list_filter = UserAdmin.list_filter + (
|
||||||
|
"profile__is_cof",
|
||||||
|
"profile__is_buro",
|
||||||
|
"profile__mailing_cof",
|
||||||
|
"profile__mailing_bda",
|
||||||
|
)
|
||||||
|
search_fields = UserAdmin.search_fields + ("profile__phone",)
|
||||||
|
inlines = [CofProfileInline]
|
||||||
|
|
||||||
|
staff_fieldsets = [
|
||||||
|
(None, {"fields": ["username", "password"]}),
|
||||||
|
(_("Personal info"), {"fields": ["first_name", "last_name", "email"]}),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_fieldsets(self, request, user=None):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return self.staff_fieldsets
|
||||||
|
return super().get_fieldsets(request, user)
|
||||||
|
|
||||||
|
def save_model(self, request, user, form, change):
|
||||||
|
cof_group, created = Group.objects.get_or_create(name="COF")
|
||||||
|
if created:
|
||||||
|
# Si le groupe COF n'était pas déjà dans la bdd
|
||||||
|
# On lui assigne les bonnes permissions
|
||||||
|
perms = Permission.objects.filter(
|
||||||
|
Q(content_type__app_label="gestioncof")
|
||||||
|
| Q(content_type__app_label="bda")
|
||||||
|
| (Q(content_type__app_label="auth") & Q(content_type__model="user"))
|
||||||
|
)
|
||||||
|
cof_group.permissions = perms
|
||||||
|
# On y associe les membres du Burô
|
||||||
|
cof_group.user_set = User.objects.filter(profile__is_buro=True)
|
||||||
|
# Sauvegarde
|
||||||
|
cof_group.save()
|
||||||
|
# le Burô est staff et appartient au groupe COF
|
||||||
|
if user.profile.is_buro:
|
||||||
|
user.is_staff = True
|
||||||
|
user.groups.add(cof_group)
|
||||||
|
else:
|
||||||
|
user.is_staff = False
|
||||||
|
user.groups.remove(cof_group)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: This is absolutely horrible.
|
||||||
|
def user_str(self):
|
||||||
|
if self.first_name and self.last_name:
|
||||||
|
return "{} ({})".format(self.get_full_name(), self.username)
|
||||||
|
else:
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
|
User.__str__ = user_str
|
||||||
|
|
||||||
|
|
||||||
|
class EventRegistrationAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {"user": ModelSelect2(url="cof-user-autocomplete")}
|
||||||
|
|
||||||
|
|
||||||
|
class EventRegistrationAdmin(admin.ModelAdmin):
|
||||||
|
form = EventRegistrationAdminForm
|
||||||
|
|
||||||
|
list_display = ("__str__", "event", "user", "paid")
|
||||||
|
list_filter = ("paid",)
|
||||||
|
search_fields = (
|
||||||
|
"user__username",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__email",
|
||||||
|
"event__title",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursAbilityAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("user", "matiere", "niveau", "agrege")
|
||||||
|
search_fields = (
|
||||||
|
"user__username",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__email",
|
||||||
|
"matiere__name",
|
||||||
|
"niveau",
|
||||||
|
)
|
||||||
|
list_filter = ("matiere", "niveau", "agrege")
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursAttributionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("user", "demande", "matiere", "rank")
|
||||||
|
search_fields = ("user__username", "matiere__name")
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursAttributionCounterAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("user", "matiere", "count")
|
||||||
|
list_filter = ("matiere",)
|
||||||
|
search_fields = (
|
||||||
|
"user__username",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__email",
|
||||||
|
"matiere__name",
|
||||||
|
)
|
||||||
|
actions = ["reset"]
|
||||||
|
actions_on_bottom = True
|
||||||
|
|
||||||
|
def reset(self, request, queryset):
|
||||||
|
queryset.update(count=0)
|
||||||
|
|
||||||
|
reset.short_description = "Remise à zéro du compteur"
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursDemandeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"email",
|
||||||
|
"agrege_requis",
|
||||||
|
"niveau",
|
||||||
|
"created",
|
||||||
|
"traitee",
|
||||||
|
"processed",
|
||||||
|
)
|
||||||
|
list_filter = ("traitee", "niveau")
|
||||||
|
search_fields = ("name", "email", "phone", "lieu", "remarques")
|
||||||
|
|
||||||
|
|
||||||
|
class ClubAdminForm(forms.ModelForm):
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
respos = cleaned_data.get("respos")
|
||||||
|
members = cleaned_data.get("membres")
|
||||||
|
for respo in respos.all():
|
||||||
|
if respo not in members:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Erreur : le respo %s n'est pas membre du club."
|
||||||
|
% respo.get_full_name()
|
||||||
|
)
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class ClubAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name"]
|
||||||
|
form = ClubAdminForm
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Survey, SurveyAdmin)
|
||||||
|
admin.site.register(SurveyQuestion, SurveyQuestionAdmin)
|
||||||
|
admin.site.register(Event, EventAdmin)
|
||||||
|
admin.site.register(EventOption, EventOptionAdmin)
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.register(User, UserProfileAdmin)
|
||||||
|
admin.site.register(CofProfile)
|
||||||
|
admin.site.register(Club, ClubAdmin)
|
||||||
|
admin.site.register(PetitCoursSubject)
|
||||||
|
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
|
||||||
|
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)
|
||||||
|
admin.site.register(PetitCoursAttributionCounter, PetitCoursAttributionCounterAdmin)
|
||||||
|
admin.site.register(PetitCoursDemande, PetitCoursDemandeAdmin)
|
||||||
|
admin.site.register(EventRegistration, EventRegistrationAdmin)
|
17
gestioncof/apps.py
Normal file
17
gestioncof/apps.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GestioncofConfig(AppConfig):
|
||||||
|
name = "gestioncof"
|
||||||
|
verbose_name = "Gestion des adhérents du COF"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals # noqa
|
||||||
|
|
||||||
|
self.register_config()
|
||||||
|
|
||||||
|
def register_config(self):
|
||||||
|
import djconfig
|
||||||
|
from .forms import GestioncofConfigForm
|
||||||
|
|
||||||
|
djconfig.register(GestioncofConfigForm)
|
88
gestioncof/autocomplete.py
Normal file
88
gestioncof/autocomplete.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
from django import shortcuts
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.http import Http404
|
||||||
|
from ldap3 import Connection
|
||||||
|
|
||||||
|
from gestioncof.decorators import buro_required
|
||||||
|
from gestioncof.models import CofProfile
|
||||||
|
|
||||||
|
|
||||||
|
class Clipper(object):
|
||||||
|
def __init__(self, clipper, fullname):
|
||||||
|
if fullname is None:
|
||||||
|
fullname = ""
|
||||||
|
assert isinstance(clipper, str)
|
||||||
|
assert isinstance(fullname, str)
|
||||||
|
self.clipper = clipper
|
||||||
|
self.fullname = fullname
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{} ({})".format(self.clipper, self.fullname)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.clipper == other.clipper and self.fullname == other.fullname
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def autocomplete(request):
|
||||||
|
if "q" not in request.GET:
|
||||||
|
raise Http404
|
||||||
|
q = request.GET["q"]
|
||||||
|
data = {"q": q}
|
||||||
|
|
||||||
|
queries = {}
|
||||||
|
bits = q.split()
|
||||||
|
|
||||||
|
# Fetching data from User and CofProfile tables
|
||||||
|
queries["members"] = CofProfile.objects.filter(is_cof=True)
|
||||||
|
queries["users"] = User.objects.filter(profile__is_cof=False)
|
||||||
|
for bit in bits:
|
||||||
|
queries["members"] = queries["members"].filter(
|
||||||
|
Q(user__first_name__icontains=bit)
|
||||||
|
| Q(user__last_name__icontains=bit)
|
||||||
|
| Q(user__username__icontains=bit)
|
||||||
|
| Q(login_clipper__icontains=bit)
|
||||||
|
)
|
||||||
|
queries["users"] = queries["users"].filter(
|
||||||
|
Q(first_name__icontains=bit)
|
||||||
|
| Q(last_name__icontains=bit)
|
||||||
|
| Q(username__icontains=bit)
|
||||||
|
)
|
||||||
|
queries["members"] = queries["members"].distinct()
|
||||||
|
queries["users"] = queries["users"].distinct()
|
||||||
|
|
||||||
|
# Clearing redundancies
|
||||||
|
usernames = set(queries["members"].values_list("login_clipper", flat="True")) | set(
|
||||||
|
queries["users"].values_list("profile__login_clipper", flat="True")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetching data from the SPI
|
||||||
|
if getattr(settings, "LDAP_SERVER_URL", None):
|
||||||
|
# Fetching
|
||||||
|
ldap_query = "(&{:s})".format(
|
||||||
|
"".join(
|
||||||
|
"(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=bit)
|
||||||
|
for bit in bits
|
||||||
|
if bit.isalnum()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if ldap_query != "(&)":
|
||||||
|
# If none of the bits were legal, we do not perform the query
|
||||||
|
entries = None
|
||||||
|
with Connection(settings.LDAP_SERVER_URL) as conn:
|
||||||
|
conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"])
|
||||||
|
entries = conn.entries
|
||||||
|
# Clearing redundancies
|
||||||
|
queries["clippers"] = [
|
||||||
|
Clipper(entry.uid.value, entry.cn.value)
|
||||||
|
for entry in entries
|
||||||
|
if entry.uid.value and entry.uid.value not in usernames
|
||||||
|
]
|
||||||
|
|
||||||
|
# Resulting data
|
||||||
|
data.update(queries)
|
||||||
|
data["options"] = sum(len(query) for query in queries)
|
||||||
|
|
||||||
|
return shortcuts.render(request, "autocomplete_user.html", data)
|
72
gestioncof/csv_views.py
Normal file
72
gestioncof/csv_views.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.http import HttpResponse, HttpResponseForbidden
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
|
||||||
|
|
||||||
|
def export(qs, fields=None):
|
||||||
|
model = qs.model
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = "attachment; filename=%s.csv" % slugify(
|
||||||
|
model.__name__
|
||||||
|
)
|
||||||
|
writer = csv.writer(response)
|
||||||
|
# Write headers to CSV file
|
||||||
|
if fields:
|
||||||
|
headers = fields
|
||||||
|
else:
|
||||||
|
headers = []
|
||||||
|
for field in model._meta.fields:
|
||||||
|
headers.append(field.name)
|
||||||
|
writer.writerow(headers)
|
||||||
|
# Write data to CSV file
|
||||||
|
for obj in qs:
|
||||||
|
row = []
|
||||||
|
for field in headers:
|
||||||
|
if field in headers:
|
||||||
|
val = getattr(obj, field)
|
||||||
|
if callable(val):
|
||||||
|
val = val()
|
||||||
|
row.append(val)
|
||||||
|
writer.writerow(row)
|
||||||
|
# Return CSV file to browser as download
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def admin_list_export(
|
||||||
|
request, model_name, app_label, queryset=None, fields=None, list_display=True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Put the following line in your urls.py BEFORE your admin include
|
||||||
|
(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',
|
||||||
|
'util.csv_view.admin_list_export'),
|
||||||
|
"""
|
||||||
|
if not request.user.is_staff:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
if not queryset:
|
||||||
|
model = apps.get_model(app_label, model_name)
|
||||||
|
queryset = model.objects.all()
|
||||||
|
queryset = queryset.filter(profile__is_cof=True)
|
||||||
|
if not fields:
|
||||||
|
if list_display and len(queryset.model._meta.admin.list_display) > 1:
|
||||||
|
fields = queryset.model._meta.admin.list_display
|
||||||
|
else:
|
||||||
|
fields = None
|
||||||
|
return export(queryset, fields)
|
||||||
|
"""
|
||||||
|
Create your own change_list.html for your admin view and put something
|
||||||
|
like this in it:
|
||||||
|
{% block object-tools %}
|
||||||
|
<ul class="object-tools">
|
||||||
|
<li><a href="csv/{%if request.GET%}?{{request.GET.urlencode}}
|
||||||
|
{%endif%}" class="addlink">Export to CSV</a></li>
|
||||||
|
{% if has_add_permission %}
|
||||||
|
<li><a href="add/{% if is_popup %}?_popup=1{% endif %}"
|
||||||
|
class="addlink">
|
||||||
|
{% blocktrans with cl.opts.verbose_name|escape as name %}
|
||||||
|
Add {{ name }}{% endblocktrans %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
"""
|
23
gestioncof/decorators.py
Normal file
23
gestioncof/decorators.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
|
|
||||||
|
|
||||||
|
def is_cof(user):
|
||||||
|
try:
|
||||||
|
profile = user.profile
|
||||||
|
return profile.is_cof
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
cof_required = user_passes_test(is_cof)
|
||||||
|
|
||||||
|
|
||||||
|
def is_buro(user):
|
||||||
|
try:
|
||||||
|
profile = user.profile
|
||||||
|
return profile.is_buro
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
buro_required = user_passes_test(is_buro)
|
199
gestioncof/fixtures/gestion.json
Normal file
199
gestioncof/fixtures/gestion.json
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"old": false,
|
||||||
|
"details": "Il nous casse les oreilles, qu'est ce qu'on en fait\u00a0?",
|
||||||
|
"survey_open": true,
|
||||||
|
"title": "Sort du barde"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.survey",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"question": "Sanction s'il chante",
|
||||||
|
"survey": 1,
|
||||||
|
"multi_answers": true
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestion",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"question": "Est-ce qu'on le garde\u00a0?",
|
||||||
|
"survey": 1,
|
||||||
|
"multi_answers": false
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestion",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "On l'ernestise",
|
||||||
|
"survey_question": 1
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "On ligote",
|
||||||
|
"survey_question": 1
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "On le prive de banquet",
|
||||||
|
"survey_question": 1
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "Oui",
|
||||||
|
"survey_question": 2
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "Non",
|
||||||
|
"survey_question": 2
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"old": false,
|
||||||
|
"description": "On va casser du romain.",
|
||||||
|
"end_date": "2016-09-12T00:00:00Z",
|
||||||
|
"title": "Bataille de Gergovie",
|
||||||
|
"image": "",
|
||||||
|
"location": "Gergovie",
|
||||||
|
"registration_open": true,
|
||||||
|
"start_date": "2016-09-09T00:00:00Z"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.event",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"default": "",
|
||||||
|
"event": 1,
|
||||||
|
"fieldtype": "text",
|
||||||
|
"name": "Commentaires"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventcommentfield",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"multi_choices": true,
|
||||||
|
"event": 1,
|
||||||
|
"name": "Potion magique"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoption",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"event_option": 1,
|
||||||
|
"value": "Je suis alergique"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoptionchoice",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"event_option": 1,
|
||||||
|
"value": "J'en veux"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoptionchoice",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"event_option": 1,
|
||||||
|
"value": "Je suis tomb\u00e9 dans la marmite quand j'\u00e9tais petit"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoptionchoice",
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Bagarre"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Lancer de menhir"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Pr\u00e9paration de potions"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Chant"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"traitee": false,
|
||||||
|
"remarques": "En grande difficult\u00e9",
|
||||||
|
"quand": "weekend (dimanche) / soir apr\u00e8s les cours",
|
||||||
|
"name": "Jules C\u00e9sar",
|
||||||
|
"created": "2016-07-15T11:12:35Z",
|
||||||
|
"niveau": "prepa1styear",
|
||||||
|
"agrege_requis": false,
|
||||||
|
"phone": "",
|
||||||
|
"traitee_par": null,
|
||||||
|
"matieres": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"lieu": "Al\u00e9sia",
|
||||||
|
"freq": "3 fois / semaine",
|
||||||
|
"email": "jules.cesar@polytechnique.edu",
|
||||||
|
"processed": null
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcoursdemande",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"traitee": false,
|
||||||
|
"remarques": "",
|
||||||
|
"quand": "Weekends",
|
||||||
|
"name": "Jules C\u00e9sar",
|
||||||
|
"created": "2016-07-15T11:13:26Z",
|
||||||
|
"niveau": "lycee",
|
||||||
|
"agrege_requis": true,
|
||||||
|
"phone": "",
|
||||||
|
"traitee_par": null,
|
||||||
|
"matieres": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"lieu": "\u00e0 domicile",
|
||||||
|
"freq": "toutes les semaines",
|
||||||
|
"email": "jules.cesar@polytechnique.edu",
|
||||||
|
"processed": null
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcoursdemande",
|
||||||
|
"pk": 2
|
||||||
|
}
|
||||||
|
]
|
10
gestioncof/fixtures/sites.json
Normal file
10
gestioncof/fixtures/sites.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"domain": "localhost",
|
||||||
|
"name": "GestioCOF - dev - local"
|
||||||
|
},
|
||||||
|
"model": "sites.site",
|
||||||
|
"pk": 1
|
||||||
|
}
|
||||||
|
]
|
419
gestioncof/forms.py
Normal file
419
gestioncof/forms.py
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.forms.formsets import BaseFormSet, formset_factory
|
||||||
|
from django.forms.widgets import CheckboxSelectMultiple, RadioSelect
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from djconfig.forms import ConfigForm
|
||||||
|
|
||||||
|
from bda.models import Spectacle
|
||||||
|
from gestioncof.models import CalendarSubscription, Club, CofProfile, EventCommentValue
|
||||||
|
from gestioncof.widgets import TriStateCheckbox
|
||||||
|
|
||||||
|
|
||||||
|
class EventForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs.pop("event")
|
||||||
|
self.event = event
|
||||||
|
current_choices = kwargs.pop("current_choices", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
choices = {}
|
||||||
|
if current_choices:
|
||||||
|
for choice in current_choices.all():
|
||||||
|
if choice.event_option.id not in choices:
|
||||||
|
choices[choice.event_option.id] = [choice.id]
|
||||||
|
else:
|
||||||
|
choices[choice.event_option.id].append(choice.id)
|
||||||
|
all_choices = choices
|
||||||
|
for option in event.options.all():
|
||||||
|
choices = [(choice.id, choice.value) for choice in option.choices.all()]
|
||||||
|
if option.multi_choices:
|
||||||
|
initial = [] if option.id not in all_choices else all_choices[option.id]
|
||||||
|
field = forms.MultipleChoiceField(
|
||||||
|
label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
initial = (
|
||||||
|
None if option.id not in all_choices else all_choices[option.id][0]
|
||||||
|
)
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
field.option_id = option.id
|
||||||
|
self.fields["option_%d" % option.id] = field
|
||||||
|
|
||||||
|
def choices(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith("option_"):
|
||||||
|
yield (self.fields[name].option_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
survey = kwargs.pop("survey")
|
||||||
|
current_answers = kwargs.pop("current_answers", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
answers = {}
|
||||||
|
if current_answers:
|
||||||
|
for answer in current_answers.all():
|
||||||
|
if answer.survey_question.id not in answers:
|
||||||
|
answers[answer.survey_question.id] = [answer.id]
|
||||||
|
else:
|
||||||
|
answers[answer.survey_question.id].append(answer.id)
|
||||||
|
for question in survey.questions.all():
|
||||||
|
choices = [(answer.id, answer.answer) for answer in question.answers.all()]
|
||||||
|
if question.multi_answers:
|
||||||
|
initial = [] if question.id not in answers else answers[question.id]
|
||||||
|
field = forms.MultipleChoiceField(
|
||||||
|
label=question.question,
|
||||||
|
choices=choices,
|
||||||
|
widget=CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
initial = (
|
||||||
|
None if question.id not in answers else answers[question.id][0]
|
||||||
|
)
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label=question.question,
|
||||||
|
choices=choices,
|
||||||
|
widget=RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
field.question_id = question.id
|
||||||
|
self.fields["question_%d" % question.id] = field
|
||||||
|
|
||||||
|
def answers(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith("question_"):
|
||||||
|
yield (self.fields[name].question_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyStatusFilterForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
survey = kwargs.pop("survey")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for question in survey.questions.all():
|
||||||
|
for answer in question.answers.all():
|
||||||
|
name = "question_%d_answer_%d" % (question.id, answer.id)
|
||||||
|
if self.is_bound and self.data.get(self.add_prefix(name), None):
|
||||||
|
initial = self.data.get(self.add_prefix(name), None)
|
||||||
|
else:
|
||||||
|
initial = "none"
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label="%s : %s" % (question.question, answer.answer),
|
||||||
|
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
|
||||||
|
widget=TriStateCheckbox,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
field.question_id = question.id
|
||||||
|
field.answer_id = answer.id
|
||||||
|
self.fields[name] = field
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith("question_"):
|
||||||
|
yield (
|
||||||
|
self.fields[name].question_id,
|
||||||
|
self.fields[name].answer_id,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EventStatusFilterForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs.pop("event")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for option in event.options.all():
|
||||||
|
for choice in option.choices.all():
|
||||||
|
name = "option_%d_choice_%d" % (option.id, choice.id)
|
||||||
|
if self.is_bound and self.data.get(self.add_prefix(name), None):
|
||||||
|
initial = self.data.get(self.add_prefix(name), None)
|
||||||
|
else:
|
||||||
|
initial = "none"
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label="%s : %s" % (option.name, choice.value),
|
||||||
|
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
|
||||||
|
widget=TriStateCheckbox,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
field.option_id = option.id
|
||||||
|
field.choice_id = choice.id
|
||||||
|
self.fields[name] = field
|
||||||
|
# has_paid
|
||||||
|
name = "event_has_paid"
|
||||||
|
if self.is_bound and self.data.get(self.add_prefix(name), None):
|
||||||
|
initial = self.data.get(self.add_prefix(name), None)
|
||||||
|
else:
|
||||||
|
initial = "none"
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label="Événement payé",
|
||||||
|
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
|
||||||
|
widget=TriStateCheckbox,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
self.fields[name] = field
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith("option_"):
|
||||||
|
yield (self.fields[name].option_id, self.fields[name].choice_id, value)
|
||||||
|
elif name == "event_has_paid":
|
||||||
|
yield ("has_paid", None, value)
|
||||||
|
|
||||||
|
|
||||||
|
class UserForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CofProfile
|
||||||
|
fields = [
|
||||||
|
"phone",
|
||||||
|
"mailing_cof",
|
||||||
|
"mailing_bda",
|
||||||
|
"mailing_bda_revente",
|
||||||
|
"mailing_unernestaparis",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationUserForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
super().__init__(*args, **kw)
|
||||||
|
self.fields["username"].help_text = ""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("username", "first_name", "last_name", "email")
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationPassUserForm(RegistrationUserForm):
|
||||||
|
"""
|
||||||
|
Formulaire pour changer le mot de passe d'un utilisateur.
|
||||||
|
"""
|
||||||
|
|
||||||
|
password1 = forms.CharField(label=_("Mot de passe"), widget=forms.PasswordInput)
|
||||||
|
password2 = forms.CharField(
|
||||||
|
label=_("Confirmation du mot de passe"), widget=forms.PasswordInput
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_password2(self):
|
||||||
|
pass1 = self.cleaned_data["password1"]
|
||||||
|
pass2 = self.cleaned_data["password2"]
|
||||||
|
if pass1 and pass2:
|
||||||
|
if pass1 != pass2:
|
||||||
|
raise forms.ValidationError(_("Mots de passe non identiques."))
|
||||||
|
return pass2
|
||||||
|
|
||||||
|
def save(self, commit=True, *args, **kwargs):
|
||||||
|
user = super().save(commit, *args, **kwargs)
|
||||||
|
user.set_password(self.cleaned_data["password2"])
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationProfileForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
super().__init__(*args, **kw)
|
||||||
|
self.fields["mailing_cof"].initial = True
|
||||||
|
self.fields["mailing_bda"].initial = True
|
||||||
|
self.fields["mailing_bda_revente"].initial = True
|
||||||
|
self.fields["mailing_unernestaparis"].initial = True
|
||||||
|
|
||||||
|
self.fields.keyOrder = [
|
||||||
|
"login_clipper",
|
||||||
|
"phone",
|
||||||
|
"occupation",
|
||||||
|
"departement",
|
||||||
|
"is_cof",
|
||||||
|
"type_cotiz",
|
||||||
|
"mailing_cof",
|
||||||
|
"mailing_bda",
|
||||||
|
"mailing_bda_revente",
|
||||||
|
"mailing_unernestaparis",
|
||||||
|
"comments",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CofProfile
|
||||||
|
fields = (
|
||||||
|
"login_clipper",
|
||||||
|
"phone",
|
||||||
|
"occupation",
|
||||||
|
"departement",
|
||||||
|
"is_cof",
|
||||||
|
"type_cotiz",
|
||||||
|
"mailing_cof",
|
||||||
|
"mailing_bda",
|
||||||
|
"mailing_bda_revente",
|
||||||
|
"mailing_unernestaparis",
|
||||||
|
"comments",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
("no", "Non"),
|
||||||
|
("wait", "Oui mais attente paiement"),
|
||||||
|
("paid", "Oui payé"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminEventForm(forms.Form):
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
label="Inscription", initial="no", choices=STATUS_CHOICES, widget=RadioSelect
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop("event")
|
||||||
|
registration = kwargs.pop("current_registration", None)
|
||||||
|
current_choices, paid = (
|
||||||
|
(registration.options.all(), registration.paid)
|
||||||
|
if registration is not None
|
||||||
|
else ([], None)
|
||||||
|
)
|
||||||
|
if paid is True:
|
||||||
|
kwargs["initial"] = {"status": "paid"}
|
||||||
|
elif paid is False:
|
||||||
|
kwargs["initial"] = {"status": "wait"}
|
||||||
|
else:
|
||||||
|
kwargs["initial"] = {"status": "no"}
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
choices = {}
|
||||||
|
for choice in current_choices:
|
||||||
|
if choice.event_option.id not in choices:
|
||||||
|
choices[choice.event_option.id] = [choice.id]
|
||||||
|
else:
|
||||||
|
choices[choice.event_option.id].append(choice.id)
|
||||||
|
all_choices = choices
|
||||||
|
for option in self.event.options.all():
|
||||||
|
choices = [(choice.id, choice.value) for choice in option.choices.all()]
|
||||||
|
if option.multi_choices:
|
||||||
|
initial = [] if option.id not in all_choices else all_choices[option.id]
|
||||||
|
field = forms.MultipleChoiceField(
|
||||||
|
label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
initial = (
|
||||||
|
None if option.id not in all_choices else all_choices[option.id][0]
|
||||||
|
)
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial=initial,
|
||||||
|
)
|
||||||
|
field.option_id = option.id
|
||||||
|
self.fields["option_%d" % option.id] = field
|
||||||
|
for commentfield in self.event.commentfields.all():
|
||||||
|
initial = commentfield.default
|
||||||
|
if registration is not None:
|
||||||
|
try:
|
||||||
|
initial = registration.comments.get(
|
||||||
|
commentfield=commentfield
|
||||||
|
).content
|
||||||
|
except EventCommentValue.DoesNotExist:
|
||||||
|
pass
|
||||||
|
widget = (
|
||||||
|
forms.Textarea if commentfield.fieldtype == "text" else forms.TextInput
|
||||||
|
)
|
||||||
|
field = forms.CharField(
|
||||||
|
label=commentfield.name, widget=widget, required=False, initial=initial
|
||||||
|
)
|
||||||
|
field.comment_id = commentfield.id
|
||||||
|
self.fields["comment_%d" % commentfield.id] = field
|
||||||
|
|
||||||
|
def choices(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith("option_"):
|
||||||
|
yield (self.fields[name].option_id, value)
|
||||||
|
|
||||||
|
def comments(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith("comment_"):
|
||||||
|
yield (self.fields[name].comment_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEventRegistrationFormset(BaseFormSet):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.events = kwargs.pop("events")
|
||||||
|
self.current_registrations = kwargs.pop("current_registrations", None)
|
||||||
|
self.extra = len(self.events)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _construct_form(self, index, **kwargs):
|
||||||
|
kwargs["event"] = self.events[index]
|
||||||
|
if self.current_registrations is not None:
|
||||||
|
kwargs["current_registration"] = self.current_registrations[index]
|
||||||
|
return super()._construct_form(index, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarForm(forms.ModelForm):
|
||||||
|
subscribe_to_events = forms.BooleanField(
|
||||||
|
initial=True, label="Événements du COF", required=False
|
||||||
|
)
|
||||||
|
subscribe_to_my_shows = forms.BooleanField(
|
||||||
|
initial=True,
|
||||||
|
label="Les spectacles pour lesquels j'ai obtenu une place",
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
other_shows = forms.ModelMultipleChoiceField(
|
||||||
|
label="Spectacles supplémentaires",
|
||||||
|
queryset=Spectacle.objects.filter(tirage__active=True),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CalendarSubscription
|
||||||
|
fields = ["subscribe_to_events", "subscribe_to_my_shows", "other_shows"]
|
||||||
|
|
||||||
|
|
||||||
|
class ClubsForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Formulaire d'inscription d'un membre à plusieurs clubs du COF.
|
||||||
|
"""
|
||||||
|
|
||||||
|
clubs = forms.ModelMultipleChoiceField(
|
||||||
|
label="Inscriptions aux clubs du COF",
|
||||||
|
queryset=Club.objects.all(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Announcements banner
|
||||||
|
# TODO: move this to the `gestion` app once the supportBDS branch is merged
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
class GestioncofConfigForm(ConfigForm):
|
||||||
|
gestion_banner = forms.CharField(
|
||||||
|
label=_("Announcements banner"),
|
||||||
|
help_text=_("An empty banner disables annoucements"),
|
||||||
|
max_length=2048,
|
||||||
|
)
|
0
gestioncof/management/__init__.py
Normal file
0
gestioncof/management/__init__.py
Normal file
41
gestioncof/management/base.py
Normal file
41
gestioncof/management/base.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
"""
|
||||||
|
Un mixin à utiliser avec BaseCommand pour charger des objets depuis un json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class MyBaseCommand(BaseCommand):
|
||||||
|
"""
|
||||||
|
Ajoute une méthode ``from_json`` qui charge des objets à partir d'un json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def from_json(self, filename, data_dir, klass, callback=lambda obj: obj):
|
||||||
|
"""
|
||||||
|
Charge les objets contenus dans le fichier json référencé par
|
||||||
|
``filename`` dans la base de donnée. La fonction callback est appelées
|
||||||
|
sur chaque objet avant enregistrement.
|
||||||
|
"""
|
||||||
|
self.stdout.write("Chargement de {:s}".format(filename))
|
||||||
|
with open(os.path.join(data_dir, filename), "r") as file:
|
||||||
|
descriptions = json.load(file)
|
||||||
|
objects = []
|
||||||
|
nb_new = 0
|
||||||
|
for description in descriptions:
|
||||||
|
qset = klass.objects.filter(**description)
|
||||||
|
try:
|
||||||
|
objects.append(qset.get())
|
||||||
|
except klass.DoesNotExist:
|
||||||
|
obj = klass(**description)
|
||||||
|
obj = callback(obj)
|
||||||
|
obj.save()
|
||||||
|
objects.append(obj)
|
||||||
|
nb_new += 1
|
||||||
|
self.stdout.write("- {:d} objets créés".format(nb_new))
|
||||||
|
self.stdout.write(
|
||||||
|
"- {:d} objets gardés en l'état".format(len(objects) - nb_new)
|
||||||
|
)
|
||||||
|
return objects
|
0
gestioncof/management/commands/__init__.py
Normal file
0
gestioncof/management/commands/__init__.py
Normal file
116
gestioncof/management/commands/loaddevdata.py
Normal file
116
gestioncof/management/commands/loaddevdata.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
Charge des données de test dans la BDD
|
||||||
|
- Utilisateurs
|
||||||
|
- Sondage
|
||||||
|
- Événement
|
||||||
|
- Petits cours
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
from gestioncof.management.base import MyBaseCommand
|
||||||
|
from gestioncof.petits_cours_models import (
|
||||||
|
LEVELS_CHOICES,
|
||||||
|
PetitCoursAbility,
|
||||||
|
PetitCoursAttributionCounter,
|
||||||
|
PetitCoursSubject,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Où sont stockés les fichiers json
|
||||||
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
||||||
|
|
||||||
|
|
||||||
|
class Command(MyBaseCommand):
|
||||||
|
help = "Charge des données de test dans la BDD"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""
|
||||||
|
Permet de ne pas créer l'utilisateur "root".
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-root",
|
||||||
|
action="store_true",
|
||||||
|
dest="no-root",
|
||||||
|
default=False,
|
||||||
|
help='Ne crée pas l\'utilisateur "root"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# ---
|
||||||
|
# Utilisateurs
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# Gaulois
|
||||||
|
gaulois = self.from_json("gaulois.json", DATA_DIR, User)
|
||||||
|
for user in gaulois:
|
||||||
|
user.profile.is_cof = True
|
||||||
|
user.profile.save()
|
||||||
|
|
||||||
|
# Romains
|
||||||
|
self.from_json("romains.json", DATA_DIR, User)
|
||||||
|
|
||||||
|
# Root
|
||||||
|
no_root = options.get("no-root", False)
|
||||||
|
if not no_root:
|
||||||
|
self.stdout.write("Création de l'utilisateur root")
|
||||||
|
root, _ = User.objects.get_or_create(
|
||||||
|
username="root",
|
||||||
|
first_name="super",
|
||||||
|
last_name="user",
|
||||||
|
email="root@localhost",
|
||||||
|
)
|
||||||
|
root.set_password("root")
|
||||||
|
root.is_staff = True
|
||||||
|
root.is_superuser = True
|
||||||
|
root.profile.is_cof = True
|
||||||
|
root.profile.is_buro = True
|
||||||
|
root.profile.save()
|
||||||
|
root.save()
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Petits cours
|
||||||
|
# ---
|
||||||
|
|
||||||
|
self.stdout.write("Inscriptions au système des petits cours")
|
||||||
|
levels = [id for (id, verbose) in LEVELS_CHOICES]
|
||||||
|
subjects = list(PetitCoursSubject.objects.all())
|
||||||
|
nb_of_teachers = 0
|
||||||
|
for user in gaulois:
|
||||||
|
if random.randint(0, 1):
|
||||||
|
nb_of_teachers += 1
|
||||||
|
# L'utilisateur reçoit les demandes de petits cours
|
||||||
|
user.profile.petits_cours_accept = True
|
||||||
|
user.save()
|
||||||
|
# L'utilisateur est compétent dans une matière
|
||||||
|
subject = random.choice(subjects)
|
||||||
|
if not PetitCoursAbility.objects.filter(
|
||||||
|
user=user, matiere=subject
|
||||||
|
).exists():
|
||||||
|
PetitCoursAbility.objects.create(
|
||||||
|
user=user,
|
||||||
|
matiere=subject,
|
||||||
|
niveau=random.choice(levels),
|
||||||
|
agrege=bool(random.randint(0, 1)),
|
||||||
|
)
|
||||||
|
# On initialise son compteur d'attributions
|
||||||
|
PetitCoursAttributionCounter.objects.get_or_create(
|
||||||
|
user=user, matiere=subject
|
||||||
|
)
|
||||||
|
self.stdout.write("- {:d} inscriptions".format(nb_of_teachers))
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Le BdA
|
||||||
|
# ---
|
||||||
|
|
||||||
|
call_command("loadbdadevdata")
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# La K-Fêt
|
||||||
|
# ---
|
||||||
|
|
||||||
|
call_command("loadkfetdevdata")
|
89
gestioncof/management/commands/syncmails.py
Normal file
89
gestioncof/management/commands/syncmails.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
Import des mails de GestioCOF dans la base de donnée
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from custommail.models import CustomMail, Type, Variable
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
DATA_LOCATION = os.path.join(os.path.dirname(__file__), "..", "data", "custommail.json")
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_log(__):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# XXX. this should probably be in the custommail package
|
||||||
|
def load_from_file(log=dummy_log, verbosity=1):
|
||||||
|
with open(DATA_LOCATION, "r") as jsonfile:
|
||||||
|
mail_data = json.load(jsonfile)
|
||||||
|
|
||||||
|
# On se souvient à quel objet correspond quel pk du json
|
||||||
|
assoc = {"types": {}, "mails": {}}
|
||||||
|
status = {"synced": 0, "unchanged": 0}
|
||||||
|
|
||||||
|
for obj in mail_data:
|
||||||
|
fields = obj["fields"]
|
||||||
|
|
||||||
|
# Pour les trois types d'objets :
|
||||||
|
# - On récupère les objets référencés par les clefs étrangères
|
||||||
|
# - On crée l'objet si nécessaire
|
||||||
|
# - On le stocke éventuellement dans les deux dictionnaires définis
|
||||||
|
# plus haut
|
||||||
|
|
||||||
|
# Variable types
|
||||||
|
if obj["model"] == "custommail.variabletype":
|
||||||
|
fields["inner1"] = assoc["types"].get(fields["inner1"])
|
||||||
|
fields["inner2"] = assoc["types"].get(fields["inner2"])
|
||||||
|
if fields["kind"] == "model":
|
||||||
|
fields["content_type"] = ContentType.objects.get_by_natural_key(
|
||||||
|
*fields["content_type"]
|
||||||
|
)
|
||||||
|
var_type, _ = Type.objects.get_or_create(**fields)
|
||||||
|
assoc["types"][obj["pk"]] = var_type
|
||||||
|
|
||||||
|
# Custom mails
|
||||||
|
if obj["model"] == "custommail.custommail":
|
||||||
|
mail = None
|
||||||
|
try:
|
||||||
|
mail = CustomMail.objects.get(shortname=fields["shortname"])
|
||||||
|
status["unchanged"] += 1
|
||||||
|
except CustomMail.DoesNotExist:
|
||||||
|
mail = CustomMail.objects.create(**fields)
|
||||||
|
status["synced"] += 1
|
||||||
|
if verbosity:
|
||||||
|
log("SYNCED {:s}".format(fields["shortname"]))
|
||||||
|
assoc["mails"][obj["pk"]] = mail
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
if obj["model"] == "custommail.custommailvariable":
|
||||||
|
fields["custommail"] = assoc["mails"].get(fields["custommail"])
|
||||||
|
fields["type"] = assoc["types"].get(fields["type"])
|
||||||
|
try:
|
||||||
|
Variable.objects.get(
|
||||||
|
custommail=fields["custommail"], name=fields["name"]
|
||||||
|
)
|
||||||
|
except Variable.DoesNotExist:
|
||||||
|
Variable.objects.create(**fields)
|
||||||
|
|
||||||
|
if verbosity:
|
||||||
|
log("{synced:d} mails synchronized {unchanged:d} unchanged".format(**status))
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
|
"Va chercher les données mails de GestioCOF stocké au format json "
|
||||||
|
"dans /gestioncof/management/data/custommails.json. Le format des "
|
||||||
|
"données est celui donné par la commande :"
|
||||||
|
" `python manage.py dumpdata custommail --natural-foreign` "
|
||||||
|
"La bonne façon de mettre à jour ce fichier est donc de le "
|
||||||
|
"charger à l'aide de syncmails, le faire les modifications à "
|
||||||
|
"l'aide de l'interface administration et/ou du shell puis de le "
|
||||||
|
"remplacer par le nouveau résultat de la commande précédente."
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
load_from_file(log=self.stdout.write)
|
600
gestioncof/management/data/custommail.json
Normal file
600
gestioncof/management/data/custommail.json
Normal file
|
@ -0,0 +1,600 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "model",
|
||||||
|
"content_type": [
|
||||||
|
"auth",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "int",
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "model",
|
||||||
|
"content_type": [
|
||||||
|
"bda",
|
||||||
|
"spectacle"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "model",
|
||||||
|
"content_type": [
|
||||||
|
"bda",
|
||||||
|
"spectaclerevente"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "model",
|
||||||
|
"content_type": [
|
||||||
|
"sites",
|
||||||
|
"site"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "model",
|
||||||
|
"content_type": [
|
||||||
|
"gestioncof",
|
||||||
|
"petitcoursdemande"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "list",
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 12,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "list",
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 1,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "pair",
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 12,
|
||||||
|
"inner2": 8
|
||||||
|
},
|
||||||
|
"pk": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "list",
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 9,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "list",
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 3,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.type",
|
||||||
|
"fields": {
|
||||||
|
"kind": "model",
|
||||||
|
"content_type": [
|
||||||
|
"gestioncof",
|
||||||
|
"petitcourssubject"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"inner2": null
|
||||||
|
},
|
||||||
|
"pk": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "welcome",
|
||||||
|
"subject": "Bienvenue au COF",
|
||||||
|
"body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime.",
|
||||||
|
"description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre"
|
||||||
|
},
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-rappel",
|
||||||
|
"subject": "{{ show }}",
|
||||||
|
"body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts",
|
||||||
|
"description": "Mail de rappel pour les spectacles BdA"
|
||||||
|
},
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente",
|
||||||
|
"subject": "{{ show }}",
|
||||||
|
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour leur signaler qu'une place vient d'\u00eatre mise en vente.",
|
||||||
|
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-shotgun",
|
||||||
|
"subject": "{{ show }}",
|
||||||
|
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
|
||||||
|
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-winner",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA",
|
||||||
|
"description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente"
|
||||||
|
},
|
||||||
|
"pk": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-loser",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts",
|
||||||
|
"description": "Notification envoy\u00e9e aux perdants d'un tirage de revente."
|
||||||
|
},
|
||||||
|
"pk": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-seller",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA",
|
||||||
|
"description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e"
|
||||||
|
},
|
||||||
|
"pk": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-new",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts",
|
||||||
|
"description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires."
|
||||||
|
},
|
||||||
|
"pk": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-buy-shotgun",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})",
|
||||||
|
"description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun."
|
||||||
|
},
|
||||||
|
"pk": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "petit-cours-mail-eleve",
|
||||||
|
"subject": "Petits cours ENS par le COF",
|
||||||
|
"body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours",
|
||||||
|
"description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours"
|
||||||
|
},
|
||||||
|
"pk": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "petits-cours-mail-demandeur",
|
||||||
|
"subject": "Cours particuliers ENS",
|
||||||
|
"body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS",
|
||||||
|
"description": "Mail envoy\u00e9 aux personnes qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})"
|
||||||
|
},
|
||||||
|
"pk": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-attributions",
|
||||||
|
"subject": "R\u00e9sultats du tirage au sort",
|
||||||
|
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts",
|
||||||
|
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places"
|
||||||
|
},
|
||||||
|
"pk": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-attributions-decus",
|
||||||
|
"subject": "R\u00e9sultats du tirage au sort",
|
||||||
|
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts",
|
||||||
|
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place"
|
||||||
|
},
|
||||||
|
"pk": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 1,
|
||||||
|
"type": 1,
|
||||||
|
"name": "member",
|
||||||
|
"description": "Utilisateur de GestioCOF"
|
||||||
|
},
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 2,
|
||||||
|
"type": 1,
|
||||||
|
"name": "member",
|
||||||
|
"description": "Utilisateur ayant eu une place pour ce spectacle"
|
||||||
|
},
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 2,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 2,
|
||||||
|
"type": 2,
|
||||||
|
"name": "nb_attr",
|
||||||
|
"description": "Nombre de places obtenues"
|
||||||
|
},
|
||||||
|
"pk": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 4,
|
||||||
|
"name": "revente",
|
||||||
|
"description": "Revente mentionn\u00e9e dans le mail"
|
||||||
|
},
|
||||||
|
"pk": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 1,
|
||||||
|
"name": "member",
|
||||||
|
"description": "Personne int\u00e9ress\u00e9e par la place"
|
||||||
|
},
|
||||||
|
"pk": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 5,
|
||||||
|
"name": "site",
|
||||||
|
"description": "Site web (gestioCOF)"
|
||||||
|
},
|
||||||
|
"pk": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 4,
|
||||||
|
"type": 5,
|
||||||
|
"name": "site",
|
||||||
|
"description": "Site web (gestioCOF)"
|
||||||
|
},
|
||||||
|
"pk": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 4,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 4,
|
||||||
|
"type": 1,
|
||||||
|
"name": "member",
|
||||||
|
"description": "Personne int\u00e9ress\u00e9e par la place"
|
||||||
|
},
|
||||||
|
"pk": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 5,
|
||||||
|
"type": 1,
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Gagnant-e du tirage"
|
||||||
|
},
|
||||||
|
"pk": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 5,
|
||||||
|
"type": 1,
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend une place"
|
||||||
|
},
|
||||||
|
"pk": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 5,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 6,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 6,
|
||||||
|
"type": 1,
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend une place"
|
||||||
|
},
|
||||||
|
"pk": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 6,
|
||||||
|
"type": 1,
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Personne inscrite au tirage qui n'a pas eu la place"
|
||||||
|
},
|
||||||
|
"pk": 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 7,
|
||||||
|
"type": 1,
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Gagnant-e du tirage"
|
||||||
|
},
|
||||||
|
"pk": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 7,
|
||||||
|
"type": 1,
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend une place"
|
||||||
|
},
|
||||||
|
"pk": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 7,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 8,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 8,
|
||||||
|
"type": 1,
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend la place"
|
||||||
|
},
|
||||||
|
"pk": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 8,
|
||||||
|
"type": 4,
|
||||||
|
"name": "revente",
|
||||||
|
"description": "Revente mentionn\u00e9e dans le mail"
|
||||||
|
},
|
||||||
|
"pk": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 9,
|
||||||
|
"type": 1,
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend la place"
|
||||||
|
},
|
||||||
|
"pk": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 9,
|
||||||
|
"type": 3,
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle"
|
||||||
|
},
|
||||||
|
"pk": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 9,
|
||||||
|
"type": 1,
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Personne qui prend la place au shotgun"
|
||||||
|
},
|
||||||
|
"pk": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 10,
|
||||||
|
"type": 6,
|
||||||
|
"name": "demande",
|
||||||
|
"description": "Demande de petit cours"
|
||||||
|
},
|
||||||
|
"pk": 27
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 10,
|
||||||
|
"type": 7,
|
||||||
|
"name": "matieres",
|
||||||
|
"description": "Liste des mati\u00e8res concern\u00e9es par la demande"
|
||||||
|
},
|
||||||
|
"pk": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 11,
|
||||||
|
"type": 10,
|
||||||
|
"name": "proposals",
|
||||||
|
"description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re"
|
||||||
|
},
|
||||||
|
"pk": 29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 11,
|
||||||
|
"type": 7,
|
||||||
|
"name": "unsatisfied",
|
||||||
|
"description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer"
|
||||||
|
},
|
||||||
|
"pk": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 12,
|
||||||
|
"type": 11,
|
||||||
|
"name": "places",
|
||||||
|
"description": "Places de spectacle du participant"
|
||||||
|
},
|
||||||
|
"pk": 31
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 12,
|
||||||
|
"type": 1,
|
||||||
|
"name": "member",
|
||||||
|
"description": "Participant du tirage au sort"
|
||||||
|
},
|
||||||
|
"pk": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variable",
|
||||||
|
"fields": {
|
||||||
|
"custommail": 13,
|
||||||
|
"type": 1,
|
||||||
|
"name": "member",
|
||||||
|
"description": "Participant du tirage au sort"
|
||||||
|
},
|
||||||
|
"pk": 33
|
||||||
|
}
|
||||||
|
]
|
368
gestioncof/management/data/gaulois.json
Normal file
368
gestioncof/management/data/gaulois.json
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "Abraracourcix",
|
||||||
|
"email": "Abraracourcix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Abraracourcix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acidenitrix",
|
||||||
|
"email": "Acidenitrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Acidenitrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agecanonix",
|
||||||
|
"email": "Agecanonix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Agecanonix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alambix",
|
||||||
|
"email": "Alambix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Alambix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amerix",
|
||||||
|
"email": "Amerix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Amerix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amnesix",
|
||||||
|
"email": "Amnesix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Amnesix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aniline",
|
||||||
|
"email": "Aniline.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Aniline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aplusbegalix",
|
||||||
|
"email": "Aplusbegalix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Aplusbegalix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Archeopterix",
|
||||||
|
"email": "Archeopterix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Archeopterix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Assurancetourix",
|
||||||
|
"email": "Assurancetourix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Assurancetourix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Asterix",
|
||||||
|
"email": "Asterix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Asterix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Astronomix",
|
||||||
|
"email": "Astronomix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Astronomix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Avoranfix",
|
||||||
|
"email": "Avoranfix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Avoranfix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Barometrix",
|
||||||
|
"email": "Barometrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Barometrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Beaufix",
|
||||||
|
"email": "Beaufix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Beaufix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Berlix",
|
||||||
|
"email": "Berlix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Berlix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Bonemine",
|
||||||
|
"email": "Bonemine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Bonemine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Boufiltre",
|
||||||
|
"email": "Boufiltre.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Boufiltre"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Catedralgotix",
|
||||||
|
"email": "Catedralgotix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Catedralgotix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "CesarLabeldecadix",
|
||||||
|
"email": "CesarLabeldecadix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "CesarLabeldecadix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Cetautomatix",
|
||||||
|
"email": "Cetautomatix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Cetautomatix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Cetyounix",
|
||||||
|
"email": "Cetyounix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Cetyounix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Changeledix",
|
||||||
|
"email": "Changeledix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Changeledix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Chanteclairix",
|
||||||
|
"email": "Chanteclairix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Chanteclairix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Cicatrix",
|
||||||
|
"email": "Cicatrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Cicatrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Comix",
|
||||||
|
"email": "Comix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Comix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Diagnostix",
|
||||||
|
"email": "Diagnostix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Diagnostix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Doublepolemix",
|
||||||
|
"email": "Doublepolemix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Doublepolemix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Eponine",
|
||||||
|
"email": "Eponine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Eponine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Falbala",
|
||||||
|
"email": "Falbala.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Falbala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Fanzine",
|
||||||
|
"email": "Fanzine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Fanzine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Gelatine",
|
||||||
|
"email": "Gelatine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Gelatine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Goudurix",
|
||||||
|
"email": "Goudurix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Goudurix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Homeopatix",
|
||||||
|
"email": "Homeopatix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Homeopatix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Idefix",
|
||||||
|
"email": "Idefix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Idefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ielosubmarine",
|
||||||
|
"email": "Ielosubmarine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Ielosubmarine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Keskonrix",
|
||||||
|
"email": "Keskonrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Keskonrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Lentix",
|
||||||
|
"email": "Lentix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Lentix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Maestria",
|
||||||
|
"email": "Maestria.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Maestria"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "MaitrePanix",
|
||||||
|
"email": "MaitrePanix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "MaitrePanix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "MmeAgecanonix",
|
||||||
|
"email": "MmeAgecanonix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "MmeAgecanonix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Moralelastix",
|
||||||
|
"email": "Moralelastix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Moralelastix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Obelix",
|
||||||
|
"email": "Obelix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Obelix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Obelodalix",
|
||||||
|
"email": "Obelodalix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Obelodalix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Odalix",
|
||||||
|
"email": "Odalix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Odalix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ordralfabetix",
|
||||||
|
"email": "Ordralfabetix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Ordralfabetix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Orthopedix",
|
||||||
|
"email": "Orthopedix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Orthopedix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Panoramix",
|
||||||
|
"email": "Panoramix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Panoramix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Plaintcontrix",
|
||||||
|
"email": "Plaintcontrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Plaintcontrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Praline",
|
||||||
|
"email": "Praline.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Praline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Prefix",
|
||||||
|
"email": "Prefix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Prolix",
|
||||||
|
"email": "Prolix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Prolix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Pronostix",
|
||||||
|
"email": "Pronostix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Pronostix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Quatredeusix",
|
||||||
|
"email": "Quatredeusix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Quatredeusix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Saingesix",
|
||||||
|
"email": "Saingesix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Saingesix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Segregationnix",
|
||||||
|
"email": "Segregationnix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Segregationnix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Septantesix",
|
||||||
|
"email": "Septantesix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Septantesix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Tournedix",
|
||||||
|
"email": "Tournedix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Tournedix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Tragicomix",
|
||||||
|
"email": "Tragicomix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Tragicomix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Coriza",
|
||||||
|
"email": "Coriza.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Coriza"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Zerozerosix",
|
||||||
|
"email": "Zerozerosix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Zerozerosix"
|
||||||
|
}
|
||||||
|
]
|
614
gestioncof/management/data/romains.json
Normal file
614
gestioncof/management/data/romains.json
Normal file
|
@ -0,0 +1,614 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "Abel",
|
||||||
|
"email": "Abel.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Abel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Abelardus",
|
||||||
|
"email": "Abelardus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Abelardus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Abrahamus",
|
||||||
|
"email": "Abrahamus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Abrahamus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acacius",
|
||||||
|
"email": "Acacius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Acacius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Accius",
|
||||||
|
"email": "Accius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Accius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achaicus",
|
||||||
|
"email": "Achaicus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achaicus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achill",
|
||||||
|
"email": "Achill.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achill"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achilles",
|
||||||
|
"email": "Achilles.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achilles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achilleus",
|
||||||
|
"email": "Achilleus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achilleus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acrisius",
|
||||||
|
"email": "Acrisius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Acrisius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Actaeon",
|
||||||
|
"email": "Actaeon.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Actaeon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acteon",
|
||||||
|
"email": "Acteon.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Acteon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adalricus",
|
||||||
|
"email": "Adalricus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adalricus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adelfonsus",
|
||||||
|
"email": "Adelfonsus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adelfonsus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adelphus",
|
||||||
|
"email": "Adelphus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adelphus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adeodatus",
|
||||||
|
"email": "Adeodatus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adeodatus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adolfus",
|
||||||
|
"email": "Adolfus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adolfus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adolphus",
|
||||||
|
"email": "Adolphus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adolphus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adrastus",
|
||||||
|
"email": "Adrastus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adrastus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adrianus",
|
||||||
|
"email": "Adrianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adrianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6gidius",
|
||||||
|
"email": "\u00c6gidius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6gidius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6lia",
|
||||||
|
"email": "\u00c6lia.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6lia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6lianus",
|
||||||
|
"email": "\u00c6lianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6lianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6milianus",
|
||||||
|
"email": "\u00c6milianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6milianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6milius",
|
||||||
|
"email": "\u00c6milius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6milius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aeneas",
|
||||||
|
"email": "Aeneas.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aeneas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6olus",
|
||||||
|
"email": "\u00c6olus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6olus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6schylus",
|
||||||
|
"email": "\u00c6schylus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6schylus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6son",
|
||||||
|
"email": "\u00c6son.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6son"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6sop",
|
||||||
|
"email": "\u00c6sop.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6sop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6ther",
|
||||||
|
"email": "\u00c6ther.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6ther"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6tius",
|
||||||
|
"email": "\u00c6tius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6tius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agapetus",
|
||||||
|
"email": "Agapetus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agapetus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agapitus",
|
||||||
|
"email": "Agapitus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agapitus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agapius",
|
||||||
|
"email": "Agapius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agapius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agathangelus",
|
||||||
|
"email": "Agathangelus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agathangelus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aigidius",
|
||||||
|
"email": "Aigidius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aigidius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aiolus",
|
||||||
|
"email": "Aiolus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aiolus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ajax",
|
||||||
|
"email": "Ajax.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ajax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alair",
|
||||||
|
"email": "Alair.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alair"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alaricus",
|
||||||
|
"email": "Alaricus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alaricus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albanus",
|
||||||
|
"email": "Albanus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albanus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alberic",
|
||||||
|
"email": "Alberic.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alberic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albericus",
|
||||||
|
"email": "Albericus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albericus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albertus",
|
||||||
|
"email": "Albertus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albertus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albinus",
|
||||||
|
"email": "Albinus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albinus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albus",
|
||||||
|
"email": "Albus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcaeus",
|
||||||
|
"email": "Alcaeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcaeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcander",
|
||||||
|
"email": "Alcander.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcander"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcimus",
|
||||||
|
"email": "Alcimus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcimus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcinder",
|
||||||
|
"email": "Alcinder.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcinder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alerio",
|
||||||
|
"email": "Alerio.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alerio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexandrus",
|
||||||
|
"email": "Alexandrus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexandrus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexis",
|
||||||
|
"email": "Alexis.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexius",
|
||||||
|
"email": "Alexius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexus",
|
||||||
|
"email": "Alexus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alfonsus",
|
||||||
|
"email": "Alfonsus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alfonsus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alfredus",
|
||||||
|
"email": "Alfredus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alfredus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Almericus",
|
||||||
|
"email": "Almericus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Almericus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aloisius",
|
||||||
|
"email": "Aloisius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aloisius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aloysius",
|
||||||
|
"email": "Aloysius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aloysius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphaeus",
|
||||||
|
"email": "Alphaeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphaeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alpheaus",
|
||||||
|
"email": "Alpheaus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alpheaus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alpheus",
|
||||||
|
"email": "Alpheus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alpheus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphoeus",
|
||||||
|
"email": "Alphoeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphoeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphonsus",
|
||||||
|
"email": "Alphonsus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphonsus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphonzus",
|
||||||
|
"email": "Alphonzus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphonzus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alvinius",
|
||||||
|
"email": "Alvinius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alvinius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alvredus",
|
||||||
|
"email": "Alvredus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alvredus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amadeus",
|
||||||
|
"email": "Amadeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amadeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amaliricus",
|
||||||
|
"email": "Amaliricus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amaliricus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amandus",
|
||||||
|
"email": "Amandus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amandus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amantius",
|
||||||
|
"email": "Amantius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amantius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amarandus",
|
||||||
|
"email": "Amarandus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amarandus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amaranthus",
|
||||||
|
"email": "Amaranthus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amaranthus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amatus",
|
||||||
|
"email": "Amatus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amatus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ambrosianus",
|
||||||
|
"email": "Ambrosianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ambrosianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ambrosius",
|
||||||
|
"email": "Ambrosius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ambrosius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amedeus",
|
||||||
|
"email": "Amedeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amedeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Americus",
|
||||||
|
"email": "Americus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Americus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amlethus",
|
||||||
|
"email": "Amlethus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amlethus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amletus",
|
||||||
|
"email": "Amletus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amletus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amor",
|
||||||
|
"email": "Amor.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ampelius",
|
||||||
|
"email": "Ampelius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ampelius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amphion",
|
||||||
|
"email": "Amphion.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amphion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anacletus",
|
||||||
|
"email": "Anacletus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anacletus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anastasius",
|
||||||
|
"email": "Anastasius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anastasius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anastatius",
|
||||||
|
"email": "Anastatius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anastatius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anastius",
|
||||||
|
"email": "Anastius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anastius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anatolius",
|
||||||
|
"email": "Anatolius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anatolius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Androcles",
|
||||||
|
"email": "Androcles.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Androcles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Andronicus",
|
||||||
|
"email": "Andronicus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Andronicus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anencletus",
|
||||||
|
"email": "Anencletus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anencletus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Angelicus",
|
||||||
|
"email": "Angelicus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Angelicus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Angelus",
|
||||||
|
"email": "Angelus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Angelus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anicetus",
|
||||||
|
"email": "Anicetus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anicetus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antigonus",
|
||||||
|
"email": "Antigonus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antigonus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antipater",
|
||||||
|
"email": "Antipater.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antipater"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antoninus",
|
||||||
|
"email": "Antoninus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antoninus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antonius",
|
||||||
|
"email": "Antonius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antonius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aphrodisius",
|
||||||
|
"email": "Aphrodisius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aphrodisius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Apollinaris",
|
||||||
|
"email": "Apollinaris.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Apollinaris"
|
||||||
|
}
|
||||||
|
]
|
856
gestioncof/migrations/0001_initial.py
Normal file
856
gestioncof/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,856 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Clipper",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.CharField(max_length=20, verbose_name=b"Identifiant"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fullname",
|
||||||
|
models.CharField(max_length=200, verbose_name=b"Nom complet"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Club",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200, verbose_name=b"Nom")),
|
||||||
|
("description", models.TextField(verbose_name=b"Description")),
|
||||||
|
(
|
||||||
|
"membres",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="clubs", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"respos",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="clubs_geres", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CofProfile",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"login_clipper",
|
||||||
|
models.CharField(
|
||||||
|
max_length=8, verbose_name=b"Login clipper", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_cof",
|
||||||
|
models.BooleanField(default=False, verbose_name=b"Membre du COF"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"num",
|
||||||
|
models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name=b"Num\xc3\xa9ro d'adh\xc3\xa9rent",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phone",
|
||||||
|
models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
verbose_name=b"T\xc3\xa9l\xc3\xa9phone",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"occupation",
|
||||||
|
models.CharField(
|
||||||
|
default=b"1A",
|
||||||
|
max_length=9,
|
||||||
|
verbose_name="Occupation",
|
||||||
|
choices=[
|
||||||
|
(b"exterieur", "Ext\xe9rieur"),
|
||||||
|
(b"1A", "1A"),
|
||||||
|
(b"2A", "2A"),
|
||||||
|
(b"3A", "3A"),
|
||||||
|
(b"4A", "4A"),
|
||||||
|
(b"archicube", "Archicube"),
|
||||||
|
(b"doctorant", "Doctorant"),
|
||||||
|
(b"CST", "CST"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"departement",
|
||||||
|
models.CharField(
|
||||||
|
max_length=50, verbose_name="D\xe9partement", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"type_cotiz",
|
||||||
|
models.CharField(
|
||||||
|
default=b"normalien",
|
||||||
|
max_length=9,
|
||||||
|
verbose_name="Type de cotisation",
|
||||||
|
choices=[
|
||||||
|
(b"etudiant", "Normalien \xe9tudiant"),
|
||||||
|
(b"normalien", "Normalien \xe9l\xe8ve"),
|
||||||
|
(b"exterieur", "Ext\xe9rieur"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mailing_cof",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name=b"Recevoir les mails COF"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mailing_bda",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name=b"Recevoir les mails BdA"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mailing_bda_revente",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=b"Recevoir les mails de revente de places BdA",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"comments",
|
||||||
|
models.TextField(
|
||||||
|
verbose_name=b"Commentaires visibles uniquement par le Buro",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_buro",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name=b"Membre du Bur\xc3\xb4"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"petits_cours_accept",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name=b"Recevoir des petits cours"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"petits_cours_remarques",
|
||||||
|
models.TextField(
|
||||||
|
default=b"",
|
||||||
|
verbose_name="Remarques et pr\xe9cisions pour les petits cours",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.OneToOneField(
|
||||||
|
related_name="profile",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Profil COF",
|
||||||
|
"verbose_name_plural": "Profils COF",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CustomMail",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("shortname", models.SlugField()),
|
||||||
|
("title", models.CharField(max_length=200, verbose_name=b"Titre")),
|
||||||
|
("content", models.TextField(verbose_name=b"Contenu")),
|
||||||
|
(
|
||||||
|
"comments",
|
||||||
|
models.TextField(
|
||||||
|
verbose_name=b"Informations contextuelles sur le mail",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Mails personnalisables"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Event",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=200, verbose_name=b"Titre")),
|
||||||
|
("location", models.CharField(max_length=200, verbose_name=b"Lieu")),
|
||||||
|
(
|
||||||
|
"start_date",
|
||||||
|
models.DateField(
|
||||||
|
null=True, verbose_name=b"Date de d\xc3\xa9but", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"end_date",
|
||||||
|
models.DateField(
|
||||||
|
null=True, verbose_name=b"Date de fin", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(verbose_name=b"Description", blank=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"registration_open",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True, verbose_name=b"Inscriptions ouvertes"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"old",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=b"Archiver (\xc3\xa9v\xc3\xa9nement fini)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "\xc9v\xe9nement"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EventCommentField",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200, verbose_name=b"Champ")),
|
||||||
|
(
|
||||||
|
"fieldtype",
|
||||||
|
models.CharField(
|
||||||
|
default=b"text",
|
||||||
|
max_length=10,
|
||||||
|
verbose_name=b"Type",
|
||||||
|
choices=[(b"text", "Texte long"), (b"char", "Texte court")],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"default",
|
||||||
|
models.TextField(
|
||||||
|
verbose_name=b"Valeur par d\xc3\xa9faut", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"event",
|
||||||
|
models.ForeignKey(
|
||||||
|
related_name="commentfields",
|
||||||
|
to="gestioncof.Event",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Champ"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EventCommentValue",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"content",
|
||||||
|
models.TextField(null=True, verbose_name=b"Contenu", blank=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"commentfield",
|
||||||
|
models.ForeignKey(
|
||||||
|
related_name="values",
|
||||||
|
to="gestioncof.EventCommentField",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EventOption",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200, verbose_name=b"Option")),
|
||||||
|
(
|
||||||
|
"multi_choices",
|
||||||
|
models.BooleanField(default=False, verbose_name=b"Choix multiples"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"event",
|
||||||
|
models.ForeignKey(
|
||||||
|
related_name="options",
|
||||||
|
to="gestioncof.Event",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Option"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EventOptionChoice",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("value", models.CharField(max_length=200, verbose_name=b"Valeur")),
|
||||||
|
(
|
||||||
|
"event_option",
|
||||||
|
models.ForeignKey(
|
||||||
|
related_name="choices",
|
||||||
|
to="gestioncof.EventOption",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Choix"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EventRegistration",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"paid",
|
||||||
|
models.BooleanField(default=False, verbose_name=b"A pay\xc3\xa9"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"event",
|
||||||
|
models.ForeignKey(to="gestioncof.Event", on_delete=models.CASCADE),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"filledcomments",
|
||||||
|
models.ManyToManyField(
|
||||||
|
to="gestioncof.EventCommentField",
|
||||||
|
through="gestioncof.EventCommentValue",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("options", models.ManyToManyField(to="gestioncof.EventOptionChoice")),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Inscription"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PetitCoursAbility",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"niveau",
|
||||||
|
models.CharField(
|
||||||
|
max_length=12,
|
||||||
|
verbose_name="Niveau",
|
||||||
|
choices=[
|
||||||
|
(b"college", "Coll\xe8ge"),
|
||||||
|
(b"lycee", "Lyc\xe9e"),
|
||||||
|
(b"prepa1styear", "Pr\xe9pa 1\xe8re ann\xe9e / L1"),
|
||||||
|
(b"prepa2ndyear", "Pr\xe9pa 2\xe8me ann\xe9e / L2"),
|
||||||
|
(b"licence3", "Licence 3"),
|
||||||
|
(b"other", "Autre (pr\xe9ciser dans les commentaires)"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"agrege",
|
||||||
|
models.BooleanField(default=False, verbose_name="Agr\xe9g\xe9"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Comp\xe9tence petits cours",
|
||||||
|
"verbose_name_plural": "Comp\xe9tences des petits cours",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PetitCoursAttribution",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Date d'attribution"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("rank", models.IntegerField(verbose_name=b"Rang dans l'email")),
|
||||||
|
(
|
||||||
|
"selected",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name="S\xe9lectionn\xe9 par le demandeur"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Attribution de petits cours",
|
||||||
|
"verbose_name_plural": "Attributions de petits cours",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PetitCoursAttributionCounter",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"count",
|
||||||
|
models.IntegerField(default=0, verbose_name=b"Nombre d'envois"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Compteur d'attribution de petits cours",
|
||||||
|
"verbose_name_plural": "Compteurs d'attributions de petits cours",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PetitCoursDemande",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(max_length=200, verbose_name="Nom/pr\xe9nom"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.CharField(max_length=300, verbose_name="Adresse email"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phone",
|
||||||
|
models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
verbose_name="T\xe9l\xe9phone (facultatif)",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"quand",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Indiquez ici la p\xe9riode d\xe9sir\xe9e pour les petits cours (vacances scolaires, semaine, week-end).",
|
||||||
|
max_length=300,
|
||||||
|
verbose_name="Quand ?",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"freq",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Indiquez ici la fr\xe9quence envisag\xe9e (hebdomadaire, 2 fois par semaine, ...)",
|
||||||
|
max_length=300,
|
||||||
|
verbose_name="Fr\xe9quence",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lieu",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Si vous avez avez une pr\xe9f\xe9rence sur le lieu.",
|
||||||
|
max_length=300,
|
||||||
|
verbose_name="Lieu (si pr\xe9f\xe9rence)",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"agrege_requis",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name="Agr\xe9g\xe9 requis"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"niveau",
|
||||||
|
models.CharField(
|
||||||
|
default=b"",
|
||||||
|
max_length=12,
|
||||||
|
verbose_name="Niveau",
|
||||||
|
choices=[
|
||||||
|
(b"college", "Coll\xe8ge"),
|
||||||
|
(b"lycee", "Lyc\xe9e"),
|
||||||
|
(b"prepa1styear", "Pr\xe9pa 1\xe8re ann\xe9e / L1"),
|
||||||
|
(b"prepa2ndyear", "Pr\xe9pa 2\xe8me ann\xe9e / L2"),
|
||||||
|
(b"licence3", "Licence 3"),
|
||||||
|
(b"other", "Autre (pr\xe9ciser dans les commentaires)"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"remarques",
|
||||||
|
models.TextField(
|
||||||
|
verbose_name="Remarques et pr\xe9cisions", blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"traitee",
|
||||||
|
models.BooleanField(default=False, verbose_name="Trait\xe9e"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"processed",
|
||||||
|
models.DateTimeField(verbose_name="Date de traitement", blank=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Date de cr\xe9ation"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Demande de petits cours",
|
||||||
|
"verbose_name_plural": "Demandes de petits cours",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PetitCoursSubject",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=30, verbose_name="Mati\xe8re")),
|
||||||
|
(
|
||||||
|
"users",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="petits_cours_matieres",
|
||||||
|
through="gestioncof.PetitCoursAbility",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Mati\xe8re de petits cours",
|
||||||
|
"verbose_name_plural": "Mati\xe8res des petits cours",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Survey",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=200, verbose_name=b"Titre")),
|
||||||
|
(
|
||||||
|
"details",
|
||||||
|
models.TextField(verbose_name=b"D\xc3\xa9tails", blank=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"survey_open",
|
||||||
|
models.BooleanField(default=True, verbose_name=b"Sondage ouvert"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"old",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name=b"Archiver (sondage fini)"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Sondage"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SurveyAnswer",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
options={"verbose_name": "R\xe9ponses"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SurveyQuestion",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"question",
|
||||||
|
models.CharField(max_length=200, verbose_name=b"Question"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"multi_answers",
|
||||||
|
models.BooleanField(default=False, verbose_name=b"Choix multiples"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"survey",
|
||||||
|
models.ForeignKey(
|
||||||
|
related_name="questions",
|
||||||
|
to="gestioncof.Survey",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Question"},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SurveyQuestionAnswer",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"answer",
|
||||||
|
models.CharField(max_length=200, verbose_name=b"R\xc3\xa9ponse"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"survey_question",
|
||||||
|
models.ForeignKey(
|
||||||
|
related_name="answers",
|
||||||
|
to="gestioncof.SurveyQuestion",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "R\xe9ponse"},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="surveyanswer",
|
||||||
|
name="answers",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="selected_by", to="gestioncof.SurveyQuestionAnswer"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="surveyanswer",
|
||||||
|
name="survey",
|
||||||
|
field=models.ForeignKey(to="gestioncof.Survey", on_delete=models.CASCADE),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="surveyanswer",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursdemande",
|
||||||
|
name="matieres",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="demandes",
|
||||||
|
verbose_name="Mati\xe8res",
|
||||||
|
to="gestioncof.PetitCoursSubject",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursdemande",
|
||||||
|
name="traitee_par",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursattributioncounter",
|
||||||
|
name="matiere",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
verbose_name="Matiere",
|
||||||
|
to="gestioncof.PetitCoursSubject",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursattributioncounter",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursattribution",
|
||||||
|
name="demande",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
verbose_name="Demande",
|
||||||
|
to="gestioncof.PetitCoursDemande",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursattribution",
|
||||||
|
name="matiere",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
verbose_name="Mati\xe8re",
|
||||||
|
to="gestioncof.PetitCoursSubject",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursattribution",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursability",
|
||||||
|
name="matiere",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
verbose_name="Mati\xe8re",
|
||||||
|
to="gestioncof.PetitCoursSubject",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="petitcoursability",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="eventcommentvalue",
|
||||||
|
name="registration",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
related_name="comments",
|
||||||
|
to="gestioncof.EventRegistration",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="surveyanswer", unique_together=set([("user", "survey")])
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="eventregistration", unique_together=set([("user", "event")])
|
||||||
|
),
|
||||||
|
]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue