Compare commits
79 commits
master
...
Kerl/suppo
Author | SHA1 | Date | |
---|---|---|---|
|
ac4dc0c192 | ||
|
75bfe98006 | ||
|
0f491ef396 | ||
|
f93a68b942 | ||
|
8a346bf834 | ||
|
95b96d470f | ||
|
33dedc7474 | ||
|
1a107be4ba | ||
|
e2c4214efc | ||
|
a98c6b233e | ||
|
e1713a1d4f | ||
|
9c6f5533ec | ||
|
486d3c4ced | ||
|
80f1514d39 | ||
|
1663a03a33 | ||
|
0e4cfc5121 | ||
|
856faf2b73 | ||
|
646b213d97 | ||
|
cc25685aa3 | ||
|
5ce4809f06 | ||
|
4d825b485d | ||
|
7988fb24a0 | ||
|
4b4d570e07 | ||
|
83e73376ad | ||
|
7742ad999f | ||
|
6444ae3b92 | ||
|
213c11721e | ||
|
7d1c1fc868 | ||
|
6c34742cc4 | ||
|
7abcf28666 | ||
|
3f52af8ca0 | ||
|
6bf16441e6 | ||
|
69f748acbd | ||
|
8b905f66dc | ||
|
e1bab7e4ed | ||
|
68c0ff559d | ||
|
2dcc17298a | ||
|
1aed36330f | ||
|
9f401b66e9 | ||
|
c81b849785 | ||
|
d36d69238d | ||
|
f8a8465630 | ||
|
5632cdaa22 | ||
|
68b38228a9 | ||
|
1f85f75896 | ||
|
669129e30d | ||
|
859f191894 | ||
|
52bdd9824a | ||
|
d7a13229ad | ||
|
7f5132961f | ||
|
659c6e720a | ||
|
e1a8c0e8dd | ||
|
1d7499d3b2 | ||
|
0420839b20 | ||
|
8b620a5319 | ||
|
a28c00e474 | ||
|
50b667993f | ||
|
94937fc7cd | ||
|
b5037329dd | ||
|
d46ab87e9b | ||
|
f53ced6a33 | ||
|
ee1f29b17d | ||
|
b9ed7320ec | ||
|
fcf392f40d | ||
|
3365d7b9a1 | ||
|
b16219f8ee | ||
|
6c3e1bd2db | ||
|
a2b8dee022 | ||
|
f0c3def935 | ||
|
376e829502 | ||
|
f50ef1d51a | ||
|
b639c04549 | ||
|
b1cf96d0ae | ||
|
815a5f274c | ||
|
25c3106168 | ||
|
22da04c3e2 | ||
|
58d708b791 | ||
|
f39d1545f0 | ||
|
5aff771d9c |
838 changed files with 24139 additions and 107930 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use nix
|
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -5,19 +5,7 @@ cof/settings.py
|
||||||
settings.py
|
settings.py
|
||||||
*~
|
*~
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
|
||||||
.vagrant
|
.vagrant
|
||||||
/src
|
/src
|
||||||
media/
|
media/
|
||||||
*.log
|
*.log
|
||||||
.sass-cache/
|
|
||||||
*.sqlite3
|
|
||||||
.coverage
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
.idea
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# VSCode
|
|
||||||
.vscode/
|
|
||||||
.direnv
|
|
||||||
|
|
117
.gitlab-ci.yml
117
.gitlab-ci.yml
|
@ -1,103 +1,40 @@
|
||||||
image: "python:3.7"
|
services:
|
||||||
|
- mysql:latest
|
||||||
|
- redis:latest
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# GestioCOF settings
|
# GestioCOF settings
|
||||||
DBHOST: "postgres"
|
DJANGO_SETTINGS_MODULE: "gestioCOF.settings_dev"
|
||||||
|
DBNAME: "cof_gestion"
|
||||||
|
DBUSER: "cof_gestion"
|
||||||
|
DBPASSWD: "cof_password"
|
||||||
|
DBHOST: "mysql"
|
||||||
REDIS_HOST: "redis"
|
REDIS_HOST: "redis"
|
||||||
REDIS_PASSWD: "dummy"
|
|
||||||
|
|
||||||
# Cached packages
|
# Cached packages
|
||||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
|
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
|
||||||
|
|
||||||
# postgres service configuration
|
# mysql service configuration
|
||||||
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
MYSQL_DATABASE: "$DBNAME"
|
||||||
POSTGRES_USER: "cof_gestion"
|
MYSQL_USER: "$DBUSER"
|
||||||
POSTGRES_DB: "cof_gestion"
|
MYSQL_PASSWORD: "$DBPASSWD"
|
||||||
|
MYSQL_ROOT_PASSWORD: "root_password"
|
||||||
|
|
||||||
# psql password authentication
|
|
||||||
PGPASSWORD: $POSTGRES_PASSWORD
|
|
||||||
|
|
||||||
# apps to check migrations for
|
cache:
|
||||||
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
|
|
||||||
|
|
||||||
.test_template:
|
|
||||||
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
|
|
||||||
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py
|
|
||||||
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py
|
|
||||||
# Remove the old test database if it has not been done yet
|
|
||||||
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
|
||||||
- pip install --upgrade -r requirements-prod.txt coverage tblib
|
|
||||||
- python --version
|
|
||||||
after_script:
|
|
||||||
- coverage report
|
|
||||||
services:
|
|
||||||
- postgres:11.7
|
|
||||||
- redis:latest
|
|
||||||
cache:
|
|
||||||
key: test
|
|
||||||
paths:
|
paths:
|
||||||
- vendor/
|
- vendor/python
|
||||||
# For GitLab CI to get coverage from build.
|
- vendor/pip
|
||||||
# Keep this disabled for now, as it may kill GitLab...
|
- vendor/apt
|
||||||
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
|
|
||||||
|
|
||||||
kfettest:
|
before_script:
|
||||||
|
- mkdir -p vendor/{python,pip,apt}
|
||||||
|
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq mysql-client
|
||||||
|
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST"
|
||||||
|
-e "GRANT ALL ON test_$DBNAME.* TO '$DBUSER'@'%'"
|
||||||
|
- pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt
|
||||||
|
|
||||||
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
extends: .test_template
|
|
||||||
variables:
|
|
||||||
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
|
|
||||||
script:
|
script:
|
||||||
- coverage run manage.py test kfet
|
- python manage.py test
|
||||||
|
|
||||||
coftest:
|
|
||||||
stage: test
|
|
||||||
extends: .test_template
|
|
||||||
variables:
|
|
||||||
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
|
|
||||||
script:
|
|
||||||
- coverage run manage.py test gestioncof bda 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:
|
|
||||||
stage: test
|
|
||||||
before_script:
|
|
||||||
- mkdir -p vendor/pip
|
|
||||||
- pip install --upgrade black isort flake8
|
|
||||||
script:
|
|
||||||
- black --check .
|
|
||||||
- isort --check --diff .
|
|
||||||
# Print errors only
|
|
||||||
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared
|
|
||||||
cache:
|
|
||||||
key: linters
|
|
||||||
paths:
|
|
||||||
- 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/
|
|
||||||
|
|
106
.pre-commit.sh
106
.pre-commit.sh
|
@ -1,106 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# pre-commit hook for gestioCOF project.
|
|
||||||
#
|
|
||||||
# Run formatters first, then checkers.
|
|
||||||
# Formatters which changed a file must set the flag 'formatter_updated'.
|
|
||||||
|
|
||||||
exit_code=0
|
|
||||||
formatter_updated=0
|
|
||||||
checker_dirty=0
|
|
||||||
|
|
||||||
# TODO(AD): We should check only staged changes.
|
|
||||||
# Working? -> Stash unstaged changes, run it, pop stash
|
|
||||||
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$")
|
|
||||||
|
|
||||||
|
|
||||||
# Formatter: black
|
|
||||||
|
|
||||||
printf "> black ... "
|
|
||||||
|
|
||||||
if type black &>/dev/null; then
|
|
||||||
if [ -z "$STAGED_PYTHON_FILES" ]; then
|
|
||||||
printf "OK\n"
|
|
||||||
else
|
|
||||||
BLACK_OUTPUT="/tmp/gc-black-output.log"
|
|
||||||
touch $BLACK_OUTPUT
|
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black --check &>$BLACK_OUTPUT; then
|
|
||||||
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black &>$BLACK_OUTPUT
|
|
||||||
tail -1 $BLACK_OUTPUT
|
|
||||||
formatter_updated=1
|
|
||||||
else
|
|
||||||
printf "OK\n"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "SKIP: program not found\n"
|
|
||||||
printf "HINT: Install black with 'pip3 install black' (black requires Python>=3.6)\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Formatter: isort
|
|
||||||
|
|
||||||
printf "> isort ... "
|
|
||||||
|
|
||||||
if type isort &>/dev/null; then
|
|
||||||
if [ -z "$STAGED_PYTHON_FILES" ]; then
|
|
||||||
printf "OK\n"
|
|
||||||
else
|
|
||||||
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
|
||||||
touch $ISORT_OUTPUT
|
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then
|
|
||||||
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
|
|
||||||
printf "Reformatted.\n"
|
|
||||||
formatter_updated=1
|
|
||||||
else
|
|
||||||
printf "OK\n"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "SKIP: program not found\n"
|
|
||||||
printf "HINT: Install isort with 'pip install isort'\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Checker: flake8
|
|
||||||
|
|
||||||
printf "> flake8 ... "
|
|
||||||
|
|
||||||
if type flake8 &>/dev/null; then
|
|
||||||
if [ -z "$STAGED_PYTHON_FILES" ]; then
|
|
||||||
printf "OK\n"
|
|
||||||
else
|
|
||||||
FLAKE8_OUTPUT="/tmp/gc-flake8-output.log"
|
|
||||||
touch $FLAKE8_OUTPUT
|
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then
|
|
||||||
printf "FAIL\n"
|
|
||||||
cat $FLAKE8_OUTPUT
|
|
||||||
checker_dirty=1
|
|
||||||
else
|
|
||||||
printf "OK\n"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "SKIP: program not found\n"
|
|
||||||
printf "HINT: Install flake8 with 'pip install flake8'\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# End
|
|
||||||
|
|
||||||
if [ $checker_dirty -ne 0 ]
|
|
||||||
then
|
|
||||||
printf ">>> Checker(s) detect(s) issue(s)\n"
|
|
||||||
printf " You can still commit and push :)\n"
|
|
||||||
printf " Be warned that our CI may cause you more trouble.\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $formatter_updated -ne 0 ]
|
|
||||||
then
|
|
||||||
printf ">>> Working tree updated by formatter(s)\n"
|
|
||||||
printf " Add changes to staging area and retry.\n"
|
|
||||||
exit_code=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "\n"
|
|
||||||
|
|
||||||
exit $exit_code
|
|
303
CHANGELOG.md
303
CHANGELOG.md
|
@ -1,303 +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
|
|
||||||
|
|
||||||
- Lancer `python manage.py update_translation_fields` après la migration
|
|
||||||
- Mettre à jour les units systemd `daphne.service` et `worker.service`
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- Mise à jour vers Channels 3.x et Django 3.2
|
|
||||||
|
|
||||||
## 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
|
|
240
README.md
240
README.md
|
@ -1,79 +1,10 @@
|
||||||
# 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)
|
|
||||||
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Il est possible d'installer GestioCOF sur votre machine de deux façons différentes :
|
|
||||||
|
|
||||||
- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère
|
|
||||||
- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde
|
|
||||||
|
|
||||||
### Installation manuelle
|
|
||||||
|
|
||||||
Il est fortement conseillé d'utiliser un environnement virtuel pour Python.
|
|
||||||
|
|
||||||
Il vous faudra installer pip, les librairies de développement de python ainsi
|
|
||||||
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
|
|
||||||
Debian et dérivées (Ubuntu, ...) :
|
|
||||||
|
|
||||||
sudo apt-get install python3-pip python3-dev python3-venv sqlite3
|
|
||||||
|
|
||||||
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
|
||||||
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
|
||||||
(le dossier où se trouve ce README), et créez-le maintenant :
|
|
||||||
|
|
||||||
python3 -m venv venv
|
|
||||||
|
|
||||||
Pour l'activer, il faut taper
|
|
||||||
|
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
depuis le même dossier.
|
|
||||||
|
|
||||||
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
|
||||||
`requirements-devel.txt` :
|
|
||||||
|
|
||||||
pip install -U pip # parfois nécessaire la première fois
|
|
||||||
pip install -r requirements-devel.txt
|
|
||||||
|
|
||||||
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers
|
|
||||||
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
|
|
||||||
pour profiter de façon transparente des mises à jour du fichier:
|
|
||||||
|
|
||||||
ln -s secret_example.py gestioasso/settings/secret.py
|
|
||||||
|
|
||||||
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
|
|
||||||
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
|
|
||||||
encore l'installation *via* un lien symbolique:
|
|
||||||
|
|
||||||
ln -s ../../.pre-commit.sh .git/hooks/pre-commit
|
|
||||||
|
|
||||||
Pour plus d'informations à ce sujet, consulter la
|
|
||||||
[page](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/coding-style)
|
|
||||||
du wiki gestioCOF liée aux conventions.
|
|
||||||
|
|
||||||
|
|
||||||
#### Fin d'installation
|
|
||||||
|
|
||||||
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
|
|
||||||
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
|
|
||||||
données bidons bien pratiques pour développer avec la commande suivante :
|
|
||||||
|
|
||||||
bash provisioning/prepare_django.sh
|
|
||||||
|
|
||||||
Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas
|
|
||||||
pour plus de détails.
|
|
||||||
|
|
||||||
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
|
|
||||||
|
|
||||||
python manage.py runserver
|
|
||||||
|
|
||||||
|
|
||||||
### Vagrant
|
### Vagrant
|
||||||
|
|
||||||
Une autre façon d'installer GestioCOF sur votre machine est d'utiliser
|
La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser
|
||||||
[Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
|
[Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
|
||||||
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
|
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
|
||||||
le monde à la même configuration de développement (même sous Windows !), et
|
le monde à la même configuration de développement (même sous Windows !), et
|
||||||
|
@ -135,72 +66,137 @@ car par défaut Django n'écoute que sur l'adresse locale de la machine virtuell
|
||||||
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
|
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
|
||||||
entrer dans le navigateur est `localhost:8000`.
|
entrer dans le navigateur est `localhost:8000`.
|
||||||
|
|
||||||
|
|
||||||
#### Serveur de développement type production
|
#### Serveur de développement type production
|
||||||
|
|
||||||
Juste histoire de jouer, pas indispensable pour développer :
|
Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de
|
||||||
|
façon similaire à la version en production : on utilise
|
||||||
La VM Vagrant héberge en plus un serveur nginx configuré pour servir GestioCOF
|
|
||||||
comme en production : on utilise
|
|
||||||
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
|
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
|
||||||
derrière un reverse-proxy nginx.
|
derrière un reverse-proxy apache. Le tout est monitoré par
|
||||||
|
[supervisor](http://supervisord.org/).
|
||||||
|
|
||||||
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
|
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
|
||||||
`localhost:8080/gestion/`. Toutefois il ne se recharge pas tout seul lorsque le
|
`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code
|
||||||
code change, il faut relancer le worker avec `sudo systemctl restart
|
change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour
|
||||||
worker.service` pour visualiser la dernière version du code.
|
visualiser la dernière version du code.
|
||||||
|
|
||||||
|
### Installation manuelle
|
||||||
|
|
||||||
|
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il
|
||||||
|
est fortement conseillé d'utiliser un environnement virtuel pour Python.
|
||||||
|
|
||||||
|
Il vous faudra installer pip, les librairies de développement de python, un
|
||||||
|
client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées
|
||||||
|
(Ubuntu, ...) :
|
||||||
|
|
||||||
|
sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server
|
||||||
|
|
||||||
|
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
||||||
|
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
||||||
|
(le dossier où se trouve ce README), et créez-le maintenant :
|
||||||
|
|
||||||
|
virtualenv env -p $(which python3)
|
||||||
|
|
||||||
|
L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir
|
||||||
|
python3, si c'est la version de python par défaut sur votre système, ceci n'est
|
||||||
|
pas nécessaire. Pour l'activer, il faut faire
|
||||||
|
|
||||||
|
. env/bin/activate
|
||||||
|
|
||||||
|
dans le même dossier.
|
||||||
|
|
||||||
|
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||||
|
`requirements-devel.txt` :
|
||||||
|
|
||||||
|
pip install -r requirements-devel.txt
|
||||||
|
|
||||||
|
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.
|
||||||
|
|
||||||
|
#### Installation avec MySQL
|
||||||
|
|
||||||
|
Il faut maintenant installer MySQL. Si vous n'avez pas déjà MySQL installé sur
|
||||||
|
votre serveur, il faut l'installer ; sous Debian et dérivées (Ubuntu, ...) :
|
||||||
|
|
||||||
|
sudo apt-get install mysql-server
|
||||||
|
|
||||||
|
Il vous demandera un mot de passe pour le compte d'administration MySQL,
|
||||||
|
notez-le quelque part (ou n'en mettez pas, le serveur n'est accessible que
|
||||||
|
localement par défaut). Si vous utilisez une autre distribution, consultez la
|
||||||
|
documentation de votre distribution pour savoir comment changer ce mot de passe
|
||||||
|
et démarrer le serveur MySQL (c'est automatique sous Ubuntu).
|
||||||
|
|
||||||
|
Vous devez alors créer un utilisateur local et une base `cof_gestion`, avec le
|
||||||
|
mot de passe de votre choix (remplacez `mot_de_passe`) :
|
||||||
|
|
||||||
|
mysql -uroot -e "CREATE DATABASE cof_gestion; GRANT ALL PRIVILEGES ON cof_gestion.* TO 'cof_gestion'@'localhost' IDENTIFIER BY 'mot_de_passe'"
|
||||||
|
|
||||||
|
Éditez maintenant le fichier `cof/settings.py` pour y intégrer ces changements ;
|
||||||
|
la définition de `DATABASES` doit ressembler à (à nouveau, remplacez
|
||||||
|
`mot_de_passe` de façon appropriée) :
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
|
'NAME': 'cof_gestion',
|
||||||
|
'USER': 'cof_gestion',
|
||||||
|
'PASSWORD': 'mot_de_passe',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#### Installation avec SQLite
|
||||||
|
|
||||||
|
GestioCOF est installé avec MySQL sur la VM COF, et afin d'avoir un
|
||||||
|
environnement de développement aussi proche que possible de ce qui tourne en
|
||||||
|
vrai pour éviter les mauvaises surprises, il est conseillé d'utiliser MySQL sur
|
||||||
|
votre machine de développement également. Toutefois, GestioCOF devrait
|
||||||
|
fonctionner avec d'autres moteurs SQL, et certains préfèrent utiliser SQLite
|
||||||
|
pour sa légèreté et facilité d'installation.
|
||||||
|
|
||||||
|
Si vous décidez d'utiliser SQLite, il faut l'installer ; sous Debian et dérivées :
|
||||||
|
|
||||||
|
sudo apt-get install sqlite3
|
||||||
|
|
||||||
|
puis éditer le fichier `cof/settings.py` pour que la définition de `DATABASES`
|
||||||
|
ressemble à :
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#### Fin d'installation
|
||||||
|
|
||||||
|
Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande suivante :
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
Charger les mails indispensables au bon fonctionnement de GestioCOF :
|
||||||
|
|
||||||
|
python manage.py syncmails
|
||||||
|
|
||||||
|
Une base de donnée pré-remplie est disponible en lançant les commandes :
|
||||||
|
|
||||||
|
python manage.py loaddata gestion sites accounts groups articles
|
||||||
|
python manage.py loaddevdata
|
||||||
|
|
||||||
|
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
|
||||||
|
|
||||||
|
python manage.py runserver
|
||||||
|
|
||||||
### Mise à jour
|
### Mise à jour
|
||||||
|
|
||||||
Pour mettre à jour les paquets Python, utiliser la commande suivante :
|
Pour mettre à jour les paquets Python, utiliser la commande suivante :
|
||||||
|
|
||||||
pip install --upgrade -r requirements-devel.txt
|
pip install --upgrade -r requirements.txt -r requirements-devel.txt
|
||||||
|
|
||||||
Pour mettre à jour les modèles après une migration, il faut ensuite faire :
|
Pour mettre à jour les modèles après une migration, il faut ensuite faire :
|
||||||
|
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
|
|
||||||
|
|
||||||
## Outils pour développer
|
|
||||||
|
|
||||||
### Base de donnée
|
|
||||||
|
|
||||||
Quelle que soit la méthode d'installation choisie, la base de donnée locale est
|
|
||||||
peuplée avec des données artificielles pour faciliter le développement.
|
|
||||||
|
|
||||||
- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez
|
|
||||||
vous sur ce compte pour accéder à tout GestioCOF.
|
|
||||||
- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques
|
|
||||||
spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA.
|
|
||||||
- À chaque compte est associé un trigramme K-Fêt
|
|
||||||
- Un certain nombre d'articles K-Fêt sont renseignés.
|
|
||||||
|
|
||||||
### Tests unitaires
|
|
||||||
|
|
||||||
On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab
|
|
||||||
à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante.
|
|
||||||
|
|
||||||
Pour lancer les tests :
|
|
||||||
|
|
||||||
```
|
|
||||||
python manage.py test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
Une brève documentation utilisateur est accessible sur le
|
Une brève documentation utilisateur pour se familiariser plus vite avec l'outil
|
||||||
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home) pour avoir une
|
est accessible sur le
|
||||||
idée de la façon dont le COF utilise GestioCOF.
|
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home).
|
||||||
|
|
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
|
||||||
|
|
301
bda/admin.py
301
bda/admin.py
|
@ -1,181 +1,85 @@
|
||||||
from datetime import timedelta
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
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.contrib import admin
|
||||||
from django.core.mail import send_mass_mail
|
from django.db.models import Sum, Count
|
||||||
from django.db.models import Count, Q, Sum
|
|
||||||
from django.template import loader
|
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django import forms
|
||||||
from bda.models import (
|
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
|
||||||
Attribution,
|
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
|
||||||
CategorieSpectacle,
|
|
||||||
ChoixSpectacle,
|
|
||||||
Participant,
|
|
||||||
Quote,
|
|
||||||
Salle,
|
|
||||||
Spectacle,
|
|
||||||
SpectacleRevente,
|
|
||||||
Tirage,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyMixin(object):
|
class ChoixSpectacleInline(admin.TabularInline):
|
||||||
readonly_fields_update = ()
|
model = ChoixSpectacle
|
||||||
|
sortable_field_name = "priority"
|
||||||
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 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):
|
class AttributionInline(admin.TabularInline):
|
||||||
model = Attribution
|
model = Attribution
|
||||||
extra = 0
|
extra = 0
|
||||||
listing = None
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super(AttributionInline, self).get_queryset(request)
|
||||||
if self.listing is not None:
|
return qs.filter(spectacle__listing=False)
|
||||||
qs = qs.filter(spectacle__listing=self.listing)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
class WithListingAttributionInline(AttributionInline):
|
class AttributionInlineListing(admin.TabularInline):
|
||||||
exclude = ("given",)
|
model = Attribution
|
||||||
form = WithListingAttributionTabularAdminForm
|
exclude = ('given', )
|
||||||
listing = True
|
extra = 0
|
||||||
verbose_name_plural = "Attributions sur listing"
|
|
||||||
|
|
||||||
|
|
||||||
class WithoutListingAttributionInline(AttributionInline):
|
|
||||||
form = WithoutListingAttributionTabularAdminForm
|
|
||||||
listing = False
|
|
||||||
verbose_name_plural = "Attributions hors listing"
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdminForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
queryset = Spectacle.objects.select_related("location")
|
|
||||||
|
|
||||||
if self.instance.pk is not None:
|
|
||||||
queryset = queryset.filter(tirage=self.instance.tirage)
|
|
||||||
|
|
||||||
self.fields["choicesrevente"].queryset = queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantPaidFilter(admin.SimpleListFilter):
|
|
||||||
"""
|
|
||||||
Permet de filtrer les participants sur s'ils ont payé leurs places ou pas
|
|
||||||
"""
|
|
||||||
|
|
||||||
title = "A payé"
|
|
||||||
parameter_name = "paid"
|
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
|
||||||
return ((True, "Oui"), (False, "Non"))
|
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
|
||||||
return queryset.filter(paid=self.value())
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
|
||||||
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return self.model.objects.annotate_paid().annotate(
|
qs = super(AttributionInlineListing, self).get_queryset(request)
|
||||||
nb_places=Count("attributions"),
|
return qs.filter(spectacle__listing=True)
|
||||||
remain=Sum(
|
|
||||||
"attribution__spectacle__price", filter=Q(attribution__paid=False)
|
|
||||||
),
|
class ParticipantAdmin(admin.ModelAdmin):
|
||||||
total=Sum("attributions__price"),
|
inlines = [AttributionInline, AttributionInlineListing]
|
||||||
)
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return Participant.objects.annotate(nb_places=Count('attributions'),
|
||||||
|
total=Sum('attributions__price'))
|
||||||
|
|
||||||
def nb_places(self, obj):
|
def nb_places(self, obj):
|
||||||
return obj.nb_places
|
return obj.nb_places
|
||||||
|
|
||||||
nb_places.admin_order_field = "nb_places"
|
nb_places.admin_order_field = "nb_places"
|
||||||
nb_places.short_description = "Nombre de places"
|
nb_places.short_description = "Nombre de places"
|
||||||
|
|
||||||
def paid(self, obj):
|
|
||||||
return obj.paid
|
|
||||||
|
|
||||||
paid.short_description = "A payé"
|
|
||||||
paid.boolean = True
|
|
||||||
paid.admin_order_field = "paid"
|
|
||||||
|
|
||||||
def total(self, obj):
|
def total(self, obj):
|
||||||
tot = obj.total
|
tot = obj.total
|
||||||
if tot:
|
if tot:
|
||||||
return "%.02f €" % tot
|
return "%.02f €" % tot
|
||||||
else:
|
else:
|
||||||
return "0 €"
|
return "0 €"
|
||||||
|
|
||||||
total.admin_order_field = "total"
|
total.admin_order_field = "total"
|
||||||
total.short_description = "Total des places"
|
total.short_description = "Total à payer"
|
||||||
|
list_display = ("user", "nb_places", "total", "paid", "paymenttype",
|
||||||
def remain(self, obj):
|
"tirage")
|
||||||
rem = obj.remain
|
list_filter = ("paid", "tirage")
|
||||||
if rem:
|
search_fields = ('user__username', 'user__first_name', 'user__last_name')
|
||||||
return "%.02f €" % rem
|
actions = ['send_attribs', ]
|
||||||
else:
|
|
||||||
return "0 €"
|
|
||||||
|
|
||||||
remain.admin_order_field = "remain"
|
|
||||||
remain.short_description = "Reste à payer"
|
|
||||||
|
|
||||||
list_display = ("user", "nb_places", "total", "paid", "remain", "tirage")
|
|
||||||
list_filter = (ParticipantPaidFilter, "tirage")
|
|
||||||
search_fields = ("user__username", "user__first_name", "user__last_name")
|
|
||||||
actions = ["send_attribs"]
|
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
list_per_page = 400
|
list_per_page = 400
|
||||||
readonly_fields = ("total", "paid")
|
readonly_fields = ("total",)
|
||||||
readonly_fields_update = ("user", "tirage")
|
|
||||||
form = ParticipantAdminForm
|
|
||||||
|
|
||||||
def send_attribs(self, request, queryset):
|
def send_attribs(self, request, queryset):
|
||||||
emails = []
|
datatuple = []
|
||||||
for member in queryset.all():
|
for member in queryset.all():
|
||||||
subject = "Résultats du tirage au sort"
|
|
||||||
attribs = member.attributions.all()
|
attribs = member.attributions.all()
|
||||||
context = {"member": member.user}
|
context = {'member': member.user}
|
||||||
|
shortname = ""
|
||||||
template_name = ""
|
|
||||||
if len(attribs) == 0:
|
if len(attribs) == 0:
|
||||||
template_name = "bda/mails/attributions-decus.txt"
|
shortname = "bda-attributions-decus"
|
||||||
else:
|
else:
|
||||||
template_name = "bda/mails/attributions.txt"
|
shortname = "bda-attributions"
|
||||||
context["places"] = attribs
|
context['places'] = attribs
|
||||||
|
print(context)
|
||||||
message = loader.render_to_string(template_name, context)
|
datatuple.append((shortname, context, "bda@ens.fr",
|
||||||
emails.append((subject, message, "bda@ens.fr", [member.user.email]))
|
[member.user.email]))
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
send_mass_mail(emails)
|
|
||||||
count = len(queryset.all())
|
count = len(queryset.all())
|
||||||
if count == 1:
|
if count == 1:
|
||||||
message_bit = "1 membre a"
|
message_bit = "1 membre a"
|
||||||
|
@ -183,59 +87,46 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
else:
|
else:
|
||||||
message_bit = "%d membres ont" % count
|
message_bit = "%d membres ont" % count
|
||||||
plural = "s"
|
plural = "s"
|
||||||
self.message_user(
|
self.message_user(request, "%s été informé%s avec succès."
|
||||||
request, "%s été informé%s avec succès." % (message_bit, plural)
|
% (message_bit, plural))
|
||||||
)
|
|
||||||
|
|
||||||
send_attribs.short_description = "Envoyer les résultats par mail"
|
send_attribs.short_description = "Envoyer les résultats par mail"
|
||||||
|
|
||||||
|
|
||||||
class AttributionAdminForm(forms.ModelForm):
|
class AttributionAdminForm(forms.ModelForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super(AttributionAdminForm, self).clean()
|
||||||
participant = cleaned_data.get("participant")
|
participant = cleaned_data.get("participant")
|
||||||
spectacle = cleaned_data.get("spectacle")
|
spectacle = cleaned_data.get("spectacle")
|
||||||
if participant and spectacle:
|
if participant and spectacle:
|
||||||
if participant.tirage != spectacle.tirage:
|
if participant.tirage != spectacle.tirage:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Erreur : le participant et le spectacle n'appartiennent"
|
"Erreur : le participant et le spectacle n'appartiennent"
|
||||||
"pas au même tirage"
|
"pas au même tirage")
|
||||||
)
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
class Meta:
|
|
||||||
widgets = {
|
|
||||||
"participant": ModelSelect2(url="bda-participant-autocomplete"),
|
|
||||||
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class AttributionAdmin(admin.ModelAdmin):
|
||||||
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")
|
list_display = ("id", "spectacle", "participant", "given", "paid")
|
||||||
search_fields = (
|
search_fields = ('spectacle__title', 'participant__user__username',
|
||||||
"spectacle__title",
|
'participant__user__first_name',
|
||||||
"participant__user__username",
|
'participant__user__last_name')
|
||||||
"participant__user__first_name",
|
|
||||||
"participant__user__last_name",
|
|
||||||
)
|
|
||||||
form = AttributionAdminForm
|
form = AttributionAdminForm
|
||||||
readonly_fields_update = ("spectacle", "participant")
|
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
||||||
autocomplete_fields = ["participant", "spectacle"]
|
|
||||||
|
|
||||||
def tirage(self, obj):
|
def tirage(self, obj):
|
||||||
return obj.participant.tirage
|
return obj.participant.tirage
|
||||||
|
list_display = ("participant", "tirage", "spectacle", "priority",
|
||||||
list_display = ("participant", "tirage", "spectacle", "priority", "double_choice")
|
"double_choice")
|
||||||
list_filter = ("double_choice", "participant__tirage")
|
list_filter = ("double_choice", "participant__tirage")
|
||||||
search_fields = (
|
search_fields = ('participant__user__username',
|
||||||
"participant__user__username",
|
'participant__user__first_name',
|
||||||
"participant__user__first_name",
|
'participant__user__last_name',
|
||||||
"participant__user__last_name",
|
'spectacle__title')
|
||||||
"spectacle__title",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class QuoteInline(admin.TabularInline):
|
class QuoteInline(admin.TabularInline):
|
||||||
|
@ -245,43 +136,31 @@ class QuoteInline(admin.TabularInline):
|
||||||
class SpectacleAdmin(admin.ModelAdmin):
|
class SpectacleAdmin(admin.ModelAdmin):
|
||||||
inlines = [QuoteInline]
|
inlines = [QuoteInline]
|
||||||
model = Spectacle
|
model = Spectacle
|
||||||
list_display = ("title", "date", "tirage", "location", "slots", "price", "listing")
|
list_display = ("title", "date", "tirage", "location", "slots", "price",
|
||||||
list_filter = ("location", "tirage")
|
"listing")
|
||||||
|
list_filter = ("location", "tirage",)
|
||||||
search_fields = ("title", "location__name")
|
search_fields = ("title", "location__name")
|
||||||
readonly_fields = ("rappel_sent",)
|
readonly_fields = ("rappel_sent", )
|
||||||
|
|
||||||
|
|
||||||
class TirageAdmin(admin.ModelAdmin):
|
class TirageAdmin(admin.ModelAdmin):
|
||||||
model = Tirage
|
model = Tirage
|
||||||
list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage")
|
list_display = ("title", "ouverture", "fermeture", "active",
|
||||||
readonly_fields = ("tokens",)
|
"enable_do_tirage")
|
||||||
list_filter = ("active",)
|
readonly_fields = ("tokens", )
|
||||||
search_fields = ("title",)
|
list_filter = ("active", )
|
||||||
|
search_fields = ("title", )
|
||||||
|
|
||||||
|
|
||||||
class SalleAdmin(admin.ModelAdmin):
|
class SalleAdmin(admin.ModelAdmin):
|
||||||
model = Salle
|
model = Salle
|
||||||
search_fields = ("name", "address")
|
search_fields = ('name', 'address')
|
||||||
|
|
||||||
|
|
||||||
class SpectacleReventeAdminForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
qset = Participant.objects.select_related("user", "tirage")
|
|
||||||
|
|
||||||
if self.instance.pk is not None:
|
|
||||||
qset = qset.filter(tirage=self.instance.seller.tirage)
|
|
||||||
|
|
||||||
self.fields["confirmed_entry"].queryset = qset
|
|
||||||
self.fields["seller"].queryset = qset
|
|
||||||
self.fields["soldTo"].queryset = qset
|
|
||||||
|
|
||||||
|
|
||||||
class SpectacleReventeAdmin(admin.ModelAdmin):
|
class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Administration des reventes de spectacles
|
Administration des reventes de spectacles
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = SpectacleRevente
|
model = SpectacleRevente
|
||||||
|
|
||||||
def spectacle(self, obj):
|
def spectacle(self, obj):
|
||||||
|
@ -293,16 +172,13 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
list_display = ("spectacle", "seller", "date", "soldTo")
|
list_display = ("spectacle", "seller", "date", "soldTo")
|
||||||
raw_id_fields = ("attribution",)
|
raw_id_fields = ("attribution",)
|
||||||
readonly_fields = ("date_tirage",)
|
readonly_fields = ("date_tirage",)
|
||||||
search_fields = [
|
search_fields = ['attribution__spectacle__title',
|
||||||
"attribution__spectacle__title",
|
'seller__user__username',
|
||||||
"seller__user__username",
|
'seller__user__first_name',
|
||||||
"seller__user__first_name",
|
'seller__user__last_name']
|
||||||
"seller__user__last_name",
|
|
||||||
]
|
|
||||||
|
|
||||||
actions = ["transfer", "reinit"]
|
actions = ['transfer', 'reinit']
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = SpectacleReventeAdminForm
|
|
||||||
|
|
||||||
def transfer(self, request, queryset):
|
def transfer(self, request, queryset):
|
||||||
"""
|
"""
|
||||||
|
@ -316,10 +192,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
attrib.save()
|
attrib.save()
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
"%d attribution%s %s été transférée%s avec succès."
|
"%d attribution%s %s été transférée%s avec succès." % (
|
||||||
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
|
count, pluralize(count),
|
||||||
|
pluralize(count, "a,ont"), pluralize(count))
|
||||||
)
|
)
|
||||||
|
|
||||||
transfer.short_description = "Transférer les reventes sélectionnées"
|
transfer.short_description = "Transférer les reventes sélectionnées"
|
||||||
|
|
||||||
def reinit(self, request, queryset):
|
def reinit(self, request, queryset):
|
||||||
|
@ -328,15 +204,20 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
count = queryset.count()
|
count = queryset.count()
|
||||||
for revente in queryset.filter(
|
for revente in queryset.filter(
|
||||||
attribution__spectacle__date__gte=timezone.now()
|
attribution__spectacle__date__gte=timezone.now()):
|
||||||
):
|
revente.date = timezone.now() - timedelta(hours=1)
|
||||||
revente.reset(new_date=timezone.now() - timedelta(hours=1))
|
revente.soldTo = None
|
||||||
|
revente.notif_sent = False
|
||||||
|
revente.tirage_done = False
|
||||||
|
if revente.answered_mail:
|
||||||
|
revente.answered_mail.clear()
|
||||||
|
revente.save()
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
"%d attribution%s %s été réinitialisée%s avec succès."
|
"%d attribution%s %s été réinitialisée%s avec succès." % (
|
||||||
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
|
count, pluralize(count),
|
||||||
|
pluralize(count, "a,ont"), pluralize(count))
|
||||||
)
|
)
|
||||||
|
|
||||||
reinit.short_description = "Réinitialiser les reventes sélectionnées"
|
reinit.short_description = "Réinitialiser les reventes sélectionnées"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
||||||
class Algorithm(object):
|
class Algorithm(object):
|
||||||
|
|
||||||
shows = None
|
shows = None
|
||||||
ranks = None
|
ranks = None
|
||||||
origranks = None
|
origranks = None
|
||||||
|
@ -13,7 +22,8 @@ class Algorithm(object):
|
||||||
show.requests
|
show.requests
|
||||||
- on crée des tables de demandes pour chaque personne, afin de
|
- on crée des tables de demandes pour chaque personne, afin de
|
||||||
pouvoir modifier les rankings"""
|
pouvoir modifier les rankings"""
|
||||||
self.max_group = 2 * max(choice.priority for choice in choices)
|
self.max_group = \
|
||||||
|
2 * choices.aggregate(Max('priority'))['priority__max']
|
||||||
self.shows = []
|
self.shows = []
|
||||||
showdict = {}
|
showdict = {}
|
||||||
for show in shows:
|
for show in shows:
|
||||||
|
@ -51,19 +61,16 @@ class Algorithm(object):
|
||||||
self.ranks[member][show] -= increment
|
self.ranks[member][show] -= increment
|
||||||
|
|
||||||
def appendResult(self, l, member, show):
|
def appendResult(self, l, member, show):
|
||||||
l.append(
|
l.append((member,
|
||||||
(
|
|
||||||
member,
|
|
||||||
self.ranks[member][show],
|
self.ranks[member][show],
|
||||||
self.origranks[member][show],
|
self.origranks[member][show],
|
||||||
self.choices[member][show].double,
|
self.choices[member][show].double))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __call__(self, seed):
|
def __call__(self, seed):
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
results = []
|
results = []
|
||||||
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True)
|
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots,
|
||||||
|
reverse=True)
|
||||||
for show in shows:
|
for show in shows:
|
||||||
# On regroupe tous les gens ayant le même rang
|
# On regroupe tous les gens ayant le même rang
|
||||||
groups = dict([(i, []) for i in range(1, self.max_group + 1)])
|
groups = dict([(i, []) for i in range(1, self.max_group + 1)])
|
||||||
|
@ -82,10 +89,8 @@ class Algorithm(object):
|
||||||
if len(winners) + 1 < show.slots:
|
if len(winners) + 1 < show.slots:
|
||||||
self.appendResult(winners, member, show)
|
self.appendResult(winners, member, show)
|
||||||
self.appendResult(winners, member, show)
|
self.appendResult(winners, member, show)
|
||||||
elif (
|
elif not self.choices[member][show].autoquit \
|
||||||
not self.choices[member][show].autoquit
|
and len(winners) < show.slots:
|
||||||
and len(winners) < show.slots
|
|
||||||
):
|
|
||||||
self.appendResult(winners, member, show)
|
self.appendResult(winners, member, show)
|
||||||
self.appendResult(losers, member, show)
|
self.appendResult(losers, member, show)
|
||||||
else:
|
else:
|
||||||
|
|
6
bda/apps.py
Normal file
6
bda/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BdAConfig(AppConfig):
|
||||||
|
name = "bda"
|
||||||
|
verbose_name = "Gestion des tirages du BdA"
|
215
bda/forms.py
215
bda/forms.py
|
@ -1,184 +1,83 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.models import BaseInlineFormSet
|
from django.forms.models import BaseInlineFormSet
|
||||||
from django.template import loader
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from bda.models import Attribution, Spectacle
|
||||||
from bda.models import SpectacleRevente
|
|
||||||
|
|
||||||
|
|
||||||
class InscriptionInlineFormSet(BaseInlineFormSet):
|
class BaseBdaFormSet(BaseInlineFormSet):
|
||||||
def __init__(self, *args, **kwargs):
|
def clean(self):
|
||||||
super().__init__(*args, **kwargs)
|
"""Checks that no two articles have the same title."""
|
||||||
|
super(BaseBdaFormSet, self).clean()
|
||||||
# self.instance is a Participant object
|
if any(self.errors):
|
||||||
tirage = self.instance.tirage
|
# Don't bother validating the formset unless each form is valid on
|
||||||
|
# its own
|
||||||
# set once for all "spectacle" field choices
|
return
|
||||||
# - restrict choices to the spectacles of this tirage
|
spectacles = []
|
||||||
# - force_choices avoid many db requests
|
for i in range(0, self.total_form_count()):
|
||||||
spectacles = tirage.spectacle_set.select_related("location")
|
form = self.forms[i]
|
||||||
choices = [(sp.pk, str(sp)) for sp in spectacles]
|
if not form.cleaned_data:
|
||||||
self.force_choices("spectacle", choices)
|
continue
|
||||||
|
spectacle = form.cleaned_data['spectacle']
|
||||||
def force_choices(self, name, choices):
|
delete = form.cleaned_data['DELETE']
|
||||||
"""Set choices of a field.
|
if not delete and spectacle in spectacles:
|
||||||
|
raise forms.ValidationError(
|
||||||
As ModelChoiceIterator (default use to get choices of a
|
"Vous ne pouvez pas vous inscrire deux fois pour le "
|
||||||
ModelChoiceField), it appends an empty selection if requested.
|
"même spectacle.")
|
||||||
|
spectacles.append(spectacle)
|
||||||
"""
|
|
||||||
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):
|
class TokenForm(forms.Form):
|
||||||
token = forms.CharField(widget=forms.widgets.Textarea())
|
token = forms.CharField(widget=forms.widgets.Textarea())
|
||||||
|
|
||||||
|
|
||||||
class TemplateLabelField(forms.ModelMultipleChoiceField):
|
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
"""
|
|
||||||
Extends ModelMultipleChoiceField to offer two more customization options :
|
|
||||||
- `label_from_instance` can be used with a template file
|
|
||||||
- the widget rendering template can be specified with `option_template_name`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
label_template_name=None,
|
|
||||||
context_object_name="obj",
|
|
||||||
option_template_name=None,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.label_template_name = label_template_name
|
|
||||||
self.context_object_name = context_object_name
|
|
||||||
if option_template_name is not None:
|
|
||||||
self.widget.option_template_name = option_template_name
|
|
||||||
|
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj):
|
||||||
if self.label_template_name is None:
|
return "%s" % obj.spectacle
|
||||||
return super().label_from_instance(obj)
|
|
||||||
else:
|
|
||||||
return loader.render_to_string(
|
|
||||||
self.label_template_name, context={self.context_object_name: obj}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Formulaires pour revente_manage
|
|
||||||
|
|
||||||
|
|
||||||
class ResellForm(forms.Form):
|
class ResellForm(forms.Form):
|
||||||
def __init__(self, participant, *args, **kwargs):
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
super().__init__(*args, **kwargs)
|
queryset=Attribution.objects.none(),
|
||||||
self.fields["attributions"] = TemplateLabelField(
|
|
||||||
queryset=participant.attribution_set.filter(
|
|
||||||
spectacle__date__gte=timezone.now(), paid=True
|
|
||||||
)
|
|
||||||
.exclude(revente__seller=participant)
|
|
||||||
.select_related("spectacle", "spectacle__location", "participant__user"),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=False,
|
required=False)
|
||||||
label_template_name="bda/forms/attribution_label_table.html",
|
|
||||||
option_template_name="bda/forms/checkbox_table.html",
|
def __init__(self, participant, *args, **kwargs):
|
||||||
context_object_name="attribution",
|
super(ResellForm, self).__init__(*args, **kwargs)
|
||||||
)
|
self.fields['attributions'].queryset = participant.attribution_set\
|
||||||
|
.filter(spectacle__date__gte=timezone.now())\
|
||||||
|
.exclude(revente__seller=participant)
|
||||||
|
|
||||||
|
|
||||||
class AnnulForm(forms.Form):
|
class AnnulForm(forms.Form):
|
||||||
def __init__(self, participant, *args, **kwargs):
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
super().__init__(*args, **kwargs)
|
queryset=Attribution.objects.none(),
|
||||||
self.fields["reventes"] = TemplateLabelField(
|
|
||||||
label="",
|
|
||||||
queryset=participant.original_shows.filter(
|
|
||||||
attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
|
|
||||||
)
|
|
||||||
.select_related(
|
|
||||||
"attribution__spectacle", "attribution__spectacle__location"
|
|
||||||
)
|
|
||||||
.order_by("-date"),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=False,
|
required=False)
|
||||||
label_template_name="bda/forms/revente_self_label_table.html",
|
|
||||||
option_template_name="bda/forms/checkbox_table.html",
|
|
||||||
context_object_name="revente",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SoldForm(forms.Form):
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
def __init__(self, participant, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(AnnulForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["reventes"] = TemplateLabelField(
|
self.fields['attributions'].queryset = participant.attribution_set\
|
||||||
queryset=participant.original_shows.filter(soldTo__isnull=False)
|
.filter(spectacle__date__gte=timezone.now(),
|
||||||
.exclude(soldTo=participant)
|
revente__isnull=False,
|
||||||
.select_related(
|
revente__date__gt=timezone.now()-timedelta(hours=1),
|
||||||
"attribution__spectacle", "attribution__spectacle__location"
|
revente__soldTo__isnull=True)
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
label_template_name="bda/forms/revente_sold_label_table.html",
|
|
||||||
option_template_name="bda/forms/checkbox_table.html",
|
|
||||||
context_object_name="revente",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Formulaire pour revente_subscribe
|
|
||||||
|
|
||||||
|
|
||||||
class InscriptionReventeForm(forms.Form):
|
class InscriptionReventeForm(forms.Form):
|
||||||
|
spectacles = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Spectacle.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False)
|
||||||
|
|
||||||
def __init__(self, tirage, *args, **kwargs):
|
def __init__(self, tirage, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["spectacles"] = TemplateLabelField(
|
self.fields['spectacles'].queryset = tirage.spectacle_set.filter(
|
||||||
queryset=tirage.spectacle_set.select_related("location").filter(
|
date__gte=timezone.now())
|
||||||
date__gte=timezone.now()
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
label_template_name="bda/forms/spectacle_label_table.html",
|
|
||||||
option_template_name="bda/forms/checkbox_table.html",
|
|
||||||
context_object_name="spectacle",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Formulaires pour revente_tirages
|
|
||||||
|
|
||||||
|
|
||||||
class ReventeTirageAnnulForm(forms.Form):
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["reventes"] = TemplateLabelField(
|
|
||||||
queryset=participant.entered.filter(soldTo__isnull=True).select_related(
|
|
||||||
"attribution__spectacle", "seller__user"
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
label_template_name="bda/forms/revente_other_label_table.html",
|
|
||||||
option_template_name="bda/forms/checkbox_table.html",
|
|
||||||
context_object_name="revente",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ReventeTirageForm(forms.Form):
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.fields["reventes"] = TemplateLabelField(
|
|
||||||
queryset=(
|
|
||||||
SpectacleRevente.objects.filter(
|
|
||||||
notif_sent=True,
|
|
||||||
shotgun=False,
|
|
||||||
tirage_done=False,
|
|
||||||
attribution__spectacle__tirage=participant.tirage,
|
|
||||||
)
|
|
||||||
.exclude(confirmed_entry=participant)
|
|
||||||
.select_related("attribution__spectacle")
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
label_template_name="bda/forms/revente_other_label_table.html",
|
|
||||||
option_template_name="bda/forms/checkbox_table.html",
|
|
||||||
context_object_name="revente",
|
|
||||||
)
|
|
||||||
|
|
|
@ -5,15 +5,17 @@ Crée deux tirages de test et y inscrit les utilisateurs
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage
|
from cof.management.base import MyBaseCommand
|
||||||
|
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
|
||||||
from bda.views import do_tirage
|
from bda.views import do_tirage
|
||||||
from gestioncof.management.base import MyBaseCommand
|
|
||||||
|
|
||||||
# Où sont stockés les fichiers json
|
# Où sont stockés les fichiers json
|
||||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
'data')
|
||||||
|
|
||||||
|
|
||||||
class Command(MyBaseCommand):
|
class Command(MyBaseCommand):
|
||||||
|
@ -25,29 +27,27 @@ class Command(MyBaseCommand):
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
Tirage.objects.all().delete()
|
Tirage.objects.all().delete()
|
||||||
Tirage.objects.bulk_create(
|
Tirage.objects.bulk_create([
|
||||||
[
|
|
||||||
Tirage(
|
Tirage(
|
||||||
title="Tirage de test 1",
|
title="Tirage de test 1",
|
||||||
ouverture=timezone.now() - timezone.timedelta(days=7),
|
ouverture=timezone.now()-timezone.timedelta(days=7),
|
||||||
fermeture=timezone.now(),
|
fermeture=timezone.now(),
|
||||||
active=True,
|
active=True
|
||||||
),
|
),
|
||||||
Tirage(
|
Tirage(
|
||||||
title="Tirage de test 2",
|
title="Tirage de test 2",
|
||||||
ouverture=timezone.now(),
|
ouverture=timezone.now(),
|
||||||
fermeture=timezone.now() + timezone.timedelta(days=60),
|
fermeture=timezone.now()+timezone.timedelta(days=60),
|
||||||
active=True,
|
active=True
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
])
|
||||||
tirages = Tirage.objects.all()
|
tirages = Tirage.objects.all()
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Salles
|
# Salles
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
locations = self.from_json("locations.json", DATA_DIR, Salle)
|
locations = self.from_json('locations.json', DATA_DIR, Salle)
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Spectacles
|
# Spectacles
|
||||||
|
@ -60,13 +60,15 @@ class Command(MyBaseCommand):
|
||||||
"""
|
"""
|
||||||
show.tirage = random.choice(tirages)
|
show.tirage = random.choice(tirages)
|
||||||
show.listing = bool(random.randint(0, 1))
|
show.listing = bool(random.randint(0, 1))
|
||||||
show.date = show.tirage.fermeture + timezone.timedelta(
|
show.date = (
|
||||||
days=random.randint(60, 90)
|
show.tirage.fermeture
|
||||||
|
+ timezone.timedelta(days=random.randint(60, 90))
|
||||||
)
|
)
|
||||||
show.location = random.choice(locations)
|
show.location = random.choice(locations)
|
||||||
return show
|
return show
|
||||||
|
shows = self.from_json(
|
||||||
shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback)
|
'shows.json', DATA_DIR, Spectacle, show_callback
|
||||||
|
)
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Inscriptions
|
# Inscriptions
|
||||||
|
@ -75,21 +77,26 @@ class Command(MyBaseCommand):
|
||||||
self.stdout.write("Inscription des utilisateurs aux tirages")
|
self.stdout.write("Inscription des utilisateurs aux tirages")
|
||||||
ChoixSpectacle.objects.all().delete()
|
ChoixSpectacle.objects.all().delete()
|
||||||
choices = []
|
choices = []
|
||||||
for user in User.objects.filter(profile__is_cof=True):
|
cof_members = Group.objects.get(name="cof_members")
|
||||||
|
for user in cof_members.user_set.all():
|
||||||
for tirage in tirages:
|
for tirage in tirages:
|
||||||
part, _ = Participant.objects.get_or_create(user=user, tirage=tirage)
|
part, _ = Participant.objects.get_or_create(
|
||||||
shows = random.sample(
|
user=user,
|
||||||
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
|
tirage=tirage
|
||||||
)
|
)
|
||||||
for rank, show in enumerate(shows):
|
shows = random.sample(
|
||||||
choices.append(
|
list(tirage.spectacle_set.all()),
|
||||||
ChoixSpectacle(
|
tirage.spectacle_set.count() // 2
|
||||||
|
)
|
||||||
|
for (rank, show) in enumerate(shows):
|
||||||
|
choices.append(ChoixSpectacle(
|
||||||
participant=part,
|
participant=part,
|
||||||
spectacle=show,
|
spectacle=show,
|
||||||
priority=rank + 1,
|
priority=rank + 1,
|
||||||
double_choice=random.choice(["1", "double", "autoquit"]),
|
double_choice=random.choice(
|
||||||
)
|
['1', 'double', 'autoquit']
|
||||||
)
|
)
|
||||||
|
))
|
||||||
ChoixSpectacle.objects.bulk_create(choices)
|
ChoixSpectacle.objects.bulk_create(choices)
|
||||||
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))
|
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))
|
||||||
|
|
||||||
|
|
|
@ -1,49 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Gestion en ligne de commande des reventes.
|
Gestion en ligne de commande des reventes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bda.models import SpectacleRevente
|
from bda.models import SpectacleRevente
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = (
|
help = "Envoie les mails de notification et effectue " \
|
||||||
"Envoie les mails de notification et effectue les tirages au sort des reventes"
|
"les tirages au sort des reventes"
|
||||||
)
|
|
||||||
leave_locale_alone = True
|
leave_locale_alone = True
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
reventes = SpectacleRevente.objects.all()
|
reventes = SpectacleRevente.objects.all()
|
||||||
for revente in reventes:
|
for revente in reventes:
|
||||||
# Le spectacle est bientôt et on a pas encore envoyé de mail :
|
# Check si < 24h
|
||||||
# on met la place au shotgun et on prévient.
|
if (revente.attribution.spectacle.date <=
|
||||||
if revente.is_urgent and not revente.notif_sent:
|
revente.date + timedelta(days=1)) and \
|
||||||
if revente.can_notif:
|
now >= revente.date + timedelta(minutes=15) and \
|
||||||
|
not revente.notif_sent:
|
||||||
self.stdout.write(str(now))
|
self.stdout.write(str(now))
|
||||||
revente.mail_shotgun()
|
revente.mail_shotgun()
|
||||||
self.stdout.write(
|
self.stdout.write("Mail de disponibilité immédiate envoyé")
|
||||||
"Mails de disponibilité immédiate envoyés "
|
# Check si délai de retrait dépassé
|
||||||
"pour la revente [%s]" % revente
|
elif (now >= revente.date + timedelta(hours=1) and
|
||||||
)
|
not revente.notif_sent):
|
||||||
|
|
||||||
# Le spectacle est dans plus longtemps : on prévient
|
|
||||||
elif revente.can_notif and not revente.notif_sent:
|
|
||||||
self.stdout.write(str(now))
|
self.stdout.write(str(now))
|
||||||
revente.send_notif()
|
revente.send_notif()
|
||||||
self.stdout.write(
|
self.stdout.write("Mail d'inscription à une revente envoyé")
|
||||||
"Mails d'inscription à la revente [%s] envoyés" % revente
|
# Check si tirage à faire
|
||||||
)
|
elif (now >= revente.date_tirage and
|
||||||
|
not revente.tirage_done):
|
||||||
# On fait le tirage
|
|
||||||
elif now >= revente.date_tirage and not revente.tirage_done:
|
|
||||||
self.stdout.write(str(now))
|
self.stdout.write(str(now))
|
||||||
winner = revente.tirage()
|
revente.tirage()
|
||||||
self.stdout.write("Tirage effectué pour la revente [%s]" % revente)
|
self.stdout.write("Tirage effectué, mails envoyés")
|
||||||
|
|
||||||
if winner:
|
|
||||||
self.stdout.write("Gagnant : %s" % winner.user)
|
|
||||||
else:
|
|
||||||
self.stdout.write("Pas de gagnant ; place au shotgun")
|
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Gestion en ligne de commande des mails de rappel.
|
Gestion en ligne de commande des mails de rappel.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bda.models import Spectacle
|
from bda.models import Spectacle
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = (
|
help = 'Envoie les mails de rappel des spectacles dont la date ' \
|
||||||
"Envoie les mails de rappel des spectacles dont la date approche.\n"
|
'approche.\nNe renvoie pas les mails déjà envoyés.'
|
||||||
"Ne renvoie pas les mails déjà envoyés."
|
|
||||||
)
|
|
||||||
leave_locale_alone = True
|
leave_locale_alone = True
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
delay = timedelta(days=4)
|
delay = timedelta(days=4)
|
||||||
shows = (
|
shows = Spectacle.objects \
|
||||||
Spectacle.objects.filter(date__range=(now, now + delay))
|
.filter(date__range=(now, now+delay)) \
|
||||||
.filter(tirage__active=True)
|
.filter(tirage__active=True) \
|
||||||
.filter(rappel_sent__isnull=True)
|
.filter(rappel_sent__isnull=True) \
|
||||||
.all()
|
.all()
|
||||||
)
|
|
||||||
for show in shows:
|
for show in shows:
|
||||||
show.send_rappel()
|
show.send_rappel()
|
||||||
self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show)
|
self.stdout.write(
|
||||||
|
'Mails de rappels pour %s envoyés avec succès.' % show)
|
||||||
if not shows:
|
if not shows:
|
||||||
self.stdout.write("Aucun mail à envoyer.")
|
self.stdout.write('Aucun mail à envoyer.')
|
||||||
|
|
|
@ -1,205 +1,122 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Attribution",
|
name='Attribution',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
"id",
|
('given', models.BooleanField(default=False, verbose_name='Donn\xe9e')),
|
||||||
models.AutoField(
|
|
||||||
verbose_name="ID",
|
|
||||||
serialize=False,
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("given", models.BooleanField(default=False, verbose_name="Donn\xe9e")),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="ChoixSpectacle",
|
name='ChoixSpectacle',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
"id",
|
('priority', models.PositiveIntegerField(verbose_name=b'Priorit\xc3\xa9')),
|
||||||
models.AutoField(
|
('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')])),
|
||||||
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={
|
options={
|
||||||
"ordering": ("priority",),
|
'ordering': ('priority',),
|
||||||
"verbose_name": "voeu",
|
'verbose_name': 'voeu',
|
||||||
"verbose_name_plural": "voeux",
|
'verbose_name_plural': 'voeux',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Participant",
|
name='Participant',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
"id",
|
('paid', models.BooleanField(default=False, verbose_name='A pay\xe9')),
|
||||||
models.AutoField(
|
('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')])),
|
||||||
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(
|
migrations.CreateModel(
|
||||||
name="Salle",
|
name='Salle',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
"id",
|
('name', models.CharField(max_length=300, verbose_name=b'Nom')),
|
||||||
models.AutoField(
|
('address', models.TextField(verbose_name=b'Adresse')),
|
||||||
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(
|
migrations.CreateModel(
|
||||||
name="Spectacle",
|
name='Spectacle',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
"id",
|
('title', models.CharField(max_length=300, verbose_name=b'Titre')),
|
||||||
models.AutoField(
|
('date', models.DateTimeField(verbose_name=b'Date & heure')),
|
||||||
verbose_name="ID",
|
('description', models.TextField(verbose_name=b'Description', blank=True)),
|
||||||
serialize=False,
|
('slots_description', models.TextField(verbose_name=b'Description des places', blank=True)),
|
||||||
auto_created=True,
|
('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)),
|
||||||
primary_key=True,
|
('slots', models.IntegerField(verbose_name=b'Places')),
|
||||||
),
|
('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')),
|
||||||
),
|
('location', models.ForeignKey(
|
||||||
("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,
|
on_delete=models.CASCADE,
|
||||||
),
|
to='bda.Salle')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('priority', 'date', 'title'),
|
||||||
|
'verbose_name': 'Spectacle',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="attribution",
|
model_name='participant',
|
||||||
name="participant",
|
name='attributions',
|
||||||
field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
|
field=models.ManyToManyField(related_name='attributed_to', through='bda.Attribution', to='bda.Spectacle'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="attribution",
|
model_name='participant',
|
||||||
name="spectacle",
|
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(
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='choixspectacle',
|
||||||
|
name='participant',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
related_name="attribues", to="bda.Spectacle", on_delete=models.CASCADE
|
on_delete=models.CASCADE,
|
||||||
|
to='bda.Participant'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='choixspectacle',
|
||||||
|
name='spectacle',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='participants',
|
||||||
|
to='bda.Spectacle'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='attribution',
|
||||||
|
name='participant',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
to='bda.Participant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='attribution',
|
||||||
|
name='spectacle',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
related_name='attribues',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
to='bda.Spectacle'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="choixspectacle", unique_together=set([("participant", "spectacle")])
|
name='choixspectacle',
|
||||||
|
unique_together=set([('participant', 'spectacle')]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,111 +1,64 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
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")
|
Tirage = apps.get_model("bda", "Tirage")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
# These querysets only contains instances not linked to any `Tirage`.
|
Tirage.objects.using(db_alias).bulk_create([
|
||||||
participants = Participant.objects.filter(tirage=None)
|
Tirage(
|
||||||
spectacles = Spectacle.objects.filter(tirage=None)
|
id=1,
|
||||||
|
|
||||||
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)",
|
title="Tirage de test (migration)",
|
||||||
active=False,
|
active=False,
|
||||||
ouverture=timezone.now(),
|
ouverture=timezone.now(),
|
||||||
fermeture=timezone.now(),
|
fermeture=timezone.now()),
|
||||||
)
|
])
|
||||||
|
|
||||||
participants.update(tirage=tirage)
|
|
||||||
spectacles.update(tirage=tirage)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0001_initial")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Tirage",
|
name='Tirage',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
"id",
|
('title', models.CharField(max_length=300, verbose_name=b'Titre')),
|
||||||
models.AutoField(
|
('ouverture', models.DateTimeField(verbose_name=b"Date et heure d'ouverture du tirage")),
|
||||||
verbose_name="ID",
|
('fermeture', models.DateTimeField(verbose_name=b'Date et heure de fermerture du tirage')),
|
||||||
serialize=False,
|
('token', models.TextField(verbose_name=b'Graine du tirage', blank=True)),
|
||||||
auto_created=True,
|
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
|
||||||
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.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="participant",
|
model_name='participant',
|
||||||
name="user",
|
name='user',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
on_delete=models.CASCADE,
|
||||||
),
|
to=settings.AUTH_USER_MODEL),
|
||||||
),
|
|
||||||
# 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(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='participant',
|
||||||
name="tirage",
|
name='tirage',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
to="bda.Tirage", null=True, on_delete=models.CASCADE
|
on_delete=models.CASCADE,
|
||||||
|
default=1,
|
||||||
|
to='bda.Tirage'),
|
||||||
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
),
|
migrations.AddField(
|
||||||
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
|
model_name='spectacle',
|
||||||
migrations.AlterField(
|
name='tirage',
|
||||||
model_name="participant",
|
field=models.ForeignKey(
|
||||||
name="tirage",
|
on_delete=models.CASCADE,
|
||||||
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
|
default=1,
|
||||||
),
|
to='bda.Tirage'),
|
||||||
migrations.AlterField(
|
preserve_default=False,
|
||||||
model_name="spectacle",
|
|
||||||
name="tirage",
|
|
||||||
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,17 +5,20 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0002_add_tirage")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0002_add_tirage'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="price",
|
name='price',
|
||||||
field=models.FloatField(verbose_name=b"Prix d'une place"),
|
field=models.FloatField(verbose_name=b"Prix d'une place"),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="active",
|
name='active',
|
||||||
field=models.BooleanField(default=False, verbose_name=b"Tirage actif"),
|
field=models.BooleanField(default=False, verbose_name=b'Tirage actif'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,22 +5,21 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0003_update_tirage_and_spectacle")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0003_update_tirage_and_spectacle'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="listing",
|
name='listing',
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'),
|
||||||
default=False, verbose_name=b"Les places sont sur listing"
|
|
||||||
),
|
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="rappel_sent",
|
name='rappel_sent',
|
||||||
field=models.DateTimeField(
|
field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True),
|
||||||
null=True, verbose_name=b"Mail de rappel envoy\xc3\xa9", blank=True
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,24 +5,25 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0004_mails-rappel")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0004_mails-rappel'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="choixspectacle",
|
model_name='choixspectacle',
|
||||||
name="priority",
|
name='priority',
|
||||||
field=models.PositiveIntegerField(verbose_name="Priorit\xe9"),
|
field=models.PositiveIntegerField(verbose_name='Priorit\xe9'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="priority",
|
name='priority',
|
||||||
field=models.IntegerField(default=1000, verbose_name="Priorit\xe9"),
|
field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="rappel_sent",
|
name='rappel_sent',
|
||||||
field=models.DateTimeField(
|
field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True),
|
||||||
null=True, verbose_name="Mail de rappel envoy\xe9", blank=True
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,24 +10,26 @@ def forwards_func(apps, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
for tirage in Tirage.objects.using(db_alias).all():
|
for tirage in Tirage.objects.using(db_alias).all():
|
||||||
if tirage.tokens:
|
if tirage.tokens:
|
||||||
tirage.tokens = 'Before %s\n"""%s"""\n' % (
|
tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % (
|
||||||
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
|
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
|
||||||
tirage.tokens,
|
tirage.tokens)
|
||||||
)
|
|
||||||
tirage.save()
|
tirage.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0005_encoding")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0005_encoding'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField("tirage", "token", "tokens"),
|
migrations.RenameField('tirage', 'token', 'tokens'),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="enable_do_tirage",
|
name='enable_do_tirage',
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9"
|
default=False,
|
||||||
),
|
verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'),
|
||||||
),
|
),
|
||||||
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,99 +1,94 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0006_add_tirage_switch")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0006_add_tirage_switch'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="CategorieSpectacle",
|
name='CategorieSpectacle',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False,
|
||||||
"id",
|
auto_created=True, primary_key=True)),
|
||||||
models.AutoField(
|
('name', models.CharField(max_length=100, verbose_name='Nom',
|
||||||
verbose_name="ID",
|
unique=True)),
|
||||||
serialize=False,
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(max_length=100, verbose_name="Nom", unique=True),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={"verbose_name": "Cat\xe9gorie"},
|
options={
|
||||||
|
'verbose_name': 'Cat\xe9gorie',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Quote",
|
name='Quote',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(verbose_name='ID', serialize=False,
|
||||||
"id",
|
auto_created=True, primary_key=True)),
|
||||||
models.AutoField(
|
('text', models.TextField(verbose_name='Citation')),
|
||||||
verbose_name="ID",
|
('author', models.CharField(max_length=200,
|
||||||
serialize=False,
|
verbose_name='Auteur')),
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("text", models.TextField(verbose_name="Citation")),
|
|
||||||
("author", models.CharField(max_length=200, verbose_name="Auteur")),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name="spectacle",
|
name='spectacle',
|
||||||
options={"ordering": ("date", "title"), "verbose_name": "Spectacle"},
|
options={'ordering': ('date', 'title'),
|
||||||
|
'verbose_name': 'Spectacle'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='priority',
|
||||||
),
|
),
|
||||||
migrations.RemoveField(model_name="spectacle", name="priority"),
|
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="ext_link",
|
name='ext_link',
|
||||||
field=models.CharField(
|
field=models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
verbose_name="Lien vers le site du spectacle",
|
verbose_name='Lien vers le site du spectacle',
|
||||||
blank=True,
|
blank=True),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="image",
|
name='image',
|
||||||
field=models.ImageField(
|
field=models.ImageField(upload_to='imgs/shows/', null=True,
|
||||||
upload_to="imgs/shows/", null=True, verbose_name="Image", blank=True
|
verbose_name='Image', blank=True),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="enable_do_tirage",
|
name='enable_do_tirage',
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=False, verbose_name="Le tirage peut \xeatre lanc\xe9"
|
default=False,
|
||||||
),
|
verbose_name='Le tirage peut \xeatre lanc\xe9'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="tokens",
|
name='tokens',
|
||||||
field=models.TextField(verbose_name="Graine(s) du tirage", blank=True),
|
field=models.TextField(verbose_name='Graine(s) du tirage',
|
||||||
|
blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="category",
|
name='category',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
blank=True,
|
|
||||||
to="bda.CategorieSpectacle",
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
blank=True,
|
||||||
),
|
to='bda.CategorieSpectacle',
|
||||||
|
null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="vips",
|
name='vips',
|
||||||
field=models.TextField(verbose_name="Personnalit\xe9s", blank=True),
|
field=models.TextField(verbose_name='Personnalit\xe9s',
|
||||||
|
blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="quote",
|
model_name='quote',
|
||||||
name="spectacle",
|
name='spectacle',
|
||||||
field=models.ForeignKey(to="bda.Spectacle", on_delete=models.CASCADE),
|
field=models.ForeignKey(
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
to='bda.Spectacle'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,109 +1,103 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0007_extends_spectacle")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0007_extends_spectacle'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="choixspectacle",
|
model_name='choixspectacle',
|
||||||
name="double_choice",
|
name='double_choice',
|
||||||
field=models.CharField(
|
field=models.CharField(
|
||||||
verbose_name="Nombre de places",
|
verbose_name='Nombre de places',
|
||||||
choices=[
|
choices=[('1', '1 place'),
|
||||||
("1", "1 place"),
|
('autoquit', '2 places si possible, 1 sinon'),
|
||||||
("autoquit", "2 places si possible, 1 sinon"),
|
('double', '2 places sinon rien')],
|
||||||
("double", "2 places sinon rien"),
|
max_length=10, default='1'),
|
||||||
],
|
|
||||||
max_length=10,
|
|
||||||
default="1",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="participant",
|
model_name='participant',
|
||||||
name="paymenttype",
|
name='paymenttype',
|
||||||
field=models.CharField(
|
field=models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
choices=[
|
choices=[('cash', 'Cash'), ('cb', 'CB'),
|
||||||
("cash", "Cash"),
|
('cheque', 'Chèque'), ('autre', 'Autre')],
|
||||||
("cb", "CB"),
|
max_length=6, verbose_name='Moyen de paiement'),
|
||||||
("cheque", "Chèque"),
|
|
||||||
("autre", "Autre"),
|
|
||||||
],
|
|
||||||
max_length=6,
|
|
||||||
verbose_name="Moyen de paiement",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="salle",
|
model_name='salle',
|
||||||
name="address",
|
name='address',
|
||||||
field=models.TextField(verbose_name="Adresse"),
|
field=models.TextField(verbose_name='Adresse'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="salle",
|
model_name='salle',
|
||||||
name="name",
|
name='name',
|
||||||
field=models.CharField(verbose_name="Nom", max_length=300),
|
field=models.CharField(verbose_name='Nom', max_length=300),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="date",
|
name='date',
|
||||||
field=models.DateTimeField(verbose_name="Date & heure"),
|
field=models.DateTimeField(verbose_name='Date & heure'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="description",
|
name='description',
|
||||||
field=models.TextField(verbose_name="Description", blank=True),
|
field=models.TextField(verbose_name='Description', blank=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="listing",
|
name='listing',
|
||||||
field=models.BooleanField(verbose_name="Les places sont sur listing"),
|
field=models.BooleanField(
|
||||||
|
verbose_name='Les places sont sur listing'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="price",
|
name='price',
|
||||||
field=models.FloatField(verbose_name="Prix d'une place"),
|
field=models.FloatField(verbose_name="Prix d'une place"),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="slots",
|
name='slots',
|
||||||
field=models.IntegerField(verbose_name="Places"),
|
field=models.IntegerField(verbose_name='Places'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="slots_description",
|
name='slots_description',
|
||||||
field=models.TextField(verbose_name="Description des places", blank=True),
|
field=models.TextField(verbose_name='Description des places',
|
||||||
|
blank=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="spectacle",
|
model_name='spectacle',
|
||||||
name="title",
|
name='title',
|
||||||
field=models.CharField(verbose_name="Titre", max_length=300),
|
field=models.CharField(verbose_name='Titre', max_length=300),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="active",
|
name='active',
|
||||||
field=models.BooleanField(verbose_name="Tirage actif", default=False),
|
field=models.BooleanField(verbose_name='Tirage actif',
|
||||||
|
default=False),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="fermeture",
|
name='fermeture',
|
||||||
field=models.DateTimeField(
|
field=models.DateTimeField(
|
||||||
verbose_name="Date et heure de fermerture du tirage"
|
verbose_name='Date et heure de fermerture du tirage'),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="ouverture",
|
name='ouverture',
|
||||||
field=models.DateTimeField(
|
field=models.DateTimeField(
|
||||||
verbose_name="Date et heure d'ouverture du tirage"
|
verbose_name="Date et heure d'ouverture du tirage"),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="tirage",
|
model_name='tirage',
|
||||||
name="title",
|
name='title',
|
||||||
field=models.CharField(verbose_name="Titre", max_length=300),
|
field=models.CharField(verbose_name='Titre', max_length=300),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,86 +1,73 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0008_py3")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0008_py3'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="SpectacleRevente",
|
name='SpectacleRevente',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(serialize=False, primary_key=True,
|
||||||
"id",
|
auto_created=True, verbose_name='ID')),
|
||||||
models.AutoField(
|
('date', models.DateTimeField(
|
||||||
serialize=False,
|
verbose_name='Date de mise en vente',
|
||||||
primary_key=True,
|
default=django.utils.timezone.now)),
|
||||||
auto_created=True,
|
('notif_sent', models.BooleanField(
|
||||||
verbose_name="ID",
|
verbose_name='Notification envoyée', default=False)),
|
||||||
),
|
('tirage_done', models.BooleanField(
|
||||||
),
|
verbose_name='Tirage effectué', default=False)),
|
||||||
(
|
|
||||||
"date",
|
|
||||||
models.DateTimeField(
|
|
||||||
verbose_name="Date de mise en vente",
|
|
||||||
default=django.utils.timezone.now,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"notif_sent",
|
|
||||||
models.BooleanField(
|
|
||||||
verbose_name="Notification envoyée", default=False
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tirage_done",
|
|
||||||
models.BooleanField(verbose_name="Tirage effectué", default=False),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={"verbose_name": "Revente"},
|
options={
|
||||||
|
'verbose_name': 'Revente',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="participant",
|
model_name='participant',
|
||||||
name="choicesrevente",
|
name='choicesrevente',
|
||||||
field=models.ManyToManyField(
|
field=models.ManyToManyField(to='bda.Spectacle',
|
||||||
to="bda.Spectacle", related_name="subscribed", blank=True
|
related_name='subscribed',
|
||||||
),
|
blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectaclerevente",
|
model_name='spectaclerevente',
|
||||||
name="answered_mail",
|
name='answered_mail',
|
||||||
field=models.ManyToManyField(
|
field=models.ManyToManyField(to='bda.Participant',
|
||||||
to="bda.Participant", related_name="wanted", blank=True
|
related_name='wanted',
|
||||||
),
|
blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectaclerevente",
|
model_name='spectaclerevente',
|
||||||
name="attribution",
|
name='attribution',
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
to="bda.Attribution", on_delete=models.CASCADE, related_name="revente"
|
to='bda.Attribution',
|
||||||
),
|
on_delete=models.CASCADE,
|
||||||
|
related_name='revente'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectaclerevente",
|
model_name='spectaclerevente',
|
||||||
name="seller",
|
name='seller',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
to="bda.Participant",
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name="Vendeur",
|
to='bda.Participant',
|
||||||
related_name="original_shows",
|
verbose_name='Vendeur',
|
||||||
),
|
related_name='original_shows'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectaclerevente",
|
model_name='spectaclerevente',
|
||||||
name="soldTo",
|
name='soldTo',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
to="bda.Participant",
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name="Vendue à",
|
to='bda.Participant',
|
||||||
|
verbose_name='Vendue à',
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from datetime import timedelta
|
from django.db import models, migrations
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
def forwards_func(apps, schema_editor):
|
def forwards_func(apps, schema_editor):
|
||||||
|
@ -12,24 +11,23 @@ def forwards_func(apps, schema_editor):
|
||||||
|
|
||||||
for revente in SpectacleRevente.objects.all():
|
for revente in SpectacleRevente.objects.all():
|
||||||
is_expired = timezone.now() > revente.date_tirage()
|
is_expired = timezone.now() > revente.date_tirage()
|
||||||
is_direct = (
|
is_direct = (revente.attribution.spectacle.date >= revente.date and
|
||||||
revente.attribution.spectacle.date >= revente.date
|
timezone.now() > revente.date + timedelta(minutes=15))
|
||||||
and timezone.now() > revente.date + timedelta(minutes=15)
|
|
||||||
)
|
|
||||||
revente.shotgun = is_expired or is_direct
|
revente.shotgun = is_expired or is_direct
|
||||||
revente.save()
|
revente.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [("bda", "0009_revente")]
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0009_revente'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="spectaclerevente",
|
model_name='spectaclerevente',
|
||||||
name="shotgun",
|
name='shotgun',
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(default=False, verbose_name='Disponible imm\xe9diatement'),
|
||||||
default=False, verbose_name="Disponible imm\xe9diatement"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
# -*- 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"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
|
@ -1,30 +0,0 @@
|
||||||
# -*- 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
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,50 +0,0 @@
|
||||||
# -*- 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"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,11 +0,0 @@
|
||||||
# -*- 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 = []
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Generated by Django 2.2 on 2019-06-03 19:13
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("bda", "0013_merge_20180524_2123")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="attribution",
|
|
||||||
name="paid",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="Payée"),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="attribution",
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,36 +0,0 @@
|
||||||
# Generated by Django 2.2 on 2019-06-03 19:30
|
|
||||||
|
|
||||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def set_attr_payment(apps, schema_editor):
|
|
||||||
Attribution = apps.get_model("bda", "Attribution")
|
|
||||||
for attr in Attribution.objects.all():
|
|
||||||
attr.paid = attr.participant.paid
|
|
||||||
attr.paymenttype = attr.participant.paymenttype
|
|
||||||
attr.save()
|
|
||||||
|
|
||||||
|
|
||||||
def set_participant_payment(apps, schema_editor):
|
|
||||||
Participant = apps.get_model("bda", "Participant")
|
|
||||||
for part in Participant.objects.all():
|
|
||||||
attr_set = part.attribution_set
|
|
||||||
part.paid = attr_set.exists() and not attr_set.filter(paid=False).exists()
|
|
||||||
try:
|
|
||||||
# S'il n'y a qu'un seul type de paiement, on le set
|
|
||||||
part.paymenttype = (
|
|
||||||
attr_set.values_list("paymenttype", flat=True).distinct().get()
|
|
||||||
)
|
|
||||||
# Sinon, whatever
|
|
||||||
except (ObjectDoesNotExist, MultipleObjectsReturned):
|
|
||||||
pass
|
|
||||||
part.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("bda", "0014_attribution_paid_field")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(set_attr_payment, set_participant_payment, atomic=True)
|
|
||||||
]
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Generated by Django 2.2 on 2019-06-03 19:30
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("bda", "0015_move_bda_payment")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(model_name="participant", name="paid"),
|
|
||||||
migrations.RemoveField(model_name="participant", name="paymenttype"),
|
|
||||||
]
|
|
|
@ -1,17 +0,0 @@
|
||||||
# Generated by Django 2.2 on 2019-09-18 16:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("bda", "0016_delete_participant_paid")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="participant",
|
|
||||||
name="accepte_charte",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=False, verbose_name="A accepté la charte BdA"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Generated by Django 2.2.12 on 2020-10-21 16:18
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("bda", "0017_participant_accepte_charte"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="participant",
|
|
||||||
options={"ordering": ("-tirage", "user__last_name", "user__first_name")},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="tirage",
|
|
||||||
name="archived",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="Archivé"),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="participant",
|
|
||||||
name="tirage",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
limit_choices_to={"archived": False},
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="bda.Tirage",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="participant",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("tirage", "user"), name="unique_tirage"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 3.2.13 on 2022-06-30 10:45
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bda", "0018_auto_20201021_1818"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name="choixspectacle",
|
|
||||||
unique_together=set(),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="choixspectacle",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("participant", "spectacle"), name="unique_participation"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
461
bda/models.py
461
bda/models.py
|
@ -1,24 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
import random
|
import random
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
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.contrib.sites.models import Site
|
||||||
from django.core import mail
|
|
||||||
from django.core.mail import EmailMessage, send_mass_mail
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Exists
|
from django.contrib.auth.models import User
|
||||||
from django.template import loader
|
from django.conf import settings
|
||||||
from django.utils import formats, timezone
|
from django.utils import timezone, formats
|
||||||
|
|
||||||
|
|
||||||
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):
|
class Tirage(models.Model):
|
||||||
|
@ -27,17 +18,12 @@ class Tirage(models.Model):
|
||||||
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
||||||
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
||||||
active = models.BooleanField("Tirage actif", default=False)
|
active = models.BooleanField("Tirage actif", default=False)
|
||||||
appear_catalogue = models.BooleanField(
|
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
|
||||||
"Tirage à afficher dans le catalogue", default=False
|
default=False)
|
||||||
)
|
|
||||||
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
|
|
||||||
archived = models.BooleanField("Archivé", default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s" % (
|
return "%s - %s" % (self.title, formats.localize(
|
||||||
self.title,
|
timezone.template_localtime(self.fermeture)))
|
||||||
formats.localize(timezone.template_localtime(self.fermeture)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Salle(models.Model):
|
class Salle(models.Model):
|
||||||
|
@ -49,7 +35,7 @@ class Salle(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class CategorieSpectacle(models.Model):
|
class CategorieSpectacle(models.Model):
|
||||||
name = models.CharField("Nom", max_length=100, unique=True)
|
name = models.CharField('Nom', max_length=100, unique=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -61,26 +47,30 @@ class CategorieSpectacle(models.Model):
|
||||||
class Spectacle(models.Model):
|
class Spectacle(models.Model):
|
||||||
title = models.CharField("Titre", max_length=300)
|
title = models.CharField("Titre", max_length=300)
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True
|
CategorieSpectacle,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
)
|
)
|
||||||
date = models.DateTimeField("Date & heure")
|
date = models.DateTimeField("Date & heure")
|
||||||
location = models.ForeignKey(Salle, on_delete=models.CASCADE)
|
location = models.ForeignKey(Salle, on_delete=models.CASCADE)
|
||||||
vips = models.TextField("Personnalités", blank=True)
|
vips = models.TextField('Personnalités', blank=True)
|
||||||
description = models.TextField("Description", blank=True)
|
description = models.TextField("Description", blank=True)
|
||||||
slots_description = models.TextField("Description des places", blank=True)
|
slots_description = models.TextField("Description des places", blank=True)
|
||||||
image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/")
|
image = models.ImageField('Image', blank=True, null=True,
|
||||||
ext_link = models.CharField(
|
upload_to='imgs/shows/')
|
||||||
"Lien vers le site du spectacle", blank=True, max_length=500
|
ext_link = models.CharField('Lien vers le site du spectacle', blank=True,
|
||||||
)
|
max_length=500)
|
||||||
price = models.FloatField("Prix d'une place")
|
price = models.FloatField("Prix d'une place")
|
||||||
slots = models.IntegerField("Places")
|
slots = models.IntegerField("Places")
|
||||||
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
|
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
|
||||||
listing = models.BooleanField("Les places sont sur listing")
|
listing = models.BooleanField("Les places sont sur listing")
|
||||||
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True)
|
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Spectacle"
|
verbose_name = "Spectacle"
|
||||||
ordering = ("date", "title")
|
ordering = ("date", "title",)
|
||||||
|
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
return "%d" % calendar.timegm(self.date.utctimetuple())
|
return "%d" % calendar.timegm(self.date.utctimetuple())
|
||||||
|
@ -90,51 +80,40 @@ class Spectacle(models.Model):
|
||||||
self.title,
|
self.title,
|
||||||
formats.localize(timezone.template_localtime(self.date)),
|
formats.localize(timezone.template_localtime(self.date)),
|
||||||
self.location,
|
self.location,
|
||||||
self.price,
|
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):
|
def send_rappel(self):
|
||||||
"""
|
"""
|
||||||
Envoie un mail de rappel à toutes les personnes qui ont une place pour
|
Envoie un mail de rappel à toutes les personnes qui ont une place pour
|
||||||
ce spectacle.
|
ce spectacle.
|
||||||
"""
|
"""
|
||||||
# On récupère la liste des participants + le BdA
|
# On récupère la liste des participants
|
||||||
members = list(
|
members = {}
|
||||||
User.objects.filter(participant__attributions=self)
|
for attr in Attribution.objects.filter(spectacle=self).all():
|
||||||
.annotate(nb_attr=Count("id"))
|
member = attr.participant.user
|
||||||
.order_by()
|
if member.id in members:
|
||||||
)
|
members[member.id][1] = 2
|
||||||
bda_generic = get_generic_user()
|
else:
|
||||||
bda_generic.nb_attr = 1
|
members[member.id] = [member, 1]
|
||||||
members.append(bda_generic)
|
# FIXME : faire quelque chose de ça, un utilisateur bda_generic ?
|
||||||
|
# # Pour le BdA
|
||||||
|
# members[0] = ['BdA', 1, 'bda@ens.fr']
|
||||||
|
# members[-1] = ['BdA', 2, 'bda@ens.fr']
|
||||||
# On écrit un mail personnalisé à chaque participant
|
# On écrit un mail personnalisé à chaque participant
|
||||||
mails = [
|
datatuple = [(
|
||||||
(
|
'bda-rappel',
|
||||||
str(self),
|
{'member': member[0], 'nb_attr': member[1], 'show': self},
|
||||||
loader.render_to_string(
|
settings.MAIL_DATA['rappels']['FROM'],
|
||||||
"bda/mails/rappel.txt",
|
[member[0].email])
|
||||||
context={"member": member, "nb_attr": member.nb_attr, "show": self},
|
for member in members.values()
|
||||||
),
|
|
||||||
settings.MAIL_DATA["rappels"]["FROM"],
|
|
||||||
[member.email],
|
|
||||||
)
|
|
||||||
for member in members
|
|
||||||
]
|
]
|
||||||
send_mass_mail(mails)
|
send_mass_custom_mail(datatuple)
|
||||||
# On enregistre le fait que l'envoi a bien eu lieu
|
# On enregistre le fait que l'envoi a bien eu lieu
|
||||||
self.rappel_sent = timezone.now()
|
self.rappel_sent = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
# On renvoie la liste des destinataires
|
# On renvoie la liste des destinataires
|
||||||
return members
|
return members.values()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_past(self):
|
def is_past(self):
|
||||||
|
@ -143,8 +122,8 @@ class Spectacle(models.Model):
|
||||||
|
|
||||||
class Quote(models.Model):
|
class Quote(models.Model):
|
||||||
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
|
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
|
||||||
text = models.TextField("Citation")
|
text = models.TextField('Citation')
|
||||||
author = models.CharField("Auteur", max_length=200)
|
author = models.CharField('Auteur', max_length=200)
|
||||||
|
|
||||||
|
|
||||||
PAYMENT_TYPES = (
|
PAYMENT_TYPES = (
|
||||||
|
@ -155,235 +134,154 @@ PAYMENT_TYPES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
paid = models.BooleanField("Payée", default=False)
|
|
||||||
paymenttype = models.CharField(
|
|
||||||
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s -- %s, %s" % (
|
|
||||||
self.participant.user,
|
|
||||||
self.spectacle.title,
|
|
||||||
self.spectacle.date,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantPaidQueryset(models.QuerySet):
|
|
||||||
"""
|
|
||||||
Un manager qui annote le queryset avec un champ `paid`,
|
|
||||||
indiquant si un participant a payé toutes ses attributions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def annotate_paid(self):
|
|
||||||
# OuterRef permet de se référer à un champ d'un modèle non encore fixé
|
|
||||||
# Voir:
|
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/models/expressions/#django.db.models.OuterRef
|
|
||||||
unpaid = Attribution.objects.filter(
|
|
||||||
participant=models.OuterRef("pk"), paid=False
|
|
||||||
)
|
|
||||||
return self.annotate(paid=~Exists(unpaid))
|
|
||||||
|
|
||||||
|
|
||||||
class Participant(models.Model):
|
class Participant(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
choices = models.ManyToManyField(
|
choices = models.ManyToManyField(Spectacle,
|
||||||
Spectacle, through="ChoixSpectacle", related_name="chosen_by"
|
through="ChoixSpectacle",
|
||||||
)
|
related_name="chosen_by")
|
||||||
attributions = models.ManyToManyField(
|
attributions = models.ManyToManyField(Spectacle,
|
||||||
Spectacle, through="Attribution", related_name="attributed_to"
|
through="Attribution",
|
||||||
)
|
related_name="attributed_to")
|
||||||
tirage = models.ForeignKey(
|
paid = models.BooleanField("A payé", default=False)
|
||||||
Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False}
|
paymenttype = models.CharField("Moyen de paiement",
|
||||||
)
|
max_length=6, choices=PAYMENT_TYPES,
|
||||||
accepte_charte = models.BooleanField("A accepté la charte BdA", default=False)
|
blank=True)
|
||||||
choicesrevente = models.ManyToManyField(
|
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
|
||||||
Spectacle, related_name="subscribed", blank=True
|
choicesrevente = models.ManyToManyField(Spectacle,
|
||||||
)
|
related_name="subscribed",
|
||||||
|
blank=True)
|
||||||
objects = ParticipantPaidQueryset.as_manager()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s" % (self.user, self.tirage.title)
|
return "%s - %s" % (self.user, self.tirage.title)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("-tirage", "user__last_name", "user__first_name")
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(fields=("tirage", "user"), name="unique_tirage"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
DOUBLE_CHOICES = (
|
DOUBLE_CHOICES = (
|
||||||
("1", "1 place"),
|
("1", "1 place"),
|
||||||
("double", "2 places si possible, 1 sinon"),
|
("autoquit", "2 places si possible, 1 sinon"),
|
||||||
("autoquit", "2 places sinon rien"),
|
("double", "2 places sinon rien"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacle(models.Model):
|
class ChoixSpectacle(models.Model):
|
||||||
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
|
participant = models.ForeignKey(
|
||||||
|
Participant,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
spectacle = models.ForeignKey(
|
spectacle = models.ForeignKey(
|
||||||
Spectacle, on_delete=models.CASCADE, related_name="participants"
|
Spectacle,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="participants"
|
||||||
)
|
)
|
||||||
priority = models.PositiveIntegerField("Priorité")
|
priority = models.PositiveIntegerField("Priorité")
|
||||||
double_choice = models.CharField(
|
double_choice = models.CharField("Nombre de places",
|
||||||
"Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10
|
default="1", choices=DOUBLE_CHOICES,
|
||||||
)
|
max_length=10)
|
||||||
|
|
||||||
def get_double(self):
|
def get_double(self):
|
||||||
return self.double_choice != "1"
|
return self.double_choice != "1"
|
||||||
|
|
||||||
double = property(get_double)
|
double = property(get_double)
|
||||||
|
|
||||||
def get_autoquit(self):
|
def get_autoquit(self):
|
||||||
return self.double_choice == "autoquit"
|
return self.double_choice == "autoquit"
|
||||||
|
|
||||||
autoquit = property(get_autoquit)
|
autoquit = property(get_autoquit)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Vœux de %s pour %s" % (
|
return "Vœux de %s pour %s" % (
|
||||||
self.participant.user.get_full_name(),
|
self.participant.user.get_full_name(),
|
||||||
self.spectacle.title,
|
self.spectacle.title)
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("priority",)
|
ordering = ("priority",)
|
||||||
constraints = [
|
unique_together = (("participant", "spectacle",),)
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["participant", "spectacle"], name="unique_participation"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
verbose_name = "voeu"
|
verbose_name = "voeu"
|
||||||
verbose_name_plural = "voeux"
|
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):
|
class SpectacleRevente(models.Model):
|
||||||
attribution = models.OneToOneField(
|
attribution = models.OneToOneField(
|
||||||
Attribution, on_delete=models.CASCADE, related_name="revente"
|
Attribution,
|
||||||
)
|
on_delete=models.CASCADE,
|
||||||
date = models.DateTimeField("Date de mise en vente", default=timezone.now)
|
related_name="revente"
|
||||||
confirmed_entry = models.ManyToManyField(
|
|
||||||
Participant, related_name="entered", blank=True
|
|
||||||
)
|
)
|
||||||
|
date = models.DateTimeField("Date de mise en vente",
|
||||||
|
default=timezone.now)
|
||||||
|
answered_mail = models.ManyToManyField(Participant,
|
||||||
|
related_name="wanted",
|
||||||
|
blank=True)
|
||||||
seller = models.ForeignKey(
|
seller = models.ForeignKey(
|
||||||
Participant,
|
Participant,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name="Vendeur",
|
|
||||||
related_name="original_shows",
|
related_name="original_shows",
|
||||||
|
verbose_name="Vendeur"
|
||||||
)
|
)
|
||||||
soldTo = models.ForeignKey(
|
soldTo = models.ForeignKey(
|
||||||
Participant,
|
Participant,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name="Vendue à",
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
verbose_name="Vendue à"
|
||||||
)
|
)
|
||||||
|
notif_sent = models.BooleanField("Notification envoyée",
|
||||||
notif_sent = models.BooleanField("Notification envoyée", default=False)
|
default=False)
|
||||||
|
tirage_done = models.BooleanField("Tirage effectué",
|
||||||
notif_time = models.DateTimeField(
|
default=False)
|
||||||
"Moment d'envoi de la notification", blank=True, null=True
|
shotgun = models.BooleanField("Disponible immédiatement",
|
||||||
)
|
default=False)
|
||||||
|
|
||||||
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
|
@property
|
||||||
def date_tirage(self):
|
def date_tirage(self):
|
||||||
"""Renvoie la date du tirage au sort de la revente."""
|
"""Renvoie la date du tirage au sort de la revente."""
|
||||||
|
# L'acheteur doit être connu au plus 12h avant le spectacle
|
||||||
remaining_time = (
|
remaining_time = (self.attribution.spectacle.date
|
||||||
self.attribution.spectacle.date - self.real_notif_time - self.min_margin
|
- self.date - timedelta(hours=13))
|
||||||
)
|
# Au minimum, on attend 2 jours avant le tirage
|
||||||
|
delay = min(remaining_time, timedelta(days=2))
|
||||||
delay = min(remaining_time, self.max_wait_time)
|
# Le vendeur a aussi 1h pour changer d'avis
|
||||||
|
return self.date + delay + timedelta(hours=1)
|
||||||
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):
|
def __str__(self):
|
||||||
return "%s -- %s" % (self.seller, self.attribution.spectacle.title)
|
return "%s -- %s" % (self.seller,
|
||||||
|
self.attribution.spectacle.title)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Revente"
|
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):
|
def send_notif(self):
|
||||||
"""
|
"""
|
||||||
Envoie une notification pour indiquer la mise en vente d'une place sur
|
Envoie une notification pour indiquer la mise en vente d'une place sur
|
||||||
BdA-Revente à tous les intéressés.
|
BdA-Revente à tous les intéressés.
|
||||||
"""
|
"""
|
||||||
inscrits = self.attribution.spectacle.subscribed.select_related("user")
|
inscrits = self.attribution.spectacle.subscribed.select_related('user')
|
||||||
mails = [
|
datatuple = [(
|
||||||
(
|
'bda-revente',
|
||||||
"BdA-Revente : {}".format(self.attribution.spectacle.title),
|
{
|
||||||
loader.render_to_string(
|
'member': participant.user,
|
||||||
"bda/mails/revente-new.txt",
|
'show': self.attribution.spectacle,
|
||||||
context={
|
'revente': self,
|
||||||
"member": participant.user,
|
'site': Site.objects.get_current()
|
||||||
"show": self.attribution.spectacle,
|
|
||||||
"revente": self,
|
|
||||||
"site": Site.objects.get_current(),
|
|
||||||
},
|
},
|
||||||
),
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
settings.MAIL_DATA["revente"]["FROM"],
|
[participant.user.email])
|
||||||
[participant.user.email],
|
|
||||||
)
|
|
||||||
for participant in inscrits
|
for participant in inscrits
|
||||||
]
|
]
|
||||||
send_mass_mail(mails)
|
send_mass_custom_mail(datatuple)
|
||||||
self.notif_sent = True
|
self.notif_sent = True
|
||||||
self.notif_time = timezone.now()
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def mail_shotgun(self):
|
def mail_shotgun(self):
|
||||||
|
@ -391,104 +289,71 @@ class SpectacleRevente(models.Model):
|
||||||
Envoie un mail à toutes les personnes intéréssées par le spectacle pour
|
Envoie un mail à toutes les personnes intéréssées par le spectacle pour
|
||||||
leur indiquer qu'il est désormais disponible au shotgun.
|
leur indiquer qu'il est désormais disponible au shotgun.
|
||||||
"""
|
"""
|
||||||
inscrits = self.attribution.spectacle.subscribed.select_related("user")
|
inscrits = self.attribution.spectacle.subscribed.select_related('user')
|
||||||
mails = [
|
datatuple = [(
|
||||||
(
|
'bda-shotgun',
|
||||||
"BdA-Revente : {}".format(self.attribution.spectacle.title),
|
{
|
||||||
loader.render_to_string(
|
'member': participant.user,
|
||||||
"bda/mails/revente-shotgun.txt",
|
'show': self.attribution.spectacle,
|
||||||
context={
|
'site': Site.objects.get_current(),
|
||||||
"member": participant.user,
|
|
||||||
"show": self.attribution.spectacle,
|
|
||||||
"site": Site.objects.get_current(),
|
|
||||||
},
|
},
|
||||||
),
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
settings.MAIL_DATA["revente"]["FROM"],
|
[participant.user.email])
|
||||||
[participant.user.email],
|
|
||||||
)
|
|
||||||
for participant in inscrits
|
for participant in inscrits
|
||||||
]
|
]
|
||||||
send_mass_mail(mails)
|
send_mass_custom_mail(datatuple)
|
||||||
self.notif_sent = True
|
self.notif_sent = True
|
||||||
self.notif_time = timezone.now()
|
|
||||||
# Flag inutile, sauf si l'horloge interne merde
|
# Flag inutile, sauf si l'horloge interne merde
|
||||||
self.tirage_done = True
|
self.tirage_done = True
|
||||||
self.shotgun = True
|
self.shotgun = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def tirage(self, send_mails=True):
|
def tirage(self):
|
||||||
"""
|
"""
|
||||||
Lance le tirage au sort associé à la revente. Un gagnant est choisi
|
Lance le tirage au sort associé à la revente. Un gagnant est choisi
|
||||||
parmis les personnes intéressées par le spectacle. Les personnes sont
|
parmis les personnes intéressées par le spectacle. Les personnes sont
|
||||||
ensuites prévenues par mail du résultat du tirage.
|
ensuites prévenues par mail du résultat du tirage.
|
||||||
"""
|
"""
|
||||||
inscrits = list(self.confirmed_entry.all())
|
inscrits = list(self.answered_mail.all())
|
||||||
spectacle = self.attribution.spectacle
|
spectacle = self.attribution.spectacle
|
||||||
seller = self.seller
|
seller = self.seller
|
||||||
winner = None
|
|
||||||
|
|
||||||
if inscrits:
|
if inscrits:
|
||||||
# Envoie un mail au gagnant et au vendeur
|
# Envoie un mail au gagnant et au vendeur
|
||||||
winner = random.choice(inscrits)
|
winner = random.choice(inscrits)
|
||||||
self.soldTo = winner
|
self.soldTo = winner
|
||||||
if send_mails:
|
datatuple = []
|
||||||
mails = []
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"acheteur": winner.user,
|
'acheteur': winner.user,
|
||||||
"vendeur": seller.user,
|
'vendeur': seller.user,
|
||||||
"show": spectacle,
|
'show': spectacle,
|
||||||
}
|
}
|
||||||
|
datatuple.append((
|
||||||
subject = "BdA-Revente : {}".format(spectacle.title)
|
'bda-revente-winner',
|
||||||
|
context,
|
||||||
mails.append(
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
EmailMessage(
|
[winner.user.email],
|
||||||
subject=subject,
|
))
|
||||||
body=loader.render_to_string(
|
datatuple.append((
|
||||||
"bda/mails/revente-tirage-winner.txt",
|
'bda-revente-seller',
|
||||||
context=context,
|
context,
|
||||||
),
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
[seller.user.email]
|
||||||
to=[winner.user.email],
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
mails.append(
|
|
||||||
EmailMessage(
|
|
||||||
subject=subject,
|
|
||||||
body=loader.render_to_string(
|
|
||||||
"bda/mails/revente-tirage-seller.txt",
|
|
||||||
context=context,
|
|
||||||
),
|
|
||||||
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
|
||||||
to=[seller.user.email],
|
|
||||||
reply_to=[winner.user.email],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Envoie un mail aux perdants
|
# Envoie un mail aux perdants
|
||||||
for inscrit in inscrits:
|
for inscrit in inscrits:
|
||||||
if inscrit != winner:
|
if inscrit != winner:
|
||||||
new_context = dict(context)
|
context['acheteur'] = inscrit.user
|
||||||
new_context["acheteur"] = inscrit.user
|
datatuple.append((
|
||||||
|
'bda-revente-loser',
|
||||||
mails.append(
|
context,
|
||||||
EmailMessage(
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
subject=subject,
|
[inscrit.user.email]
|
||||||
body=loader.render_to_string(
|
))
|
||||||
"bda/mails/revente-tirage-loser.txt",
|
send_mass_custom_mail(datatuple)
|
||||||
context=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
|
# Si personne ne veut de la place, elle part au shotgun
|
||||||
else:
|
else:
|
||||||
self.shotgun = True
|
self.shotgun = True
|
||||||
self.tirage_done = True
|
self.tirage_done = True
|
||||||
self.save()
|
self.save()
|
||||||
return winner
|
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** JQuery-Confirm box **/
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-bg {
|
|
||||||
background-color: rgb(0,0,0,0.6) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box {
|
|
||||||
padding:0;
|
|
||||||
border-radius:0 !important;
|
|
||||||
font-family:Roboto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .content-pane {
|
|
||||||
border-bottom:1px solid #ddd;
|
|
||||||
margin: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .content {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .content-pane {
|
|
||||||
border-bottom:1px solid #ddd;
|
|
||||||
margin: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .content {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .content a,
|
|
||||||
.jconfirm .jconfirm-box .content a:hover {
|
|
||||||
color: #D81138;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .buttons {
|
|
||||||
margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */
|
|
||||||
padding:0;
|
|
||||||
height:40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .buttons button {
|
|
||||||
min-width:40px;
|
|
||||||
height:100%;
|
|
||||||
margin:0;
|
|
||||||
margin:0 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .buttons button:first-child:focus,
|
|
||||||
.jconfirm .jconfirm-box .buttons button:first-child:hover {
|
|
||||||
color:#FFF !important;
|
|
||||||
background:forestgreen !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .buttons button:nth-child(2):focus,
|
|
||||||
.jconfirm .jconfirm-box .buttons button:nth-child(2):hover {
|
|
||||||
color:#FFF !important;
|
|
||||||
background:#D93A32 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jconfirm .jconfirm-box div.title-c .title {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
padding:0 15px;
|
|
||||||
height:40px;
|
|
||||||
line-height:40px;
|
|
||||||
|
|
||||||
font-family:Dosis;
|
|
||||||
font-size:20px;
|
|
||||||
font-weight:bold;
|
|
||||||
|
|
||||||
color:#FFF;
|
|
||||||
background-color:rgb(222, 130, 107);
|
|
||||||
}
|
|
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.
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Inscription à une revente</h2>
|
<h2>Inscription à une revente</h2>
|
6
bda/templates/bda-notpaid.html
Normal file
6
bda/templates/bda-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 %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>{{ spectacle }}</h2>
|
<h2>{{ spectacle }}</h2>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for participant in participants %}
|
{% for participant in participants %}
|
||||||
<tr>
|
<tr>
|
||||||
<td data-sort-value="{{ participant.name}}">{{participant.name}}</td>
|
<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.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.email}}">{{participant.email}}</td>
|
||||||
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
|
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
|
||||||
|
@ -36,27 +36,20 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
|
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
|
||||||
<div>
|
<br>
|
||||||
<div>
|
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
|
||||||
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participant⋅e⋅s</button>
|
<pre id="export-mails" style="display:none">
|
||||||
<pre id="export-mails" style="display:none">{% spaceless %}
|
{%for participant in participants %}{{participant.email}}, {%endfor%}
|
||||||
{% for participant in participants %}{{ participant.email }}, {% endfor %}
|
</pre>
|
||||||
{% endspaceless %}</pre>
|
<br>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
|
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
|
||||||
<pre id="export-salle" style="display:none">{% spaceless %}
|
<pre id="export-salle" style="display:none">
|
||||||
{% for participant in participants %}{{ participant.name }} : {{ participant.nb_places }} place{{ participant.nb_places|pluralize }}
|
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endspaceless %}</pre>
|
</pre>
|
||||||
</div>
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
|
||||||
<div>
|
<script>
|
||||||
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
function toggle(id) {
|
function toggle(id) {
|
||||||
var pre = document.getElementById(id) ;
|
var pre = document.getElementById(id) ;
|
||||||
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
|
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
|
73
bda/templates/bda-revente.html
Normal file
73
bda/templates/bda-revente.html
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Revente de place</h2>
|
||||||
|
<h3>Places non revendues</h3>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="multiple-checkbox">
|
||||||
|
<ul>
|
||||||
|
{% for box in resellform.attributions %}
|
||||||
|
<li>
|
||||||
|
{{box.tag}}
|
||||||
|
{{box.choice_label}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
{% if annulform.attributions or overdue %}
|
||||||
|
<h3>Places en cours de revente</h3>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="multiple-checkbox">
|
||||||
|
<ul>
|
||||||
|
{% for box in annulform.attributions %}
|
||||||
|
<li>
|
||||||
|
{{box.tag}}
|
||||||
|
{{box.choice_label}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for attrib in overdue %}
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" style="visibility:hidden">
|
||||||
|
{{attrib.spectacle}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if annulform.attributions %}
|
||||||
|
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
{% if sold %}
|
||||||
|
<h3>Places revendues</h3>
|
||||||
|
<table class="table">
|
||||||
|
{% for attrib in sold %}
|
||||||
|
<tr>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<td>{{attrib.spectacle}}</td>
|
||||||
|
<td>{{attrib.revente.soldTo.user.get_full_name}}</td>
|
||||||
|
<td><button type="submit" class="btn btn-primary" name="transfer"
|
||||||
|
value="{{attrib.revente.id}}">Transférer</button></td>
|
||||||
|
<td><button type="submit" class="btn btn-primary" name="reinit"
|
||||||
|
value="{{attrib.revente.id}}">Réinitialiser</button></td>
|
||||||
|
</form>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
14
bda/templates/bda-shotgun.html
Normal file
14
bda/templates/bda-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-buy-revente" spectacle.id %}">{{spectacle}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p> Pas de places disponibles immédiatement, désolé !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
|
<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
|
<p>Si personne n'était intéressé, elle est maintenant disponible
|
||||||
<a href="{% url "bda-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
|
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
|
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>État des inscriptions BdA</h2>
|
<h2>État des inscriptions BdA</h2>
|
||||||
|
@ -42,6 +42,9 @@
|
||||||
Total : {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
|
Total : {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
|
||||||
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
|
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
|
||||||
</span>
|
</span>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
||||||
|
</script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function(){
|
$(function(){
|
||||||
$("table.etat-bda").stupidtable();
|
$("table.etat-bda").stupidtable();
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{% include 'bda/forms/spectacle_label_table.html' with spectacle=attribution.spectacle %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
<tr>
|
|
||||||
<td><input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}></td>
|
|
||||||
{{ widget.label }}
|
|
||||||
</tr>
|
|
|
@ -1 +0,0 @@
|
||||||
<td data-sort-value="{{ revente.date_tirage | date:"U" }}">{{ revente.date_tirage }}</td>
|
|
|
@ -1,3 +0,0 @@
|
||||||
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
|
|
||||||
{% with user=revente.seller.user %} <td>{{user.first_name}} {{user.last_name}}</td> {% endwith%}
|
|
||||||
{% include 'bda/forms/date_tirage.html' %}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
|
|
||||||
{% include 'bda/forms/date_tirage.html' %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
|
|
||||||
{% with user=revente.soldTo.user %}
|
|
||||||
<td><a href="mailto:{{ user.email }}">{{user.first_name}} {{user.last_name}}</a></td>
|
|
||||||
{% endwith %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
<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.price |stringformat:".3f" }}">{{ spectacle.price |floatformat }}€</td>
|
|
|
@ -14,7 +14,7 @@
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody class="bda_formset_content">
|
<tbody class="bda_formset_content">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||||
{% for field in form.visible_fields %}
|
{% for field in form.visible_fields %}
|
||||||
{% if field.name != "DELETE" and field.name != "priority" %}
|
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||||
<td class="bda-field-{{ field.name }}">
|
<td class="bda-field-{{ field.name }}">
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script>
|
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
|
||||||
<script type="text/javascript" src="{% static "vendor/jquery/jquery-confirm.js" %}"></script>
|
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
|
||||||
<script type="text/javascript" src="{% static 'gestioncof/vendor/jquery.ui.touch-punch.min.js' %}" ></script>
|
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
|
||||||
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-confirm.css' %}">
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-ui.min.css' %}" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="{% static 'bda/css/bda.css' %}" />
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
@ -29,14 +27,6 @@ var django = {
|
||||||
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
|
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
|
||||||
$(this).attr('for', newFor);
|
$(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++;
|
total++;
|
||||||
$('#id_' + type + '-TOTAL_FORMS').val(total);
|
$('#id_' + type + '-TOTAL_FORMS').val(total);
|
||||||
$(selector).after(newElement);
|
$(selector).after(newElement);
|
||||||
|
@ -54,11 +44,6 @@ var django = {
|
||||||
} else {
|
} else {
|
||||||
deleteInput.attr("checked", true);
|
deleteInput.attr("checked", true);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Reset the default values
|
|
||||||
var selects = $(form).find("select");
|
|
||||||
$(selects[0]).val("");
|
|
||||||
$(selects[1]).val("1");
|
|
||||||
}
|
}
|
||||||
// callback
|
// callback
|
||||||
});
|
});
|
||||||
|
@ -120,50 +105,11 @@ var django = {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
|
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
|
||||||
<input type="submit" class="btn btn-primary" id="bda-inscr" value="Enregistrer" />
|
<input type="submit" class="btn btn-primary" value="Enregistrer" />
|
||||||
</div>
|
</div>
|
||||||
<p class="footnotes">
|
<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 />
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if not charte %}
|
|
||||||
<script>
|
|
||||||
(function ($) {
|
|
||||||
var charte_ok = false ;
|
|
||||||
function link_charte() {
|
|
||||||
$.confirm({
|
|
||||||
title: 'Charte du BdA',
|
|
||||||
columnClass: 'col-md-6 col-md-offset-3',
|
|
||||||
content: `
|
|
||||||
<div>
|
|
||||||
En vous inscrivant à ce tirage du Bureau des Arts, vous vous engagez à \
|
|
||||||
respecter la charte du BdA:</br> \
|
|
||||||
<a target="_blank" href='https://bda.ens.fr/lequipe/charte-bda/'>https://bda.ens.fr/lequipe/charte-bda/</a>
|
|
||||||
</div>`,
|
|
||||||
backgroundDismiss: true,
|
|
||||||
opacity: 1,
|
|
||||||
animation:'top',
|
|
||||||
closeAnimation:'bottom',
|
|
||||||
keyboardEnabled: true,
|
|
||||||
confirmButton: '<span class="glyphicon glyphicon-ok"></span>',
|
|
||||||
cancelButton: '<span class="glyphicon glyphicon-remove"></span>',
|
|
||||||
confirm: function() {
|
|
||||||
charte_ok = true ;
|
|
||||||
$("#bda_form").submit();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$(document).ready(function($) {
|
|
||||||
$("#bda_form").submit(function(e) {
|
|
||||||
if (!charte_ok) {
|
|
||||||
e.preventDefault();
|
|
||||||
link_charte();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})(django.jQuery);
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
33
bda/templates/bda/liste-reventes.html
Normal file
33
bda/templates/bda/liste-reventes.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Inscriptions pour BdA-Revente</h2>
|
||||||
|
<form action="" class="form-horizontal" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Spectacles</h3>
|
||||||
|
<br/>
|
||||||
|
<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 %}
|
|
@ -10,10 +10,14 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }} ?</h3>
|
<h3>Voulez vous envoyer les mails de rappel pour le spectacle
|
||||||
|
{{ show.title }} ?</h3>
|
||||||
|
{% if show.rappel_sent %}
|
||||||
|
<p class="error">Attention, les mails ont déjà été envoyés le
|
||||||
|
{{ show.rappel_sent }}</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="empty-form">
|
|
||||||
{% if not sent %}
|
{% if not sent %}
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -22,10 +26,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr \>
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<hr/>
|
||||||
<h3>Forme des mails</h3>
|
<h3>Forme des mails</h3>
|
||||||
|
|
||||||
<h4>Une seule place</h4>
|
<h4>Une seule place</h4>
|
||||||
|
@ -37,5 +40,4 @@
|
||||||
{% for part in exemple_mail_2places %}
|
{% for part in exemple_mail_2places %}
|
||||||
<pre>{{ part }}</pre>
|
<pre>{{ part }}</pre>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
Cher-e {{ member.first_name }},
|
|
||||||
|
|
||||||
Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as
|
|
||||||
obtenu aucune place.
|
|
||||||
|
|
||||||
Nous proposons cependant de nombreuses offres hors-tirage tout au long de
|
|
||||||
l'année, et nous t'invitons à nous contacter si l'une d'entre elles
|
|
||||||
t'intéresse !
|
|
||||||
--
|
|
||||||
Le Bureau des Arts
|
|
|
@ -1,31 +0,0 @@
|
||||||
Cher-e {{ member.first_name }},
|
|
||||||
|
|
||||||
Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e
|
|
||||||
pour les spectacles suivants :
|
|
||||||
{% for place in places %}
|
|
||||||
- 1 place pour {{ place }}{% endfor %}
|
|
||||||
|
|
||||||
*Paiement*
|
|
||||||
L'intégralité de ces places de spectacles est à régler dès maintenant et AVANT
|
|
||||||
vendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi
|
|
||||||
entre 12h et 14h, et entre 18h et 20h). Des facilités de paiement sont bien
|
|
||||||
évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement,
|
|
||||||
ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas
|
|
||||||
venir payer au bureau, merci de nous contacter par mail.
|
|
||||||
|
|
||||||
*Mode de retrait des places*
|
|
||||||
Au moment du paiement, certaines places vous seront remises directement,
|
|
||||||
d'autres seront à récupérer au cours de l'année, d'autres encore seront
|
|
||||||
nominatives et à retirer le soir même dans les théâtres correspondants.
|
|
||||||
Pour chaque spectacle, vous recevrez un mail quelques jours avant la
|
|
||||||
représentation vous indiquant le mode de retrait.
|
|
||||||
|
|
||||||
Nous vous rappelons que l'obtention de places du BdA vous engage à
|
|
||||||
respecter les règles de fonctionnement :
|
|
||||||
https://bda.ens.fr/lequipe/charte-bda/
|
|
||||||
Un système de revente des places via les mails BdA-revente est disponible
|
|
||||||
directement sur votre compte GestioCOF.
|
|
||||||
|
|
||||||
En vous souhaitant de très beaux spectacles tout au long de l'année,
|
|
||||||
--
|
|
||||||
Le Bureau des Arts
|
|
|
@ -1,23 +0,0 @@
|
||||||
Bonjour {{ member.first_name }},
|
|
||||||
|
|
||||||
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
|
|
||||||
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
|
|
||||||
{% if nb_attr == 2 %}
|
|
||||||
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
|
|
||||||
ces places sont strictement réservées aux personnes de moins de 28 ans.
|
|
||||||
{% endif %}
|
|
||||||
{% if show.listing %}Pour ce spectacle, tu as reçu {{ nb_attr|pluralize:"une place,des places" }} sur
|
|
||||||
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
|
|
||||||
pour {{ nb_attr|pluralize:"la,les" }} retirer.
|
|
||||||
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
|
|
||||||
été distribués au burô.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
Si tu ne peux plus assister à cette représentation, tu peux
|
|
||||||
revendre ta place via BdA-revente, accessible directement sur
|
|
||||||
GestioCOF (lien "revendre une place du premier tirage" sur la page
|
|
||||||
d'accueil https://www.cof.ens.fr/gestion/).
|
|
||||||
|
|
||||||
En te souhaitant un excellent spectacle,
|
|
||||||
--
|
|
||||||
Le Bureau des Arts
|
|
|
@ -1,12 +0,0 @@
|
||||||
Bonjour {{ member.first_name }}
|
|
||||||
|
|
||||||
Une place pour le spectacle {{ show.title }} ({{ show.date }})
|
|
||||||
a été postée sur BdA-Revente.
|
|
||||||
|
|
||||||
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
|
|
||||||
sur ce lien : https://{{ site }}{% url "bda-revente-confirm" revente.id %}.
|
|
||||||
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
|
|
||||||
un tirage au sort le {{ revente.date_tirage|date:"DATE_FORMAT" }}.
|
|
||||||
|
|
||||||
Chaleureusement,
|
|
||||||
Le BdA
|
|
|
@ -1,13 +0,0 @@
|
||||||
Bonjour {{ vendeur.first_name }},
|
|
||||||
|
|
||||||
Tu t’es bien inscrit·e pour revendre une place pour {{ show.title }}.
|
|
||||||
|
|
||||||
{% with revente.date_tirage as time %}
|
|
||||||
Le tirage au sort entre tout·e·s les racheteuse·eur·s potentiel·le·s aura lieu
|
|
||||||
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
|
|
||||||
Si personne ne s’est inscrit pour racheter la place, celle-ci apparaîtra parmi
|
|
||||||
les « Places disponibles immédiatement à la revente » sur GestioCOF.
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
Bonne revente !
|
|
||||||
Le Bureau des Arts
|
|
|
@ -1,6 +0,0 @@
|
||||||
Bonjour {{ vendeur.first_name }} !
|
|
||||||
|
|
||||||
Je souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
|
|
||||||
Contacte-moi si tu es toujours intéressé·e !
|
|
||||||
|
|
||||||
{{ acheteur.get_full_name }} ({{ acheteur.email }})
|
|
|
@ -1,11 +0,0 @@
|
||||||
Bonjour {{ member.first_name }}
|
|
||||||
|
|
||||||
Une place pour le spectacle {{ show.title }} ({{ show.date }})
|
|
||||||
a été postée sur BdA-Revente.
|
|
||||||
|
|
||||||
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour
|
|
||||||
cette place : elle est disponible immédiatement à l'adresse
|
|
||||||
https://{{ site }}{% url "bda-revente-buy" show.id %}, à la disposition de tous.
|
|
||||||
|
|
||||||
Chaleureusement,
|
|
||||||
Le BdA
|
|
|
@ -1,9 +0,0 @@
|
||||||
Bonjour {{ acheteur.first_name }},
|
|
||||||
|
|
||||||
Tu t'étais inscrit·e pour la revente de la place de {{ vendeur.get_full_name }}
|
|
||||||
pour {{ show.title }}.
|
|
||||||
Malheureusement, une autre personne a été tirée au sort pour racheter la place.
|
|
||||||
Tu pourras certainement retenter ta chance pour une autre revente !
|
|
||||||
|
|
||||||
À très bientôt,
|
|
||||||
Le Bureau des Arts
|
|
|
@ -1,7 +0,0 @@
|
||||||
Bonjour {{ vendeur.first_name }},
|
|
||||||
|
|
||||||
La personne tirée au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.
|
|
||||||
Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail.
|
|
||||||
|
|
||||||
Chaleureusement,
|
|
||||||
Le BdA
|
|
|
@ -1,7 +0,0 @@
|
||||||
Bonjour {{ acheteur.first_name }},
|
|
||||||
|
|
||||||
Tu as été tiré·e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
|
|
||||||
Tu peux contacter le/la vendeur·se à l'adresse {{ vendeur.email }}.
|
|
||||||
|
|
||||||
Chaleureusement,
|
|
||||||
Le BdA
|
|
|
@ -10,7 +10,6 @@
|
||||||
<td>{{place.spectacle.location}}</td>
|
<td>{{place.spectacle.location}}</td>
|
||||||
<td>{{place.spectacle.date}}</td>
|
<td>{{place.spectacle.date}}</td>
|
||||||
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
||||||
<td>{% if place.spectacle.listing %}sur listing{% else %}place physique{% endif %}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
{% extends "base_title.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block realcontent %}
|
|
||||||
|
|
||||||
<h2>Gestion des places que je revends</h2>
|
|
||||||
|
|
||||||
{% if resell_exists %}
|
|
||||||
<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>
|
|
||||||
{% csrf_token %}
|
|
||||||
<table class="table table-striped stupidtable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for checkbox in resellform.attributions %}{{ checkbox }}{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<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_exists %}
|
|
||||||
<h3>Places en cours de revente</h3>
|
|
||||||
<form action="" method="post">
|
|
||||||
<div class="bg-info text-info center-block">
|
|
||||||
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
|
||||||
Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se.
|
|
||||||
</div>
|
|
||||||
{% csrf_token %}
|
|
||||||
<table class="table table-striped stupidtable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
<th data-sort="int">Tirage le</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if sold_exists %}
|
|
||||||
<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>
|
|
||||||
{% csrf_token %}
|
|
||||||
<table class="table table-striped stupidtable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
<th>Vendue à</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for checkbox in soldform.reventes %}{{ checkbox }}{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<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_exists and not annul_exists and not sold_exists %}
|
|
||||||
<p>Plus de reventes possibles !</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script language="JavaScript">
|
|
||||||
$(function(){
|
|
||||||
$("table.stupidtable").stupidtable();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("tr").click(function() {
|
|
||||||
$(this).find("input[type=checkbox]").click()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[type=checkbox]").click(function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,29 +0,0 @@
|
||||||
{% extends "base_title.html" %}
|
|
||||||
|
|
||||||
{% block realcontent %}
|
|
||||||
<h2>Places disponibles immédiatement</h2>
|
|
||||||
{% if spectacles %}
|
|
||||||
<table class="table table-striped stupidtable" id="bda-shotgun">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for spectacle in spectacles %}
|
|
||||||
<tr>
|
|
||||||
{% include "bda/forms/spectacle_label_table.html" with spectacle=spectacle %}
|
|
||||||
<td class="button"><a role="button" class="btn btn-primary" href="{% url 'bda-revente-buy' spectacle.id %}">Racheter</a>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p> Pas de places disponibles immédiatement, désolé !</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,64 +0,0 @@
|
||||||
{% extends "base_title.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% 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 une
|
|
||||||
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>
|
|
||||||
<table class="table table-striped stupidtable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for checkbox in form.spectacles %}{{ checkbox }}{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$(function(){
|
|
||||||
$("table.stupidtable").stupidtable();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("tr").click(function() {
|
|
||||||
$(this).find("input[type=checkbox]").click()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[type=checkbox]").click(function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,99 +0,0 @@
|
||||||
{% extends "base_title.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block realcontent %}
|
|
||||||
|
|
||||||
<h2>Tirages au sort de reventes</h2>
|
|
||||||
|
|
||||||
{% if annul_exists %}
|
|
||||||
<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>
|
|
||||||
Voici la liste des reventes auxquelles vous êtes inscrit·e ; si vous ne souhaitez plus participer au tirage au sort vous pouvez vous en désister.
|
|
||||||
</div>
|
|
||||||
{% csrf_token %}
|
|
||||||
<table class="table table-striped stupidtable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
<th>Vendue par</th>
|
|
||||||
<th data-sort="int">Tirage le</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="form-actions">
|
|
||||||
<input type="submit"
|
|
||||||
class="btn btn-primary"
|
|
||||||
name="annul"
|
|
||||||
value="Se désister des tirages sélectionnés">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
{% if sub_exists %}
|
|
||||||
|
|
||||||
<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 tirages en cours suivants.
|
|
||||||
</div>
|
|
||||||
{% csrf_token %}
|
|
||||||
<table class="table table-striped stupidtable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th data-sort="string">Titre</th>
|
|
||||||
<th data-sort="int">Date</th>
|
|
||||||
<th data-sort="string">Lieu</th>
|
|
||||||
<th data-sort="int">Prix</th>
|
|
||||||
<th>Vendue par</th>
|
|
||||||
<th data-sort="int">Tirage le</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for checkbox in subform.reventes %}{{ checkbox }}{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="form-actions">
|
|
||||||
<input type="submit"
|
|
||||||
class="btn btn-primary"
|
|
||||||
name="subscribe"
|
|
||||||
value="S'inscrire aux tirages sélectionnés">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not annul_exists and not sub_exists %}
|
|
||||||
<div class="bg-info text-info center-block">
|
|
||||||
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
|
||||||
Aucune revente n'est active pour le moment !
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script language="JavaScript">
|
|
||||||
$(function(){
|
|
||||||
$("table.stupidtable").stupidtable();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("tr").click(function() {
|
|
||||||
$(this).find("input[type=checkbox]").click()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("input[type=checkbox]").click(function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
{% 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>
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
|
||||||
{%block realcontent %}
|
{%block realcontent %}
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
@ -32,7 +32,9 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
||||||
|
</script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function(){
|
$(function(){
|
||||||
$("table.etat-bda").stupidtable();
|
$("table.etat-bda").stupidtable();
|
||||||
|
@ -49,5 +51,6 @@
|
||||||
<h3> Exports </h3>
|
<h3> Exports </h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
|
<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>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
22
bda/tests.py
Normal file
22
bda/tests.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This file demonstrates writing tests using the unittest module. These will pass
|
||||||
|
when you run "manage.py test".
|
||||||
|
|
||||||
|
Replace this with more appropriate tests for your application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleTest(TestCase):
|
||||||
|
def test_basic_addition(self):
|
||||||
|
"""
|
||||||
|
Tests that 1 + 1 always equals 2.
|
||||||
|
"""
|
||||||
|
self.assertEqual(1 + 1, 2)
|
|
@ -1,65 +0,0 @@
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from shared.tests.mixins import ViewTestCaseMixin
|
|
||||||
|
|
||||||
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
|
|
||||||
from .utils import create_user
|
|
||||||
|
|
||||||
|
|
||||||
class BdAViewTestCaseMixin(ViewTestCaseMixin):
|
|
||||||
def get_users_base(self):
|
|
||||||
return {
|
|
||||||
"bda_other": create_user(username="bda_other"),
|
|
||||||
"bda_member": create_user(username="bda_member", is_cof=True),
|
|
||||||
"bda_staff": create_user(username="bda_staff", is_cof=True, is_buro=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BdATestHelpers:
|
|
||||||
bda_testdata = False
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
if self.bda_testdata:
|
|
||||||
self.load_bda_testdata()
|
|
||||||
|
|
||||||
def load_bda_testdata(self):
|
|
||||||
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")
|
|
||||||
self.show1 = Spectacle.objects.create(
|
|
||||||
title="foo",
|
|
||||||
date=timezone.now(),
|
|
||||||
location=self.location,
|
|
||||||
price=0,
|
|
||||||
slots=42,
|
|
||||||
tirage=self.tirage,
|
|
||||||
listing=False,
|
|
||||||
category=self.category,
|
|
||||||
)
|
|
||||||
self.show2 = Spectacle.objects.create(
|
|
||||||
title="bar",
|
|
||||||
date=timezone.now(),
|
|
||||||
location=self.location,
|
|
||||||
price=1,
|
|
||||||
slots=142,
|
|
||||||
tirage=self.tirage,
|
|
||||||
listing=False,
|
|
||||||
category=self.category,
|
|
||||||
)
|
|
||||||
self.show3 = Spectacle.objects.create(
|
|
||||||
title="baz",
|
|
||||||
date=timezone.now(),
|
|
||||||
location=self.location,
|
|
||||||
price=2,
|
|
||||||
slots=242,
|
|
||||||
tirage=self.tirage,
|
|
||||||
listing=False,
|
|
||||||
category=self.category,
|
|
||||||
)
|
|
|
@ -1,100 +0,0 @@
|
||||||
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):
|
|
||||||
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.set(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"]],
|
|
||||||
)
|
|
|
@ -1,79 +0,0 @@
|
||||||
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))
|
|
|
@ -1,368 +0,0 @@
|
||||||
import json
|
|
||||||
from datetime import timedelta
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import Client, TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import formats, timezone
|
|
||||||
|
|
||||||
from ..models import Participant, Tirage
|
|
||||||
from .mixins import BdATestHelpers, BdAViewTestCaseMixin
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-tirage-inscription"
|
|
||||||
|
|
||||||
http_methods = ["GET", "POST"]
|
|
||||||
|
|
||||||
auth_user = "bda_member"
|
|
||||||
auth_forbidden = [None, "bda_other"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/inscription/{}".format(self.tirage.id)
|
|
||||||
|
|
||||||
def test_get_opened(self):
|
|
||||||
self.tirage.ouverture = timezone.now() - timedelta(days=1)
|
|
||||||
self.tirage.fermeture = timezone.now() + timedelta(days=1)
|
|
||||||
self.tirage.save()
|
|
||||||
|
|
||||||
resp = self.client.get(self.url)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertFalse(resp.context["messages"])
|
|
||||||
|
|
||||||
def test_get_closed_future(self):
|
|
||||||
self.tirage.ouverture = timezone.now() + timedelta(days=1)
|
|
||||||
self.tirage.fermeture = timezone.now() + timedelta(days=2)
|
|
||||||
self.tirage.save()
|
|
||||||
|
|
||||||
resp = self.client.get(self.url)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertIn(
|
|
||||||
"Le tirage n'est pas encore ouvert : ouverture le {}".format(
|
|
||||||
formats.localize(timezone.template_localtime(self.tirage.ouverture))
|
|
||||||
),
|
|
||||||
[str(msg) for msg in resp.context["messages"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_closed_past(self):
|
|
||||||
self.tirage.ouverture = timezone.now() - timedelta(days=2)
|
|
||||||
self.tirage.fermeture = timezone.now() - timedelta(days=1)
|
|
||||||
self.tirage.save()
|
|
||||||
|
|
||||||
resp = self.client.get(self.url)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertIn(
|
|
||||||
" C'est fini : tirage au sort dans la journée !",
|
|
||||||
[str(msg) for msg in resp.context["messages"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_base_post_data(self):
|
|
||||||
return {
|
|
||||||
"choixspectacle_set-TOTAL_FORMS": "3",
|
|
||||||
"choixspectacle_set-INITIAL_FORMS": "0",
|
|
||||||
"choixspectacle_set-MIN_NUM_FORMS": "0",
|
|
||||||
"choixspectacle_set-MAX_NUM_FORMS": "1000",
|
|
||||||
}
|
|
||||||
|
|
||||||
base_post_data = property(get_base_post_data)
|
|
||||||
|
|
||||||
def test_post(self):
|
|
||||||
self.tirage.ouverture = timezone.now() - timedelta(days=1)
|
|
||||||
self.tirage.fermeture = timezone.now() + timedelta(days=1)
|
|
||||||
self.tirage.save()
|
|
||||||
|
|
||||||
data = dict(
|
|
||||||
self.base_post_data,
|
|
||||||
**{
|
|
||||||
"choixspectacle_set-TOTAL_FORMS": "2",
|
|
||||||
"choixspectacle_set-0-id": "",
|
|
||||||
"choixspectacle_set-0-participant": "",
|
|
||||||
"choixspectacle_set-0-spectacle": str(self.show1.pk),
|
|
||||||
"choixspectacle_set-0-double_choice": "1",
|
|
||||||
"choixspectacle_set-0-priority": "2",
|
|
||||||
"choixspectacle_set-1-id": "",
|
|
||||||
"choixspectacle_set-1-participant": "",
|
|
||||||
"choixspectacle_set-1-spectacle": str(self.show2.pk),
|
|
||||||
"choixspectacle_set-1-double_choice": "autoquit",
|
|
||||||
"choixspectacle_set-1-priority": "1",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
resp = self.client.post(self.url, data)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertIn(
|
|
||||||
"Votre inscription a été mise à jour avec succès !",
|
|
||||||
[str(msg) for msg in resp.context["messages"]],
|
|
||||||
)
|
|
||||||
participant = Participant.objects.get(
|
|
||||||
user=self.users["bda_member"], tirage=self.tirage
|
|
||||||
)
|
|
||||||
self.assertSetEqual(
|
|
||||||
set(
|
|
||||||
participant.choixspectacle_set.values_list(
|
|
||||||
"priority", "spectacle_id", "double_choice"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
{(1, self.show2.pk, "autoquit"), (2, self.show1.pk, "1")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_post_state_changed(self):
|
|
||||||
self.tirage.ouverture = timezone.now() - timedelta(days=1)
|
|
||||||
self.tirage.fermeture = timezone.now() + timedelta(days=1)
|
|
||||||
self.tirage.save()
|
|
||||||
|
|
||||||
data = {"dbstate": "different"}
|
|
||||||
resp = self.client.post(self.url, data)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertIn(
|
|
||||||
"Impossible d'enregistrer vos modifications : vous avez apporté d'autres "
|
|
||||||
"modifications entre temps.",
|
|
||||||
[str(msg) for msg in resp.context["messages"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-places-attribuees"
|
|
||||||
|
|
||||||
auth_user = "bda_member"
|
|
||||||
auth_forbidden = [None, "bda_other"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/places/{}".format(self.tirage.id)
|
|
||||||
|
|
||||||
|
|
||||||
class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-etat-places"
|
|
||||||
|
|
||||||
auth_user = "bda_member"
|
|
||||||
auth_forbidden = [None, "bda_other"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/etat-places/{}".format(self.tirage.id)
|
|
||||||
|
|
||||||
|
|
||||||
class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-tirage"
|
|
||||||
|
|
||||||
http_methods = ["GET", "POST"]
|
|
||||||
|
|
||||||
auth_user = "bda_staff"
|
|
||||||
auth_forbidden = [None, "bda_other", "bda_member"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/tirage/{}".format(self.tirage.id)
|
|
||||||
|
|
||||||
def test_perform_tirage_disabled(self):
|
|
||||||
# Cannot be performed if disabled
|
|
||||||
self.tirage.enable_do_tirage = False
|
|
||||||
self.tirage.save()
|
|
||||||
resp = self.client.get(self.url)
|
|
||||||
self.assertTemplateUsed(resp, "tirage-failed.html")
|
|
||||||
|
|
||||||
def test_perform_tirage_opened_registrations(self):
|
|
||||||
# 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 = self.client.get(self.url)
|
|
||||||
self.assertTemplateUsed(resp, "tirage-failed.html")
|
|
||||||
|
|
||||||
def test_perform_tirage(self):
|
|
||||||
# Otherwise, perform the tirage
|
|
||||||
self.tirage.enable_do_tirage = True
|
|
||||||
self.tirage.fermeture = timezone.now()
|
|
||||||
self.tirage.save()
|
|
||||||
resp = self.client.get(self.url)
|
|
||||||
self.assertTemplateNotUsed(resp, "tirage-failed.html")
|
|
||||||
|
|
||||||
|
|
||||||
class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-liste-spectacles"
|
|
||||||
|
|
||||||
auth_user = "bda_staff"
|
|
||||||
auth_forbidden = [None, "bda_other", "bda_member"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/spectacles/{}".format(self.tirage.id)
|
|
||||||
|
|
||||||
|
|
||||||
class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-spectacle"
|
|
||||||
|
|
||||||
auth_user = "bda_staff"
|
|
||||||
auth_forbidden = [None, "bda_other", "bda_member"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id, "spectacle_id": self.show1.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id)
|
|
||||||
|
|
||||||
|
|
||||||
class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-unpaid"
|
|
||||||
|
|
||||||
auth_user = "bda_staff"
|
|
||||||
auth_forbidden = [None, "bda_other", "bda_member"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"tirage_id": self.tirage.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/spectacles/unpaid/{}".format(self.tirage.id)
|
|
||||||
|
|
||||||
|
|
||||||
class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
url_name = "bda-rappels"
|
|
||||||
|
|
||||||
auth_user = "bda_staff"
|
|
||||||
auth_forbidden = [None, "bda_other", "bda_member"]
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_kwargs(self):
|
|
||||||
return {"spectacle_id": self.show1.id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url_expected(self):
|
|
||||||
return "/gestion/bda/mails-rappel/{}".format(self.show1.id)
|
|
||||||
|
|
||||||
def test_post(self):
|
|
||||||
resp = self.client.post(self.url)
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
# TODO: check that emails are sent
|
|
||||||
|
|
||||||
|
|
||||||
class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
|
||||||
auth_user = None
|
|
||||||
auth_forbidden = []
|
|
||||||
|
|
||||||
bda_testdata = True
|
|
||||||
|
|
||||||
def test_api_list(self):
|
|
||||||
url_list = "/gestion/bda/catalogue/list"
|
|
||||||
resp = self.client.get(url_list)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
resp.content.decode("utf-8"),
|
|
||||||
[{"id": self.tirage.id, "title": self.tirage.title}],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_api_details(self):
|
|
||||||
url_details = "/gestion/bda/catalogue/details?id={}".format(self.tirage.id)
|
|
||||||
resp = self.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}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_api_descriptions(self):
|
|
||||||
url_descriptions = "/gestion/bda/catalogue/descriptions?id={}".format(
|
|
||||||
self.tirage.id
|
|
||||||
)
|
|
||||||
resp = self.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)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----- BdA Revente --------------------------------------- #
|
|
||||||
|
|
||||||
|
|
||||||
def make_participant(name: str, tirage: Tirage) -> User:
|
|
||||||
user = User.objects.create_user(username=name, password=name)
|
|
||||||
user.profile.is_cof = True
|
|
||||||
user.profile.save()
|
|
||||||
Participant.objects.create(user=user, tirage=tirage)
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
class TestReventeManageTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.tirage = Tirage.objects.create(
|
|
||||||
title="tirage1",
|
|
||||||
ouverture=timezone.now(),
|
|
||||||
fermeture=timezone.now() + timedelta(days=90),
|
|
||||||
)
|
|
||||||
self.user = make_participant("toto", self.tirage)
|
|
||||||
self.url = reverse("bda-revente-manage", args=[self.tirage.id])
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
def test_can_get(self):
|
|
||||||
client = Client()
|
|
||||||
client.force_login(
|
|
||||||
self.user, backend="django.contrib.auth.backends.ModelBackend"
|
|
||||||
)
|
|
||||||
r = client.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBdaRevente:
|
|
||||||
pass
|
|
||||||
# TODO
|
|
|
@ -1,36 +0,0 @@
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def create_spectacle(**kwargs):
|
|
||||||
defaults = {
|
|
||||||
"title": "Title",
|
|
||||||
"category": CategorieSpectacle.objects.first(),
|
|
||||||
"date": (timezone.now() + timedelta(days=7)).date(),
|
|
||||||
"location": Salle.objects.first(),
|
|
||||||
"price": 10.0,
|
|
||||||
"slots": 20,
|
|
||||||
"tirage": Tirage.objects.first(),
|
|
||||||
"listing": False,
|
|
||||||
}
|
|
||||||
return Spectacle.objects.create(**dict(defaults, **kwargs))
|
|
112
bda/urls.py
112
bda/urls.py
|
@ -1,80 +1,50 @@
|
||||||
from django.urls import re_path
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from bda import views
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
from cof.decorators import buro_required
|
||||||
from bda.views import SpectacleListView
|
from bda.views import SpectacleListView
|
||||||
from gestioncof.decorators import buro_required
|
from bda import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(
|
url(r'^inscription/(?P<tirage_id>\d+)$',
|
||||||
r"^inscription/(?P<tirage_id>\d+)$",
|
|
||||||
views.inscription,
|
views.inscription,
|
||||||
name="bda-tirage-inscription",
|
name='bda-tirage-inscription'),
|
||||||
),
|
url(r'^places/(?P<tirage_id>\d+)$',
|
||||||
re_path(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
|
views.places,
|
||||||
re_path(
|
name="bda-places-attribuees"),
|
||||||
r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"
|
url(r'^revente/(?P<tirage_id>\d+)$',
|
||||||
),
|
views.revente,
|
||||||
re_path(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
|
name='bda-revente'),
|
||||||
re_path(
|
url(r'^etat-places/(?P<tirage_id>\d+)$',
|
||||||
r"^spectacles/(?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()),
|
buro_required(SpectacleListView.as_view()),
|
||||||
name="bda-liste-spectacles",
|
name="bda-liste-spectacles"),
|
||||||
),
|
url(r'^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$',
|
||||||
re_path(
|
|
||||||
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
|
|
||||||
views.spectacle,
|
views.spectacle,
|
||||||
name="bda-spectacle",
|
name="bda-spectacle"),
|
||||||
),
|
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
|
||||||
re_path(
|
views.unpaid,
|
||||||
r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
|
name="bda-unpaid"),
|
||||||
views.UnpaidParticipants.as_view(),
|
url(r'^liste-revente/(?P<tirage_id>\d+)$',
|
||||||
name="bda-unpaid",
|
views.list_revente,
|
||||||
),
|
name="bda-liste-revente"),
|
||||||
re_path(
|
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
|
||||||
r"^spectacles/autocomplete$",
|
views.buy_revente,
|
||||||
views.spectacle_autocomplete,
|
name="bda-buy-revente"),
|
||||||
name="bda-spectacle-autocomplete",
|
url(r'^revente-interested/(?P<revente_id>\d+)$',
|
||||||
),
|
views.revente_interested,
|
||||||
re_path(
|
name='bda-revente-interested'),
|
||||||
r"^participants/autocomplete$",
|
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
|
||||||
views.participant_autocomplete,
|
|
||||||
name="bda-participant-autocomplete",
|
|
||||||
),
|
|
||||||
# Urls BdA-Revente
|
|
||||||
re_path(
|
|
||||||
r"^revente/(?P<tirage_id>\d+)/manage$",
|
|
||||||
views.revente_manage,
|
|
||||||
name="bda-revente-manage",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^revente/(?P<tirage_id>\d+)/subscribe$",
|
|
||||||
views.revente_subscribe,
|
|
||||||
name="bda-revente-subscribe",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^revente/(?P<tirage_id>\d+)/tirages$",
|
|
||||||
views.revente_tirages,
|
|
||||||
name="bda-revente-tirages",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^revente/(?P<spectacle_id>\d+)/buy$",
|
|
||||||
views.revente_buy,
|
|
||||||
name="bda-revente-buy",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^revente/(?P<revente_id>\d+)/confirm$",
|
|
||||||
views.revente_confirm,
|
|
||||||
name="bda-revente-confirm",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^revente/(?P<tirage_id>\d+)/shotgun$",
|
|
||||||
views.revente_shotgun,
|
views.revente_shotgun,
|
||||||
name="bda-revente-shotgun",
|
name="bda-shotgun"),
|
||||||
),
|
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
|
||||||
re_path(
|
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
|
||||||
r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"
|
name='bda-descriptions'),
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
958
bda/views.py
958
bda/views.py
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,4 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import BdsProfile
|
||||||
|
|
||||||
from bds.models import BDSProfile
|
admin.site.register(BdsProfile)
|
||||||
|
|
||||||
admin.site.register(BDSProfile)
|
|
||||||
|
|
28
bds/apps.py
28
bds/apps.py
|
@ -1,28 +1,6 @@
|
||||||
from django.apps import AppConfig, apps as global_apps
|
from django.apps import AppConfig
|
||||||
from django.db.models import Q
|
|
||||||
from django.db.models.signals import post_migrate
|
|
||||||
|
|
||||||
|
|
||||||
def bds_group_perms(app_config, apps=global_apps, **kwargs):
|
class BDSConfig(AppConfig):
|
||||||
try:
|
|
||||||
Permission = apps.get_model("auth", "Permission")
|
|
||||||
Group = apps.get_model("auth", "Group")
|
|
||||||
|
|
||||||
group = Group.objects.get(name="Burô du BDS")
|
|
||||||
perms = Permission.objects.filter(
|
|
||||||
Q(content_type__app_label="bds")
|
|
||||||
| Q(content_type__app_label="auth") & Q(content_type__model="user")
|
|
||||||
)
|
|
||||||
group.permissions.set(perms)
|
|
||||||
group.save()
|
|
||||||
|
|
||||||
except (LookupError, Group.DoesNotExist):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class BdsConfig(AppConfig):
|
|
||||||
name = "bds"
|
name = "bds"
|
||||||
verbose_name = "Gestion des adhérent·e·s du BDS"
|
verbose_name = "Application de gestion du BDS"
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
post_migrate.connect(bds_group_perms, sender=self)
|
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from shared import autocomplete
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class BDSMemberSearch(autocomplete.ModelSearch):
|
|
||||||
model = User
|
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
|
||||||
verbose_name = _("Membres du BDS")
|
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
|
||||||
qset_filter &= Q(bds__is_member=True)
|
|
||||||
return qset_filter
|
|
||||||
|
|
||||||
def result_uuid(self, user):
|
|
||||||
return user.username
|
|
||||||
|
|
||||||
def result_link(self, user):
|
|
||||||
return reverse("bds:user.update", args=(user.pk,))
|
|
||||||
|
|
||||||
|
|
||||||
class BDSOthersSearch(autocomplete.ModelSearch):
|
|
||||||
model = User
|
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
|
||||||
verbose_name = _("Non-membres du BDS")
|
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
|
||||||
qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False)
|
|
||||||
return qset_filter
|
|
||||||
|
|
||||||
def result_uuid(self, user):
|
|
||||||
return user.username
|
|
||||||
|
|
||||||
def result_link(self, user):
|
|
||||||
return reverse("bds:user.update", args=(user.pk,))
|
|
||||||
|
|
||||||
|
|
||||||
class BDSLDAPSearch(autocomplete.LDAPSearch):
|
|
||||||
def result_link(self, clipper):
|
|
||||||
url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,))
|
|
||||||
get = {"fullname": clipper.fullname, "mail": clipper.mail}
|
|
||||||
|
|
||||||
return "{}?{}".format(url, urlencode(get))
|
|
||||||
|
|
||||||
|
|
||||||
class BDSSearch(autocomplete.Compose):
|
|
||||||
search_units = [
|
|
||||||
("members", BDSMemberSearch()),
|
|
||||||
("others", BDSOthersSearch()),
|
|
||||||
("clippers", BDSLDAPSearch()),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
bds_search = BDSSearch()
|
|
41
bds/forms.py
41
bds/forms.py
|
@ -1,41 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from bds.models import BDSProfile
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class UserForm(forms.ModelForm):
|
|
||||||
is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ["email", "first_name", "last_name"]
|
|
||||||
|
|
||||||
|
|
||||||
class UserFromClipperForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["username"].disabled = True
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ["username", "email", "first_name", "last_name"]
|
|
||||||
|
|
||||||
|
|
||||||
class UserFromScratchForm(UserCreationForm):
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ["username", "email", "first_name", "last_name"]
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = BDSProfile
|
|
||||||
exclude = ["user"]
|
|
||||||
widgets = {
|
|
||||||
"birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d")
|
|
||||||
}
|
|
|
@ -1,141 +1,32 @@
|
||||||
# Generated by Django 2.2 on 2019-07-17 12:48
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import bds.models
|
import bds.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
dependencies = [
|
||||||
|
('gestion', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="BDSProfile",
|
name='BdsProfile',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
|
||||||
"id",
|
('ASPSL_number', models.CharField(null=True, blank=True, verbose_name='Numéro AS PSL', max_length=50)),
|
||||||
models.AutoField(
|
('FFSU_number', models.CharField(null=True, blank=True, verbose_name='Numéro FFSU', max_length=50)),
|
||||||
auto_created=True,
|
('have_certificate', models.BooleanField(verbose_name='Certificat médical', default=False)),
|
||||||
primary_key=True,
|
('certificate_file', models.FileField(blank=True, upload_to=bds.models.BdsProfile.issue_file_name, verbose_name='Fichier de certificat médical')),
|
||||||
serialize=False,
|
('cotisation_period', models.CharField(choices=[('ANN', 'Année'), ('SE1', 'Premier semestre'), ('SE2', 'Deuxième semestre')], verbose_name='Inscription', max_length=3, default='ANN')),
|
||||||
verbose_name="ID",
|
('registration_date', models.DateField(verbose_name="Date d'inscription", auto_now_add=True)),
|
||||||
),
|
('payment_method', models.CharField(choices=[('CASH', 'Liquide'), ('BANK', 'Transfer bancaire'), ('CHEQUE', 'Cheque'), ('OTHER', 'Autre')], verbose_name='Methode de paiement', max_length=6, default='CASH')),
|
||||||
),
|
('profile', models.OneToOneField(
|
||||||
(
|
related_name='bds',
|
||||||
"phone",
|
on_delete=models.CASCADE,
|
||||||
models.CharField(
|
to='gestion.Profile')),
|
||||||
blank=True, max_length=20, verbose_name="téléphone"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"occupation",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("EXT", "Extérieur"),
|
|
||||||
("1A", "1A"),
|
|
||||||
("2A", "2A"),
|
|
||||||
("3A", "3A"),
|
|
||||||
("4A", "4A"),
|
|
||||||
("MAG", "Magistérien"),
|
|
||||||
("ARC", "Archicube"),
|
|
||||||
("DOC", "Doctorant"),
|
|
||||||
("CST", "CST"),
|
|
||||||
("PER", "Personnel ENS"),
|
|
||||||
],
|
],
|
||||||
default="1A",
|
|
||||||
max_length=3,
|
|
||||||
verbose_name="occupation",
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
(
|
|
||||||
"departement",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, max_length=50, verbose_name="département"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"birthdate",
|
|
||||||
models.DateField(
|
|
||||||
blank=True, null=True, verbose_name="date de naissance"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"mails_bds",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, verbose_name="recevoir les mails du BDS"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_buro",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, verbose_name="membre du Burô du BDS"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"has_certificate",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, verbose_name="certificat médical"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"certificate_file",
|
|
||||||
models.FileField(
|
|
||||||
blank=True,
|
|
||||||
upload_to=bds.models.BDSProfile.get_certificate_filename,
|
|
||||||
verbose_name="fichier de certificat médical",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"ASPSL_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
max_length=50,
|
|
||||||
null=True,
|
|
||||||
verbose_name="numéro AS PSL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"FFSU_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, max_length=50, null=True, verbose_name="numéro FFSU"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"cotisation_period",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("ANN", "Année"),
|
|
||||||
("SE1", "Premier semestre"),
|
|
||||||
("SE2", "Deuxième semestre"),
|
|
||||||
("NO", "Aucune"),
|
|
||||||
],
|
|
||||||
default="NO",
|
|
||||||
max_length=3,
|
|
||||||
verbose_name="inscription",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"registration_date",
|
|
||||||
models.DateField(
|
|
||||||
auto_now_add=True, verbose_name="date d'inscription"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="bds",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Profil BDS",
|
|
||||||
"verbose_name_plural": "Profils BDS",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
35
bds/migrations/0002_add_BDS_groups.py
Normal file
35
bds/migrations/0002_add_BDS_groups.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_groups(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Creates the groups for BDS members and staff
|
||||||
|
"""
|
||||||
|
Group = apps.get_model("auth", "Group")
|
||||||
|
Group.objects.get_or_create(name="bds_members")
|
||||||
|
Group.objects.get_or_create(name="bds_buro")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bds', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='bdsprofile',
|
||||||
|
options={
|
||||||
|
'permissions': [
|
||||||
|
('member', 'Is a BDS member'),
|
||||||
|
('buro', 'Is part of the BDS staff')
|
||||||
|
],
|
||||||
|
'verbose_name': 'Profil BDS',
|
||||||
|
'verbose_name_plural': 'Profils BDS'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(create_groups, migrations.RunPython.noop),
|
||||||
|
]
|
|
@ -1,16 +0,0 @@
|
||||||
# Generated by Django 2.2 on 2019-07-17 14:56
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def create_bds_buro_group(apps, schema_editor):
|
|
||||||
Group = apps.get_model("auth", "Group")
|
|
||||||
Group.objects.get_or_create(name="Burô du BDS")
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("bds", "0001_initial")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(create_bds_buro_group, migrations.RunPython.noop)
|
|
||||||
]
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 2.2.8 on 2019-12-20 22:48
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("bds", "0002_bds_group"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="bdsprofile",
|
|
||||||
options={
|
|
||||||
"permissions": (("is_team", "est membre du burô"),),
|
|
||||||
"verbose_name": "Profil BDS",
|
|
||||||
"verbose_name_plural": "Profils BDS",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="bdsprofile",
|
|
||||||
name="is_buro",
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Generated by Django 2.2.8 on 2019-12-22 10:20
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("bds", "0003_staff_permission"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="bdsprofile",
|
|
||||||
name="cotisation_type",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("ETU", "Étudiant"),
|
|
||||||
("NOR", "Normalien"),
|
|
||||||
("EXT", "Extérieur"),
|
|
||||||
("ARC", "Archicube"),
|
|
||||||
],
|
|
||||||
default="Normalien",
|
|
||||||
max_length=9,
|
|
||||||
verbose_name="type de cotisation",
|
|
||||||
),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="bdsprofile",
|
|
||||||
name="is_member",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="adhérent⋅e du BDS"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Generated by Django 2.2.14 on 2020-07-27 20:14
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("bds", "0004_is_member_cotiz_type"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="bdsprofile",
|
|
||||||
name="certificate_file",
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Generated by Django 2.2.12 on 2020-08-28 12:14
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("bds", "0005_remove_bdsprofile_certificate_file"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="bdsprofile",
|
|
||||||
name="comments",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Attention : l'utilisateur·ice dispose d'un droit d'accès"
|
|
||||||
" aux données le/la concernant, dont le contenu de ce champ !",
|
|
||||||
verbose_name="commentaires",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
122
bds/mixins.py
122
bds/mixins.py
|
@ -1,122 +0,0 @@
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
|
||||||
|
|
||||||
|
|
||||||
class StaffRequiredMixin(PermissionRequiredMixin):
|
|
||||||
permission_required = "bds.is_team"
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleFormMixin(ContextMixin):
|
|
||||||
"""Mixin pour gérer plusieurs formulaires dans la même vue.
|
|
||||||
Le fonctionnement est relativement identique à celui de
|
|
||||||
FormMixin, dont la documentation est disponible ici :
|
|
||||||
https://docs.djangoproject.com/en/3.0/ref/class-based-views/mixins-editing/
|
|
||||||
|
|
||||||
Les principales différences sont :
|
|
||||||
- au lieu de form_class, il faut donner comme attribut un dict de la forme
|
|
||||||
{<form_name>: <form_class>}, avec tous les formulaires à instancier. On
|
|
||||||
peut aussi redéfinir `get_form_classes`
|
|
||||||
|
|
||||||
- les données initiales se récupèrent pour chaque form via l'attribut
|
|
||||||
`<form_name>_initial` ou la fonction `get_<form_name>_initial`. De même,
|
|
||||||
si certaines forms sont des `ModelForm`s, on peut définir la fonction
|
|
||||||
`get_<form_name>_instance`.
|
|
||||||
|
|
||||||
- chaque form a un préfixe rajouté, par défaut <form_name>, mais qui peut
|
|
||||||
être customisé via `prefixes` ou `get_prefixes`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_classes = {}
|
|
||||||
prefixes = {}
|
|
||||||
initial = {}
|
|
||||||
|
|
||||||
success_url = None
|
|
||||||
|
|
||||||
def get_form_classes(self):
|
|
||||||
return self.form_classes
|
|
||||||
|
|
||||||
def get_initial(self, form_name):
|
|
||||||
initial_attr = "%s_initial" % form_name
|
|
||||||
|
|
||||||
initial_method = "get_%s_initial" % form_name
|
|
||||||
initial_method = getattr(self, initial_method, None)
|
|
||||||
|
|
||||||
if hasattr(self, initial_attr):
|
|
||||||
return getattr(self, initial_attr)
|
|
||||||
elif callable(initial_method):
|
|
||||||
return initial_method()
|
|
||||||
else:
|
|
||||||
return self.initial.copy()
|
|
||||||
|
|
||||||
def get_prefix(self, form_name):
|
|
||||||
return self.prefixes.get(form_name, form_name)
|
|
||||||
|
|
||||||
def get_instance(self, form_name):
|
|
||||||
# Au cas où certaines des forms soient des ModelForms
|
|
||||||
instance_method = "get_%s_instance" % form_name
|
|
||||||
instance_method = getattr(self, instance_method, None)
|
|
||||||
|
|
||||||
if callable(instance_method):
|
|
||||||
return instance_method()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_form_kwargs(self, form_name):
|
|
||||||
kwargs = {
|
|
||||||
"initial": self.get_initial(form_name),
|
|
||||||
"prefix": self.get_prefix(form_name),
|
|
||||||
"instance": self.get_instance(form_name),
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.request.method in ("POST", "PUT"):
|
|
||||||
kwargs.update({"data": self.request.POST, "files": self.request.FILES})
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_forms(self):
|
|
||||||
form_classes = self.get_form_classes()
|
|
||||||
return {
|
|
||||||
form_name: form_class(**self.get_form_kwargs(form_name))
|
|
||||||
for form_name, form_class in form_classes.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
if not self.success_url:
|
|
||||||
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
|
|
||||||
return str(self.success_url)
|
|
||||||
|
|
||||||
def form_valid(self, forms):
|
|
||||||
# on garde le nom form_valid pour l'interface avec SuccessMessageMixin
|
|
||||||
return HttpResponseRedirect(self.get_success_url())
|
|
||||||
|
|
||||||
def form_invalid(self, forms):
|
|
||||||
"""If the form is invalid, render the invalid form."""
|
|
||||||
return self.render_to_response(self.get_context_data(forms=forms))
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessMultipleFormView(View):
|
|
||||||
"""Équivalent de `ProcessFormView` pour plusieurs forms.
|
|
||||||
Note : il faut que *tous* les formulaires soient valides pour
|
|
||||||
qu'ils soient sauvegardés !
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
forms = self.get_forms()
|
|
||||||
return self.render_to_response(self.get_context_data(forms=forms))
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
forms = self.get_forms()
|
|
||||||
if all(form.is_valid() for form in forms.values()):
|
|
||||||
return self.form_valid(forms)
|
|
||||||
else:
|
|
||||||
return self.form_invalid(forms)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMultipleFormView(MultipleFormMixin, ProcessMultipleFormView):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleFormView(TemplateResponseMixin, BaseMultipleFormView):
|
|
||||||
pass
|
|
144
bds/models.py
144
bds/models.py
|
@ -1,113 +1,65 @@
|
||||||
from datetime import date
|
import os.path
|
||||||
from os.path import splitext
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.db import models
|
||||||
|
|
||||||
from shared.utils import choices_length
|
from gestion.models import Profile
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class BDSProfile(models.Model):
|
class BdsProfile(models.Model):
|
||||||
OCCUPATION_CHOICES = (
|
profile = models.OneToOneField(Profile,
|
||||||
("EXT", "Extérieur"),
|
related_name='bds',
|
||||||
("1A", "1A"),
|
on_delete=models.CASCADE)
|
||||||
("2A", "2A"),
|
|
||||||
("3A", "3A"),
|
|
||||||
("4A", "4A"),
|
|
||||||
("MAG", "Magistérien"),
|
|
||||||
("ARC", "Archicube"),
|
|
||||||
("DOC", "Doctorant"),
|
|
||||||
("CST", "CST"),
|
|
||||||
("PER", "Personnel ENS"),
|
|
||||||
)
|
|
||||||
|
|
||||||
TYPE_COTIZ_CHOICES = (
|
def issue_file_name(sportif, filename):
|
||||||
("ETU", "Étudiant"),
|
fn, extension = os.path.splitext(filename)
|
||||||
("NOR", "Normalien"),
|
year = timezone.now().year
|
||||||
("EXT", "Extérieur"),
|
return "certifs/{!s}-{:d}{:s}".format(sportif, year, extension)
|
||||||
("ARC", "Archicube"),
|
|
||||||
)
|
|
||||||
|
|
||||||
COTIZ_DURATION_CHOICES = (
|
COTIZ_DURATION_CHOICES = (
|
||||||
("ANN", "Année"),
|
('ANN', 'Année'),
|
||||||
("SE1", "Premier semestre"),
|
('SE1', 'Premier semestre'),
|
||||||
("SE2", "Deuxième semestre"),
|
('SE2', 'Deuxième semestre'),
|
||||||
("NO", "Aucune"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_certificate_filename(instance, filename):
|
PAYMENT_METHOD_CHOICES = (
|
||||||
_, ext = splitext(filename) # récupère l'extension du fichier
|
('CASH', 'Liquide'),
|
||||||
year = str(date.now().year)
|
('BANK', 'Transfer bancaire'),
|
||||||
return "certifs/{username}-{year}.{ext}".format(
|
('CHEQUE', 'Cheque'),
|
||||||
username=instance.user.username, year=year, ext=ext
|
('OTHER', 'Autre'),
|
||||||
)
|
)
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="bds")
|
ASPSL_number = models.CharField("Numéro AS PSL",
|
||||||
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
|
max_length=50,
|
||||||
occupation = models.CharField(
|
|
||||||
_("occupation"),
|
|
||||||
default="1A",
|
|
||||||
choices=OCCUPATION_CHOICES,
|
|
||||||
max_length=choices_length(OCCUPATION_CHOICES),
|
|
||||||
)
|
|
||||||
departement = models.CharField(_("département"), max_length=50, blank=True)
|
|
||||||
birthdate = models.DateField(
|
|
||||||
auto_now_add=False,
|
|
||||||
auto_now=False,
|
|
||||||
verbose_name=_("date de naissance"),
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True)
|
||||||
)
|
FFSU_number = models.CharField("Numéro FFSU",
|
||||||
|
max_length=50,
|
||||||
is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False)
|
|
||||||
|
|
||||||
mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False)
|
|
||||||
|
|
||||||
has_certificate = models.BooleanField(_("certificat médical"), default=False)
|
|
||||||
|
|
||||||
ASPSL_number = models.CharField(
|
|
||||||
_("numéro AS PSL"), max_length=50, blank=True, null=True
|
|
||||||
)
|
|
||||||
FFSU_number = models.CharField(
|
|
||||||
_("numéro FFSU"), max_length=50, blank=True, null=True
|
|
||||||
)
|
|
||||||
cotisation_period = models.CharField(
|
|
||||||
_("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3
|
|
||||||
)
|
|
||||||
registration_date = models.DateField(
|
|
||||||
auto_now_add=True, verbose_name=_("date d'inscription")
|
|
||||||
)
|
|
||||||
cotisation_type = models.CharField(
|
|
||||||
_("type de cotisation"), choices=TYPE_COTIZ_CHOICES, max_length=9
|
|
||||||
)
|
|
||||||
|
|
||||||
comments = models.TextField(
|
|
||||||
_("commentaires"),
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_(
|
null=True)
|
||||||
"Attention : l'utilisateur·ice dispose d'un droit d'accès aux données "
|
|
||||||
"le/la concernant, dont le contenu de ce champ !"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
have_certificate = models.BooleanField("Certificat médical",
|
||||||
def expired_members(cls):
|
default=False)
|
||||||
now = timezone.now()
|
certificate_file = models.FileField("Fichier de certificat médical",
|
||||||
qs = cls.objects.filter(is_member=True)
|
upload_to=issue_file_name,
|
||||||
if now.month > 1 and now.month < 7:
|
blank=True)
|
||||||
return qs.filter(cotisation_period="SE1")
|
|
||||||
elif now.month < 2 or now.month > 8:
|
cotisation_period = models.CharField("Inscription",
|
||||||
return qs.none()
|
default="ANN",
|
||||||
return qs
|
choices=COTIZ_DURATION_CHOICES,
|
||||||
|
max_length=3)
|
||||||
|
registration_date = models.DateField(auto_now_add=True,
|
||||||
|
verbose_name="Date d'inscription")
|
||||||
|
|
||||||
|
payment_method = models.CharField('Methode de paiement',
|
||||||
|
default='CASH',
|
||||||
|
choices=PAYMENT_METHOD_CHOICES,
|
||||||
|
max_length=6)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Profil BDS")
|
verbose_name = "Profil BDS"
|
||||||
verbose_name_plural = _("Profils BDS")
|
verbose_name_plural = "Profils BDS"
|
||||||
permissions = (("is_team", _("est membre du burô")),)
|
permissions = [
|
||||||
|
("member", "Is a BDS member"),
|
||||||
def __str__(self):
|
("buro", "Is part of the BDS staff")
|
||||||
return self.user.username
|
]
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue