forked from DGNum/gestioCOF
Compare commits
26 commits
master
...
aureplop/k
Author | SHA1 | Date | |
---|---|---|---|
|
e3e8608563 | ||
|
02f1e83420 | ||
|
6d90311ae1 | ||
|
a7dbc64e2b | ||
|
94ab754f82 | ||
|
13f01020f7 | ||
|
2b62c3a785 | ||
|
d36a813e15 | ||
|
94f21c062a | ||
|
e39f88991e | ||
|
03a0c58869 | ||
|
aa405d212a | ||
|
c524da22fe | ||
|
09290131d5 | ||
|
40ceaf411a | ||
|
097ee44131 | ||
|
2c76bea1e6 | ||
|
07f1a53532 | ||
|
82582866b4 | ||
|
5502c6876a | ||
|
df7594a105 | ||
|
e6fab703ee | ||
|
c17ed416c4 | ||
|
085a068020 | ||
|
8ea5775d61 | ||
|
ded824bddd |
637 changed files with 64775 additions and 77940 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use nix
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -5,19 +5,12 @@ cof/settings.py
|
||||||
settings.py
|
settings.py
|
||||||
*~
|
*~
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
|
||||||
.vagrant
|
.vagrant
|
||||||
/src
|
/src
|
||||||
media/
|
media/
|
||||||
*.log
|
*.log
|
||||||
.sass-cache/
|
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.coverage
|
|
||||||
|
|
||||||
# PyCharm
|
# PyCharm
|
||||||
.idea
|
.idea
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
# VSCode
|
|
||||||
.vscode/
|
|
||||||
.direnv
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
image: "python:3.7"
|
services:
|
||||||
|
- postgres:latest
|
||||||
|
- redis:latest
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# GestioCOF settings
|
# GestioCOF settings
|
||||||
|
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
|
||||||
DBHOST: "postgres"
|
DBHOST: "postgres"
|
||||||
REDIS_HOST: "redis"
|
REDIS_HOST: "redis"
|
||||||
REDIS_PASSWD: "dummy"
|
REDIS_PASSWD: "dummy"
|
||||||
|
|
||||||
# Cached packages
|
# Cached packages
|
||||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
|
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
|
||||||
|
|
||||||
# postgres service configuration
|
# postgres service configuration
|
||||||
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
||||||
|
@ -17,79 +20,22 @@ variables:
|
||||||
# psql password authentication
|
# psql password authentication
|
||||||
PGPASSWORD: $POSTGRES_PASSWORD
|
PGPASSWORD: $POSTGRES_PASSWORD
|
||||||
|
|
||||||
# apps to check migrations for
|
cache:
|
||||||
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
|
paths:
|
||||||
|
- vendor/python
|
||||||
|
- vendor/pip
|
||||||
|
- vendor/apt
|
||||||
|
|
||||||
.test_template:
|
before_script:
|
||||||
before_script:
|
- mkdir -p vendor/{python,pip,apt}
|
||||||
- mkdir -p vendor/{pip,apt}
|
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
|
||||||
- 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"/' cof/settings/secret_example.py > cof/settings/secret.py
|
||||||
- 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 = "";' cof/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
|
# Remove the old test database if it has not been done yet
|
||||||
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
||||||
- pip install --upgrade -r requirements-prod.txt coverage tblib
|
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
|
||||||
- python --version
|
|
||||||
after_script:
|
|
||||||
- coverage report
|
|
||||||
services:
|
|
||||||
- postgres:11.7
|
|
||||||
- redis:latest
|
|
||||||
cache:
|
|
||||||
key: test
|
|
||||||
paths:
|
|
||||||
- vendor/
|
|
||||||
# For GitLab CI to get coverage from build.
|
|
||||||
# Keep this disabled for now, as it may kill GitLab...
|
|
||||||
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
|
|
||||||
|
|
||||||
coftest:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
extends: .test_template
|
|
||||||
variables:
|
|
||||||
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
|
|
||||||
script:
|
script:
|
||||||
- coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
|
- python manage.py test
|
||||||
|
|
||||||
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
|
|
298
CHANGELOG.md
298
CHANGELOG.md
|
@ -1,298 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
|
|
||||||
2018).
|
|
||||||
|
|
||||||
## Le FUTUR ! (pas prêt pour la prod)
|
|
||||||
|
|
||||||
### Nouveau module de gestion des événements
|
|
||||||
|
|
||||||
- Désormais complet niveau modèles
|
|
||||||
- Export des participants implémenté
|
|
||||||
|
|
||||||
#### TODO
|
|
||||||
|
|
||||||
- Vue de création d'événements ergonomique
|
|
||||||
- Vue d'inscription à un événement **ou** intégration propre dans la vue
|
|
||||||
"inscription d'un nouveau membre"
|
|
||||||
|
|
||||||
### Nouveau module de gestion des clubs
|
|
||||||
|
|
||||||
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
|
|
||||||
adhérents ni des cotisations.
|
|
||||||
|
|
||||||
## TODO Prod
|
|
||||||
|
|
||||||
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
|
|
||||||
|
|
||||||
## Version ??? - ??/??/????
|
|
||||||
|
|
||||||
## Version 0.15.1 - 15/06/2023
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Rattrape les erreurs d'envoi de mail de négatif
|
|
||||||
- Utilise l'adresse chefs pour les envois de négatifs
|
|
||||||
|
|
||||||
## Version 0.15 - 22/05/2023
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Rajoute un formulaire de contact
|
|
||||||
- Rajoute un formulaire de demande de soirée
|
|
||||||
- Désactive les mails d'envoi de négatifs sur les comptes gelés
|
|
||||||
|
|
||||||
## Version 0.14 - 19/05/2023
|
|
||||||
|
|
||||||
- Répare les dépendances en spécifiant toutes les versions
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Répare la gestion des changement d'heure via moment.js
|
|
||||||
|
|
||||||
## Version 0.13 - 19/02/2023
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Rajoute la valeur des inventaires
|
|
||||||
- Résout les problèmes de négatif ne disparaissant pas
|
|
||||||
- Affiche son surnom s'il y en a un
|
|
||||||
- Bugfixes
|
|
||||||
|
|
||||||
## Version 0.12.1 - 03/10/2022
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Fixe un problème de rendu causé par l'agrandissement du menu
|
|
||||||
|
|
||||||
## Version 0.12 - 17/06/2022
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13`
|
|
||||||
- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt
|
|
||||||
|
|
||||||
## Version 0.11 - 26/10/2021
|
|
||||||
|
|
||||||
### COF
|
|
||||||
|
|
||||||
- Répare un problème de rendu sur le wagtail du COF
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- Ajoute de mails de rappels pour les comptes en négatif
|
|
||||||
- La recherche de comptes sur K-Psul remarche normalement
|
|
||||||
- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion
|
|
||||||
- Modification du gel de compte:
|
|
||||||
- on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt")
|
|
||||||
- on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul
|
|
||||||
- les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement
|
|
||||||
- Modification du fonctionnement des négatifs
|
|
||||||
- impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount`
|
|
||||||
- il n'y a plus de limite de temps sur les négatifs
|
|
||||||
- supression des autorisations de négatif
|
|
||||||
- il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge
|
|
||||||
- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites
|
|
||||||
- La supression d'opérations anciennes est réparée
|
|
||||||
|
|
||||||
## Version 0.10 - 18/04/2021
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à
|
|
||||||
la fois.
|
|
||||||
- L'interface indique plus clairement quand on fait une erreur en modifiant un
|
|
||||||
compte.
|
|
||||||
- On supprime la fonction "décalage de balance".
|
|
||||||
- L'accès à l'historique est maintenant limité à 7 jours pour raison de
|
|
||||||
confidentialité. Les chefs/trez peuvent disposer d'une permission
|
|
||||||
supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta.
|
|
||||||
L'accès à son historique personnel n'est pas limité. Les durées sont
|
|
||||||
configurables dans `settings/cof_prod.py`.
|
|
||||||
|
|
||||||
### COF
|
|
||||||
|
|
||||||
- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha
|
|
||||||
au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices
|
|
||||||
|
|
||||||
## Version 0.9 - 06/02/2020
|
|
||||||
|
|
||||||
### COF / BdA
|
|
||||||
|
|
||||||
- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par
|
|
||||||
KDE).
|
|
||||||
- La page d'accueil affiche la date de fermeture des tirages BdA.
|
|
||||||
- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes
|
|
||||||
ses places pour pouvoir revendre.
|
|
||||||
- On s'assure que l'email fourni lors d'une demande de petit cours est valide.
|
|
||||||
|
|
||||||
### BDS
|
|
||||||
|
|
||||||
- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô
|
|
||||||
en modifiant le profil d'un membre du BDS.
|
|
||||||
- Le burô peut exporter la liste de ses membres avec email au format CSV depuis
|
|
||||||
la page d'accueil.
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- On affiche les articles actuellement en vente en premier lors des inventaires
|
|
||||||
et des commandes.
|
|
||||||
- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier
|
|
||||||
inventaire sont affectés.
|
|
||||||
|
|
||||||
## Version 0.8 - 03/12/2020
|
|
||||||
|
|
||||||
### COF
|
|
||||||
|
|
||||||
- La page "Mes places" dans la section BdA indique quelles places sont sur
|
|
||||||
listing.
|
|
||||||
- ergonomie de l'interface admin du BdA : moins d'options inutiles lors de
|
|
||||||
la sélection de participants.
|
|
||||||
- les tirages sont maintenant archivables pour éviter d'avoir encore d'autres
|
|
||||||
options inutiles.
|
|
||||||
- l'autocomplétion dans l'admin BdA est réparée.
|
|
||||||
- Les icones de la page de gestion des petits cours sont (à nouveau) réparées.
|
|
||||||
- On a supprimé la possibilité de modifier les mails automatiques depuis
|
|
||||||
l'interface admin car trop problématique. Faute de mieux, envoyer un mail à
|
|
||||||
KDE pour modifier ces mails.
|
|
||||||
- corrige un crash sporadique sur la page d'inscription au système de petits
|
|
||||||
cours
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- (fix partiel) Empêche la K-Fêt de modifier des données COF (e.g. nom, prénom,
|
|
||||||
username) lors de la création d'un nouveau compte.
|
|
||||||
- Les statistiques de conso globales montrent deux courbes COF / non-COF au
|
|
||||||
lieu de LIQ / sur compte.
|
|
||||||
- Un bug empêchait de fermer manuellement la K-Fêt depuis un compte non
|
|
||||||
privilégié en tapant un mot de passe. C'est corrigé.
|
|
||||||
|
|
||||||
## Version 0.7.2 - 08/09/2020
|
|
||||||
|
|
||||||
- Nouvelle page 404
|
|
||||||
- Correction de bug en K-Fêt : le lien pour créer un nouveau compte exté apparaît
|
|
||||||
à nouveau dans l'autocomplétion
|
|
||||||
|
|
||||||
## Version 0.7.1 - 05/09/2020
|
|
||||||
|
|
||||||
Petits ajustements sur le site du COF :
|
|
||||||
|
|
||||||
- Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et
|
|
||||||
de la page web dans les annuaires (clubs et partenaires).
|
|
||||||
- Corrige un bug d'affichage des adresses emails de clubs
|
|
||||||
|
|
||||||
## Version 0.7 - 29/08/2020
|
|
||||||
|
|
||||||
### GestioBDS
|
|
||||||
|
|
||||||
- Ajout d'un bouton pour supprimer un compte
|
|
||||||
- Le nombre d'adhérent⋅es est affiché sur la page d'accueil
|
|
||||||
- le groupe BDS a les bonnes permissions
|
|
||||||
|
|
||||||
### Site du COF
|
|
||||||
|
|
||||||
- Captcha fonctionnel pour les mailing-listes
|
|
||||||
|
|
||||||
### K-Fêt
|
|
||||||
|
|
||||||
- L'autocomplétion pour la création de compte K-Fêt se lance à 3 caractères seulement,
|
|
||||||
donc est plus rapide.
|
|
||||||
|
|
||||||
## Version 0.6 - 27/07/2020
|
|
||||||
|
|
||||||
Arrivée du BDS !
|
|
||||||
GestioCOF et GestioBDS ont du code en commun mais tournent de façon séparée, les
|
|
||||||
deux bases de données sont distinctes.
|
|
||||||
|
|
||||||
## Version 0.5 - 11/07/2020
|
|
||||||
|
|
||||||
### Problèmes corrigés
|
|
||||||
|
|
||||||
- La recherche d'utilisateurices (COF + K-Fêt) fonctionne de nouveau
|
|
||||||
- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur
|
|
||||||
la page des clubs (nouveau site du COF)
|
|
||||||
- Version mobile plus ergonimique sur le nouveau site du COF
|
|
||||||
- Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus
|
|
||||||
d'erreurs 500 (nouveau site du COF)
|
|
||||||
- L'historique des ventes des articles K-Fêt fonctionne à nouveau
|
|
||||||
- Les montants en K-Fêt sont à nouveau affichés en UKF (et non en €).
|
|
||||||
- Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un
|
|
||||||
spectacle BdA fonctionnent à nouveau.
|
|
||||||
- on ne peut plus compter de consos sur ☠☠☠, ni éditer les comptes spéciaux
|
|
||||||
(LIQ, GNR, ☠☠☠, #13).
|
|
||||||
|
|
||||||
### Nouvelles fonctionnalités
|
|
||||||
|
|
||||||
- On n'affiche que 4 articles sur la pages "nouveautés" (nouveau site du COF)
|
|
||||||
- Plus de traductions sur le nouveau site du COF
|
|
||||||
- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique
|
|
||||||
personnel.
|
|
||||||
- les statistiques K-Fêt remontent à plus d'un an (et le code est simplifié)
|
|
||||||
|
|
||||||
## Version 0.4.1 - 17/01/2020
|
|
||||||
|
|
||||||
- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés
|
|
||||||
aux urls (\#, /...)
|
|
||||||
|
|
||||||
## Version 0.4 - 15/01/2020
|
|
||||||
|
|
||||||
- Corrige un bug d'affichage d'images sur l'interface des petits cours
|
|
||||||
- La page des transferts permet de créer un nombre illimité de transferts en
|
|
||||||
une fois.
|
|
||||||
- Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs
|
|
||||||
- Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil
|
|
||||||
- Certaines opérations sont à nouveau accessibles depuis la session partagée
|
|
||||||
K-Fêt.
|
|
||||||
- Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper
|
|
||||||
- Corrige un crash sur la page des reventes pour les nouveaux participants.
|
|
||||||
- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux
|
|
||||||
|
|
||||||
## Version 0.3.3 - 30/11/2019
|
|
||||||
|
|
||||||
- Corrige un problème de redirection lors de la déconnexion (CAS seulement)
|
|
||||||
- Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF
|
|
||||||
- Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ
|
|
||||||
- Corrige une privilege escalation liée aux sessions partagées en K-Fêt
|
|
||||||
https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240
|
|
||||||
|
|
||||||
## Version 0.3.2 - 04/11/2019
|
|
||||||
|
|
||||||
- Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom
|
|
||||||
|
|
||||||
## Version 0.3.1 - 19/10/2019
|
|
||||||
|
|
||||||
- Bugfix: l'historique des utilisateurices s'affiche à nouveau
|
|
||||||
|
|
||||||
## Version 0.3 - 16/10/2019
|
|
||||||
|
|
||||||
- Comptes extés: lien pour changer son mot de passe sur la page d'accueil
|
|
||||||
- Les utilisateurices non-COF peuvent éditer leur profil
|
|
||||||
- Un peu de pub pour KDEns sur la page d'accueil
|
|
||||||
- Fix erreur 500 sur /bda/revente/<tirage_id>/manage
|
|
||||||
- Si on essaie d'accéder au compte que qqn d'autre on a une 404 (et plus une 403)
|
|
||||||
- On ne peut plus modifier des comptes COF depuis l'interface K-Fêt
|
|
||||||
- Le champ de paiement BdA se fait au niveau des attributions
|
|
||||||
- Affiche un message d'erreur plutôt que de crasher si échec de l'envoi du mail
|
|
||||||
de bienvenue aux nouveaux membres
|
|
||||||
- On peut supprimer des comptes et des articles K-Fêt
|
|
||||||
- Passage à Django2
|
|
||||||
- Dev : on peut désactiver la barre de debug avec une variable shell
|
|
||||||
- Remplace les CSS de Google par des polices de proximité
|
|
||||||
- Passage du site du COF et de la K-Fêt en Wagtail 2.3 et Wagtail-modeltranslation 0.9
|
|
||||||
- Ajoute un lien vers l'administration générale depuis les petits cours
|
|
||||||
- Abandon de l'ancien catalogue BdA (déjà plus utilisé depuis longtemps)
|
|
||||||
- Force l'unicité des logins clipper
|
|
||||||
- Nouveau site du COF en wagtail
|
|
||||||
- Meilleurs affichage des longues listes de spectacles à cocher dans BdA-Revente
|
|
||||||
- Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du
|
|
||||||
COF
|
|
||||||
|
|
||||||
## Version 0.2 - 07/11/2018
|
|
||||||
|
|
||||||
- Corrections de bugs d'interface dans l'inscription aux tirages BdA
|
|
||||||
- On peut annuler une revente à tout moment
|
|
||||||
- Pleiiiiin de tests
|
|
||||||
|
|
||||||
## Version 0.1 - 09/09/2018
|
|
||||||
|
|
||||||
Début de la numérotation des versions, début du changelog
|
|
160
README.md
160
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
|
||||||
|
@ -150,6 +81,55 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
|
||||||
code change, il faut relancer le worker avec `sudo systemctl restart
|
code change, il faut relancer le worker avec `sudo systemctl restart
|
||||||
worker.service` pour visualiser la dernière version du code.
|
worker.service` pour visualiser la dernière version du code.
|
||||||
|
|
||||||
|
|
||||||
|
### Installation manuelle
|
||||||
|
|
||||||
|
Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant,
|
||||||
|
il est fortement conseillé d'utiliser un environnement virtuel pour Python.
|
||||||
|
|
||||||
|
Il vous faudra installer pip, les librairies de développement de python ainsi
|
||||||
|
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
|
||||||
|
Debian et dérivées (Ubuntu, ...) :
|
||||||
|
|
||||||
|
sudo apt-get install python3-pip python3-dev sqlite3
|
||||||
|
|
||||||
|
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
||||||
|
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
||||||
|
(le dossier où se trouve ce README), et créez-le maintenant :
|
||||||
|
|
||||||
|
python3 -m venv venv
|
||||||
|
|
||||||
|
Pour l'activer, il faut faire
|
||||||
|
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
|
dans le même dossier.
|
||||||
|
|
||||||
|
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||||
|
`requirements-devel.txt` :
|
||||||
|
|
||||||
|
pip install -U pip
|
||||||
|
pip install -r requirements-devel.txt
|
||||||
|
|
||||||
|
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
|
||||||
|
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
|
||||||
|
pour profiter de façon transparente des mises à jour du fichier:
|
||||||
|
|
||||||
|
ln -s secret_example.py cof/settings/secret.py
|
||||||
|
|
||||||
|
|
||||||
|
#### Fin d'installation
|
||||||
|
|
||||||
|
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
|
||||||
|
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
|
||||||
|
données bidons bien pratiques pour développer avec la commande suivante :
|
||||||
|
|
||||||
|
bash provisioning/prepare_django.sh
|
||||||
|
|
||||||
|
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
|
||||||
|
|
||||||
|
python manage.py runserver
|
||||||
|
|
||||||
### Mise à jour
|
### Mise à jour
|
||||||
|
|
||||||
Pour mettre à jour les paquets Python, utiliser la commande suivante :
|
Pour mettre à jour les paquets Python, utiliser la commande suivante :
|
||||||
|
@ -161,44 +141,6 @@ 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 est accessible sur le
|
||||||
|
|
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
|
||||||
|
|
1
bda/__init__.py
Normal file
1
bda/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
314
bda/admin.py
Normal file
314
bda/admin.py
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import autocomplete_light
|
||||||
|
from datetime import timedelta
|
||||||
|
from custommail.shortcuts import send_mass_custom_mail
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.db.models import Sum, Count
|
||||||
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.utils import timezone
|
||||||
|
from django import forms
|
||||||
|
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
|
||||||
|
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyMixin(object):
|
||||||
|
readonly_fields_update = ()
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
readonly_fields = super().get_readonly_fields(request, obj)
|
||||||
|
if obj is None:
|
||||||
|
return readonly_fields
|
||||||
|
else:
|
||||||
|
return readonly_fields + self.readonly_fields_update
|
||||||
|
|
||||||
|
|
||||||
|
class ChoixSpectacleInline(admin.TabularInline):
|
||||||
|
model = ChoixSpectacle
|
||||||
|
sortable_field_name = "priority"
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionTabularAdminForm(forms.ModelForm):
|
||||||
|
listing = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
spectacles = Spectacle.objects.select_related('location')
|
||||||
|
if self.listing is not None:
|
||||||
|
spectacles = spectacles.filter(listing=self.listing)
|
||||||
|
self.fields['spectacle'].queryset = spectacles
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
|
||||||
|
listing = False
|
||||||
|
|
||||||
|
|
||||||
|
class WithListingAttributionTabularAdminForm(AttributionTabularAdminForm):
|
||||||
|
listing = True
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionInline(admin.TabularInline):
|
||||||
|
model = Attribution
|
||||||
|
extra = 0
|
||||||
|
listing = None
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
if self.listing is not None:
|
||||||
|
qs = qs.filter(spectacle__listing=self.listing)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class WithListingAttributionInline(AttributionInline):
|
||||||
|
exclude = ('given', )
|
||||||
|
form = WithListingAttributionTabularAdminForm
|
||||||
|
listing = True
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutListingAttributionInline(AttributionInline):
|
||||||
|
form = WithoutListingAttributionTabularAdminForm
|
||||||
|
listing = False
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['choicesrevente'].queryset = (
|
||||||
|
Spectacle.objects
|
||||||
|
.select_related('location')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
|
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return Participant.objects.annotate(nb_places=Count('attributions'),
|
||||||
|
total=Sum('attributions__price'))
|
||||||
|
|
||||||
|
def nb_places(self, obj):
|
||||||
|
return obj.nb_places
|
||||||
|
nb_places.admin_order_field = "nb_places"
|
||||||
|
nb_places.short_description = "Nombre de places"
|
||||||
|
|
||||||
|
def total(self, obj):
|
||||||
|
tot = obj.total
|
||||||
|
if tot:
|
||||||
|
return "%.02f €" % tot
|
||||||
|
else:
|
||||||
|
return "0 €"
|
||||||
|
total.admin_order_field = "total"
|
||||||
|
total.short_description = "Total à payer"
|
||||||
|
list_display = ("user", "nb_places", "total", "paid", "paymenttype",
|
||||||
|
"tirage")
|
||||||
|
list_filter = ("paid", "tirage")
|
||||||
|
search_fields = ('user__username', 'user__first_name', 'user__last_name')
|
||||||
|
actions = ['send_attribs', ]
|
||||||
|
actions_on_bottom = True
|
||||||
|
list_per_page = 400
|
||||||
|
readonly_fields = ("total",)
|
||||||
|
readonly_fields_update = ('user', 'tirage')
|
||||||
|
form = ParticipantAdminForm
|
||||||
|
|
||||||
|
def send_attribs(self, request, queryset):
|
||||||
|
datatuple = []
|
||||||
|
for member in queryset.all():
|
||||||
|
attribs = member.attributions.all()
|
||||||
|
context = {'member': member.user}
|
||||||
|
shortname = ""
|
||||||
|
if len(attribs) == 0:
|
||||||
|
shortname = "bda-attributions-decus"
|
||||||
|
else:
|
||||||
|
shortname = "bda-attributions"
|
||||||
|
context['places'] = attribs
|
||||||
|
print(context)
|
||||||
|
datatuple.append((shortname, context, "bda@ens.fr",
|
||||||
|
[member.user.email]))
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
count = len(queryset.all())
|
||||||
|
if count == 1:
|
||||||
|
message_bit = "1 membre a"
|
||||||
|
plural = ""
|
||||||
|
else:
|
||||||
|
message_bit = "%d membres ont" % count
|
||||||
|
plural = "s"
|
||||||
|
self.message_user(request, "%s été informé%s avec succès."
|
||||||
|
% (message_bit, plural))
|
||||||
|
send_attribs.short_description = "Envoyer les résultats par mail"
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if 'spectacle' in self.fields:
|
||||||
|
self.fields['spectacle'].queryset = (
|
||||||
|
Spectacle.objects
|
||||||
|
.select_related('location')
|
||||||
|
)
|
||||||
|
if 'participant' in self.fields:
|
||||||
|
self.fields['participant'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(AttributionAdminForm, self).clean()
|
||||||
|
participant = cleaned_data.get("participant")
|
||||||
|
spectacle = cleaned_data.get("spectacle")
|
||||||
|
if participant and spectacle:
|
||||||
|
if participant.tirage != spectacle.tirage:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Erreur : le participant et le spectacle n'appartiennent"
|
||||||
|
"pas au même tirage")
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
|
def paid(self, obj):
|
||||||
|
return obj.participant.paid
|
||||||
|
paid.short_description = 'A payé'
|
||||||
|
paid.boolean = True
|
||||||
|
list_display = ("id", "spectacle", "participant", "given", "paid")
|
||||||
|
search_fields = ('spectacle__title', 'participant__user__username',
|
||||||
|
'participant__user__first_name',
|
||||||
|
'participant__user__last_name')
|
||||||
|
form = AttributionAdminForm
|
||||||
|
readonly_fields_update = ('spectacle', 'participant')
|
||||||
|
|
||||||
|
|
||||||
|
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
||||||
|
form = autocomplete_light.modelform_factory(ChoixSpectacle, exclude=[])
|
||||||
|
|
||||||
|
def tirage(self, obj):
|
||||||
|
return obj.participant.tirage
|
||||||
|
list_display = ("participant", "tirage", "spectacle", "priority",
|
||||||
|
"double_choice")
|
||||||
|
list_filter = ("double_choice", "participant__tirage")
|
||||||
|
search_fields = ('participant__user__username',
|
||||||
|
'participant__user__first_name',
|
||||||
|
'participant__user__last_name',
|
||||||
|
'spectacle__title')
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteInline(admin.TabularInline):
|
||||||
|
model = Quote
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleAdmin(admin.ModelAdmin):
|
||||||
|
inlines = [QuoteInline]
|
||||||
|
model = Spectacle
|
||||||
|
list_display = ("title", "date", "tirage", "location", "slots", "price",
|
||||||
|
"listing")
|
||||||
|
list_filter = ("location", "tirage",)
|
||||||
|
search_fields = ("title", "location__name")
|
||||||
|
readonly_fields = ("rappel_sent", )
|
||||||
|
|
||||||
|
|
||||||
|
class TirageAdmin(admin.ModelAdmin):
|
||||||
|
model = Tirage
|
||||||
|
list_display = ("title", "ouverture", "fermeture", "active",
|
||||||
|
"enable_do_tirage")
|
||||||
|
readonly_fields = ("tokens", )
|
||||||
|
list_filter = ("active", )
|
||||||
|
search_fields = ("title", )
|
||||||
|
|
||||||
|
|
||||||
|
class SalleAdmin(admin.ModelAdmin):
|
||||||
|
model = Salle
|
||||||
|
search_fields = ('name', 'address')
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleReventeAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['answered_mail'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
self.fields['seller'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
self.fields['soldTo'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Administration des reventes de spectacles
|
||||||
|
"""
|
||||||
|
model = SpectacleRevente
|
||||||
|
|
||||||
|
def spectacle(self, obj):
|
||||||
|
"""
|
||||||
|
Raccourci vers le spectacle associé à la revente.
|
||||||
|
"""
|
||||||
|
return obj.attribution.spectacle
|
||||||
|
|
||||||
|
list_display = ("spectacle", "seller", "date", "soldTo")
|
||||||
|
raw_id_fields = ("attribution",)
|
||||||
|
readonly_fields = ("date_tirage",)
|
||||||
|
search_fields = ['attribution__spectacle__title',
|
||||||
|
'seller__user__username',
|
||||||
|
'seller__user__first_name',
|
||||||
|
'seller__user__last_name']
|
||||||
|
|
||||||
|
actions = ['transfer', 'reinit']
|
||||||
|
actions_on_bottom = True
|
||||||
|
form = SpectacleReventeAdminForm
|
||||||
|
|
||||||
|
def transfer(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Effectue le transfert des reventes pour lesquels on connaît l'acheteur.
|
||||||
|
"""
|
||||||
|
reventes = queryset.exclude(soldTo__isnull=True).all()
|
||||||
|
count = reventes.count()
|
||||||
|
for revente in reventes:
|
||||||
|
attrib = revente.attribution
|
||||||
|
attrib.participant = revente.soldTo
|
||||||
|
attrib.save()
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"%d attribution%s %s été transférée%s avec succès." % (
|
||||||
|
count, pluralize(count),
|
||||||
|
pluralize(count, "a,ont"), pluralize(count))
|
||||||
|
)
|
||||||
|
transfer.short_description = "Transférer les reventes sélectionnées"
|
||||||
|
|
||||||
|
def reinit(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Réinitialise les reventes.
|
||||||
|
"""
|
||||||
|
count = queryset.count()
|
||||||
|
for revente in queryset.filter(
|
||||||
|
attribution__spectacle__date__gte=timezone.now()):
|
||||||
|
revente.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(
|
||||||
|
request,
|
||||||
|
"%d attribution%s %s été réinitialisée%s avec succès." % (
|
||||||
|
count, pluralize(count),
|
||||||
|
pluralize(count, "a,ont"), pluralize(count))
|
||||||
|
)
|
||||||
|
reinit.short_description = "Réinitialiser les reventes sélectionnées"
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CategorieSpectacle)
|
||||||
|
admin.site.register(Spectacle, SpectacleAdmin)
|
||||||
|
admin.site.register(Salle, SalleAdmin)
|
||||||
|
admin.site.register(Participant, ParticipantAdmin)
|
||||||
|
admin.site.register(Attribution, AttributionAdmin)
|
||||||
|
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
|
||||||
|
admin.site.register(Tirage, TirageAdmin)
|
||||||
|
admin.site.register(SpectacleRevente, SpectacleReventeAdmin)
|
106
bda/algorithm.py
Normal file
106
bda/algorithm.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
# -*- 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
|
||||||
|
|
||||||
|
|
||||||
|
class Algorithm(object):
|
||||||
|
|
||||||
|
shows = None
|
||||||
|
ranks = None
|
||||||
|
origranks = None
|
||||||
|
double = None
|
||||||
|
|
||||||
|
def __init__(self, shows, members, choices):
|
||||||
|
"""Initialisation :
|
||||||
|
- on aggrège toutes les demandes pour chaque spectacle dans
|
||||||
|
show.requests
|
||||||
|
- on crée des tables de demandes pour chaque personne, afin de
|
||||||
|
pouvoir modifier les rankings"""
|
||||||
|
self.max_group = 2*max(choice.priority for choice in choices)
|
||||||
|
self.shows = []
|
||||||
|
showdict = {}
|
||||||
|
for show in shows:
|
||||||
|
show.nrequests = 0
|
||||||
|
showdict[show] = show
|
||||||
|
show.requests = []
|
||||||
|
self.shows.append(show)
|
||||||
|
self.ranks = {}
|
||||||
|
self.origranks = {}
|
||||||
|
self.choices = {}
|
||||||
|
next_rank = {}
|
||||||
|
member_shows = {}
|
||||||
|
for member in members:
|
||||||
|
self.ranks[member] = {}
|
||||||
|
self.choices[member] = {}
|
||||||
|
next_rank[member] = 1
|
||||||
|
member_shows[member] = {}
|
||||||
|
for choice in choices:
|
||||||
|
member = choice.participant
|
||||||
|
if choice.spectacle in member_shows[member]:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
member_shows[member][choice.spectacle] = True
|
||||||
|
showdict[choice.spectacle].requests.append(member)
|
||||||
|
showdict[choice.spectacle].nrequests += 2 if choice.double else 1
|
||||||
|
self.ranks[member][choice.spectacle] = next_rank[member]
|
||||||
|
next_rank[member] += 2 if choice.double else 1
|
||||||
|
self.choices[member][choice.spectacle] = choice
|
||||||
|
for member in members:
|
||||||
|
self.origranks[member] = dict(self.ranks[member])
|
||||||
|
|
||||||
|
def IncrementRanks(self, member, currank, increment=1):
|
||||||
|
for show in self.ranks[member]:
|
||||||
|
if self.ranks[member][show] > currank:
|
||||||
|
self.ranks[member][show] -= increment
|
||||||
|
|
||||||
|
def appendResult(self, l, member, show):
|
||||||
|
l.append((member,
|
||||||
|
self.ranks[member][show],
|
||||||
|
self.origranks[member][show],
|
||||||
|
self.choices[member][show].double))
|
||||||
|
|
||||||
|
def __call__(self, seed):
|
||||||
|
random.seed(seed)
|
||||||
|
results = []
|
||||||
|
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots,
|
||||||
|
reverse=True)
|
||||||
|
for show in shows:
|
||||||
|
# On regroupe tous les gens ayant le même rang
|
||||||
|
groups = dict([(i, []) for i in range(1, self.max_group + 1)])
|
||||||
|
for member in show.requests:
|
||||||
|
if self.ranks[member][show] == 0:
|
||||||
|
raise RuntimeError(member, show.title)
|
||||||
|
groups[self.ranks[member][show]].append(member)
|
||||||
|
# On passe à l'attribution
|
||||||
|
winners = []
|
||||||
|
losers = []
|
||||||
|
for i in range(1, self.max_group + 1):
|
||||||
|
group = list(groups[i])
|
||||||
|
random.shuffle(group)
|
||||||
|
for member in group:
|
||||||
|
if self.choices[member][show].double: # double
|
||||||
|
if len(winners) + 1 < show.slots:
|
||||||
|
self.appendResult(winners, member, show)
|
||||||
|
self.appendResult(winners, member, show)
|
||||||
|
elif not self.choices[member][show].autoquit \
|
||||||
|
and len(winners) < show.slots:
|
||||||
|
self.appendResult(winners, member, show)
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
else:
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
self.IncrementRanks(member, i, 2)
|
||||||
|
else: # simple
|
||||||
|
if len(winners) < show.slots:
|
||||||
|
self.appendResult(winners, member, show)
|
||||||
|
else:
|
||||||
|
self.appendResult(losers, member, show)
|
||||||
|
self.IncrementRanks(member, i)
|
||||||
|
results.append((show, winners, losers))
|
||||||
|
return results
|
18
bda/autocomplete_light_registry.py
Normal file
18
bda/autocomplete_light_registry.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import autocomplete_light
|
||||||
|
|
||||||
|
from bda.models import Participant, Spectacle
|
||||||
|
|
||||||
|
autocomplete_light.register(
|
||||||
|
Participant, search_fields=('user__username', 'user__first_name',
|
||||||
|
'user__last_name'),
|
||||||
|
autocomplete_js_attributes={'placeholder': 'participant...'})
|
||||||
|
|
||||||
|
autocomplete_light.register(
|
||||||
|
Spectacle, search_fields=('title', ),
|
||||||
|
autocomplete_js_attributes={'placeholder': 'spectacle...'})
|
117
bda/forms.py
Normal file
117
bda/forms.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.forms.models import BaseInlineFormSet
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bda.models import Attribution, Spectacle
|
||||||
|
|
||||||
|
|
||||||
|
class InscriptionInlineFormSet(BaseInlineFormSet):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# self.instance is a Participant object
|
||||||
|
tirage = self.instance.tirage
|
||||||
|
|
||||||
|
# set once for all "spectacle" field choices
|
||||||
|
# - restrict choices to the spectacles of this tirage
|
||||||
|
# - force_choices avoid many db requests
|
||||||
|
spectacles = tirage.spectacle_set.select_related('location')
|
||||||
|
choices = [(sp.pk, str(sp)) for sp in spectacles]
|
||||||
|
self.force_choices('spectacle', choices)
|
||||||
|
|
||||||
|
def force_choices(self, name, choices):
|
||||||
|
"""Set choices of a field.
|
||||||
|
|
||||||
|
As ModelChoiceIterator (default use to get choices of a
|
||||||
|
ModelChoiceField), it appends an empty selection if requested.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for form in self.forms:
|
||||||
|
field = form.fields[name]
|
||||||
|
if field.empty_label is not None:
|
||||||
|
field.choices = [('', field.empty_label)] + choices
|
||||||
|
else:
|
||||||
|
field.choices = choices
|
||||||
|
|
||||||
|
|
||||||
|
class TokenForm(forms.Form):
|
||||||
|
token = forms.CharField(widget=forms.widgets.Textarea())
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
return "%s" % str(obj.spectacle)
|
||||||
|
|
||||||
|
|
||||||
|
class ResellForm(forms.Form):
|
||||||
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label='',
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super(ResellForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['attributions'].queryset = (
|
||||||
|
participant.attribution_set
|
||||||
|
.filter(spectacle__date__gte=timezone.now())
|
||||||
|
.exclude(revente__seller=participant)
|
||||||
|
.select_related('spectacle', 'spectacle__location',
|
||||||
|
'participant__user')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnulForm(forms.Form):
|
||||||
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label='',
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super(AnnulForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['attributions'].queryset = (
|
||||||
|
participant.attribution_set
|
||||||
|
.filter(spectacle__date__gte=timezone.now(),
|
||||||
|
revente__isnull=False,
|
||||||
|
revente__notif_sent=False,
|
||||||
|
revente__soldTo__isnull=True)
|
||||||
|
.select_related('spectacle', 'spectacle__location',
|
||||||
|
'participant__user')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InscriptionReventeForm(forms.Form):
|
||||||
|
spectacles = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Spectacle.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
def __init__(self, tirage, *args, **kwargs):
|
||||||
|
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['spectacles'].queryset = (
|
||||||
|
tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
.filter(date__gte=timezone.now())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoldForm(forms.Form):
|
||||||
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label='',
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super(SoldForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['attributions'].queryset = (
|
||||||
|
participant.attribution_set
|
||||||
|
.filter(revente__isnull=False,
|
||||||
|
revente__soldTo__isnull=False)
|
||||||
|
.exclude(revente__soldTo=participant)
|
||||||
|
.select_related('spectacle', 'spectacle__location',
|
||||||
|
'participant__user')
|
||||||
|
)
|
107
bda/management/commands/loadbdadevdata.py
Normal file
107
bda/management/commands/loadbdadevdata.py
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
"""
|
||||||
|
Crée deux tirages de test et y inscrit les utilisateurs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from gestioncof.management.base import MyBaseCommand
|
||||||
|
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
|
||||||
|
from bda.views import do_tirage
|
||||||
|
|
||||||
|
|
||||||
|
# Où sont stockés les fichiers json
|
||||||
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
'data')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(MyBaseCommand):
|
||||||
|
help = "Crée deux tirages de test et y inscrit les utilisateurs."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# ---
|
||||||
|
# Tirages
|
||||||
|
# ---
|
||||||
|
|
||||||
|
Tirage.objects.all().delete()
|
||||||
|
Tirage.objects.bulk_create([
|
||||||
|
Tirage(
|
||||||
|
title="Tirage de test 1",
|
||||||
|
ouverture=timezone.now()-timezone.timedelta(days=7),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
active=True
|
||||||
|
),
|
||||||
|
Tirage(
|
||||||
|
title="Tirage de test 2",
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now()+timezone.timedelta(days=60),
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
])
|
||||||
|
tirages = Tirage.objects.all()
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Salles
|
||||||
|
# ---
|
||||||
|
|
||||||
|
locations = self.from_json('locations.json', DATA_DIR, Salle)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Spectacles
|
||||||
|
# ---
|
||||||
|
|
||||||
|
def show_callback(show):
|
||||||
|
"""
|
||||||
|
Assigne un tirage, une date et un lieu à un spectacle et décide si
|
||||||
|
les places sont sur listing.
|
||||||
|
"""
|
||||||
|
show.tirage = random.choice(tirages)
|
||||||
|
show.listing = bool(random.randint(0, 1))
|
||||||
|
show.date = (
|
||||||
|
show.tirage.fermeture
|
||||||
|
+ timezone.timedelta(days=random.randint(60, 90))
|
||||||
|
)
|
||||||
|
show.location = random.choice(locations)
|
||||||
|
return show
|
||||||
|
shows = self.from_json(
|
||||||
|
'shows.json', DATA_DIR, Spectacle, show_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Inscriptions
|
||||||
|
# ---
|
||||||
|
|
||||||
|
self.stdout.write("Inscription des utilisateurs aux tirages")
|
||||||
|
ChoixSpectacle.objects.all().delete()
|
||||||
|
choices = []
|
||||||
|
for user in User.objects.filter(profile__is_cof=True):
|
||||||
|
for tirage in tirages:
|
||||||
|
part, _ = Participant.objects.get_or_create(
|
||||||
|
user=user,
|
||||||
|
tirage=tirage
|
||||||
|
)
|
||||||
|
shows = random.sample(
|
||||||
|
list(tirage.spectacle_set.all()),
|
||||||
|
tirage.spectacle_set.count() // 2
|
||||||
|
)
|
||||||
|
for (rank, show) in enumerate(shows):
|
||||||
|
choices.append(ChoixSpectacle(
|
||||||
|
participant=part,
|
||||||
|
spectacle=show,
|
||||||
|
priority=rank + 1,
|
||||||
|
double_choice=random.choice(
|
||||||
|
['1', 'double', 'autoquit']
|
||||||
|
)
|
||||||
|
))
|
||||||
|
ChoixSpectacle.objects.bulk_create(choices)
|
||||||
|
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# On lance le premier tirage
|
||||||
|
# ---
|
||||||
|
|
||||||
|
self.stdout.write("Lancement du premier tirage")
|
||||||
|
do_tirage(tirages[0], "dummy_token")
|
43
bda/management/commands/manage_reventes.py
Normal file
43
bda/management/commands/manage_reventes.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Gestion en ligne de commande des reventes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from bda.models import SpectacleRevente
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Envoie les mails de notification et effectue " \
|
||||||
|
"les tirages au sort des reventes"
|
||||||
|
leave_locale_alone = True
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
now = timezone.now()
|
||||||
|
reventes = SpectacleRevente.objects.all()
|
||||||
|
for revente in reventes:
|
||||||
|
# Check si < 24h
|
||||||
|
if (revente.attribution.spectacle.date <=
|
||||||
|
revente.date + timedelta(days=1)) and \
|
||||||
|
now >= revente.date + timedelta(minutes=15) and \
|
||||||
|
not revente.notif_sent:
|
||||||
|
self.stdout.write(str(now))
|
||||||
|
revente.mail_shotgun()
|
||||||
|
self.stdout.write("Mail de disponibilité immédiate envoyé")
|
||||||
|
# Check si délai de retrait dépassé
|
||||||
|
elif (now >= revente.date + timedelta(hours=1) and
|
||||||
|
not revente.notif_sent):
|
||||||
|
self.stdout.write(str(now))
|
||||||
|
revente.send_notif()
|
||||||
|
self.stdout.write("Mail d'inscription à une revente envoyé")
|
||||||
|
# Check si tirage à faire
|
||||||
|
elif (now >= revente.date_tirage and
|
||||||
|
not revente.tirage_done):
|
||||||
|
self.stdout.write(str(now))
|
||||||
|
revente.tirage()
|
||||||
|
self.stdout.write("Tirage effectué, mails envoyés")
|
33
bda/management/commands/sendrappels.py
Normal file
33
bda/management/commands/sendrappels.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Gestion en ligne de commande des mails de rappel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from bda.models import Spectacle
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Envoie les mails de rappel des spectacles dont la date ' \
|
||||||
|
'approche.\nNe renvoie pas les mails déjà envoyés.'
|
||||||
|
leave_locale_alone = True
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
now = timezone.now()
|
||||||
|
delay = timedelta(days=4)
|
||||||
|
shows = Spectacle.objects \
|
||||||
|
.filter(date__range=(now, now+delay)) \
|
||||||
|
.filter(tirage__active=True) \
|
||||||
|
.filter(rappel_sent__isnull=True) \
|
||||||
|
.all()
|
||||||
|
for show in shows:
|
||||||
|
show.send_rappel()
|
||||||
|
self.stdout.write(
|
||||||
|
'Mails de rappels pour %s envoyés avec succès.' % show)
|
||||||
|
if not shows:
|
||||||
|
self.stdout.write('Aucun mail à envoyer.')
|
26
bda/management/data/locations.json
Normal file
26
bda/management/data/locations.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Cour\u00f4",
|
||||||
|
"address": "45 rue d'Ulm, cour\u00f4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "K-F\u00eat",
|
||||||
|
"address": "45 rue d'Ulm, escalier C, niveau -1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Th\u00e9\u00e2tre",
|
||||||
|
"address": "45 rue d'Ulm, escalier C, niveau -1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cours Pasteur",
|
||||||
|
"address": "45 rue d'Ulm, cours pasteur"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Salle des actes",
|
||||||
|
"address": "45 rue d'Ulm, escalier A, niveau 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Amphi Rataud",
|
||||||
|
"address": "45 rue d'Ulm, NIR, niveau PB"
|
||||||
|
}
|
||||||
|
]
|
100
bda/management/data/shows.json
Normal file
100
bda/management/data/shows.json
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "Jazz / Funk",
|
||||||
|
"title": "Un super concert",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Debout",
|
||||||
|
"slots": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Homemade",
|
||||||
|
"title": "Une super pi\u00e8ce",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Assises",
|
||||||
|
"slots": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Plein air, soleil, bonne musique",
|
||||||
|
"title": "Concert pour la f\u00eate de la musique",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "Debout, attention \u00e0 la fontaine",
|
||||||
|
"slots": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Sous le regard s\u00e9v\u00e8re de Louis Pasteur",
|
||||||
|
"title": "Op\u00e9ra sans d\u00e9cors",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "Assis sur l'herbe",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Buffet \u00e0 la fin",
|
||||||
|
"title": "Concert Trouv\u00e8re",
|
||||||
|
"price": 20.0,
|
||||||
|
"slots_description": "Assises",
|
||||||
|
"slots": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Vive les maths",
|
||||||
|
"title": "Dessin \u00e0 la craie sur tableau noir",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Assises, tablette pour prendre des notes",
|
||||||
|
"slots": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Une pi\u00e8ce \u00e0 un personnage",
|
||||||
|
"title": "D\u00e9cors, d\u00e9montage en musique",
|
||||||
|
"price": 0.0,
|
||||||
|
"slots_description": "Assises",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Annulera, annulera pas\u00a0?",
|
||||||
|
"title": "La Nuit",
|
||||||
|
"price": 27.0,
|
||||||
|
"slots_description": "",
|
||||||
|
"slots": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Le boum fait sa carte blanche",
|
||||||
|
"title": "Turbomix",
|
||||||
|
"price": 10.0,
|
||||||
|
"slots_description": "Debout les mains en l'air",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Unique repr\u00e9sentation",
|
||||||
|
"title": "Carinettes et trombone",
|
||||||
|
"price": 15.0,
|
||||||
|
"slots_description": "Chaises ikea",
|
||||||
|
"slots": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Suivi d'une jam session",
|
||||||
|
"title": "Percussion sur rondins",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "B\u00fbches",
|
||||||
|
"slots": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "\u00c9preuve sportive et artistique",
|
||||||
|
"title": "Bassin aux ernests, nage libre",
|
||||||
|
"price": 5.0,
|
||||||
|
"slots_description": "Humides",
|
||||||
|
"slots": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Sonore",
|
||||||
|
"title": "Chant du barde",
|
||||||
|
"price": 13.0,
|
||||||
|
"slots_description": "Ne venez pas",
|
||||||
|
"slots": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Cocorico",
|
||||||
|
"title": "Chant du coq",
|
||||||
|
"price": 4.0,
|
||||||
|
"slots_description": "bancs",
|
||||||
|
"slots": 15
|
||||||
|
}
|
||||||
|
]
|
108
bda/migrations/0001_initial.py
Normal file
108
bda/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Attribution',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('given', models.BooleanField(default=False, verbose_name='Donn\xe9e')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ChoixSpectacle',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('priority', models.PositiveIntegerField(verbose_name=b'Priorit\xc3\xa9')),
|
||||||
|
('double_choice', models.CharField(default=b'1', max_length=10, verbose_name=b'Nombre de places', choices=[(b'1', b'1 place'), (b'autoquit', b'2 places si possible, 1 sinon'), (b'double', b'2 places sinon rien')])),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('priority',),
|
||||||
|
'verbose_name': 'voeu',
|
||||||
|
'verbose_name_plural': 'voeux',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Participant',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('paid', models.BooleanField(default=False, verbose_name='A pay\xe9')),
|
||||||
|
('paymenttype', models.CharField(blank=True, max_length=6, verbose_name='Moyen de paiement', choices=[(b'cash', 'Cash'), (b'cb', b'CB'), (b'cheque', 'Ch\xe8que'), (b'autre', 'Autre')])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Salle',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name=b'Nom')),
|
||||||
|
('address', models.TextField(verbose_name=b'Adresse')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Spectacle',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('title', models.CharField(max_length=300, verbose_name=b'Titre')),
|
||||||
|
('date', models.DateTimeField(verbose_name=b'Date & heure')),
|
||||||
|
('description', models.TextField(verbose_name=b'Description', blank=True)),
|
||||||
|
('slots_description', models.TextField(verbose_name=b'Description des places', blank=True)),
|
||||||
|
('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)),
|
||||||
|
('slots', models.IntegerField(verbose_name=b'Places')),
|
||||||
|
('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')),
|
||||||
|
('location', models.ForeignKey(to='bda.Salle')),
|
||||||
|
],
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='choixspectacle',
|
||||||
|
name='participant',
|
||||||
|
field=models.ForeignKey(to='bda.Participant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='choixspectacle',
|
||||||
|
name='spectacle',
|
||||||
|
field=models.ForeignKey(related_name='participants', to='bda.Spectacle'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='attribution',
|
||||||
|
name='participant',
|
||||||
|
field=models.ForeignKey(to='bda.Participant'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='attribution',
|
||||||
|
name='spectacle',
|
||||||
|
field=models.ForeignKey(related_name='attribues', to='bda.Spectacle'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='choixspectacle',
|
||||||
|
unique_together=set([('participant', 'spectacle')]),
|
||||||
|
),
|
||||||
|
]
|
84
bda/migrations/0002_add_tirage.py
Normal file
84
bda/migrations/0002_add_tirage.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
def fill_tirage_fields(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Create a `Tirage` to fill new field `tirage` of `Participant`
|
||||||
|
and `Spectacle` already existing.
|
||||||
|
"""
|
||||||
|
Participant = apps.get_model("bda", "Participant")
|
||||||
|
Spectacle = apps.get_model("bda", "Spectacle")
|
||||||
|
Tirage = apps.get_model("bda", "Tirage")
|
||||||
|
|
||||||
|
# These querysets only contains instances not linked to any `Tirage`.
|
||||||
|
participants = Participant.objects.filter(tirage=None)
|
||||||
|
spectacles = Spectacle.objects.filter(tirage=None)
|
||||||
|
|
||||||
|
if not participants.count() and not spectacles.count():
|
||||||
|
# No need to create a "trash" tirage.
|
||||||
|
return
|
||||||
|
|
||||||
|
tirage = Tirage.objects.create(
|
||||||
|
title="Tirage de test (migration)",
|
||||||
|
active=False,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
participants.update(tirage=tirage)
|
||||||
|
spectacles.update(tirage=tirage)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tirage',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('title', models.CharField(max_length=300, verbose_name=b'Titre')),
|
||||||
|
('ouverture', models.DateTimeField(verbose_name=b"Date et heure d'ouverture du tirage")),
|
||||||
|
('fermeture', models.DateTimeField(verbose_name=b'Date et heure de fermerture du tirage')),
|
||||||
|
('token', models.TextField(verbose_name=b'Graine du tirage', blank=True)),
|
||||||
|
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='participant',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
# 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),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='tirage',
|
||||||
|
field=models.ForeignKey(to='bda.Tirage', null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='participant',
|
||||||
|
name='tirage',
|
||||||
|
field=models.ForeignKey(to='bda.Tirage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='tirage',
|
||||||
|
field=models.ForeignKey(to='bda.Tirage'),
|
||||||
|
),
|
||||||
|
]
|
24
bda/migrations/0003_update_tirage_and_spectacle.py
Normal file
24
bda/migrations/0003_update_tirage_and_spectacle.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0002_add_tirage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='price',
|
||||||
|
field=models.FloatField(verbose_name=b"Prix d'une place"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='active',
|
||||||
|
field=models.BooleanField(default=False, verbose_name=b'Tirage actif'),
|
||||||
|
),
|
||||||
|
]
|
25
bda/migrations/0004_mails-rappel.py
Normal file
25
bda/migrations/0004_mails-rappel.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0003_update_tirage_and_spectacle'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='listing',
|
||||||
|
field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='rappel_sent',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True),
|
||||||
|
),
|
||||||
|
]
|
29
bda/migrations/0005_encoding.py
Normal file
29
bda/migrations/0005_encoding.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0004_mails-rappel'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='choixspectacle',
|
||||||
|
name='priority',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='Priorit\xe9'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='priority',
|
||||||
|
field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='rappel_sent',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True),
|
||||||
|
),
|
||||||
|
]
|
35
bda/migrations/0006_add_tirage_switch.py
Normal file
35
bda/migrations/0006_add_tirage_switch.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
|
Tirage = apps.get_model("bda", "Tirage")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for tirage in Tirage.objects.using(db_alias).all():
|
||||||
|
if tirage.tokens:
|
||||||
|
tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % (
|
||||||
|
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
|
||||||
|
tirage.tokens)
|
||||||
|
tirage.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0005_encoding'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField('tirage', 'token', 'tokens'),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='enable_do_tirage',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
|
]
|
89
bda/migrations/0007_extends_spectacle.py
Normal file
89
bda/migrations/0007_extends_spectacle.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0006_add_tirage_switch'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CategorieSpectacle',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False,
|
||||||
|
auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Nom',
|
||||||
|
unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Cat\xe9gorie',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Quote',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False,
|
||||||
|
auto_created=True, primary_key=True)),
|
||||||
|
('text', models.TextField(verbose_name='Citation')),
|
||||||
|
('author', models.CharField(max_length=200,
|
||||||
|
verbose_name='Auteur')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='spectacle',
|
||||||
|
options={'ordering': ('date', 'title'),
|
||||||
|
'verbose_name': 'Spectacle'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='priority',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='ext_link',
|
||||||
|
field=models.CharField(
|
||||||
|
max_length=500,
|
||||||
|
verbose_name='Lien vers le site du spectacle',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(upload_to='imgs/shows/', null=True,
|
||||||
|
verbose_name='Image', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='enable_do_tirage',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Le tirage peut \xeatre lanc\xe9'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='tokens',
|
||||||
|
field=models.TextField(verbose_name='Graine(s) du tirage',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle',
|
||||||
|
null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='vips',
|
||||||
|
field=models.TextField(verbose_name='Personnalit\xe9s',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='quote',
|
||||||
|
name='spectacle',
|
||||||
|
field=models.ForeignKey(to='bda.Spectacle'),
|
||||||
|
),
|
||||||
|
]
|
103
bda/migrations/0008_py3.py
Normal file
103
bda/migrations/0008_py3.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0007_extends_spectacle'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='choixspectacle',
|
||||||
|
name='double_choice',
|
||||||
|
field=models.CharField(
|
||||||
|
verbose_name='Nombre de places',
|
||||||
|
choices=[('1', '1 place'),
|
||||||
|
('autoquit', '2 places si possible, 1 sinon'),
|
||||||
|
('double', '2 places sinon rien')],
|
||||||
|
max_length=10, default='1'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='participant',
|
||||||
|
name='paymenttype',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[('cash', 'Cash'), ('cb', 'CB'),
|
||||||
|
('cheque', 'Chèque'), ('autre', 'Autre')],
|
||||||
|
max_length=6, verbose_name='Moyen de paiement'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salle',
|
||||||
|
name='address',
|
||||||
|
field=models.TextField(verbose_name='Adresse'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salle',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(verbose_name='Nom', max_length=300),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='date',
|
||||||
|
field=models.DateTimeField(verbose_name='Date & heure'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(verbose_name='Description', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='listing',
|
||||||
|
field=models.BooleanField(
|
||||||
|
verbose_name='Les places sont sur listing'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='price',
|
||||||
|
field=models.FloatField(verbose_name="Prix d'une place"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='slots',
|
||||||
|
field=models.IntegerField(verbose_name='Places'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='slots_description',
|
||||||
|
field=models.TextField(verbose_name='Description des places',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(verbose_name='Titre', max_length=300),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='active',
|
||||||
|
field=models.BooleanField(verbose_name='Tirage actif',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='fermeture',
|
||||||
|
field=models.DateTimeField(
|
||||||
|
verbose_name='Date et heure de fermerture du tirage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='ouverture',
|
||||||
|
field=models.DateTimeField(
|
||||||
|
verbose_name="Date et heure d'ouverture du tirage"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(verbose_name='Titre', max_length=300),
|
||||||
|
),
|
||||||
|
]
|
66
bda/migrations/0009_revente.py
Normal file
66
bda/migrations/0009_revente.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0008_py3'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SpectacleRevente',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, primary_key=True,
|
||||||
|
auto_created=True, verbose_name='ID')),
|
||||||
|
('date', models.DateTimeField(
|
||||||
|
verbose_name='Date de mise en vente',
|
||||||
|
default=django.utils.timezone.now)),
|
||||||
|
('notif_sent', models.BooleanField(
|
||||||
|
verbose_name='Notification envoyée', default=False)),
|
||||||
|
('tirage_done', models.BooleanField(
|
||||||
|
verbose_name='Tirage effectué', default=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Revente',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='participant',
|
||||||
|
name='choicesrevente',
|
||||||
|
field=models.ManyToManyField(to='bda.Spectacle',
|
||||||
|
related_name='subscribed',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectaclerevente',
|
||||||
|
name='answered_mail',
|
||||||
|
field=models.ManyToManyField(to='bda.Participant',
|
||||||
|
related_name='wanted',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectaclerevente',
|
||||||
|
name='attribution',
|
||||||
|
field=models.OneToOneField(to='bda.Attribution',
|
||||||
|
related_name='revente'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectaclerevente',
|
||||||
|
name='seller',
|
||||||
|
field=models.ForeignKey(to='bda.Participant',
|
||||||
|
verbose_name='Vendeur',
|
||||||
|
related_name='original_shows'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectaclerevente',
|
||||||
|
name='soldTo',
|
||||||
|
field=models.ForeignKey(to='bda.Participant',
|
||||||
|
verbose_name='Vendue à', null=True,
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
]
|
33
bda/migrations/0010_spectaclerevente_shotgun.py
Normal file
33
bda/migrations/0010_spectaclerevente_shotgun.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
|
SpectacleRevente = apps.get_model("bda", "SpectacleRevente")
|
||||||
|
|
||||||
|
for revente in SpectacleRevente.objects.all():
|
||||||
|
is_expired = timezone.now() > revente.date_tirage()
|
||||||
|
is_direct = (revente.attribution.spectacle.date >= revente.date and
|
||||||
|
timezone.now() > revente.date + timedelta(minutes=15))
|
||||||
|
revente.shotgun = is_expired or is_direct
|
||||||
|
revente.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0009_revente'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='spectaclerevente',
|
||||||
|
name='shotgun',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Disponible imm\xe9diatement'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||||
|
]
|
22
bda/migrations/0011_tirage_appear_catalogue.py
Normal file
22
bda/migrations/0011_tirage_appear_catalogue.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0010_spectaclerevente_shotgun'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='appear_catalogue',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Tirage à afficher dans le catalogue'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
350
bda/models.py
Normal file
350
bda/models.py
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
import random
|
||||||
|
from datetime import timedelta
|
||||||
|
from custommail.shortcuts import send_mass_custom_mail
|
||||||
|
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone, formats
|
||||||
|
|
||||||
|
|
||||||
|
def get_generic_user():
|
||||||
|
generic, _ = User.objects.get_or_create(
|
||||||
|
username="bda_generic",
|
||||||
|
defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"}
|
||||||
|
)
|
||||||
|
return generic
|
||||||
|
|
||||||
|
|
||||||
|
class Tirage(models.Model):
|
||||||
|
title = models.CharField("Titre", max_length=300)
|
||||||
|
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
|
||||||
|
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
||||||
|
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
||||||
|
active = models.BooleanField("Tirage actif", default=False)
|
||||||
|
appear_catalogue = models.BooleanField(
|
||||||
|
"Tirage à afficher dans le catalogue",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s" % (self.title, formats.localize(
|
||||||
|
timezone.template_localtime(self.fermeture)))
|
||||||
|
|
||||||
|
|
||||||
|
class Salle(models.Model):
|
||||||
|
name = models.CharField("Nom", max_length=300)
|
||||||
|
address = models.TextField("Adresse")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class CategorieSpectacle(models.Model):
|
||||||
|
name = models.CharField('Nom', max_length=100, unique=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Catégorie"
|
||||||
|
|
||||||
|
|
||||||
|
class Spectacle(models.Model):
|
||||||
|
title = models.CharField("Titre", max_length=300)
|
||||||
|
category = models.ForeignKey(CategorieSpectacle, blank=True, null=True)
|
||||||
|
date = models.DateTimeField("Date & heure")
|
||||||
|
location = models.ForeignKey(Salle)
|
||||||
|
vips = models.TextField('Personnalités', blank=True)
|
||||||
|
description = models.TextField("Description", blank=True)
|
||||||
|
slots_description = models.TextField("Description des places", blank=True)
|
||||||
|
image = models.ImageField('Image', blank=True, null=True,
|
||||||
|
upload_to='imgs/shows/')
|
||||||
|
ext_link = models.CharField('Lien vers le site du spectacle', blank=True,
|
||||||
|
max_length=500)
|
||||||
|
price = models.FloatField("Prix d'une place")
|
||||||
|
slots = models.IntegerField("Places")
|
||||||
|
tirage = models.ForeignKey(Tirage)
|
||||||
|
listing = models.BooleanField("Les places sont sur listing")
|
||||||
|
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Spectacle"
|
||||||
|
ordering = ("date", "title",)
|
||||||
|
|
||||||
|
def timestamp(self):
|
||||||
|
return "%d" % calendar.timegm(self.date.utctimetuple())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s, %s, %.02f€" % (
|
||||||
|
self.title,
|
||||||
|
formats.localize(timezone.template_localtime(self.date)),
|
||||||
|
self.location,
|
||||||
|
self.price
|
||||||
|
)
|
||||||
|
|
||||||
|
def getImgUrl(self):
|
||||||
|
"""
|
||||||
|
Cette fonction permet d'obtenir l'URL de l'image, si elle existe
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.image.url
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_rappel(self):
|
||||||
|
"""
|
||||||
|
Envoie un mail de rappel à toutes les personnes qui ont une place pour
|
||||||
|
ce spectacle.
|
||||||
|
"""
|
||||||
|
# On récupère la liste des participants + le BdA
|
||||||
|
members = list(
|
||||||
|
User.objects
|
||||||
|
.filter(participant__attributions=self)
|
||||||
|
.annotate(nb_attr=Count("id")).order_by()
|
||||||
|
)
|
||||||
|
bda_generic = get_generic_user()
|
||||||
|
bda_generic.nb_attr = 1
|
||||||
|
members.append(bda_generic)
|
||||||
|
# On écrit un mail personnalisé à chaque participant
|
||||||
|
datatuple = [(
|
||||||
|
'bda-rappel',
|
||||||
|
{'member': member, "nb_attr": member.nb_attr, 'show': self},
|
||||||
|
settings.MAIL_DATA['rappels']['FROM'],
|
||||||
|
[member.email])
|
||||||
|
for member in members
|
||||||
|
]
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
# On enregistre le fait que l'envoi a bien eu lieu
|
||||||
|
self.rappel_sent = timezone.now()
|
||||||
|
self.save()
|
||||||
|
# On renvoie la liste des destinataires
|
||||||
|
return members
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_past(self):
|
||||||
|
return self.date < timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
class Quote(models.Model):
|
||||||
|
spectacle = models.ForeignKey(Spectacle)
|
||||||
|
text = models.TextField('Citation')
|
||||||
|
author = models.CharField('Auteur', max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
PAYMENT_TYPES = (
|
||||||
|
("cash", "Cash"),
|
||||||
|
("cb", "CB"),
|
||||||
|
("cheque", "Chèque"),
|
||||||
|
("autre", "Autre"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Participant(models.Model):
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
choices = models.ManyToManyField(Spectacle,
|
||||||
|
through="ChoixSpectacle",
|
||||||
|
related_name="chosen_by")
|
||||||
|
attributions = models.ManyToManyField(Spectacle,
|
||||||
|
through="Attribution",
|
||||||
|
related_name="attributed_to")
|
||||||
|
paid = models.BooleanField("A payé", default=False)
|
||||||
|
paymenttype = models.CharField("Moyen de paiement",
|
||||||
|
max_length=6, choices=PAYMENT_TYPES,
|
||||||
|
blank=True)
|
||||||
|
tirage = models.ForeignKey(Tirage)
|
||||||
|
choicesrevente = models.ManyToManyField(Spectacle,
|
||||||
|
related_name="subscribed",
|
||||||
|
blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s" % (self.user, self.tirage.title)
|
||||||
|
|
||||||
|
DOUBLE_CHOICES = (
|
||||||
|
("1", "1 place"),
|
||||||
|
("autoquit", "2 places si possible, 1 sinon"),
|
||||||
|
("double", "2 places sinon rien"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChoixSpectacle(models.Model):
|
||||||
|
participant = models.ForeignKey(Participant)
|
||||||
|
spectacle = models.ForeignKey(Spectacle, related_name="participants")
|
||||||
|
priority = models.PositiveIntegerField("Priorité")
|
||||||
|
double_choice = models.CharField("Nombre de places",
|
||||||
|
default="1", choices=DOUBLE_CHOICES,
|
||||||
|
max_length=10)
|
||||||
|
|
||||||
|
def get_double(self):
|
||||||
|
return self.double_choice != "1"
|
||||||
|
double = property(get_double)
|
||||||
|
|
||||||
|
def get_autoquit(self):
|
||||||
|
return self.double_choice == "autoquit"
|
||||||
|
autoquit = property(get_autoquit)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Vœux de %s pour %s" % (
|
||||||
|
self.participant.user.get_full_name(),
|
||||||
|
self.spectacle.title)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("priority",)
|
||||||
|
unique_together = (("participant", "spectacle",),)
|
||||||
|
verbose_name = "voeu"
|
||||||
|
verbose_name_plural = "voeux"
|
||||||
|
|
||||||
|
|
||||||
|
class Attribution(models.Model):
|
||||||
|
participant = models.ForeignKey(Participant)
|
||||||
|
spectacle = models.ForeignKey(Spectacle, related_name="attribues")
|
||||||
|
given = models.BooleanField("Donnée", default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s -- %s, %s" % (self.participant.user, self.spectacle.title,
|
||||||
|
self.spectacle.date)
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleRevente(models.Model):
|
||||||
|
attribution = models.OneToOneField(Attribution,
|
||||||
|
related_name="revente")
|
||||||
|
date = models.DateTimeField("Date de mise en vente",
|
||||||
|
default=timezone.now)
|
||||||
|
answered_mail = models.ManyToManyField(Participant,
|
||||||
|
related_name="wanted",
|
||||||
|
blank=True)
|
||||||
|
seller = models.ForeignKey(Participant,
|
||||||
|
related_name="original_shows",
|
||||||
|
verbose_name="Vendeur")
|
||||||
|
soldTo = models.ForeignKey(Participant, blank=True, null=True,
|
||||||
|
verbose_name="Vendue à")
|
||||||
|
|
||||||
|
notif_sent = models.BooleanField("Notification envoyée",
|
||||||
|
default=False)
|
||||||
|
tirage_done = models.BooleanField("Tirage effectué",
|
||||||
|
default=False)
|
||||||
|
shotgun = models.BooleanField("Disponible immédiatement",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_tirage(self):
|
||||||
|
"""Renvoie la date du tirage au sort de la revente."""
|
||||||
|
# L'acheteur doit être connu au plus 12h avant le spectacle
|
||||||
|
remaining_time = (self.attribution.spectacle.date
|
||||||
|
- self.date - timedelta(hours=13))
|
||||||
|
# Au minimum, on attend 2 jours avant le tirage
|
||||||
|
delay = min(remaining_time, timedelta(days=2))
|
||||||
|
# Le vendeur a aussi 1h pour changer d'avis
|
||||||
|
return self.date + delay + timedelta(hours=1)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s -- %s" % (self.seller,
|
||||||
|
self.attribution.spectacle.title)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Revente"
|
||||||
|
|
||||||
|
def send_notif(self):
|
||||||
|
"""
|
||||||
|
Envoie une notification pour indiquer la mise en vente d'une place sur
|
||||||
|
BdA-Revente à tous les intéressés.
|
||||||
|
"""
|
||||||
|
inscrits = self.attribution.spectacle.subscribed.select_related('user')
|
||||||
|
datatuple = [(
|
||||||
|
'bda-revente',
|
||||||
|
{
|
||||||
|
'member': participant.user,
|
||||||
|
'show': self.attribution.spectacle,
|
||||||
|
'revente': self,
|
||||||
|
'site': Site.objects.get_current()
|
||||||
|
},
|
||||||
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
|
[participant.user.email])
|
||||||
|
for participant in inscrits
|
||||||
|
]
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
self.notif_sent = True
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def mail_shotgun(self):
|
||||||
|
"""
|
||||||
|
Envoie un mail à toutes les personnes intéréssées par le spectacle pour
|
||||||
|
leur indiquer qu'il est désormais disponible au shotgun.
|
||||||
|
"""
|
||||||
|
inscrits = self.attribution.spectacle.subscribed.select_related('user')
|
||||||
|
datatuple = [(
|
||||||
|
'bda-shotgun',
|
||||||
|
{
|
||||||
|
'member': participant.user,
|
||||||
|
'show': self.attribution.spectacle,
|
||||||
|
'site': Site.objects.get_current(),
|
||||||
|
},
|
||||||
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
|
[participant.user.email])
|
||||||
|
for participant in inscrits
|
||||||
|
]
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
self.notif_sent = True
|
||||||
|
# Flag inutile, sauf si l'horloge interne merde
|
||||||
|
self.tirage_done = True
|
||||||
|
self.shotgun = True
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def tirage(self):
|
||||||
|
"""
|
||||||
|
Lance le tirage au sort associé à la revente. Un gagnant est choisi
|
||||||
|
parmis les personnes intéressées par le spectacle. Les personnes sont
|
||||||
|
ensuites prévenues par mail du résultat du tirage.
|
||||||
|
"""
|
||||||
|
inscrits = list(self.answered_mail.all())
|
||||||
|
spectacle = self.attribution.spectacle
|
||||||
|
seller = self.seller
|
||||||
|
|
||||||
|
if inscrits:
|
||||||
|
# Envoie un mail au gagnant et au vendeur
|
||||||
|
winner = random.choice(inscrits)
|
||||||
|
self.soldTo = winner
|
||||||
|
datatuple = []
|
||||||
|
context = {
|
||||||
|
'acheteur': winner.user,
|
||||||
|
'vendeur': seller.user,
|
||||||
|
'show': spectacle,
|
||||||
|
}
|
||||||
|
datatuple.append((
|
||||||
|
'bda-revente-winner',
|
||||||
|
context,
|
||||||
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
|
[winner.user.email],
|
||||||
|
))
|
||||||
|
datatuple.append((
|
||||||
|
'bda-revente-seller',
|
||||||
|
context,
|
||||||
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
|
[seller.user.email]
|
||||||
|
))
|
||||||
|
|
||||||
|
# Envoie un mail aux perdants
|
||||||
|
for inscrit in inscrits:
|
||||||
|
if inscrit != winner:
|
||||||
|
new_context = dict(context)
|
||||||
|
new_context['acheteur'] = inscrit.user
|
||||||
|
datatuple.append((
|
||||||
|
'bda-revente-loser',
|
||||||
|
new_context,
|
||||||
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
|
[inscrit.user.email]
|
||||||
|
))
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
# Si personne ne veut de la place, elle part au shotgun
|
||||||
|
else:
|
||||||
|
self.shotgun = True
|
||||||
|
self.tirage_done = True
|
||||||
|
self.save()
|
48
bda/static/css/bda.css
Normal file
48
bda/static/css/bda.css
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
form#tokenform {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
form#tokenform textarea {
|
||||||
|
font-size: 2em;
|
||||||
|
width: 350px;
|
||||||
|
height: 200px;
|
||||||
|
font-family: 'Droif Serif', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wft ?
|
||||||
|
input {
|
||||||
|
width: 400px;
|
||||||
|
font-size: 2em;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
ul.losers {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.losers li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.details {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 0px solid black;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.attribresult {
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectacle-passe {
|
||||||
|
opacity:0.5;
|
||||||
|
}
|
BIN
bda/static/fonts/josefinsans.ttf
Normal file
BIN
bda/static/fonts/josefinsans.ttf
Normal file
Binary file not shown.
28
bda/templates/bda-attrib-extra.html
Normal file
28
bda/templates/bda-attrib-extra.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "bda-attrib.html" %}
|
||||||
|
|
||||||
|
{% block extracontent %}
|
||||||
|
|
||||||
|
<h2>Attributions (détails)</h2>
|
||||||
|
<h3 class="horizontal-title">Token :</h3>
|
||||||
|
<pre>{{ token }}</pre>
|
||||||
|
<h3 class="horizontal-title">Placés : {{ total_slots }} ; Déçus : {{ total_losers }}</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{% for member, shows in members2 %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ member.user.get_full_name }}</td>
|
||||||
|
<td>{{ member.user.email }}</td>
|
||||||
|
<td>Total: {{ member.total }}€</td>
|
||||||
|
<td style="width: 120px;"></td>
|
||||||
|
</tr>
|
||||||
|
{% for show in shows %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ show }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
49
bda/templates/bda-attrib.html
Normal file
49
bda/templates/bda-attrib.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Attributions</h2>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<p class="success">Pour raison de sécurité, le lancement du tirage
|
||||||
|
a été désactivé. Vous pouvez le réactiver dans
|
||||||
|
l'<a href="{% url "admin:index" %}">interface admin</a></p>
|
||||||
|
|
||||||
|
<h3 class="horizontal-title">Token :</h3>
|
||||||
|
<pre>{{ token }}</pre>
|
||||||
|
<h3 class="horizontal-title">Placés : {{ total_slots }} ; Déçus : {{ total_losers }}</h3>
|
||||||
|
{% if user.profile.is_buro %}<h3 class="horizontal-title">Déficit total: {{ total_deficit }} €, Opéra: {{ opera_deficit }} €, Attribué: {{ total_sold }} €</h3>{% endif %}
|
||||||
|
<h3 class="horizontal-title">Temps de calcul : {{ duration|floatformat }}s</h3>
|
||||||
|
|
||||||
|
{% for show, members, losers in results %}
|
||||||
|
<div class="attribresult">
|
||||||
|
<h3 class="horizontal-title">{{ show.title }} - {{ show.date }} @ {{ show.location }}</h3>
|
||||||
|
<p>
|
||||||
|
<strong>{{ show.nrequests }} demandes pour {{ show.slots }} places</strong>
|
||||||
|
{{ show.price }}€ par place{% if user.profile.is_buro and show.nrequests < show.slots %}, {{ show.deficit }}€ de déficit{% endif %}
|
||||||
|
</p>
|
||||||
|
Places :
|
||||||
|
<ul>
|
||||||
|
{% for member, rank, origrank, double in members %}
|
||||||
|
<li>{{ member.user.get_full_name }} <span class="details">(souhait {{ origrank }} — rang {{ rank }})</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
Déçus :
|
||||||
|
{% if not losers %}/{% else %}
|
||||||
|
<ul class="losers">
|
||||||
|
{% for member, rank, origrank, double in losers %}
|
||||||
|
{% if not forloop.first %} ; {% endif %}
|
||||||
|
<li>{{ member.user.get_full_name }} <span class="details">(souhait {{ origrank }} — rang {{ rank }})</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% block extracontent %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
7
bda/templates/bda-emails.html
Normal file
7
bda/templates/bda-emails.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>{{ spectacle }}</h2>
|
||||||
|
<textarea style="width: 100%; height: 100px; margin-top: 10px;">
|
||||||
|
{% for attrib in spectacle.attribues.all %}{{ attrib.participant.user.email }}, {% endfor %}</textarea>
|
||||||
|
{% endblock %}
|
9
bda/templates/bda-interested.html
Normal file
9
bda/templates/bda-interested.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Inscription à une revente</h2>
|
||||||
|
<p class="success"> Votre inscription a bien été enregistrée !</p>
|
||||||
|
<p>Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}.
|
||||||
|
|
||||||
|
{% endblock %}
|
6
bda/templates/bda-no-revente.html
Normal file
6
bda/templates/bda-no-revente.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>BdA-Revente</h2>
|
||||||
|
<p>Il n'y a plus de places en revente pour ce spectacle, désolé !</p>
|
||||||
|
{% endblock %}
|
6
bda/templates/bda-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 %}
|
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 %}
|
8
bda/templates/bda-success.html
Normal file
8
bda/templates/bda-success.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Revente de place</h2>
|
||||||
|
<p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
|
||||||
|
{% endblock %}
|
13
bda/templates/bda-token.html
Normal file
13
bda/templates/bda-token.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Tirage au sort du BdA</h2>
|
||||||
|
<form action="" method="post" id="tokenform">
|
||||||
|
{% csrf_token %}
|
||||||
|
<strong>La graine :</strong>
|
||||||
|
<div>
|
||||||
|
{{ form.token }}
|
||||||
|
</div>
|
||||||
|
<input type="submit" onsubmit="return confirm('Voulez vous lancer le Tirage maintenant ?\n\nCECI REMETTRA À ZÉRO TOUTES LES DONNÉES si le tirage a déjà été lancé.')" value="Go" />
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
8
bda/templates/bda-unpaid.html
Normal file
8
bda/templates/bda-unpaid.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Impayés</h2>
|
||||||
|
<textarea style="width: 100%; height: 100px; margin-top: 10px;">
|
||||||
|
{% for participant in unpaid %}{{ participant.user.email }}, {% endfor %}</textarea>
|
||||||
|
<h3>Total : {{ unpaid|length }}</h3>
|
||||||
|
{% endblock %}
|
13
bda/templates/bda-wrongtime.html
Normal file
13
bda/templates/bda-wrongtime.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Nope</h2>
|
||||||
|
{% if revente.shotgun %}
|
||||||
|
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
|
||||||
|
|
||||||
|
<p>Si personne n'était intéressé, elle est maintenant disponible
|
||||||
|
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
|
||||||
|
{% else %}
|
||||||
|
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
53
bda/templates/bda/etat-places.html
Normal file
53
bda/templates/bda/etat-places.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>État des inscriptions BdA</h2>
|
||||||
|
<table class="table table-striped etat-bda">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Places</th>
|
||||||
|
<th data-sort="int">Demandes</th>
|
||||||
|
<th data-sort="float">Ratio</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for spectacle in spectacles %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ spectacle.title }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.slots }}">{{ spectacle.slots }} places</td>
|
||||||
|
<td data-sort-value="{{ spectacle.total }}">{{ spectacle.total }} demandes</td>
|
||||||
|
<td data-sort-value="{{ spectacle.ratio |stringformat:".3f" }}"
|
||||||
|
class={% if spectacle.ratio < 1.0 %}
|
||||||
|
"greenratio"
|
||||||
|
{% else %}
|
||||||
|
{% if spectacle.ratio < 2.5 %}
|
||||||
|
"orangeratio"
|
||||||
|
{% else %}
|
||||||
|
"redratio"
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}>
|
||||||
|
{{ spectacle.ratio |floatformat }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<span class="bda-prix">
|
||||||
|
Total : {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
|
||||||
|
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
|
||||||
|
</span>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("table.etat-bda").stupidtable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
41
bda/templates/bda/inscription-formset.html
Normal file
41
bda/templates/bda/inscription-formset.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% load bootstrap %}
|
||||||
|
{{ formset.non_form_errors.as_ul }}
|
||||||
|
<table id="bda_formset" class="form table">
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% for form in formset.forms %}
|
||||||
|
{% if forloop.first %}
|
||||||
|
<thead><tr>
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||||
|
<th class="bda-field-{{ field.name }}">{{ field.label|safe|capfirst }}</th>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<th><sup>1</sup></th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody class="bda_formset_content">
|
||||||
|
{% endif %}
|
||||||
|
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||||
|
<td class="bda-field-{{ field.name }}">
|
||||||
|
{% if forloop.first %}
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{{ field.errors.as_ul }}
|
||||||
|
{{ field | bootstrap }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<td class="tools-cell"><div class="tools">
|
||||||
|
<a href="javascript://" class="glyphicon glyphicon-sort drag-btn" title="Déplacer"></a>
|
||||||
|
<input type="checkbox" name="{{ form.DELETE.html_name }}" style="display: none;" />
|
||||||
|
<input type="hidden" name="{{ form.priority.html_name }}" style="{{ form.priority.value }}" />
|
||||||
|
<a href="javascript://" class="glyphicon glyphicon-remove remove-btn" title="Supprimer"></a>
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
115
bda/templates/bda/inscription-tirage.html
Normal file
115
bda/templates/bda/inscription-tirage.html
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
|
||||||
|
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
var django = {
|
||||||
|
"jQuery": jQuery.noConflict(true)
|
||||||
|
};
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
cloneMore = function(selector, type) {
|
||||||
|
var newElement = $(selector).clone(true);
|
||||||
|
var total = $('#id_' + type + '-TOTAL_FORMS').val();
|
||||||
|
newElement.find(':input').each(function() {
|
||||||
|
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
|
||||||
|
var id = 'id_' + name;
|
||||||
|
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
|
||||||
|
});
|
||||||
|
newElement.find('label').each(function() {
|
||||||
|
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
|
||||||
|
$(this).attr('for', newFor);
|
||||||
|
});
|
||||||
|
total++;
|
||||||
|
$('#id_' + type + '-TOTAL_FORMS').val(total);
|
||||||
|
$(selector).after(newElement);
|
||||||
|
}
|
||||||
|
deleteButtonHandler = function(elem) {
|
||||||
|
elem.bind("click", function() {
|
||||||
|
var deleteInput = $(this).prev().prev(),
|
||||||
|
form = $(this).parents(".dynamic-form").first();
|
||||||
|
// callback
|
||||||
|
// toggle options.predeleteCssClass and toggle checkbox
|
||||||
|
if (form.hasClass("has_original")) {
|
||||||
|
form.toggleClass("predelete");
|
||||||
|
if (deleteInput.attr("checked")) {
|
||||||
|
deleteInput.attr("checked", false);
|
||||||
|
} else {
|
||||||
|
deleteInput.attr("checked", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// callback
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$(document).ready(function($) {
|
||||||
|
deleteButtonHandler($("table#bda_formset tbody.bda_formset_content").find("a.remove-btn"));
|
||||||
|
$("table#bda_formset tbody.bda_formset_content").sortable({
|
||||||
|
handle: "a.drag-btn",
|
||||||
|
items: "tr",
|
||||||
|
axis: "y",
|
||||||
|
appendTo: 'body',
|
||||||
|
forceHelperSize: true,
|
||||||
|
placeholder: 'ui-sortable-placeholder',
|
||||||
|
forcePlaceholderSize: true,
|
||||||
|
containment: 'form#bda_form',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
start: function(evt, ui) {
|
||||||
|
var template = "",
|
||||||
|
len = ui.item.children("td").length;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
template += "<td style='height:" + (ui.item.outerHeight() + 12 ) + "px' class='placeholder-cell'> </td>"
|
||||||
|
}
|
||||||
|
template += "";
|
||||||
|
ui.placeholder.html(template);
|
||||||
|
},
|
||||||
|
stop: function(evt, ui) {
|
||||||
|
// Toggle div.table twice to remove webkits border-spacing bug
|
||||||
|
$("table#bda_formset").toggle().toggle();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$("#bda_form").bind("submit", function(){
|
||||||
|
var sortable_field_name = "priority";
|
||||||
|
var i = 1;
|
||||||
|
$(".bda_formset_content").find("tr").each(function(){
|
||||||
|
var fields = $(this).find("td :input[value]"),
|
||||||
|
select = $(this).find("td select");
|
||||||
|
if (select.val() && fields.serialize()) {
|
||||||
|
$(this).find("input[name$='"+sortable_field_name+"']").val(i);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
})(django.jQuery);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2 class="no-bottom-margin">Inscription au tirage au sort du BdA</h2>
|
||||||
|
<form class="form-horizontal" id="bda_form" method="post" action="{% url 'bda-tirage-inscription' tirage.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% include "bda/inscription-formset.html" %}
|
||||||
|
<div class="inscription-bottom">
|
||||||
|
<span class="bda-prix">Prix total actuel : {{ total_price }}€</span>
|
||||||
|
<div class="pull-right">
|
||||||
|
<input type="button" class="btn btn-default" value="Ajouter un autre vœu" id="add_more">
|
||||||
|
<script>
|
||||||
|
django.jQuery('#add_more').click(function() {
|
||||||
|
cloneMore('tbody.bda_formset_content tr:last-child', 'choixspectacle_set');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
|
||||||
|
<input type="submit" class="btn btn-primary" value="Enregistrer" />
|
||||||
|
</div>
|
||||||
|
<p class="footnotes">
|
||||||
|
<sup>1</sup>: cette liste de vœux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque vœu.<br />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
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 %}
|
48
bda/templates/bda/mails-rappel.html
Normal file
48
bda/templates/bda/mails-rappel.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Mails de rappels</h2>
|
||||||
|
{% if sent %}
|
||||||
|
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
|
||||||
|
<ul>
|
||||||
|
{% for member in members %}
|
||||||
|
<li>{{ member.get_full_name }} ({{ member.email }})</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<h3>Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }} ?</h3>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="empty-form">
|
||||||
|
{% if not sent %}
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="pull-right">
|
||||||
|
<input class="btn btn-primary" type="submit" value="Envoyer" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr \>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<em>Note :</em> le template de ce mail peut être modifié à
|
||||||
|
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr \>
|
||||||
|
|
||||||
|
<h3>Forme des mails</h3>
|
||||||
|
|
||||||
|
<h4>Une seule place</h4>
|
||||||
|
{% for part in exemple_mail_1place %}
|
||||||
|
<pre>{{ part }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<h4>Deux places</h4>
|
||||||
|
{% for part in exemple_mail_2places %}
|
||||||
|
<pre>{{ part }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
73
bda/templates/bda/participants.html
Normal file
73
bda/templates/bda/participants.html
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>{{ spectacle }}</h2>
|
||||||
|
<table class='table table-striped etat-bda'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="string">Nom</th>
|
||||||
|
<th data-sort="int">Places</th>
|
||||||
|
<th data-sort="string">Adresse Mail</th>
|
||||||
|
<th data-sort="string">Payé</th>
|
||||||
|
<th data-sort="string">Donné</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for participant in participants %}
|
||||||
|
<tr>
|
||||||
|
<td data-sort-value="{{ participan.name}}">{{participant.name}}</td>
|
||||||
|
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
|
||||||
|
<td data-sort-value="{{participant.email}}">{{participant.email}}</td>
|
||||||
|
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
|
||||||
|
{% if participant.paid %}Oui{% else %}Non{%endif%}
|
||||||
|
</td>
|
||||||
|
<td data-sort-value="{{participant.given}}" class={%if participant.given == participant.nb_places %}"greenratio"
|
||||||
|
{%elif participant.given == 0%}"redratio"
|
||||||
|
{%else%}"orangeratio"
|
||||||
|
{%endif%}>
|
||||||
|
{% if participant.given == participant.nb_places %}Oui
|
||||||
|
{% elif participant.given == 0 %}Non
|
||||||
|
{% else %}{{participant.given}}/{{participant.nb_places}}
|
||||||
|
{%endif%}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
|
||||||
|
<pre id="export-mails" style="display:none">{% spaceless %}
|
||||||
|
{% for participant in participants %}{{ participant.email }}, {% endfor %}
|
||||||
|
{% endspaceless %}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
|
||||||
|
<pre id="export-salle" style="display:none">{% spaceless %}
|
||||||
|
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
|
||||||
|
{% endfor %}
|
||||||
|
{% endspaceless %}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
|
||||||
|
<script>
|
||||||
|
function toggle(id) {
|
||||||
|
var pre = document.getElementById(id) ;
|
||||||
|
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("table.etat-bda").stupidtable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
14
bda/templates/bda/resume-inscription-tirage.html
Normal file
14
bda/templates/bda/resume-inscription-tirage.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
{% if choices %}
|
||||||
|
<h3>Vos vœux:</h3>
|
||||||
|
<ol>
|
||||||
|
{% for choice in choices %}
|
||||||
|
<li>{{ choice.spectacle }}{% if choice.double %} (deux places{% if autoquit %}, abandon automatique{% endif %}){% endif %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% else %}
|
||||||
|
<h3>Vous n'avez enregistré aucun vœu pour le tirage au sort</h3>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
24
bda/templates/bda/resume_places.html
Normal file
24
bda/templates/bda/resume_places.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2><strong>Places attribuées</strong></h3>
|
||||||
|
{% if places %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
{% for place in places %}
|
||||||
|
<tr>
|
||||||
|
<td>{{place.spectacle.title}}</td>
|
||||||
|
<td>{{place.spectacle.location}}</td>
|
||||||
|
<td>{{place.spectacle.date}}</td>
|
||||||
|
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
|
||||||
|
<br/>
|
||||||
|
<p>Ne manque pas un spectacle avec le
|
||||||
|
<a href="{% url "gestioncof.views.calendar" %}">calendrier
|
||||||
|
automatique !</a></p>
|
||||||
|
{% else %}
|
||||||
|
<h3>Vous n'avez aucune place :(</h3>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
56
bda/templates/bda/reventes.html
Normal file
56
bda/templates/bda/reventes.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Revente de place</h2>
|
||||||
|
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
|
||||||
|
|
||||||
|
{% if resellform.attributions %}
|
||||||
|
<h3>Places non revendues</h3>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{resellform|bootstrap}}
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
{% if annul_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 attrib in annul_attributions %}
|
||||||
|
<li>{{attrib.tag}} {{attrib.choice_label}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for attrib in overdue %}
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" style="visibility:hidden">
|
||||||
|
{{attrib.spectacle}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if annul_attributions %}
|
||||||
|
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
{% if sold_attributions %}
|
||||||
|
<h3>Places revendues</h3>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{soldform|bootstrap}}
|
||||||
|
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
|
||||||
|
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}
|
||||||
|
<p>Plus de reventes possibles !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% 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>
|
20
bda/templates/revente-confirm.html
Normal file
20
bda/templates/revente-confirm.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
|
||||||
|
{%block realcontent %}
|
||||||
|
<h2>Rachat d'une place</h2>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<pre>
|
||||||
|
Bonjour !
|
||||||
|
|
||||||
|
Je souhaiterais racheter ta place pour {{spectacle.title}} le {{spectacle.date}} ({{spectacle.location}}) à {{spectacle.price}}€.
|
||||||
|
Contacte-moi si tu es toujours intéressé-e !
|
||||||
|
|
||||||
|
{{user.get_full_name}} ({{user.email}})
|
||||||
|
</pre>
|
||||||
|
<input type="submit" class="btn btn-primary pull-right" value="Envoyer">
|
||||||
|
</form>
|
||||||
|
<p class="bda-prix">Note : ce mail sera envoyé à une personne au hasard revendant sa place.</p>
|
||||||
|
{%endblock%}
|
56
bda/templates/spectacle_list.html
Normal file
56
bda/templates/spectacle_list.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2><strong>{{tirage_name}}</strong></h2>
|
||||||
|
<h3>Liste des spectacles</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover etat-bda">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="float">Prix</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for spectacle in object_list %}
|
||||||
|
<tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
|
||||||
|
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
|
||||||
|
<td data-sort-value="{{ spectacle.timestamp }}"">{{ spectacle.date }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
|
||||||
|
{{ spectacle.price |floatformat }}€
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function(){
|
||||||
|
$("table.etat-bda").stupidtable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$(".clickable-row").click(function() {
|
||||||
|
window.document.location = $(this).data("href");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3> Exports </h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
|
||||||
|
<li><a href="{% url 'bda-descriptions' tirage_id %}">Lien vers les descriptions des spectacles, à utiliser dans une page wordpress</a>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
10
bda/templates/tirage-failed.html
Normal file
10
bda/templates/tirage-failed.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Raté, le tirage ne peut pas être lancé !</h2>
|
||||||
|
|
||||||
|
<p>Soit les inscriptions ne sont en pas encore fermées, soit le lancement du
|
||||||
|
tirage est désactivé. Si vous savez ce que vous faites, vous pouvez autoriser
|
||||||
|
le lancement du tirage dans
|
||||||
|
l'<a href="{% url "admin:index" %}">interface admin</a>.</p>
|
||||||
|
{% endblock %}
|
105
bda/tests.py
Normal file
105
bda/tests.py
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import Tirage, Spectacle, Salle, CategorieSpectacle
|
||||||
|
|
||||||
|
|
||||||
|
class TestBdAViews(TestCase):
|
||||||
|
def setUp(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")
|
||||||
|
Spectacle.objects.bulk_create([
|
||||||
|
Spectacle(
|
||||||
|
title="foo", date=timezone.now(), location=self.location,
|
||||||
|
price=0, slots=42, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="bar", date=timezone.now(), location=self.location,
|
||||||
|
price=1, slots=142, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="baz", date=timezone.now(), location=self.location,
|
||||||
|
price=2, slots=242, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.bda_user = User.objects.create_user(
|
||||||
|
username="bda_user", password="bda4ever"
|
||||||
|
)
|
||||||
|
self.bda_user.profile.is_cof = True
|
||||||
|
self.bda_user.profile.is_buro = True
|
||||||
|
self.bda_user.profile.save()
|
||||||
|
|
||||||
|
def bda_participants(self):
|
||||||
|
"""The BdA participants views can be queried"""
|
||||||
|
client = Client()
|
||||||
|
show = self.tirage.spectacle_set.first()
|
||||||
|
|
||||||
|
client.login(self.bda_user.username, "bda4ever")
|
||||||
|
tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id))
|
||||||
|
show_resp = client.get(
|
||||||
|
"/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
|
||||||
|
)
|
||||||
|
reminder_url = "/bda/mails-rappel/{}".format(show.id)
|
||||||
|
reminder_get_resp = client.get(reminder_url)
|
||||||
|
reminder_post_resp = client.post(reminder_url)
|
||||||
|
self.assertEqual(200, tirage_resp.status_code)
|
||||||
|
self.assertEqual(200, show_resp.status_code)
|
||||||
|
self.assertEqual(200, reminder_get_resp.status_code)
|
||||||
|
self.assertEqual(200, reminder_post_resp.status_code)
|
||||||
|
|
||||||
|
def test_catalogue(self):
|
||||||
|
"""Test the catalogue JSON API"""
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
# The `list` hook
|
||||||
|
resp = client.get("/bda/catalogue/list")
|
||||||
|
self.assertJSONEqual(
|
||||||
|
resp.content.decode("utf-8"),
|
||||||
|
[{"id": self.tirage.id, "title": self.tirage.title}]
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `details` hook
|
||||||
|
resp = client.get(
|
||||||
|
"/bda/catalogue/details?id={}".format(self.tirage.id)
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `descriptions` hook
|
||||||
|
resp = client.get(
|
||||||
|
"/bda/catalogue/descriptions?id={}".format(self.tirage.id)
|
||||||
|
)
|
||||||
|
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)}
|
||||||
|
)
|
55
bda/urls.py
Normal file
55
bda/urls.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
from gestioncof.decorators import buro_required
|
||||||
|
from bda.views import SpectacleListView
|
||||||
|
from bda import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^inscription/(?P<tirage_id>\d+)$',
|
||||||
|
views.inscription,
|
||||||
|
name='bda-tirage-inscription'),
|
||||||
|
url(r'^places/(?P<tirage_id>\d+)$',
|
||||||
|
views.places,
|
||||||
|
name="bda-places-attribuees"),
|
||||||
|
url(r'^revente/(?P<tirage_id>\d+)$',
|
||||||
|
views.revente,
|
||||||
|
name='bda-revente'),
|
||||||
|
url(r'^etat-places/(?P<tirage_id>\d+)$',
|
||||||
|
views.etat_places,
|
||||||
|
name='bda-etat-places'),
|
||||||
|
url(r'^tirage/(?P<tirage_id>\d+)$', views.tirage),
|
||||||
|
url(r'^spectacles/(?P<tirage_id>\d+)$',
|
||||||
|
buro_required(SpectacleListView.as_view()),
|
||||||
|
name="bda-liste-spectacles"),
|
||||||
|
url(r'^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$',
|
||||||
|
views.spectacle,
|
||||||
|
name="bda-spectacle"),
|
||||||
|
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
|
||||||
|
views.unpaid,
|
||||||
|
name="bda-unpaid"),
|
||||||
|
url(r'^liste-revente/(?P<tirage_id>\d+)$',
|
||||||
|
views.list_revente,
|
||||||
|
name="bda-liste-revente"),
|
||||||
|
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
|
||||||
|
views.buy_revente,
|
||||||
|
name="bda-buy-revente"),
|
||||||
|
url(r'^revente-interested/(?P<revente_id>\d+)$',
|
||||||
|
views.revente_interested,
|
||||||
|
name='bda-revente-interested'),
|
||||||
|
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
|
||||||
|
views.revente_shotgun,
|
||||||
|
name="bda-shotgun"),
|
||||||
|
url(r'^mails-rappel/(?P<spectacle_id>\d+)$',
|
||||||
|
views.send_rappel,
|
||||||
|
name="bda-rappels"
|
||||||
|
),
|
||||||
|
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
|
||||||
|
name='bda-descriptions'),
|
||||||
|
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue,
|
||||||
|
name='bda-catalogue'),
|
||||||
|
]
|
815
bda/views.py
Normal file
815
bda/views.py
Normal file
|
@ -0,0 +1,815 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
|
||||||
|
from custommail.models import CustomMail
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.db import transaction
|
||||||
|
from django.core import serializers
|
||||||
|
from django.db.models import Count, Q, Prefetch
|
||||||
|
from django.forms.models import inlineformset_factory
|
||||||
|
from django.http import (
|
||||||
|
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
||||||
|
)
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone, formats
|
||||||
|
from django.views.generic.list import ListView
|
||||||
|
from gestioncof.decorators import cof_required, buro_required
|
||||||
|
from bda.models import (
|
||||||
|
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
|
||||||
|
SpectacleRevente, Salle, CategorieSpectacle
|
||||||
|
)
|
||||||
|
from bda.algorithm import Algorithm
|
||||||
|
from bda.forms import (
|
||||||
|
TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
|
||||||
|
InscriptionInlineFormSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cof_required
|
||||||
|
def etat_places(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Résumé des spectacles d'un tirage avec pour chaque spectacle :
|
||||||
|
- Le nombre de places en jeu
|
||||||
|
- Le nombre de demandes
|
||||||
|
- Le ratio demandes/places
|
||||||
|
Et le total de toutes les demandes
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
|
||||||
|
spectacles = tirage.spectacle_set.select_related('location')
|
||||||
|
spectacles_dict = {} # index of spectacle by id
|
||||||
|
|
||||||
|
for spectacle in spectacles:
|
||||||
|
spectacle.total = 0 # init total requests
|
||||||
|
spectacles_dict[spectacle.id] = spectacle
|
||||||
|
|
||||||
|
choices = (
|
||||||
|
ChoixSpectacle.objects
|
||||||
|
.filter(spectacle__in=spectacles)
|
||||||
|
.values('spectacle')
|
||||||
|
.annotate(total=Count('spectacle'))
|
||||||
|
)
|
||||||
|
|
||||||
|
# choices *by spectacles* whose only 1 place is requested
|
||||||
|
choices1 = choices.filter(double_choice="1")
|
||||||
|
# choices *by spectacles* whose 2 places is requested
|
||||||
|
choices2 = choices.exclude(double_choice="1")
|
||||||
|
|
||||||
|
for spectacle in choices1:
|
||||||
|
pk = spectacle['spectacle']
|
||||||
|
spectacles_dict[pk].total += spectacle['total']
|
||||||
|
for spectacle in choices2:
|
||||||
|
pk = spectacle['spectacle']
|
||||||
|
spectacles_dict[pk].total += 2*spectacle['total']
|
||||||
|
|
||||||
|
# here, each spectacle.total contains the number of requests
|
||||||
|
|
||||||
|
slots = 0 # proposed slots
|
||||||
|
total = 0 # requests
|
||||||
|
for spectacle in spectacles:
|
||||||
|
slots += spectacle.slots
|
||||||
|
total += spectacle.total
|
||||||
|
spectacle.ratio = spectacle.total / spectacle.slots
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"proposed": slots,
|
||||||
|
"spectacles": spectacles,
|
||||||
|
"total": total,
|
||||||
|
'tirage': tirage
|
||||||
|
}
|
||||||
|
return render(request, "bda/etat-places.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_queryset(queryset):
|
||||||
|
data = serializers.serialize("json", queryset).encode('utf-8')
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
hasher.update(data)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@cof_required
|
||||||
|
def places(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, _ = (
|
||||||
|
Participant.objects
|
||||||
|
.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
)
|
||||||
|
places = (
|
||||||
|
participant.attribution_set
|
||||||
|
.order_by("spectacle__date", "spectacle")
|
||||||
|
.select_related("spectacle", "spectacle__location")
|
||||||
|
)
|
||||||
|
total = sum(place.spectacle.price for place in places)
|
||||||
|
filtered_places = []
|
||||||
|
places_dict = {}
|
||||||
|
spectacles = []
|
||||||
|
dates = []
|
||||||
|
warning = False
|
||||||
|
for place in places:
|
||||||
|
if place.spectacle in spectacles:
|
||||||
|
places_dict[place.spectacle].double = True
|
||||||
|
else:
|
||||||
|
place.double = False
|
||||||
|
places_dict[place.spectacle] = place
|
||||||
|
spectacles.append(place.spectacle)
|
||||||
|
filtered_places.append(place)
|
||||||
|
date = place.spectacle.date.date()
|
||||||
|
if date in dates:
|
||||||
|
warning = True
|
||||||
|
else:
|
||||||
|
dates.append(date)
|
||||||
|
# On prévient l'utilisateur s'il a deux places à la même date
|
||||||
|
if warning:
|
||||||
|
messages.warning(request, "Attention, vous avez reçu des places pour "
|
||||||
|
"des spectacles différents à la même date.")
|
||||||
|
return render(request, "bda/resume_places.html",
|
||||||
|
{"participant": participant,
|
||||||
|
"places": filtered_places,
|
||||||
|
"tirage": tirage,
|
||||||
|
"total": total})
|
||||||
|
|
||||||
|
|
||||||
|
@cof_required
|
||||||
|
def inscription(request, tirage_id):
|
||||||
|
"""
|
||||||
|
Vue d'inscription à un tirage BdA.
|
||||||
|
- On vérifie qu'on se situe bien entre la date d'ouverture et la date de
|
||||||
|
fermeture des inscriptions.
|
||||||
|
- On vérifie que l'inscription n'a pas été modifiée entre le moment où le
|
||||||
|
client demande le formulaire et le moment où il soumet son inscription
|
||||||
|
(autre session par exemple).
|
||||||
|
"""
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
if timezone.now() < tirage.ouverture:
|
||||||
|
# Le tirage n'est pas encore ouvert.
|
||||||
|
opening = formats.localize(
|
||||||
|
timezone.template_localtime(tirage.ouverture))
|
||||||
|
messages.error(request, "Le tirage n'est pas encore ouvert : "
|
||||||
|
"ouverture le {:s}".format(opening))
|
||||||
|
return render(request, 'bda/resume-inscription-tirage.html', {})
|
||||||
|
|
||||||
|
participant, _ = (
|
||||||
|
Participant.objects.select_related('tirage')
|
||||||
|
.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
)
|
||||||
|
|
||||||
|
if timezone.now() > tirage.fermeture:
|
||||||
|
# Le tirage est fermé.
|
||||||
|
choices = participant.choixspectacle_set.order_by("priority")
|
||||||
|
messages.error(request,
|
||||||
|
" C'est fini : tirage au sort dans la journée !")
|
||||||
|
return render(request, "bda/resume-inscription-tirage.html",
|
||||||
|
{"choices": choices})
|
||||||
|
|
||||||
|
BdaFormSet = inlineformset_factory(
|
||||||
|
Participant,
|
||||||
|
ChoixSpectacle,
|
||||||
|
fields=("spectacle", "double_choice", "priority"),
|
||||||
|
formset=InscriptionInlineFormSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
success = False
|
||||||
|
stateerror = False
|
||||||
|
if request.method == "POST":
|
||||||
|
# use *this* queryset
|
||||||
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
|
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
||||||
|
stateerror = True
|
||||||
|
formset = BdaFormSet(instance=participant)
|
||||||
|
else:
|
||||||
|
formset = BdaFormSet(request.POST, instance=participant)
|
||||||
|
if formset.is_valid():
|
||||||
|
formset.save()
|
||||||
|
success = True
|
||||||
|
formset = BdaFormSet(instance=participant)
|
||||||
|
else:
|
||||||
|
formset = BdaFormSet(instance=participant)
|
||||||
|
# use *this* queryset
|
||||||
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
|
total_price = 0
|
||||||
|
choices = (
|
||||||
|
participant.choixspectacle_set
|
||||||
|
.select_related('spectacle')
|
||||||
|
)
|
||||||
|
for choice in choices:
|
||||||
|
total_price += choice.spectacle.price
|
||||||
|
if choice.double:
|
||||||
|
total_price += choice.spectacle.price
|
||||||
|
# Messages
|
||||||
|
if success:
|
||||||
|
messages.success(request, "Votre inscription a été mise à jour avec "
|
||||||
|
"succès !")
|
||||||
|
if stateerror:
|
||||||
|
messages.error(request, "Impossible d'enregistrer vos modifications "
|
||||||
|
": vous avez apporté d'autres modifications "
|
||||||
|
"entre temps.")
|
||||||
|
return render(request, "bda/inscription-tirage.html",
|
||||||
|
{"formset": formset,
|
||||||
|
"total_price": total_price,
|
||||||
|
"dbstate": dbstate,
|
||||||
|
'tirage': tirage})
|
||||||
|
|
||||||
|
|
||||||
|
def do_tirage(tirage_elt, token):
|
||||||
|
"""
|
||||||
|
Fonction auxiliaire à la vue ``tirage`` qui lance effectivement le tirage
|
||||||
|
après qu'on a vérifié que c'est légitime et que le token donné en argument
|
||||||
|
est correct.
|
||||||
|
Rend les résultats
|
||||||
|
"""
|
||||||
|
# Initialisation du dictionnaire data qui va contenir les résultats
|
||||||
|
start = time.time()
|
||||||
|
data = {
|
||||||
|
'shows': tirage_elt.spectacle_set.select_related('location'),
|
||||||
|
'token': token,
|
||||||
|
'members': tirage_elt.participant_set.select_related('user'),
|
||||||
|
'total_slots': 0,
|
||||||
|
'total_losers': 0,
|
||||||
|
'total_sold': 0,
|
||||||
|
'total_deficit': 0,
|
||||||
|
'opera_deficit': 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# On lance le tirage
|
||||||
|
choices = (
|
||||||
|
ChoixSpectacle.objects
|
||||||
|
.filter(spectacle__tirage=tirage_elt)
|
||||||
|
.order_by('participant', 'priority')
|
||||||
|
.select_related('participant', 'participant__user', 'spectacle')
|
||||||
|
)
|
||||||
|
results = Algorithm(data['shows'], data['members'], choices)(token)
|
||||||
|
|
||||||
|
# On compte les places attribuées et les déçus
|
||||||
|
for (_, members, losers) in results:
|
||||||
|
data['total_slots'] += len(members)
|
||||||
|
data['total_losers'] += len(losers)
|
||||||
|
|
||||||
|
# On calcule le déficit et les bénéfices pour le BdA
|
||||||
|
# FIXME: le traitement de l'opéra est sale
|
||||||
|
for (show, members, _) in results:
|
||||||
|
deficit = (show.slots - len(members)) * show.price
|
||||||
|
data['total_sold'] += show.slots * show.price
|
||||||
|
if deficit >= 0:
|
||||||
|
if "Opéra" in show.location.name:
|
||||||
|
data['opera_deficit'] += deficit
|
||||||
|
data['total_deficit'] += deficit
|
||||||
|
data["total_sold"] -= data['total_deficit']
|
||||||
|
|
||||||
|
# Participant objects are not shared accross spectacle results,
|
||||||
|
# so assign a single object for each Participant id
|
||||||
|
members_uniq = {}
|
||||||
|
members2 = {}
|
||||||
|
for (show, members, _) in results:
|
||||||
|
for (member, _, _, _) in members:
|
||||||
|
if member.id not in members_uniq:
|
||||||
|
members_uniq[member.id] = member
|
||||||
|
members2[member] = []
|
||||||
|
member.total = 0
|
||||||
|
member = members_uniq[member.id]
|
||||||
|
members2[member].append(show)
|
||||||
|
member.total += show.price
|
||||||
|
members2 = members2.items()
|
||||||
|
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# À partir d'ici, le tirage devient effectif
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# On suppression les vieilles attributions, on sauvegarde le token et on
|
||||||
|
# désactive le tirage
|
||||||
|
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
|
||||||
|
tirage_elt.tokens += '{:s}\n"""{:s}"""\n'.format(
|
||||||
|
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
|
||||||
|
token)
|
||||||
|
tirage_elt.enable_do_tirage = False
|
||||||
|
tirage_elt.save()
|
||||||
|
|
||||||
|
# On enregistre les nouvelles attributions
|
||||||
|
Attribution.objects.bulk_create([
|
||||||
|
Attribution(spectacle=show, participant=member)
|
||||||
|
for show, members, _ in results
|
||||||
|
for member, _, _, _ in members
|
||||||
|
])
|
||||||
|
|
||||||
|
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
|
||||||
|
ChoixRevente = Participant.choicesrevente.through
|
||||||
|
|
||||||
|
# Suppression des reventes demandées/enregistrées
|
||||||
|
# (si le tirage est relancé)
|
||||||
|
(
|
||||||
|
ChoixRevente.objects
|
||||||
|
.filter(spectacle__tirage=tirage_elt)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
(
|
||||||
|
SpectacleRevente.objects
|
||||||
|
.filter(attribution__spectacle__tirage=tirage_elt)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
|
||||||
|
lost_by = defaultdict(set)
|
||||||
|
for show, _, losers in results:
|
||||||
|
for loser, _, _, _ in losers:
|
||||||
|
lost_by[loser].add(show)
|
||||||
|
|
||||||
|
ChoixRevente.objects.bulk_create(
|
||||||
|
ChoixRevente(participant=member, spectacle=show)
|
||||||
|
for member, shows in lost_by.items()
|
||||||
|
for show in shows
|
||||||
|
)
|
||||||
|
|
||||||
|
data["duration"] = time.time() - start
|
||||||
|
data["results"] = results
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def tirage(request, tirage_id):
|
||||||
|
tirage_elt = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
if not (tirage_elt.enable_do_tirage
|
||||||
|
and tirage_elt.fermeture < timezone.now()):
|
||||||
|
return render(request, "tirage-failed.html", {'tirage': tirage_elt})
|
||||||
|
if request.POST:
|
||||||
|
form = TokenForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
results = do_tirage(tirage_elt, form.cleaned_data['token'])
|
||||||
|
return render(request, "bda-attrib-extra.html", results)
|
||||||
|
else:
|
||||||
|
form = TokenForm()
|
||||||
|
return render(request, "bda-token.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, created = Participant.objects.get_or_create(
|
||||||
|
user=request.user, tirage=tirage)
|
||||||
|
|
||||||
|
if not participant.paid:
|
||||||
|
return render(request, "bda-notpaid.html", {})
|
||||||
|
|
||||||
|
resellform = ResellForm(participant, prefix='resell')
|
||||||
|
annulform = AnnulForm(participant, prefix='annul')
|
||||||
|
soldform = SoldForm(participant, prefix='sold')
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# On met en vente une place
|
||||||
|
if 'resell' in request.POST:
|
||||||
|
resellform = ResellForm(participant, request.POST, prefix='resell')
|
||||||
|
if resellform.is_valid():
|
||||||
|
datatuple = []
|
||||||
|
attributions = resellform.cleaned_data["attributions"]
|
||||||
|
with transaction.atomic():
|
||||||
|
for attribution in attributions:
|
||||||
|
revente, created = \
|
||||||
|
SpectacleRevente.objects.get_or_create(
|
||||||
|
attribution=attribution,
|
||||||
|
defaults={'seller': participant})
|
||||||
|
if not created:
|
||||||
|
revente.seller = participant
|
||||||
|
revente.date = timezone.now()
|
||||||
|
revente.soldTo = None
|
||||||
|
revente.notif_sent = False
|
||||||
|
revente.tirage_done = False
|
||||||
|
revente.shotgun = False
|
||||||
|
context = {
|
||||||
|
'vendeur': participant.user,
|
||||||
|
'show': attribution.spectacle,
|
||||||
|
'revente': revente
|
||||||
|
}
|
||||||
|
datatuple.append((
|
||||||
|
'bda-revente-new', context,
|
||||||
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
|
[participant.user.email]
|
||||||
|
))
|
||||||
|
revente.save()
|
||||||
|
send_mass_custom_mail(datatuple)
|
||||||
|
# On annule une revente
|
||||||
|
elif 'annul' in request.POST:
|
||||||
|
annulform = AnnulForm(participant, request.POST, prefix='annul')
|
||||||
|
if annulform.is_valid():
|
||||||
|
attributions = annulform.cleaned_data["attributions"]
|
||||||
|
for attribution in attributions:
|
||||||
|
attribution.revente.delete()
|
||||||
|
# On confirme une vente en transférant la place à la personne qui a
|
||||||
|
# gagné le tirage
|
||||||
|
elif 'transfer' in request.POST:
|
||||||
|
soldform = SoldForm(participant, request.POST, prefix='sold')
|
||||||
|
if soldform.is_valid():
|
||||||
|
attributions = soldform.cleaned_data['attributions']
|
||||||
|
for attribution in attributions:
|
||||||
|
attribution.participant = attribution.revente.soldTo
|
||||||
|
attribution.save()
|
||||||
|
|
||||||
|
# On annule la revente après le tirage au sort (par exemple si
|
||||||
|
# la personne qui a gagné le tirage ne se manifeste pas). La place est
|
||||||
|
# alors remise en vente
|
||||||
|
elif 'reinit' in request.POST:
|
||||||
|
soldform = SoldForm(participant, request.POST, prefix='sold')
|
||||||
|
if soldform.is_valid():
|
||||||
|
attributions = soldform.cleaned_data['attributions']
|
||||||
|
for attribution in attributions:
|
||||||
|
if attribution.spectacle.date > timezone.now():
|
||||||
|
revente = attribution.revente
|
||||||
|
revente.date = timezone.now() - timedelta(minutes=65)
|
||||||
|
revente.soldTo = None
|
||||||
|
revente.notif_sent = False
|
||||||
|
revente.tirage_done = False
|
||||||
|
revente.shotgun = False
|
||||||
|
if revente.answered_mail:
|
||||||
|
revente.answered_mail.clear()
|
||||||
|
revente.save()
|
||||||
|
|
||||||
|
overdue = participant.attribution_set.filter(
|
||||||
|
spectacle__date__gte=timezone.now(),
|
||||||
|
revente__isnull=False,
|
||||||
|
revente__seller=participant,
|
||||||
|
revente__notif_sent=True)\
|
||||||
|
.filter(
|
||||||
|
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
|
||||||
|
|
||||||
|
return render(request, "bda/reventes.html",
|
||||||
|
{'tirage': tirage, 'overdue': overdue, "soldform": soldform,
|
||||||
|
"annulform": annulform, "resellform": resellform})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_interested(request, revente_id):
|
||||||
|
revente = get_object_or_404(SpectacleRevente, id=revente_id)
|
||||||
|
participant, _ = Participant.objects.get_or_create(
|
||||||
|
user=request.user, tirage=revente.attribution.spectacle.tirage)
|
||||||
|
if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
|
||||||
|
return render(request, "bda-wrongtime.html",
|
||||||
|
{"revente": revente})
|
||||||
|
|
||||||
|
revente.answered_mail.add(participant)
|
||||||
|
return render(request, "bda-interested.html",
|
||||||
|
{"spectacle": revente.attribution.spectacle,
|
||||||
|
"date": revente.date_tirage})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def list_revente(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
participant, _ = Participant.objects.get_or_create(
|
||||||
|
user=request.user, tirage=tirage)
|
||||||
|
deja_revente = False
|
||||||
|
success = False
|
||||||
|
inscrit_revente = []
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = InscriptionReventeForm(tirage, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
choices = form.cleaned_data['spectacles']
|
||||||
|
participant.choicesrevente = choices
|
||||||
|
participant.save()
|
||||||
|
for spectacle in choices:
|
||||||
|
qset = SpectacleRevente.objects.filter(
|
||||||
|
attribution__spectacle=spectacle)
|
||||||
|
if qset.filter(shotgun=True, soldTo__isnull=True).exists():
|
||||||
|
# Une place est disponible au shotgun, on suggère à
|
||||||
|
# l'utilisateur d'aller la récupérer
|
||||||
|
deja_revente = True
|
||||||
|
else:
|
||||||
|
# La place n'est pas disponible au shotgun, si des reventes
|
||||||
|
# pour ce spectacle existent déjà, on inscrit la personne à
|
||||||
|
# la revente ayant le moins d'inscrits
|
||||||
|
min_resell = (
|
||||||
|
qset.filter(shotgun=False)
|
||||||
|
.annotate(nb_subscribers=Count('answered_mail'))
|
||||||
|
.order_by('nb_subscribers')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if min_resell is not None:
|
||||||
|
min_resell.answered_mail.add(participant)
|
||||||
|
inscrit_revente.append(spectacle)
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
form = InscriptionReventeForm(
|
||||||
|
tirage,
|
||||||
|
initial={'spectacles': participant.choicesrevente.all()}
|
||||||
|
)
|
||||||
|
# Messages
|
||||||
|
if success:
|
||||||
|
messages.success(request, "Ton inscription a bien été prise en compte")
|
||||||
|
if deja_revente:
|
||||||
|
messages.info(request, "Des reventes existent déjà pour certains de "
|
||||||
|
"ces spectacles, vérifie les places "
|
||||||
|
"disponibles sans tirage !")
|
||||||
|
if inscrit_revente:
|
||||||
|
shows = map("<li>{!s}</li>".format, inscrit_revente)
|
||||||
|
msg = (
|
||||||
|
"Tu as été inscrit à des reventes en cours pour les spectacles "
|
||||||
|
"<ul>{:s}</ul>".format('\n'.join(shows))
|
||||||
|
)
|
||||||
|
messages.info(request, msg, extra_tags="safe")
|
||||||
|
|
||||||
|
return render(request, "bda/liste-reventes.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def buy_revente(request, spectacle_id):
|
||||||
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
||||||
|
tirage = spectacle.tirage
|
||||||
|
participant, _ = Participant.objects.get_or_create(
|
||||||
|
user=request.user, tirage=tirage)
|
||||||
|
reventes = SpectacleRevente.objects.filter(
|
||||||
|
attribution__spectacle=spectacle,
|
||||||
|
soldTo__isnull=True)
|
||||||
|
|
||||||
|
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
|
||||||
|
# on supprime la revente en question.
|
||||||
|
own_reventes = reventes.filter(seller=participant)
|
||||||
|
if len(own_reventes) > 0:
|
||||||
|
own_reventes[0].delete()
|
||||||
|
return HttpResponseRedirect(reverse("bda-shotgun",
|
||||||
|
args=[tirage.id]))
|
||||||
|
|
||||||
|
reventes_shotgun = reventes.filter(shotgun=True)
|
||||||
|
|
||||||
|
if not reventes_shotgun:
|
||||||
|
return render(request, "bda-no-revente.html", {})
|
||||||
|
|
||||||
|
if request.POST:
|
||||||
|
revente = random.choice(reventes_shotgun)
|
||||||
|
revente.soldTo = participant
|
||||||
|
revente.save()
|
||||||
|
context = {
|
||||||
|
'show': spectacle,
|
||||||
|
'acheteur': request.user,
|
||||||
|
'vendeur': revente.seller.user
|
||||||
|
}
|
||||||
|
send_custom_mail(
|
||||||
|
'bda-buy-shotgun',
|
||||||
|
'bda@ens.fr',
|
||||||
|
[revente.seller.user.email],
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
return render(request, "bda-success.html",
|
||||||
|
{"seller": revente.attribution.participant.user,
|
||||||
|
"spectacle": spectacle})
|
||||||
|
|
||||||
|
return render(request, "revente-confirm.html",
|
||||||
|
{"spectacle": spectacle,
|
||||||
|
"user": request.user})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def revente_shotgun(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
spectacles = (
|
||||||
|
tirage.spectacle_set
|
||||||
|
.filter(date__gte=timezone.now())
|
||||||
|
.select_related('location')
|
||||||
|
.prefetch_related(Prefetch(
|
||||||
|
'attribues',
|
||||||
|
queryset=(
|
||||||
|
Attribution.objects
|
||||||
|
.filter(revente__shotgun=True,
|
||||||
|
revente__soldTo__isnull=True)
|
||||||
|
),
|
||||||
|
to_attr='shotguns',
|
||||||
|
))
|
||||||
|
)
|
||||||
|
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
||||||
|
|
||||||
|
return render(request, "bda-shotgun.html",
|
||||||
|
{"shotgun": shotgun})
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def spectacle(request, tirage_id, spectacle_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
|
||||||
|
attributions = (
|
||||||
|
spectacle.attribues
|
||||||
|
.select_related('participant', 'participant__user')
|
||||||
|
)
|
||||||
|
participants = {}
|
||||||
|
for attrib in attributions:
|
||||||
|
participant = attrib.participant
|
||||||
|
participant_info = {'lastname': participant.user.last_name,
|
||||||
|
'name': participant.user.get_full_name,
|
||||||
|
'username': participant.user.username,
|
||||||
|
'email': participant.user.email,
|
||||||
|
'given': int(attrib.given),
|
||||||
|
'paid': participant.paid,
|
||||||
|
'nb_places': 1}
|
||||||
|
if participant.id in participants:
|
||||||
|
participants[participant.id]['nb_places'] += 1
|
||||||
|
participants[participant.id]['given'] += attrib.given
|
||||||
|
else:
|
||||||
|
participants[participant.id] = participant_info
|
||||||
|
|
||||||
|
participants_info = sorted(participants.values(),
|
||||||
|
key=lambda part: part['lastname'])
|
||||||
|
return render(request, "bda/participants.html",
|
||||||
|
{"spectacle": spectacle, "participants": participants_info})
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleListView(ListView):
|
||||||
|
model = Spectacle
|
||||||
|
template_name = 'spectacle_list.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id'])
|
||||||
|
categories = (
|
||||||
|
self.tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
)
|
||||||
|
return categories
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(SpectacleListView, self).get_context_data(**kwargs)
|
||||||
|
context['tirage_id'] = self.tirage.id
|
||||||
|
context['tirage_name'] = self.tirage.title
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def unpaid(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
unpaid = (
|
||||||
|
tirage.participant_set
|
||||||
|
.annotate(nb_attributions=Count('attribution'))
|
||||||
|
.filter(paid=False, nb_attributions__gt=0)
|
||||||
|
.select_related('user')
|
||||||
|
)
|
||||||
|
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def send_rappel(request, spectacle_id):
|
||||||
|
show = get_object_or_404(Spectacle, id=spectacle_id)
|
||||||
|
# Mails d'exemples
|
||||||
|
custommail = CustomMail.objects.get(shortname="bda-rappel")
|
||||||
|
exemple_mail_1place = custommail.render({
|
||||||
|
'member': request.user,
|
||||||
|
'show': show,
|
||||||
|
'nb_attr': 1
|
||||||
|
})
|
||||||
|
exemple_mail_2places = custommail.render({
|
||||||
|
'member': request.user,
|
||||||
|
'show': show,
|
||||||
|
'nb_attr': 2
|
||||||
|
})
|
||||||
|
# Contexte
|
||||||
|
ctxt = {
|
||||||
|
'show': show,
|
||||||
|
'exemple_mail_1place': exemple_mail_1place,
|
||||||
|
'exemple_mail_2places': exemple_mail_2places,
|
||||||
|
'custommail': custommail,
|
||||||
|
}
|
||||||
|
# Envoi confirmé
|
||||||
|
if request.method == 'POST':
|
||||||
|
members = show.send_rappel()
|
||||||
|
ctxt['sent'] = True
|
||||||
|
ctxt['members'] = members
|
||||||
|
# Demande de confirmation
|
||||||
|
else:
|
||||||
|
ctxt['sent'] = False
|
||||||
|
if show.rappel_sent:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"Attention, un mail de rappel pour ce spectale a déjà été "
|
||||||
|
"envoyé le {}".format(formats.localize(
|
||||||
|
timezone.template_localtime(show.rappel_sent)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return render(request, "bda/mails-rappel.html", ctxt)
|
||||||
|
|
||||||
|
|
||||||
|
def descriptions_spectacles(request, tirage_id):
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
shows_qs = (
|
||||||
|
tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
.prefetch_related('quote_set')
|
||||||
|
)
|
||||||
|
category_name = request.GET.get('category', '')
|
||||||
|
location_id = request.GET.get('location', '')
|
||||||
|
if category_name:
|
||||||
|
shows_qs = shows_qs.filter(category__name=category_name)
|
||||||
|
if location_id:
|
||||||
|
try:
|
||||||
|
shows_qs = shows_qs.filter(location__id=int(location_id))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"La variable GET 'location' doit contenir un entier")
|
||||||
|
return render(request, 'descriptions.html', {'shows': shows_qs})
|
||||||
|
|
||||||
|
|
||||||
|
def catalogue(request, request_type):
|
||||||
|
"""
|
||||||
|
Vue destinée à communiquer avec un client AJAX, fournissant soit :
|
||||||
|
- la liste des tirages
|
||||||
|
- les catégories et salles d'un tirage
|
||||||
|
- les descriptions d'un tirage (filtrées selon la catégorie et la salle)
|
||||||
|
"""
|
||||||
|
if request_type == "list":
|
||||||
|
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
|
||||||
|
data_return = list(
|
||||||
|
Tirage.objects.filter(appear_catalogue=True).values('id', 'title')
|
||||||
|
)
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
if request_type == "details":
|
||||||
|
# Dans ce cas on retourne une liste des catégories et des salles
|
||||||
|
tirage_id = request.GET.get('id', None)
|
||||||
|
if tirage_id is None:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Missing GET parameter: id <int>"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tirage = get_object_or_404(Tirage, id=int(tirage_id))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Bad format: int expected for `id`"
|
||||||
|
)
|
||||||
|
shows = tirage.spectacle_set.values_list("id", flat=True)
|
||||||
|
categories = list(
|
||||||
|
CategorieSpectacle.objects
|
||||||
|
.filter(spectacle__in=shows)
|
||||||
|
.distinct()
|
||||||
|
.values('id', 'name')
|
||||||
|
)
|
||||||
|
locations = list(
|
||||||
|
Salle.objects
|
||||||
|
.filter(spectacle__in=shows)
|
||||||
|
.distinct()
|
||||||
|
.values('id', 'name')
|
||||||
|
)
|
||||||
|
data_return = {'categories': categories, 'locations': locations}
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
if request_type == "descriptions":
|
||||||
|
# Ici on retourne les descriptions correspondant à la catégorie et
|
||||||
|
# à la salle spécifiées
|
||||||
|
|
||||||
|
tirage_id = request.GET.get('id', '')
|
||||||
|
categories = request.GET.get('category', '[]')
|
||||||
|
locations = request.GET.get('location', '[]')
|
||||||
|
try:
|
||||||
|
tirage_id = int(tirage_id)
|
||||||
|
categories_id = json.loads(categories)
|
||||||
|
locations_id = json.loads(locations)
|
||||||
|
# Integers expected
|
||||||
|
if not all(isinstance(id, int) for id in categories_id):
|
||||||
|
raise ValueError
|
||||||
|
if not all(isinstance(id, int) for id in locations_id):
|
||||||
|
raise ValueError
|
||||||
|
except ValueError: # Contient JSONDecodeError
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Parse error, please ensure the GET parameters have the "
|
||||||
|
"following types:\n"
|
||||||
|
"id: int, category: [int], location: [int]\n"
|
||||||
|
"Data received:\n"
|
||||||
|
"id = {}, category = {}, locations = {}"
|
||||||
|
.format(request.GET.get('id', ''),
|
||||||
|
request.GET.get('category', '[]'),
|
||||||
|
request.GET.get('location', '[]'))
|
||||||
|
)
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
|
||||||
|
shows_qs = (
|
||||||
|
tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
.prefetch_related('quote_set')
|
||||||
|
)
|
||||||
|
if categories_id and 0 not in categories_id:
|
||||||
|
shows_qs = shows_qs.filter(category__id__in=categories_id)
|
||||||
|
if locations_id and 0 not in locations_id:
|
||||||
|
shows_qs = shows_qs.filter(location__id__in=locations_id)
|
||||||
|
|
||||||
|
# On convertit les descriptions à envoyer en une liste facilement
|
||||||
|
# JSONifiable (il devrait y avoir un moyen plus efficace en
|
||||||
|
# redéfinissant le serializer de JSON)
|
||||||
|
data_return = [{
|
||||||
|
'title': spectacle.title,
|
||||||
|
'category': str(spectacle.category),
|
||||||
|
'date': str(formats.date_format(
|
||||||
|
timezone.localtime(spectacle.date),
|
||||||
|
"SHORT_DATETIME_FORMAT")),
|
||||||
|
'location': str(spectacle.location),
|
||||||
|
'vips': spectacle.vips,
|
||||||
|
'description': spectacle.description,
|
||||||
|
'slots_description': spectacle.slots_description,
|
||||||
|
'quotes': [dict(author=quote.author,
|
||||||
|
text=quote.text)
|
||||||
|
for quote in spectacle.quote_set.all()],
|
||||||
|
'image': spectacle.getImgUrl(),
|
||||||
|
'ext_link': spectacle.ext_link,
|
||||||
|
'price': spectacle.price,
|
||||||
|
'slots': spectacle.slots
|
||||||
|
}
|
||||||
|
for spectacle in shows_qs
|
||||||
|
]
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
|
||||||
|
return HttpResponseBadRequest()
|
0
cof/__init__.py
Normal file
0
cof/__init__.py
Normal file
7
cof/asgi.py
Normal file
7
cof/asgi.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import os
|
||||||
|
from channels.asgi import get_channel_layer
|
||||||
|
|
||||||
|
if "DJANGO_SETTINGS_MODULE" not in os.environ:
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
|
||||||
|
|
||||||
|
channel_layer = get_channel_layer()
|
0
cof/locale/__init__.py
Normal file
0
cof/locale/__init__.py
Normal file
0
cof/locale/fr/__init__.py
Normal file
0
cof/locale/fr/__init__.py
Normal file
9
cof/locale/fr/formats.py
Normal file
9
cof/locale/fr/formats.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formats français.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
DATETIME_FORMAT = r'l j F Y \à H:i'
|
6
cof/routing.py
Normal file
6
cof/routing.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from channels.routing import include
|
||||||
|
|
||||||
|
|
||||||
|
routing = [
|
||||||
|
include('kfet.routing.routing', path=r'^/ws/k-fet'),
|
||||||
|
]
|
1
cof/settings/.gitignore
vendored
Normal file
1
cof/settings/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
secret.py
|
235
cof/settings/common.py
Normal file
235
cof/settings/common.py
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Django common settings for cof project.
|
||||||
|
|
||||||
|
Everything which is supposed to be identical between the production server and
|
||||||
|
the local development server should be here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import secret
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"The secret.py file is missing.\n"
|
||||||
|
"For a development environment, simply copy secret_example.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def import_secret(name):
|
||||||
|
"""
|
||||||
|
Shorthand for importing a value from the secret module and raising an
|
||||||
|
informative exception if a secret is missing.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return getattr(secret, name)
|
||||||
|
except AttributeError:
|
||||||
|
raise RuntimeError("Secret missing: {}".format(name))
|
||||||
|
|
||||||
|
|
||||||
|
SECRET_KEY = import_secret("SECRET_KEY")
|
||||||
|
ADMINS = import_secret("ADMINS")
|
||||||
|
SERVER_EMAIL = import_secret("SERVER_EMAIL")
|
||||||
|
|
||||||
|
DBNAME = import_secret("DBNAME")
|
||||||
|
DBUSER = import_secret("DBUSER")
|
||||||
|
DBPASSWD = import_secret("DBPASSWD")
|
||||||
|
|
||||||
|
REDIS_PASSWD = import_secret("REDIS_PASSWD")
|
||||||
|
REDIS_DB = import_secret("REDIS_DB")
|
||||||
|
REDIS_HOST = import_secret("REDIS_HOST")
|
||||||
|
REDIS_PORT = import_secret("REDIS_PORT")
|
||||||
|
|
||||||
|
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
|
||||||
|
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
|
||||||
|
|
||||||
|
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
|
||||||
|
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'gestioncof',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'grappelli',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.admindocs',
|
||||||
|
'bda',
|
||||||
|
'autocomplete_light',
|
||||||
|
'captcha',
|
||||||
|
'django_cas_ng',
|
||||||
|
'djangoformsetjs',
|
||||||
|
'bootstrapform',
|
||||||
|
'kfet',
|
||||||
|
'kfet.open',
|
||||||
|
'channels',
|
||||||
|
'widget_tweaks',
|
||||||
|
'custommail',
|
||||||
|
'djconfig',
|
||||||
|
'wagtail.wagtailforms',
|
||||||
|
'wagtail.wagtailredirects',
|
||||||
|
'wagtail.wagtailembeds',
|
||||||
|
'wagtail.wagtailsites',
|
||||||
|
'wagtail.wagtailusers',
|
||||||
|
'wagtail.wagtailsnippets',
|
||||||
|
'wagtail.wagtaildocs',
|
||||||
|
'wagtail.wagtailimages',
|
||||||
|
'wagtail.wagtailsearch',
|
||||||
|
'wagtail.wagtailadmin',
|
||||||
|
'wagtail.wagtailcore',
|
||||||
|
'wagtail.contrib.modeladmin',
|
||||||
|
'wagtailmenus',
|
||||||
|
'modelcluster',
|
||||||
|
'taggit',
|
||||||
|
'kfet.auth',
|
||||||
|
'kfet.cms',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = [
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
|
'kfet.auth.middleware.TemporaryAuthMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'djconfig.middleware.DjConfigMiddleware',
|
||||||
|
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||||
|
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'cof.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'django.core.context_processors.i18n',
|
||||||
|
'django.core.context_processors.media',
|
||||||
|
'django.core.context_processors.static',
|
||||||
|
'wagtailmenus.context_processors.wagtailmenus',
|
||||||
|
'djconfig.context_processors.config',
|
||||||
|
'gestioncof.shared.context_processor',
|
||||||
|
'kfet.auth.context_processors.temporary_auth',
|
||||||
|
'kfet.context_processors.config',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': DBNAME,
|
||||||
|
'USER': DBUSER,
|
||||||
|
'PASSWORD': DBPASSWD,
|
||||||
|
'HOST': os.environ.get('DBHOST', 'localhost'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'fr-fr'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Paris'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Various additional settings
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
|
||||||
|
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
|
||||||
|
|
||||||
|
MAIL_DATA = {
|
||||||
|
'petits_cours': {
|
||||||
|
'FROM': "Le COF <cof@ens.fr>",
|
||||||
|
'BCC': "archivescof@gmail.com",
|
||||||
|
'REPLYTO': "cof@ens.fr"},
|
||||||
|
'rappels': {
|
||||||
|
'FROM': 'Le BdA <bda@ens.fr>',
|
||||||
|
'REPLYTO': 'Le BdA <bda@ens.fr>'},
|
||||||
|
'revente': {
|
||||||
|
'FROM': 'BdA-Revente <bda-revente@ens.fr>',
|
||||||
|
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGIN_URL = "cof-login"
|
||||||
|
LOGIN_REDIRECT_URL = "home"
|
||||||
|
|
||||||
|
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
|
||||||
|
CAS_VERSION = '3'
|
||||||
|
CAS_LOGIN_MSG = None
|
||||||
|
CAS_IGNORE_REFERER = True
|
||||||
|
CAS_REDIRECT_URL = '/'
|
||||||
|
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
'gestioncof.shared.COFCASBackend',
|
||||||
|
'kfet.auth.backends.GenericBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
RECAPTCHA_USE_SSL = True
|
||||||
|
|
||||||
|
# Cache settings
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'redis_cache.RedisCache',
|
||||||
|
'LOCATION': 'redis://:{passwd}@{host}:{port}/db'
|
||||||
|
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
|
||||||
|
port=REDIS_PORT, db=REDIS_DB),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Channels settings
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [(
|
||||||
|
"redis://:{passwd}@{host}:{port}/{db}"
|
||||||
|
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
|
||||||
|
port=REDIS_PORT, db=REDIS_DB)
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
"ROUTING": "cof.routing.routing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FORMAT_MODULE_PATH = 'cof.locale'
|
||||||
|
|
||||||
|
# Wagtail settings
|
||||||
|
|
||||||
|
WAGTAIL_SITE_NAME = 'GestioCOF'
|
||||||
|
WAGTAIL_ENABLE_UPDATE_CHECK = False
|
||||||
|
TAGGIT_CASE_INSENSITIVE = True
|
46
cof/settings/dev.py
Normal file
46
cof/settings/dev.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""
|
||||||
|
Django development settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Apache static/media config
|
||||||
|
# ---
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = '/srv/gestiocof/static/'
|
||||||
|
|
||||||
|
MEDIA_ROOT = '/srv/gestiocof/media/'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Debug tool bar
|
||||||
|
# ---
|
||||||
|
|
||||||
|
def show_toolbar(request):
|
||||||
|
"""
|
||||||
|
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
|
||||||
|
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
|
||||||
|
machine physique n'est pas forcément connue, et peut difficilement être
|
||||||
|
mise dans les INTERNAL_IPS.
|
||||||
|
"""
|
||||||
|
return DEBUG
|
||||||
|
|
||||||
|
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||||
|
MIDDLEWARE_CLASSES = (
|
||||||
|
["debug_panel.middleware.DebugPanelMiddleware"]
|
||||||
|
+ MIDDLEWARE_CLASSES
|
||||||
|
)
|
||||||
|
DEBUG_TOOLBAR_CONFIG = {
|
||||||
|
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||||
|
}
|
36
cof/settings/local.py
Normal file
36
cof/settings/local.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
Django local settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .dev import * # NOQA
|
||||||
|
from .dev import BASE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
# Use sqlite for local development
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the default cache backend for local development
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the default in memory asgi backend for local development
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgiref.inmemory.ChannelLayer",
|
||||||
|
"ROUTING": "cof.routing.routing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# No need to run collectstatic -> unset STATIC_ROOT
|
||||||
|
STATIC_ROOT = None
|
30
cof/settings/prod.py
Normal file
30
cof/settings/prod.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""
|
||||||
|
Django development settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
from .common import BASE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
"cof.ens.fr",
|
||||||
|
"www.cof.ens.fr",
|
||||||
|
"dev.cof.ens.fr"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(BASE_DIR)),
|
||||||
|
"public",
|
||||||
|
"gestion",
|
||||||
|
"static",
|
||||||
|
)
|
||||||
|
|
||||||
|
STATIC_URL = "/gestion/static/"
|
||||||
|
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
|
||||||
|
MEDIA_URL = "/gestion/media/"
|
20
cof/settings/secret_example.py
Normal file
20
cof/settings/secret_example.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
|
||||||
|
ADMINS = None
|
||||||
|
SERVER_EMAIL = "root@vagrant"
|
||||||
|
|
||||||
|
DBUSER = "cof_gestion"
|
||||||
|
DBNAME = "cof_gestion"
|
||||||
|
DBPASSWD = "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
||||||
|
|
||||||
|
REDIS_PASSWD = "dummy"
|
||||||
|
REDIS_PORT = 6379
|
||||||
|
REDIS_DB = 0
|
||||||
|
REDIS_HOST = "127.0.0.1"
|
||||||
|
|
||||||
|
RECAPTCHA_PUBLIC_KEY = "DUMMY"
|
||||||
|
RECAPTCHA_PRIVATE_KEY = "DUMMY"
|
||||||
|
|
||||||
|
EMAIL_HOST = None
|
||||||
|
|
||||||
|
KFETOPEN_TOKEN = "plop"
|
||||||
|
LDAP_SERVER_URL = None
|
110
cof/urls.py
Normal file
110
cof/urls.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fichier principal de configuration des urls du projet GestioCOF
|
||||||
|
"""
|
||||||
|
|
||||||
|
import autocomplete_light
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
from django.contrib.auth import views as django_views
|
||||||
|
from django_cas_ng import views as django_cas_views
|
||||||
|
|
||||||
|
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
||||||
|
from wagtail.wagtailcore import urls as wagtail_urls
|
||||||
|
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
||||||
|
|
||||||
|
from gestioncof import views as gestioncof_views, csv_views
|
||||||
|
from gestioncof.urls import export_patterns, petitcours_patterns, \
|
||||||
|
surveys_patterns, events_patterns, calendar_patterns, \
|
||||||
|
clubs_patterns
|
||||||
|
from gestioncof.autocomplete import autocomplete
|
||||||
|
|
||||||
|
autocomplete_light.autodiscover()
|
||||||
|
admin.autodiscover()
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Page d'accueil
|
||||||
|
url(r'^$', gestioncof_views.home, name='home'),
|
||||||
|
# Le BdA
|
||||||
|
url(r'^bda/', include('bda.urls')),
|
||||||
|
# Les exports
|
||||||
|
url(r'^export/', include(export_patterns)),
|
||||||
|
# Les petits cours
|
||||||
|
url(r'^petitcours/', include(petitcours_patterns)),
|
||||||
|
# Les sondages
|
||||||
|
url(r'^survey/', include(surveys_patterns)),
|
||||||
|
# Evenements
|
||||||
|
url(r'^event/', include(events_patterns)),
|
||||||
|
# Calendrier
|
||||||
|
url(r'^calendar/', include(calendar_patterns)),
|
||||||
|
# Clubs
|
||||||
|
url(r'^clubs/', include(clubs_patterns)),
|
||||||
|
# Authentification
|
||||||
|
url(r'^cof/denied$', TemplateView.as_view(template_name='cof-denied.html'),
|
||||||
|
name="cof-denied"),
|
||||||
|
url(r'^cas/login$', django_cas_views.login, name="cas_login_view"),
|
||||||
|
url(r'^cas/logout$', django_cas_views.logout),
|
||||||
|
url(r'^outsider/login$', gestioncof_views.login_ext),
|
||||||
|
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
|
||||||
|
url(r'^login$', gestioncof_views.login, name="cof-login"),
|
||||||
|
url(r'^logout$', gestioncof_views.logout, name="cof-logout"),
|
||||||
|
# Infos persos
|
||||||
|
url(r'^profile$', gestioncof_views.profile),
|
||||||
|
url(r'^outsider/password-change$', django_views.password_change),
|
||||||
|
url(r'^outsider/password-change-done$',
|
||||||
|
django_views.password_change_done,
|
||||||
|
name='password_change_done'),
|
||||||
|
# Inscription d'un nouveau membre
|
||||||
|
url(r'^registration$', gestioncof_views.registration),
|
||||||
|
url(r'^registration/clipper/(?P<login_clipper>[\w-]+)/'
|
||||||
|
r'(?P<fullname>.*)$',
|
||||||
|
gestioncof_views.registration_form2, name="clipper-registration"),
|
||||||
|
url(r'^registration/user/(?P<username>.+)$',
|
||||||
|
gestioncof_views.registration_form2, name="user-registration"),
|
||||||
|
url(r'^registration/empty$', gestioncof_views.registration_form2,
|
||||||
|
name="empty-registration"),
|
||||||
|
# Autocompletion
|
||||||
|
url(r'^autocomplete/registration$', autocomplete),
|
||||||
|
url(r'^autocomplete/', include('autocomplete_light.urls')),
|
||||||
|
# Interface admin
|
||||||
|
url(r'^admin/logout/', gestioncof_views.logout),
|
||||||
|
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
|
url(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',
|
||||||
|
csv_views.admin_list_export,
|
||||||
|
{'fields': ['username', ]}),
|
||||||
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
url(r'^grappelli/', include('grappelli.urls')),
|
||||||
|
# Liens utiles du COF et du BdA
|
||||||
|
url(r'^utile_cof$', gestioncof_views.utile_cof),
|
||||||
|
url(r'^utile_bda$', gestioncof_views.utile_bda),
|
||||||
|
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
|
||||||
|
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
|
||||||
|
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
|
||||||
|
url(r'^k-fet/', include('kfet.urls')),
|
||||||
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
# djconfig
|
||||||
|
url(r"^config", gestioncof_views.ConfigUpdate.as_view()),
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||||
|
import debug_toolbar
|
||||||
|
urlpatterns += [
|
||||||
|
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
# Si on est en production, MEDIA_ROOT est servi par Apache.
|
||||||
|
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
|
||||||
|
urlpatterns += static(settings.MEDIA_URL,
|
||||||
|
document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
# Wagtail for uncatched
|
||||||
|
urlpatterns += [
|
||||||
|
url(r'', include(wagtail_urls)),
|
||||||
|
]
|
1
gestioncof/__init__.py
Normal file
1
gestioncof/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'gestioncof.apps.GestioncofConfig'
|
292
gestioncof/admin.py
Normal file
292
gestioncof/admin.py
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from gestioncof.models import SurveyQuestionAnswer, SurveyQuestion, \
|
||||||
|
CofProfile, EventOption, EventOptionChoice, Event, Club, \
|
||||||
|
Survey, EventCommentField, EventRegistration
|
||||||
|
from gestioncof.petits_cours_models import PetitCoursDemande, \
|
||||||
|
PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
|
||||||
|
PetitCoursAttributionCounter
|
||||||
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
import autocomplete_light
|
||||||
|
|
||||||
|
|
||||||
|
def add_link_field(target_model='', field='', link_text=str,
|
||||||
|
desc_text=str):
|
||||||
|
def add_link(cls):
|
||||||
|
reverse_name = target_model or cls.model.__name__.lower()
|
||||||
|
|
||||||
|
def link(self, instance):
|
||||||
|
app_name = instance._meta.app_label
|
||||||
|
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
|
||||||
|
link_obj = getattr(instance, field, None) or instance
|
||||||
|
if not link_obj.id:
|
||||||
|
return ""
|
||||||
|
url = reverse(reverse_path, args=(link_obj.id,))
|
||||||
|
return mark_safe("<a href='%s'>%s</a>"
|
||||||
|
% (url, link_text(link_obj)))
|
||||||
|
link.allow_tags = True
|
||||||
|
link.short_description = desc_text(reverse_name + ' link')
|
||||||
|
cls.link = link
|
||||||
|
cls.readonly_fields =\
|
||||||
|
list(getattr(cls, 'readonly_fields', [])) + ['link']
|
||||||
|
return cls
|
||||||
|
return add_link
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyQuestionAnswerInline(admin.TabularInline):
|
||||||
|
model = SurveyQuestionAnswer
|
||||||
|
|
||||||
|
|
||||||
|
@add_link_field(desc_text=lambda x: "Réponses",
|
||||||
|
link_text=lambda x: "Éditer les réponses")
|
||||||
|
class SurveyQuestionInline(admin.TabularInline):
|
||||||
|
model = SurveyQuestion
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyQuestionAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('survey__title', 'answer')
|
||||||
|
inlines = [
|
||||||
|
SurveyQuestionAnswerInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('title', 'details')
|
||||||
|
inlines = [
|
||||||
|
SurveyQuestionInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EventOptionChoiceInline(admin.TabularInline):
|
||||||
|
model = EventOptionChoice
|
||||||
|
|
||||||
|
|
||||||
|
@add_link_field(desc_text=lambda x: "Choix",
|
||||||
|
link_text=lambda x: "Éditer les choix")
|
||||||
|
class EventOptionInline(admin.TabularInline):
|
||||||
|
model = EventOption
|
||||||
|
|
||||||
|
|
||||||
|
class EventCommentFieldInline(admin.TabularInline):
|
||||||
|
model = EventCommentField
|
||||||
|
|
||||||
|
|
||||||
|
class EventOptionAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('event__title', 'name')
|
||||||
|
inlines = [
|
||||||
|
EventOptionChoiceInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EventAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('title', 'location', 'description')
|
||||||
|
inlines = [
|
||||||
|
EventOptionInline,
|
||||||
|
EventCommentFieldInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CofProfileInline(admin.StackedInline):
|
||||||
|
model = CofProfile
|
||||||
|
inline_classes = ("collapse open",)
|
||||||
|
|
||||||
|
|
||||||
|
class FkeyLookup(object):
|
||||||
|
def __init__(self, fkeydecl, short_description=None,
|
||||||
|
admin_order_field=None):
|
||||||
|
self.fk, fkattrs = fkeydecl.split('__', 1)
|
||||||
|
self.fkattrs = fkattrs.split('__')
|
||||||
|
|
||||||
|
self.short_description = short_description or self.fkattrs[-1]
|
||||||
|
self.admin_order_field = admin_order_field or fkeydecl
|
||||||
|
|
||||||
|
def __get__(self, obj, klass):
|
||||||
|
if obj is None:
|
||||||
|
"""
|
||||||
|
hack required to make Django validate (if obj is
|
||||||
|
None, then we're a class, and classes are callable
|
||||||
|
<wink>)
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
item = getattr(obj, self.fk)
|
||||||
|
for attr in self.fkattrs:
|
||||||
|
item = getattr(item, attr)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def ProfileInfo(field, short_description, boolean=False):
|
||||||
|
def getter(self):
|
||||||
|
try:
|
||||||
|
return getattr(self.profile, field)
|
||||||
|
except CofProfile.DoesNotExist:
|
||||||
|
return ""
|
||||||
|
getter.short_description = short_description
|
||||||
|
getter.boolean = boolean
|
||||||
|
return getter
|
||||||
|
|
||||||
|
User.profile_login_clipper = FkeyLookup("profile__login_clipper",
|
||||||
|
"Login clipper")
|
||||||
|
User.profile_phone = ProfileInfo("phone", "Téléphone")
|
||||||
|
User.profile_occupation = ProfileInfo("occupation", "Occupation")
|
||||||
|
User.profile_departement = ProfileInfo("departement", "Departement")
|
||||||
|
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True)
|
||||||
|
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True)
|
||||||
|
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente",
|
||||||
|
"ML BdA-R", True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileAdmin(UserAdmin):
|
||||||
|
def is_buro(self, obj):
|
||||||
|
try:
|
||||||
|
return obj.profile.is_buro
|
||||||
|
except CofProfile.DoesNotExist:
|
||||||
|
return False
|
||||||
|
is_buro.short_description = 'Membre du Buro'
|
||||||
|
is_buro.boolean = True
|
||||||
|
|
||||||
|
def is_cof(self, obj):
|
||||||
|
try:
|
||||||
|
return obj.profile.is_cof
|
||||||
|
except CofProfile.DoesNotExist:
|
||||||
|
return False
|
||||||
|
is_cof.short_description = 'Membre du COF'
|
||||||
|
is_cof.boolean = True
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
UserAdmin.list_display
|
||||||
|
+ ('profile_login_clipper', 'profile_phone', 'profile_occupation',
|
||||||
|
'profile_mailing_cof', 'profile_mailing_bda',
|
||||||
|
'profile_mailing_bda_revente', 'is_cof', 'is_buro', )
|
||||||
|
)
|
||||||
|
list_display_links = ('username', 'email', 'first_name', 'last_name')
|
||||||
|
list_filter = UserAdmin.list_filter \
|
||||||
|
+ ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof',
|
||||||
|
'profile__mailing_bda')
|
||||||
|
search_fields = UserAdmin.search_fields + ('profile__phone',)
|
||||||
|
inlines = [
|
||||||
|
CofProfileInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
staff_fieldsets = [
|
||||||
|
(None, {'fields': ['username', 'password']}),
|
||||||
|
(_('Personal info'), {'fields': ['first_name', 'last_name', 'email']}),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_fieldsets(self, request, user=None):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return self.staff_fieldsets
|
||||||
|
return super(UserProfileAdmin, self).get_fieldsets(request, user)
|
||||||
|
|
||||||
|
def save_model(self, request, user, form, change):
|
||||||
|
cof_group, created = Group.objects.get_or_create(name='COF')
|
||||||
|
if created:
|
||||||
|
# Si le groupe COF n'était pas déjà dans la bdd
|
||||||
|
# On lui assigne les bonnes permissions
|
||||||
|
perms = Permission.objects.filter(
|
||||||
|
Q(content_type__app_label='gestioncof')
|
||||||
|
| Q(content_type__app_label='bda')
|
||||||
|
| (Q(content_type__app_label='auth')
|
||||||
|
& Q(content_type__model='user')))
|
||||||
|
cof_group.permissions = perms
|
||||||
|
# On y associe les membres du Burô
|
||||||
|
cof_group.user_set = User.objects.filter(profile__is_buro=True)
|
||||||
|
# Sauvegarde
|
||||||
|
cof_group.save()
|
||||||
|
# le Burô est staff et appartient au groupe COF
|
||||||
|
if user.profile.is_buro:
|
||||||
|
user.is_staff = True
|
||||||
|
user.groups.add(cof_group)
|
||||||
|
else:
|
||||||
|
user.is_staff = False
|
||||||
|
user.groups.remove(cof_group)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: This is absolutely horrible.
|
||||||
|
def user_str(self):
|
||||||
|
if self.first_name and self.last_name:
|
||||||
|
return "{} ({})".format(self.get_full_name(), self.username)
|
||||||
|
else:
|
||||||
|
return self.username
|
||||||
|
User.__str__ = user_str
|
||||||
|
|
||||||
|
|
||||||
|
class EventRegistrationAdmin(admin.ModelAdmin):
|
||||||
|
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[])
|
||||||
|
list_display = ('__str__', 'event', 'user', 'paid')
|
||||||
|
list_filter = ('paid',)
|
||||||
|
search_fields = ('user__username', 'user__first_name', 'user__last_name',
|
||||||
|
'user__email', 'event__title')
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursAbilityAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'matiere', 'niveau', 'agrege')
|
||||||
|
search_fields = ('user__username', 'user__first_name', 'user__last_name',
|
||||||
|
'user__email', 'matiere__name', 'niveau')
|
||||||
|
list_filter = ('matiere', 'niveau', 'agrege')
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursAttributionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'demande', 'matiere', 'rank', )
|
||||||
|
search_fields = ('user__username', 'matiere__name')
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursAttributionCounterAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'matiere', 'count', )
|
||||||
|
list_filter = ('matiere',)
|
||||||
|
search_fields = ('user__username', 'user__first_name', 'user__last_name',
|
||||||
|
'user__email', 'matiere__name')
|
||||||
|
actions = ['reset', ]
|
||||||
|
actions_on_bottom = True
|
||||||
|
|
||||||
|
def reset(self, request, queryset):
|
||||||
|
queryset.update(count=0)
|
||||||
|
reset.short_description = "Remise à zéro du compteur"
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursDemandeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'email', 'agrege_requis', 'niveau', 'created',
|
||||||
|
'traitee', 'processed')
|
||||||
|
list_filter = ('traitee', 'niveau')
|
||||||
|
search_fields = ('name', 'email', 'phone', 'lieu', 'remarques')
|
||||||
|
|
||||||
|
|
||||||
|
class ClubAdminForm(forms.ModelForm):
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(ClubAdminForm, self).clean()
|
||||||
|
respos = cleaned_data.get('respos')
|
||||||
|
members = cleaned_data.get('membres')
|
||||||
|
for respo in respos.all():
|
||||||
|
if respo not in members:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Erreur : le respo %s n'est pas membre du club."
|
||||||
|
% respo.get_full_name())
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class ClubAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name']
|
||||||
|
form = ClubAdminForm
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Survey, SurveyAdmin)
|
||||||
|
admin.site.register(SurveyQuestion, SurveyQuestionAdmin)
|
||||||
|
admin.site.register(Event, EventAdmin)
|
||||||
|
admin.site.register(EventOption, EventOptionAdmin)
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.register(User, UserProfileAdmin)
|
||||||
|
admin.site.register(CofProfile)
|
||||||
|
admin.site.register(Club, ClubAdmin)
|
||||||
|
admin.site.register(PetitCoursSubject)
|
||||||
|
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
|
||||||
|
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)
|
||||||
|
admin.site.register(PetitCoursAttributionCounter,
|
||||||
|
PetitCoursAttributionCounterAdmin)
|
||||||
|
admin.site.register(PetitCoursDemande, PetitCoursDemandeAdmin)
|
||||||
|
admin.site.register(EventRegistration, EventRegistrationAdmin)
|
15
gestioncof/apps.py
Normal file
15
gestioncof/apps.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GestioncofConfig(AppConfig):
|
||||||
|
name = 'gestioncof'
|
||||||
|
verbose_name = "Gestion des adhérents du COF"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals
|
||||||
|
self.register_config()
|
||||||
|
|
||||||
|
def register_config(self):
|
||||||
|
import djconfig
|
||||||
|
from .forms import GestioncofConfigForm
|
||||||
|
djconfig.register(GestioncofConfigForm)
|
88
gestioncof/autocomplete.py
Normal file
88
gestioncof/autocomplete.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from ldap3 import Connection
|
||||||
|
|
||||||
|
from django import shortcuts
|
||||||
|
from django.http import Http404
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from gestioncof.models import CofProfile
|
||||||
|
from gestioncof.decorators import buro_required
|
||||||
|
|
||||||
|
|
||||||
|
class Clipper(object):
|
||||||
|
def __init__(self, clipper, fullname):
|
||||||
|
if fullname is None:
|
||||||
|
fullname = ""
|
||||||
|
assert isinstance(clipper, str)
|
||||||
|
assert isinstance(fullname, str)
|
||||||
|
self.clipper = clipper
|
||||||
|
self.fullname = fullname
|
||||||
|
|
||||||
|
|
||||||
|
@buro_required
|
||||||
|
def autocomplete(request):
|
||||||
|
if "q" not in request.GET:
|
||||||
|
raise Http404
|
||||||
|
q = request.GET['q']
|
||||||
|
data = {
|
||||||
|
'q': q,
|
||||||
|
}
|
||||||
|
|
||||||
|
queries = {}
|
||||||
|
bits = q.split()
|
||||||
|
|
||||||
|
# Fetching data from User and CofProfile tables
|
||||||
|
queries['members'] = CofProfile.objects.filter(is_cof=True)
|
||||||
|
queries['users'] = User.objects.filter(profile__is_cof=False)
|
||||||
|
for bit in bits:
|
||||||
|
queries['members'] = queries['members'].filter(
|
||||||
|
Q(user__first_name__icontains=bit)
|
||||||
|
| Q(user__last_name__icontains=bit)
|
||||||
|
| Q(user__username__icontains=bit)
|
||||||
|
| Q(login_clipper__icontains=bit))
|
||||||
|
queries['users'] = queries['users'].filter(
|
||||||
|
Q(first_name__icontains=bit)
|
||||||
|
| Q(last_name__icontains=bit)
|
||||||
|
| Q(username__icontains=bit))
|
||||||
|
queries['members'] = queries['members'].distinct()
|
||||||
|
queries['users'] = queries['users'].distinct()
|
||||||
|
|
||||||
|
# Clearing redundancies
|
||||||
|
usernames = (
|
||||||
|
set(queries['members'].values_list('login_clipper', flat='True'))
|
||||||
|
| set(queries['users'].values_list('profile__login_clipper',
|
||||||
|
flat='True'))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetching data from the SPI
|
||||||
|
if getattr(settings, 'LDAP_SERVER_URL', None):
|
||||||
|
# Fetching
|
||||||
|
ldap_query = '(&{:s})'.format(''.join(
|
||||||
|
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit)
|
||||||
|
for bit in bits if bit.isalnum()
|
||||||
|
))
|
||||||
|
if ldap_query != "(&)":
|
||||||
|
# If none of the bits were legal, we do not perform the query
|
||||||
|
entries = None
|
||||||
|
with Connection(settings.LDAP_SERVER_URL) as conn:
|
||||||
|
conn.search(
|
||||||
|
'dc=spi,dc=ens,dc=fr', ldap_query,
|
||||||
|
attributes=['uid', 'cn']
|
||||||
|
)
|
||||||
|
entries = conn.entries
|
||||||
|
# Clearing redundancies
|
||||||
|
queries['clippers'] = [
|
||||||
|
Clipper(entry.uid.value, entry.cn.value)
|
||||||
|
for entry in entries
|
||||||
|
if entry.uid.value
|
||||||
|
and entry.uid.value not in usernames
|
||||||
|
]
|
||||||
|
|
||||||
|
# Resulting data
|
||||||
|
data.update(queries)
|
||||||
|
data['options'] = sum(len(query) for query in queries)
|
||||||
|
|
||||||
|
return shortcuts.render(request, "autocomplete_user.html", data)
|
10
gestioncof/autocomplete_light_registry.py
Normal file
10
gestioncof/autocomplete_light_registry.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import autocomplete_light
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
autocomplete_light.register(
|
||||||
|
User, search_fields=('username', 'first_name', 'last_name'),
|
||||||
|
attrs={'placeholder': 'membre...'}
|
||||||
|
)
|
75
gestioncof/csv_views.py
Normal file
75
gestioncof/csv_views.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from django.http import HttpResponse, HttpResponseForbidden
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
|
||||||
|
def export(qs, fields=None):
|
||||||
|
model = qs.model
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename=%s.csv' \
|
||||||
|
% slugify(model.__name__)
|
||||||
|
writer = csv.writer(response)
|
||||||
|
# Write headers to CSV file
|
||||||
|
if fields:
|
||||||
|
headers = fields
|
||||||
|
else:
|
||||||
|
headers = []
|
||||||
|
for field in model._meta.fields:
|
||||||
|
headers.append(field.name)
|
||||||
|
writer.writerow(headers)
|
||||||
|
# Write data to CSV file
|
||||||
|
for obj in qs:
|
||||||
|
row = []
|
||||||
|
for field in headers:
|
||||||
|
if field in headers:
|
||||||
|
val = getattr(obj, field)
|
||||||
|
if callable(val):
|
||||||
|
val = val()
|
||||||
|
row.append(val)
|
||||||
|
writer.writerow(row)
|
||||||
|
# Return CSV file to browser as download
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def admin_list_export(request, model_name, app_label, queryset=None,
|
||||||
|
fields=None, list_display=True):
|
||||||
|
"""
|
||||||
|
Put the following line in your urls.py BEFORE your admin include
|
||||||
|
(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',
|
||||||
|
'util.csv_view.admin_list_export'),
|
||||||
|
"""
|
||||||
|
if not request.user.is_staff:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
if not queryset:
|
||||||
|
model = apps.get_model(app_label, model_name)
|
||||||
|
queryset = model.objects.all()
|
||||||
|
queryset = queryset.filter(profile__is_cof=True)
|
||||||
|
if not fields:
|
||||||
|
if list_display and len(queryset.model._meta.admin.list_display) > 1:
|
||||||
|
fields = queryset.model._meta.admin.list_display
|
||||||
|
else:
|
||||||
|
fields = None
|
||||||
|
return export(queryset, fields)
|
||||||
|
"""
|
||||||
|
Create your own change_list.html for your admin view and put something
|
||||||
|
like this in it:
|
||||||
|
{% block object-tools %}
|
||||||
|
<ul class="object-tools">
|
||||||
|
<li><a href="csv/{%if request.GET%}?{{request.GET.urlencode}}
|
||||||
|
{%endif%}" class="addlink">Export to CSV</a></li>
|
||||||
|
{% if has_add_permission %}
|
||||||
|
<li><a href="add/{% if is_popup %}?_popup=1{% endif %}"
|
||||||
|
class="addlink">
|
||||||
|
{% blocktrans with cl.opts.verbose_name|escape as name %}
|
||||||
|
Add {{ name }}{% endblocktrans %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
"""
|
23
gestioncof/decorators.py
Normal file
23
gestioncof/decorators.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
|
|
||||||
|
|
||||||
|
def is_cof(user):
|
||||||
|
try:
|
||||||
|
profile = user.profile
|
||||||
|
return profile.is_cof
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cof_required = user_passes_test(is_cof)
|
||||||
|
|
||||||
|
|
||||||
|
def is_buro(user):
|
||||||
|
try:
|
||||||
|
profile = user.profile
|
||||||
|
return profile.is_buro
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
buro_required = user_passes_test(is_buro)
|
199
gestioncof/fixtures/gestion.json
Normal file
199
gestioncof/fixtures/gestion.json
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"old": false,
|
||||||
|
"details": "Il nous casse les oreilles, qu'est ce qu'on en fait\u00a0?",
|
||||||
|
"survey_open": true,
|
||||||
|
"title": "Sort du barde"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.survey",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"question": "Sanction s'il chante",
|
||||||
|
"survey": 1,
|
||||||
|
"multi_answers": true
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestion",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"question": "Est-ce qu'on le garde\u00a0?",
|
||||||
|
"survey": 1,
|
||||||
|
"multi_answers": false
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestion",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "On l'ernestise",
|
||||||
|
"survey_question": 1
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "On ligote",
|
||||||
|
"survey_question": 1
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "On le prive de banquet",
|
||||||
|
"survey_question": 1
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "Oui",
|
||||||
|
"survey_question": 2
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"answer": "Non",
|
||||||
|
"survey_question": 2
|
||||||
|
},
|
||||||
|
"model": "gestioncof.surveyquestionanswer",
|
||||||
|
"pk": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"old": false,
|
||||||
|
"description": "On va casser du romain.",
|
||||||
|
"end_date": "2016-09-12T00:00:00Z",
|
||||||
|
"title": "Bataille de Gergovie",
|
||||||
|
"image": "",
|
||||||
|
"location": "Gergovie",
|
||||||
|
"registration_open": true,
|
||||||
|
"start_date": "2016-09-09T00:00:00Z"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.event",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"default": "",
|
||||||
|
"event": 1,
|
||||||
|
"fieldtype": "text",
|
||||||
|
"name": "Commentaires"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventcommentfield",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"multi_choices": true,
|
||||||
|
"event": 1,
|
||||||
|
"name": "Potion magique"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoption",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"event_option": 1,
|
||||||
|
"value": "Je suis alergique"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoptionchoice",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"event_option": 1,
|
||||||
|
"value": "J'en veux"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoptionchoice",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"event_option": 1,
|
||||||
|
"value": "Je suis tomb\u00e9 dans la marmite quand j'\u00e9tais petit"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.eventoptionchoice",
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Bagarre"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Lancer de menhir"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Pr\u00e9paration de potions"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": "Chant"
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcourssubject",
|
||||||
|
"pk": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"traitee": false,
|
||||||
|
"remarques": "En grande difficult\u00e9",
|
||||||
|
"quand": "weekend (dimanche) / soir apr\u00e8s les cours",
|
||||||
|
"name": "Jules C\u00e9sar",
|
||||||
|
"created": "2016-07-15T11:12:35Z",
|
||||||
|
"niveau": "prepa1styear",
|
||||||
|
"agrege_requis": false,
|
||||||
|
"phone": "",
|
||||||
|
"traitee_par": null,
|
||||||
|
"matieres": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"lieu": "Al\u00e9sia",
|
||||||
|
"freq": "3 fois / semaine",
|
||||||
|
"email": "jules.cesar@polytechnique.edu",
|
||||||
|
"processed": null
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcoursdemande",
|
||||||
|
"pk": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"traitee": false,
|
||||||
|
"remarques": "",
|
||||||
|
"quand": "Weekends",
|
||||||
|
"name": "Jules C\u00e9sar",
|
||||||
|
"created": "2016-07-15T11:13:26Z",
|
||||||
|
"niveau": "lycee",
|
||||||
|
"agrege_requis": true,
|
||||||
|
"phone": "",
|
||||||
|
"traitee_par": null,
|
||||||
|
"matieres": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"lieu": "\u00e0 domicile",
|
||||||
|
"freq": "toutes les semaines",
|
||||||
|
"email": "jules.cesar@polytechnique.edu",
|
||||||
|
"processed": null
|
||||||
|
},
|
||||||
|
"model": "gestioncof.petitcoursdemande",
|
||||||
|
"pk": 2
|
||||||
|
}
|
||||||
|
]
|
10
gestioncof/fixtures/sites.json
Normal file
10
gestioncof/fixtures/sites.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"domain": "localhost",
|
||||||
|
"name": "GestioCOF - dev - local"
|
||||||
|
},
|
||||||
|
"model": "sites.site",
|
||||||
|
"pk": 1
|
||||||
|
}
|
||||||
|
]
|
391
gestioncof/forms.py
Normal file
391
gestioncof/forms.py
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
|
||||||
|
from django.forms.formsets import BaseFormSet, formset_factory
|
||||||
|
|
||||||
|
from djconfig.forms import ConfigForm
|
||||||
|
|
||||||
|
from gestioncof.models import CofProfile, EventCommentValue, \
|
||||||
|
CalendarSubscription, Club
|
||||||
|
from gestioncof.widgets import TriStateCheckbox
|
||||||
|
|
||||||
|
from bda.models import Spectacle
|
||||||
|
|
||||||
|
|
||||||
|
class EventForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs.pop("event")
|
||||||
|
self.event = event
|
||||||
|
current_choices = kwargs.pop("current_choices", None)
|
||||||
|
super(EventForm, self).__init__(*args, **kwargs)
|
||||||
|
choices = {}
|
||||||
|
if current_choices:
|
||||||
|
for choice in current_choices.all():
|
||||||
|
if choice.event_option.id not in choices:
|
||||||
|
choices[choice.event_option.id] = [choice.id]
|
||||||
|
else:
|
||||||
|
choices[choice.event_option.id].append(choice.id)
|
||||||
|
all_choices = choices
|
||||||
|
for option in event.options.all():
|
||||||
|
choices = [(choice.id, choice.value)
|
||||||
|
for choice in option.choices.all()]
|
||||||
|
if option.multi_choices:
|
||||||
|
initial = [] if option.id not in all_choices \
|
||||||
|
else all_choices[option.id]
|
||||||
|
field = forms.MultipleChoiceField(
|
||||||
|
label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
else:
|
||||||
|
initial = None if option.id not in all_choices \
|
||||||
|
else all_choices[option.id][0]
|
||||||
|
field = forms.ChoiceField(label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
field.option_id = option.id
|
||||||
|
self.fields["option_%d" % option.id] = field
|
||||||
|
|
||||||
|
def choices(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith('option_'):
|
||||||
|
yield (self.fields[name].option_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
survey = kwargs.pop("survey")
|
||||||
|
current_answers = kwargs.pop("current_answers", None)
|
||||||
|
super(SurveyForm, self).__init__(*args, **kwargs)
|
||||||
|
answers = {}
|
||||||
|
if current_answers:
|
||||||
|
for answer in current_answers.all():
|
||||||
|
if answer.survey_question.id not in answers:
|
||||||
|
answers[answer.survey_question.id] = [answer.id]
|
||||||
|
else:
|
||||||
|
answers[answer.survey_question.id].append(answer.id)
|
||||||
|
for question in survey.questions.all():
|
||||||
|
choices = [(answer.id, answer.answer)
|
||||||
|
for answer in question.answers.all()]
|
||||||
|
if question.multi_answers:
|
||||||
|
initial = [] if question.id not in answers\
|
||||||
|
else answers[question.id]
|
||||||
|
field = forms.MultipleChoiceField(
|
||||||
|
label=question.question,
|
||||||
|
choices=choices,
|
||||||
|
widget=CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
else:
|
||||||
|
initial = None if question.id not in answers\
|
||||||
|
else answers[question.id][0]
|
||||||
|
field = forms.ChoiceField(label=question.question,
|
||||||
|
choices=choices,
|
||||||
|
widget=RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
field.question_id = question.id
|
||||||
|
self.fields["question_%d" % question.id] = field
|
||||||
|
|
||||||
|
def answers(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith('question_'):
|
||||||
|
yield (self.fields[name].question_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyStatusFilterForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
survey = kwargs.pop("survey")
|
||||||
|
super(SurveyStatusFilterForm, self).__init__(*args, **kwargs)
|
||||||
|
for question in survey.questions.all():
|
||||||
|
for answer in question.answers.all():
|
||||||
|
name = "question_%d_answer_%d" % (question.id, answer.id)
|
||||||
|
if self.is_bound \
|
||||||
|
and self.data.get(self.add_prefix(name), None):
|
||||||
|
initial = self.data.get(self.add_prefix(name), None)
|
||||||
|
else:
|
||||||
|
initial = "none"
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label="%s : %s" % (question.question, answer.answer),
|
||||||
|
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
|
||||||
|
widget=TriStateCheckbox,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
field.question_id = question.id
|
||||||
|
field.answer_id = answer.id
|
||||||
|
self.fields[name] = field
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith('question_'):
|
||||||
|
yield (self.fields[name].question_id,
|
||||||
|
self.fields[name].answer_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class EventStatusFilterForm(forms.Form):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs.pop("event")
|
||||||
|
super(EventStatusFilterForm, self).__init__(*args, **kwargs)
|
||||||
|
for option in event.options.all():
|
||||||
|
for choice in option.choices.all():
|
||||||
|
name = "option_%d_choice_%d" % (option.id, choice.id)
|
||||||
|
if self.is_bound \
|
||||||
|
and self.data.get(self.add_prefix(name), None):
|
||||||
|
initial = self.data.get(self.add_prefix(name), None)
|
||||||
|
else:
|
||||||
|
initial = "none"
|
||||||
|
field = forms.ChoiceField(
|
||||||
|
label="%s : %s" % (option.name, choice.value),
|
||||||
|
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
|
||||||
|
widget=TriStateCheckbox,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
field.option_id = option.id
|
||||||
|
field.choice_id = choice.id
|
||||||
|
self.fields[name] = field
|
||||||
|
# has_paid
|
||||||
|
name = "event_has_paid"
|
||||||
|
if self.is_bound and self.data.get(self.add_prefix(name), None):
|
||||||
|
initial = self.data.get(self.add_prefix(name), None)
|
||||||
|
else:
|
||||||
|
initial = "none"
|
||||||
|
field = forms.ChoiceField(label="Événement payé",
|
||||||
|
choices=[("yes", "yes"), ("no", "no"),
|
||||||
|
("none", "none")],
|
||||||
|
widget=TriStateCheckbox,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
self.fields[name] = field
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith('option_'):
|
||||||
|
yield (self.fields[name].option_id,
|
||||||
|
self.fields[name].choice_id, value)
|
||||||
|
elif name == "event_has_paid":
|
||||||
|
yield ("has_paid", None, value)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
first_name = forms.CharField(label=_('Prénom'), max_length=30)
|
||||||
|
last_name = forms.CharField(label=_('Nom'), max_length=30)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
super(UserProfileForm, self).__init__(*args, **kw)
|
||||||
|
self.fields['first_name'].initial = self.instance.user.first_name
|
||||||
|
self.fields['last_name'].initial = self.instance.user.last_name
|
||||||
|
|
||||||
|
def save(self, *args, **kw):
|
||||||
|
super(UserProfileForm, self).save(*args, **kw)
|
||||||
|
self.instance.user.first_name = self.cleaned_data.get('first_name')
|
||||||
|
self.instance.user.last_name = self.cleaned_data.get('last_name')
|
||||||
|
self.instance.user.save()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CofProfile
|
||||||
|
fields = ["first_name", "last_name", "phone", "mailing_cof",
|
||||||
|
"mailing_bda", "mailing_bda_revente"]
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationUserForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
super(RegistrationUserForm, self).__init__(*args, **kw)
|
||||||
|
self.fields['username'].help_text = ""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("username", "first_name", "last_name", "email")
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationPassUserForm(RegistrationUserForm):
|
||||||
|
"""
|
||||||
|
Formulaire pour changer le mot de passe d'un utilisateur.
|
||||||
|
"""
|
||||||
|
password1 = forms.CharField(label=_('Mot de passe'),
|
||||||
|
widget=forms.PasswordInput)
|
||||||
|
password2 = forms.CharField(label=_('Confirmation du mot de passe'),
|
||||||
|
widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
def clean_password2(self):
|
||||||
|
pass1 = self.cleaned_data['password1']
|
||||||
|
pass2 = self.cleaned_data['password2']
|
||||||
|
if pass1 and pass2:
|
||||||
|
if pass1 != pass2:
|
||||||
|
raise forms.ValidationError(_('Mots de passe non identiques.'))
|
||||||
|
return pass2
|
||||||
|
|
||||||
|
def save(self, commit=True, *args, **kwargs):
|
||||||
|
user = super(RegistrationPassUserForm, self).save(commit, *args,
|
||||||
|
**kwargs)
|
||||||
|
user.set_password(self.cleaned_data['password2'])
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationProfileForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
super(RegistrationProfileForm, self).__init__(*args, **kw)
|
||||||
|
self.fields['mailing_cof'].initial = True
|
||||||
|
self.fields['mailing_bda'].initial = True
|
||||||
|
self.fields['mailing_bda_revente'].initial = True
|
||||||
|
|
||||||
|
self.fields.keyOrder = [
|
||||||
|
'login_clipper',
|
||||||
|
'phone',
|
||||||
|
'occupation',
|
||||||
|
'departement',
|
||||||
|
'is_cof',
|
||||||
|
'type_cotiz',
|
||||||
|
'mailing_cof',
|
||||||
|
'mailing_bda',
|
||||||
|
'mailing_bda_revente',
|
||||||
|
'comments'
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CofProfile
|
||||||
|
fields = ("login_clipper", "phone", "occupation",
|
||||||
|
"departement", "is_cof", "type_cotiz", "mailing_cof",
|
||||||
|
"mailing_bda", "mailing_bda_revente", "comments")
|
||||||
|
|
||||||
|
STATUS_CHOICES = (('no', 'Non'),
|
||||||
|
('wait', 'Oui mais attente paiement'),
|
||||||
|
('paid', 'Oui payé'),)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminEventForm(forms.Form):
|
||||||
|
status = forms.ChoiceField(label="Inscription", initial="no",
|
||||||
|
choices=STATUS_CHOICES, widget=RadioSelect)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop("event")
|
||||||
|
registration = kwargs.pop("current_registration", None)
|
||||||
|
current_choices, paid = \
|
||||||
|
(registration.options.all(), registration.paid) \
|
||||||
|
if registration is not None else ([], None)
|
||||||
|
if paid is True:
|
||||||
|
kwargs["initial"] = {"status": "paid"}
|
||||||
|
elif paid is False:
|
||||||
|
kwargs["initial"] = {"status": "wait"}
|
||||||
|
else:
|
||||||
|
kwargs["initial"] = {"status": "no"}
|
||||||
|
super(AdminEventForm, self).__init__(*args, **kwargs)
|
||||||
|
choices = {}
|
||||||
|
for choice in current_choices:
|
||||||
|
if choice.event_option.id not in choices:
|
||||||
|
choices[choice.event_option.id] = [choice.id]
|
||||||
|
else:
|
||||||
|
choices[choice.event_option.id].append(choice.id)
|
||||||
|
all_choices = choices
|
||||||
|
for option in self.event.options.all():
|
||||||
|
choices = [(choice.id, choice.value)
|
||||||
|
for choice in option.choices.all()]
|
||||||
|
if option.multi_choices:
|
||||||
|
initial = [] if option.id not in all_choices\
|
||||||
|
else all_choices[option.id]
|
||||||
|
field = forms.MultipleChoiceField(
|
||||||
|
label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
else:
|
||||||
|
initial = None if option.id not in all_choices\
|
||||||
|
else all_choices[option.id][0]
|
||||||
|
field = forms.ChoiceField(label=option.name,
|
||||||
|
choices=choices,
|
||||||
|
widget=RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
field.option_id = option.id
|
||||||
|
self.fields["option_%d" % option.id] = field
|
||||||
|
for commentfield in self.event.commentfields.all():
|
||||||
|
initial = commentfield.default
|
||||||
|
if registration is not None:
|
||||||
|
try:
|
||||||
|
initial = registration.comments \
|
||||||
|
.get(commentfield=commentfield).content
|
||||||
|
except EventCommentValue.DoesNotExist:
|
||||||
|
pass
|
||||||
|
widget = forms.Textarea if commentfield.fieldtype == "text" \
|
||||||
|
else forms.TextInput
|
||||||
|
field = forms.CharField(label=commentfield.name,
|
||||||
|
widget=widget,
|
||||||
|
required=False,
|
||||||
|
initial=initial)
|
||||||
|
field.comment_id = commentfield.id
|
||||||
|
self.fields["comment_%d" % commentfield.id] = field
|
||||||
|
|
||||||
|
def choices(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith('option_'):
|
||||||
|
yield (self.fields[name].option_id, value)
|
||||||
|
|
||||||
|
def comments(self):
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith('comment_'):
|
||||||
|
yield (self.fields[name].comment_id, value)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEventRegistrationFormset(BaseFormSet):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.events = kwargs.pop('events')
|
||||||
|
self.current_registrations = kwargs.pop('current_registrations', None)
|
||||||
|
self.extra = len(self.events)
|
||||||
|
super(BaseEventRegistrationFormset, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _construct_form(self, index, **kwargs):
|
||||||
|
kwargs['event'] = self.events[index]
|
||||||
|
if self.current_registrations is not None:
|
||||||
|
kwargs['current_registration'] = self.current_registrations[index]
|
||||||
|
return super(BaseEventRegistrationFormset, self)._construct_form(
|
||||||
|
index, **kwargs)
|
||||||
|
EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarForm(forms.ModelForm):
|
||||||
|
subscribe_to_events = forms.BooleanField(
|
||||||
|
initial=True,
|
||||||
|
label="Événements du COF")
|
||||||
|
subscribe_to_my_shows = forms.BooleanField(
|
||||||
|
initial=True,
|
||||||
|
label="Les spectacles pour lesquels j'ai obtenu une place")
|
||||||
|
other_shows = forms.ModelMultipleChoiceField(
|
||||||
|
label="Spectacles supplémentaires",
|
||||||
|
queryset=Spectacle.objects.filter(tirage__active=True),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CalendarSubscription
|
||||||
|
fields = ['subscribe_to_events', 'subscribe_to_my_shows',
|
||||||
|
'other_shows']
|
||||||
|
|
||||||
|
|
||||||
|
class ClubsForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Formulaire d'inscription d'un membre à plusieurs clubs du COF.
|
||||||
|
"""
|
||||||
|
clubs = forms.ModelMultipleChoiceField(
|
||||||
|
label="Inscriptions aux clubs du COF",
|
||||||
|
queryset=Club.objects.all(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Announcements banner
|
||||||
|
# TODO: move this to the `gestion` app once the supportBDS branch is merged
|
||||||
|
# ---
|
||||||
|
|
||||||
|
class GestioncofConfigForm(ConfigForm):
|
||||||
|
gestion_banner = forms.CharField(
|
||||||
|
label=_("Announcements banner"),
|
||||||
|
help_text=_("An empty banner disables annoucements"),
|
||||||
|
max_length=2048
|
||||||
|
)
|
0
gestioncof/management/__init__.py
Normal file
0
gestioncof/management/__init__.py
Normal file
41
gestioncof/management/base.py
Normal file
41
gestioncof/management/base.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
"""
|
||||||
|
Un mixin à utiliser avec BaseCommand pour charger des objets depuis un json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class MyBaseCommand(BaseCommand):
|
||||||
|
"""
|
||||||
|
Ajoute une méthode ``from_json`` qui charge des objets à partir d'un json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def from_json(self, filename, data_dir, klass,
|
||||||
|
callback=lambda obj: obj):
|
||||||
|
"""
|
||||||
|
Charge les objets contenus dans le fichier json référencé par
|
||||||
|
``filename`` dans la base de donnée. La fonction callback est appelées
|
||||||
|
sur chaque objet avant enregistrement.
|
||||||
|
"""
|
||||||
|
self.stdout.write("Chargement de {:s}".format(filename))
|
||||||
|
with open(os.path.join(data_dir, filename), 'r') as file:
|
||||||
|
descriptions = json.load(file)
|
||||||
|
objects = []
|
||||||
|
nb_new = 0
|
||||||
|
for description in descriptions:
|
||||||
|
qset = klass.objects.filter(**description)
|
||||||
|
try:
|
||||||
|
objects.append(qset.get())
|
||||||
|
except klass.DoesNotExist:
|
||||||
|
obj = klass(**description)
|
||||||
|
obj = callback(obj)
|
||||||
|
obj.save()
|
||||||
|
objects.append(obj)
|
||||||
|
nb_new += 1
|
||||||
|
self.stdout.write("- {:d} objets créés".format(nb_new))
|
||||||
|
self.stdout.write("- {:d} objets gardés en l'état"
|
||||||
|
.format(len(objects)-nb_new))
|
||||||
|
return objects
|
0
gestioncof/management/commands/__init__.py
Normal file
0
gestioncof/management/commands/__init__.py
Normal file
115
gestioncof/management/commands/loaddevdata.py
Normal file
115
gestioncof/management/commands/loaddevdata.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
"""
|
||||||
|
Charge des données de test dans la BDD
|
||||||
|
- Utilisateurs
|
||||||
|
- Sondage
|
||||||
|
- Événement
|
||||||
|
- Petits cours
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
from gestioncof.management.base import MyBaseCommand
|
||||||
|
from gestioncof.petits_cours_models import (
|
||||||
|
PetitCoursAbility, PetitCoursSubject, LEVELS_CHOICES,
|
||||||
|
PetitCoursAttributionCounter
|
||||||
|
)
|
||||||
|
|
||||||
|
# Où sont stockés les fichiers json
|
||||||
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
'data')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(MyBaseCommand):
|
||||||
|
help = "Charge des données de test dans la BDD"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""
|
||||||
|
Permet de ne pas créer l'utilisateur "root".
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-root',
|
||||||
|
action='store_true',
|
||||||
|
dest='no-root',
|
||||||
|
default=False,
|
||||||
|
help='Ne crée pas l\'utilisateur "root"'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# ---
|
||||||
|
# Utilisateurs
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# Gaulois
|
||||||
|
gaulois = self.from_json('gaulois.json', DATA_DIR, User)
|
||||||
|
for user in gaulois:
|
||||||
|
user.profile.is_cof = True
|
||||||
|
user.profile.save()
|
||||||
|
|
||||||
|
# Romains
|
||||||
|
self.from_json('romains.json', DATA_DIR, User)
|
||||||
|
|
||||||
|
# Root
|
||||||
|
no_root = options.get('no-root', False)
|
||||||
|
if not no_root:
|
||||||
|
self.stdout.write("Création de l'utilisateur root")
|
||||||
|
root, _ = User.objects.get_or_create(
|
||||||
|
username='root',
|
||||||
|
first_name='super',
|
||||||
|
last_name='user',
|
||||||
|
email='root@localhost')
|
||||||
|
root.set_password('root')
|
||||||
|
root.is_staff = True
|
||||||
|
root.is_superuser = True
|
||||||
|
root.profile.is_cof = True
|
||||||
|
root.profile.is_buro = True
|
||||||
|
root.profile.save()
|
||||||
|
root.save()
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Petits cours
|
||||||
|
# ---
|
||||||
|
|
||||||
|
self.stdout.write("Inscriptions au système des petits cours")
|
||||||
|
levels = [id for (id, verbose) in LEVELS_CHOICES]
|
||||||
|
subjects = list(PetitCoursSubject.objects.all())
|
||||||
|
nb_of_teachers = 0
|
||||||
|
for user in gaulois:
|
||||||
|
if random.randint(0, 1):
|
||||||
|
nb_of_teachers += 1
|
||||||
|
# L'utilisateur reçoit les demandes de petits cours
|
||||||
|
user.profile.petits_cours_accept = True
|
||||||
|
user.save()
|
||||||
|
# L'utilisateur est compétent dans une matière
|
||||||
|
subject = random.choice(subjects)
|
||||||
|
if not PetitCoursAbility.objects.filter(
|
||||||
|
user=user,
|
||||||
|
matiere=subject).exists():
|
||||||
|
PetitCoursAbility.objects.create(
|
||||||
|
user=user,
|
||||||
|
matiere=subject,
|
||||||
|
niveau=random.choice(levels),
|
||||||
|
agrege=bool(random.randint(0, 1))
|
||||||
|
)
|
||||||
|
# On initialise son compteur d'attributions
|
||||||
|
PetitCoursAttributionCounter.objects.get_or_create(
|
||||||
|
user=user,
|
||||||
|
matiere=subject
|
||||||
|
)
|
||||||
|
self.stdout.write("- {:d} inscriptions".format(nb_of_teachers))
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Le BdA
|
||||||
|
# ---
|
||||||
|
|
||||||
|
call_command('loadbdadevdata')
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# La K-Fêt
|
||||||
|
# ---
|
||||||
|
|
||||||
|
call_command('loadkfetdevdata')
|
86
gestioncof/management/commands/syncmails.py
Normal file
86
gestioncof/management/commands/syncmails.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Import des mails de GestioCOF dans la base de donnée
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from custommail.models import Type, CustomMail, Variable
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = ("Va chercher les données mails de GestioCOF stocké au format json "
|
||||||
|
"dans /gestioncof/management/data/custommails.json. Le format des "
|
||||||
|
"données est celui donné par la commande :"
|
||||||
|
" `python manage.py dumpdata custommail --natural-foreign` "
|
||||||
|
"La bonne façon de mettre à jour ce fichier est donc de le "
|
||||||
|
"charger à l'aide de syncmails, le faire les modifications à "
|
||||||
|
"l'aide de l'interface administration et/ou du shell puis de le "
|
||||||
|
"remplacer par le nouveau résultat de la commande précédente.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
'data', 'custommail.json')
|
||||||
|
with open(path, 'r') as jsonfile:
|
||||||
|
mail_data = json.load(jsonfile)
|
||||||
|
|
||||||
|
# On se souvient à quel objet correspond quel pk du json
|
||||||
|
assoc = {'types': {}, 'mails': {}}
|
||||||
|
status = {'synced': 0, 'unchanged': 0}
|
||||||
|
|
||||||
|
for obj in mail_data:
|
||||||
|
fields = obj['fields']
|
||||||
|
|
||||||
|
# Pour les trois types d'objets :
|
||||||
|
# - On récupère les objets référencés par les clefs étrangères
|
||||||
|
# - On crée l'objet si nécessaire
|
||||||
|
# - On le stocke éventuellement dans les deux dictionnaires définis
|
||||||
|
# plus haut
|
||||||
|
|
||||||
|
# Variable types
|
||||||
|
if obj['model'] == 'custommail.variabletype':
|
||||||
|
fields['inner1'] = assoc['types'].get(fields['inner1'])
|
||||||
|
fields['inner2'] = assoc['types'].get(fields['inner2'])
|
||||||
|
if fields['kind'] == 'model':
|
||||||
|
fields['content_type'] = (
|
||||||
|
ContentType.objects
|
||||||
|
.get_by_natural_key(*fields['content_type'])
|
||||||
|
)
|
||||||
|
var_type, _ = Type.objects.get_or_create(**fields)
|
||||||
|
assoc['types'][obj['pk']] = var_type
|
||||||
|
|
||||||
|
# Custom mails
|
||||||
|
if obj['model'] == 'custommail.custommail':
|
||||||
|
mail = None
|
||||||
|
try:
|
||||||
|
mail = CustomMail.objects.get(
|
||||||
|
shortname=fields['shortname'])
|
||||||
|
status['unchanged'] += 1
|
||||||
|
except CustomMail.DoesNotExist:
|
||||||
|
mail = CustomMail.objects.create(**fields)
|
||||||
|
status['synced'] += 1
|
||||||
|
self.stdout.write(
|
||||||
|
'SYNCED {:s}'.format(fields['shortname']))
|
||||||
|
assoc['mails'][obj['pk']] = mail
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
if obj['model'] == 'custommail.custommailvariable':
|
||||||
|
fields['custommail'] = assoc['mails'].get(fields['custommail'])
|
||||||
|
fields['type'] = assoc['types'].get(fields['type'])
|
||||||
|
try:
|
||||||
|
Variable.objects.get(
|
||||||
|
custommail=fields['custommail'],
|
||||||
|
name=fields['name']
|
||||||
|
)
|
||||||
|
except Variable.DoesNotExist:
|
||||||
|
Variable.objects.create(**fields)
|
||||||
|
|
||||||
|
# C'est agréable d'avoir le résultat affiché
|
||||||
|
self.stdout.write(
|
||||||
|
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
||||||
|
.format(**status)
|
||||||
|
)
|
587
gestioncof/management/data/custommail.json
Normal file
587
gestioncof/management/data/custommail.json
Normal file
|
@ -0,0 +1,587 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"content_type": [
|
||||||
|
"auth",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "model",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "int",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"content_type": [
|
||||||
|
"bda",
|
||||||
|
"spectacle"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "model",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"content_type": [
|
||||||
|
"bda",
|
||||||
|
"spectaclerevente"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "model",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"content_type": [
|
||||||
|
"sites",
|
||||||
|
"site"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "model",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"content_type": [
|
||||||
|
"gestioncof",
|
||||||
|
"petitcoursdemande"
|
||||||
|
],
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "model",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "list",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 1,
|
||||||
|
"kind": "list",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 9,
|
||||||
|
"fields": {
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": null,
|
||||||
|
"kind": "pair",
|
||||||
|
"inner2": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 10,
|
||||||
|
"fields": {
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 9,
|
||||||
|
"kind": "list",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.variabletype",
|
||||||
|
"pk": 11,
|
||||||
|
"fields": {
|
||||||
|
"content_type": null,
|
||||||
|
"inner1": 3,
|
||||||
|
"kind": "list",
|
||||||
|
"inner2": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "welcome",
|
||||||
|
"subject": "Bienvenue au COF",
|
||||||
|
"description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre",
|
||||||
|
"body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-rappel",
|
||||||
|
"subject": "{{ show }}",
|
||||||
|
"description": "Mail de rappel pour les spectacles BdA",
|
||||||
|
"body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente",
|
||||||
|
"subject": "{{ show }}",
|
||||||
|
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.",
|
||||||
|
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-shotgun",
|
||||||
|
"subject": "{{ show }}",
|
||||||
|
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
|
||||||
|
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-winner",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente",
|
||||||
|
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-loser",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"description": "Notification envoy\u00e9e aux perdants d'un tirage de revente.",
|
||||||
|
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-seller",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e",
|
||||||
|
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-revente-new",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires.",
|
||||||
|
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 9,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-buy-shotgun",
|
||||||
|
"subject": "BdA-Revente : {{ show.title }}",
|
||||||
|
"description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun.",
|
||||||
|
"body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 10,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "petit-cours-mail-eleve",
|
||||||
|
"subject": "Petits cours ENS par le COF",
|
||||||
|
"description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours",
|
||||||
|
"body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 11,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "petits-cours-mail-demandeur",
|
||||||
|
"subject": "Cours particuliers ENS",
|
||||||
|
"description": "Mail envoy\u00e9 aux personnent qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})",
|
||||||
|
"body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 12,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-attributions",
|
||||||
|
"subject": "R\u00e9sultats du tirage au sort",
|
||||||
|
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places",
|
||||||
|
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommail",
|
||||||
|
"pk": 13,
|
||||||
|
"fields": {
|
||||||
|
"shortname": "bda-attributions-decus",
|
||||||
|
"subject": "R\u00e9sultats du tirage au sort",
|
||||||
|
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place",
|
||||||
|
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "member",
|
||||||
|
"description": "Utilisateur de GestioCOF",
|
||||||
|
"custommail": 1,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "member",
|
||||||
|
"description": "Utilisateur ayant eu une place pour ce spectacle",
|
||||||
|
"custommail": 2,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 2,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "nb_attr",
|
||||||
|
"description": "Nombre de places obtenues",
|
||||||
|
"custommail": 2,
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "revente",
|
||||||
|
"description": "Revente mentionn\u00e9e dans le mail",
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"name": "member",
|
||||||
|
"description": "Personne int\u00e9ress\u00e9e par la place",
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"name": "site",
|
||||||
|
"description": "Site web (gestioCOF)",
|
||||||
|
"custommail": 3,
|
||||||
|
"type": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 9,
|
||||||
|
"fields": {
|
||||||
|
"name": "site",
|
||||||
|
"description": "Site web (gestioCOF)",
|
||||||
|
"custommail": 4,
|
||||||
|
"type": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 10,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 4,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 11,
|
||||||
|
"fields": {
|
||||||
|
"name": "member",
|
||||||
|
"description": "Personne int\u00e9ress\u00e9e par la place",
|
||||||
|
"custommail": 4,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 12,
|
||||||
|
"fields": {
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Gagnant-e du tirage",
|
||||||
|
"custommail": 5,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 13,
|
||||||
|
"fields": {
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend une place",
|
||||||
|
"custommail": 5,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 14,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 5,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 15,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 6,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 16,
|
||||||
|
"fields": {
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend une place",
|
||||||
|
"custommail": 6,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 17,
|
||||||
|
"fields": {
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Personne inscrite au tirage qui n'a pas eu la place",
|
||||||
|
"custommail": 6,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 18,
|
||||||
|
"fields": {
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Gagnant-e du tirage",
|
||||||
|
"custommail": 7,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 19,
|
||||||
|
"fields": {
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend une place",
|
||||||
|
"custommail": 7,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 20,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 7,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 21,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 8,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 22,
|
||||||
|
"fields": {
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend la place",
|
||||||
|
"custommail": 8,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 23,
|
||||||
|
"fields": {
|
||||||
|
"name": "revente",
|
||||||
|
"description": "Revente mentionn\u00e9e dans le mail",
|
||||||
|
"custommail": 8,
|
||||||
|
"type": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 24,
|
||||||
|
"fields": {
|
||||||
|
"name": "vendeur",
|
||||||
|
"description": "Personne qui vend la place",
|
||||||
|
"custommail": 9,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 25,
|
||||||
|
"fields": {
|
||||||
|
"name": "show",
|
||||||
|
"description": "Spectacle",
|
||||||
|
"custommail": 9,
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 26,
|
||||||
|
"fields": {
|
||||||
|
"name": "acheteur",
|
||||||
|
"description": "Personne qui prend la place au shotgun",
|
||||||
|
"custommail": 9,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 27,
|
||||||
|
"fields": {
|
||||||
|
"name": "demande",
|
||||||
|
"description": "Demande de petit cours",
|
||||||
|
"custommail": 10,
|
||||||
|
"type": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 28,
|
||||||
|
"fields": {
|
||||||
|
"name": "matieres",
|
||||||
|
"description": "Liste des mati\u00e8res concern\u00e9es par la demande",
|
||||||
|
"custommail": 10,
|
||||||
|
"type": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 29,
|
||||||
|
"fields": {
|
||||||
|
"name": "proposals",
|
||||||
|
"description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re",
|
||||||
|
"custommail": 11,
|
||||||
|
"type": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 30,
|
||||||
|
"fields": {
|
||||||
|
"name": "unsatisfied",
|
||||||
|
"description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer",
|
||||||
|
"custommail": 11,
|
||||||
|
"type": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 31,
|
||||||
|
"fields": {
|
||||||
|
"name": "places",
|
||||||
|
"description": "Places de spectacle du participant",
|
||||||
|
"custommail": 12,
|
||||||
|
"type": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 32,
|
||||||
|
"fields": {
|
||||||
|
"name": "member",
|
||||||
|
"description": "Participant du tirage au sort",
|
||||||
|
"custommail": 12,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "custommail.custommailvariable",
|
||||||
|
"pk": 33,
|
||||||
|
"fields": {
|
||||||
|
"name": "member",
|
||||||
|
"description": "Participant du tirage au sort",
|
||||||
|
"custommail": 13,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
368
gestioncof/management/data/gaulois.json
Normal file
368
gestioncof/management/data/gaulois.json
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "Abraracourcix",
|
||||||
|
"email": "Abraracourcix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Abraracourcix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acidenitrix",
|
||||||
|
"email": "Acidenitrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Acidenitrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agecanonix",
|
||||||
|
"email": "Agecanonix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Agecanonix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alambix",
|
||||||
|
"email": "Alambix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Alambix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amerix",
|
||||||
|
"email": "Amerix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Amerix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amnesix",
|
||||||
|
"email": "Amnesix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Amnesix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aniline",
|
||||||
|
"email": "Aniline.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Aniline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aplusbegalix",
|
||||||
|
"email": "Aplusbegalix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Aplusbegalix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Archeopterix",
|
||||||
|
"email": "Archeopterix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Archeopterix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Assurancetourix",
|
||||||
|
"email": "Assurancetourix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Assurancetourix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Asterix",
|
||||||
|
"email": "Asterix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Asterix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Astronomix",
|
||||||
|
"email": "Astronomix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Astronomix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Avoranfix",
|
||||||
|
"email": "Avoranfix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Avoranfix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Barometrix",
|
||||||
|
"email": "Barometrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Barometrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Beaufix",
|
||||||
|
"email": "Beaufix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Beaufix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Berlix",
|
||||||
|
"email": "Berlix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Berlix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Bonemine",
|
||||||
|
"email": "Bonemine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Bonemine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Boufiltre",
|
||||||
|
"email": "Boufiltre.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Boufiltre"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Catedralgotix",
|
||||||
|
"email": "Catedralgotix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Catedralgotix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "CesarLabeldecadix",
|
||||||
|
"email": "CesarLabeldecadix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "CesarLabeldecadix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Cetautomatix",
|
||||||
|
"email": "Cetautomatix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Cetautomatix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Cetyounix",
|
||||||
|
"email": "Cetyounix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Cetyounix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Changeledix",
|
||||||
|
"email": "Changeledix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Changeledix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Chanteclairix",
|
||||||
|
"email": "Chanteclairix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Chanteclairix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Cicatrix",
|
||||||
|
"email": "Cicatrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Cicatrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Comix",
|
||||||
|
"email": "Comix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Comix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Diagnostix",
|
||||||
|
"email": "Diagnostix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Diagnostix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Doublepolemix",
|
||||||
|
"email": "Doublepolemix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Doublepolemix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Eponine",
|
||||||
|
"email": "Eponine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Eponine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Falbala",
|
||||||
|
"email": "Falbala.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Falbala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Fanzine",
|
||||||
|
"email": "Fanzine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Fanzine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Gelatine",
|
||||||
|
"email": "Gelatine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Gelatine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Goudurix",
|
||||||
|
"email": "Goudurix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Goudurix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Homeopatix",
|
||||||
|
"email": "Homeopatix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Homeopatix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Idefix",
|
||||||
|
"email": "Idefix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Idefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ielosubmarine",
|
||||||
|
"email": "Ielosubmarine.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Ielosubmarine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Keskonrix",
|
||||||
|
"email": "Keskonrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Keskonrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Lentix",
|
||||||
|
"email": "Lentix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Lentix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Maestria",
|
||||||
|
"email": "Maestria.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Maestria"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "MaitrePanix",
|
||||||
|
"email": "MaitrePanix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "MaitrePanix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "MmeAgecanonix",
|
||||||
|
"email": "MmeAgecanonix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "MmeAgecanonix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Moralelastix",
|
||||||
|
"email": "Moralelastix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Moralelastix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Obelix",
|
||||||
|
"email": "Obelix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Obelix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Obelodalix",
|
||||||
|
"email": "Obelodalix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Obelodalix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Odalix",
|
||||||
|
"email": "Odalix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Odalix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ordralfabetix",
|
||||||
|
"email": "Ordralfabetix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Ordralfabetix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Orthopedix",
|
||||||
|
"email": "Orthopedix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Orthopedix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Panoramix",
|
||||||
|
"email": "Panoramix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Panoramix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Plaintcontrix",
|
||||||
|
"email": "Plaintcontrix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Plaintcontrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Praline",
|
||||||
|
"email": "Praline.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Praline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Prefix",
|
||||||
|
"email": "Prefix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Prefix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Prolix",
|
||||||
|
"email": "Prolix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Prolix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Pronostix",
|
||||||
|
"email": "Pronostix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Pronostix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Quatredeusix",
|
||||||
|
"email": "Quatredeusix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Quatredeusix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Saingesix",
|
||||||
|
"email": "Saingesix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Saingesix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Segregationnix",
|
||||||
|
"email": "Segregationnix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Segregationnix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Septantesix",
|
||||||
|
"email": "Septantesix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Septantesix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Tournedix",
|
||||||
|
"email": "Tournedix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Tournedix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Tragicomix",
|
||||||
|
"email": "Tragicomix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Tragicomix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Coriza",
|
||||||
|
"email": "Coriza.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Coriza"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Zerozerosix",
|
||||||
|
"email": "Zerozerosix.gaulois@ens.fr",
|
||||||
|
"last_name": "Gaulois",
|
||||||
|
"first_name": "Zerozerosix"
|
||||||
|
}
|
||||||
|
]
|
614
gestioncof/management/data/romains.json
Normal file
614
gestioncof/management/data/romains.json
Normal file
|
@ -0,0 +1,614 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "Abel",
|
||||||
|
"email": "Abel.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Abel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Abelardus",
|
||||||
|
"email": "Abelardus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Abelardus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Abrahamus",
|
||||||
|
"email": "Abrahamus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Abrahamus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acacius",
|
||||||
|
"email": "Acacius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Acacius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Accius",
|
||||||
|
"email": "Accius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Accius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achaicus",
|
||||||
|
"email": "Achaicus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achaicus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achill",
|
||||||
|
"email": "Achill.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achill"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achilles",
|
||||||
|
"email": "Achilles.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achilles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Achilleus",
|
||||||
|
"email": "Achilleus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Achilleus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acrisius",
|
||||||
|
"email": "Acrisius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Acrisius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Actaeon",
|
||||||
|
"email": "Actaeon.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Actaeon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Acteon",
|
||||||
|
"email": "Acteon.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Acteon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adalricus",
|
||||||
|
"email": "Adalricus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adalricus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adelfonsus",
|
||||||
|
"email": "Adelfonsus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adelfonsus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adelphus",
|
||||||
|
"email": "Adelphus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adelphus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adeodatus",
|
||||||
|
"email": "Adeodatus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adeodatus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adolfus",
|
||||||
|
"email": "Adolfus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adolfus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adolphus",
|
||||||
|
"email": "Adolphus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adolphus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adrastus",
|
||||||
|
"email": "Adrastus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adrastus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Adrianus",
|
||||||
|
"email": "Adrianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Adrianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6gidius",
|
||||||
|
"email": "\u00c6gidius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6gidius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6lia",
|
||||||
|
"email": "\u00c6lia.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6lia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6lianus",
|
||||||
|
"email": "\u00c6lianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6lianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6milianus",
|
||||||
|
"email": "\u00c6milianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6milianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6milius",
|
||||||
|
"email": "\u00c6milius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6milius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aeneas",
|
||||||
|
"email": "Aeneas.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aeneas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6olus",
|
||||||
|
"email": "\u00c6olus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6olus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6schylus",
|
||||||
|
"email": "\u00c6schylus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6schylus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6son",
|
||||||
|
"email": "\u00c6son.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6son"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6sop",
|
||||||
|
"email": "\u00c6sop.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6sop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6ther",
|
||||||
|
"email": "\u00c6ther.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6ther"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "\u00c6tius",
|
||||||
|
"email": "\u00c6tius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "\u00c6tius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agapetus",
|
||||||
|
"email": "Agapetus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agapetus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agapitus",
|
||||||
|
"email": "Agapitus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agapitus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agapius",
|
||||||
|
"email": "Agapius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agapius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Agathangelus",
|
||||||
|
"email": "Agathangelus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Agathangelus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aigidius",
|
||||||
|
"email": "Aigidius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aigidius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aiolus",
|
||||||
|
"email": "Aiolus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aiolus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ajax",
|
||||||
|
"email": "Ajax.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ajax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alair",
|
||||||
|
"email": "Alair.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alair"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alaricus",
|
||||||
|
"email": "Alaricus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alaricus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albanus",
|
||||||
|
"email": "Albanus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albanus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alberic",
|
||||||
|
"email": "Alberic.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alberic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albericus",
|
||||||
|
"email": "Albericus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albericus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albertus",
|
||||||
|
"email": "Albertus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albertus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albinus",
|
||||||
|
"email": "Albinus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albinus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Albus",
|
||||||
|
"email": "Albus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Albus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcaeus",
|
||||||
|
"email": "Alcaeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcaeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcander",
|
||||||
|
"email": "Alcander.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcander"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcimus",
|
||||||
|
"email": "Alcimus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcimus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alcinder",
|
||||||
|
"email": "Alcinder.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alcinder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alerio",
|
||||||
|
"email": "Alerio.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alerio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexandrus",
|
||||||
|
"email": "Alexandrus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexandrus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexis",
|
||||||
|
"email": "Alexis.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexius",
|
||||||
|
"email": "Alexius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexus",
|
||||||
|
"email": "Alexus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alexus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alfonsus",
|
||||||
|
"email": "Alfonsus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alfonsus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alfredus",
|
||||||
|
"email": "Alfredus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alfredus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Almericus",
|
||||||
|
"email": "Almericus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Almericus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aloisius",
|
||||||
|
"email": "Aloisius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aloisius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aloysius",
|
||||||
|
"email": "Aloysius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aloysius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphaeus",
|
||||||
|
"email": "Alphaeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphaeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alpheaus",
|
||||||
|
"email": "Alpheaus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alpheaus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alpheus",
|
||||||
|
"email": "Alpheus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alpheus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphoeus",
|
||||||
|
"email": "Alphoeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphoeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphonsus",
|
||||||
|
"email": "Alphonsus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphonsus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alphonzus",
|
||||||
|
"email": "Alphonzus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alphonzus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alvinius",
|
||||||
|
"email": "Alvinius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alvinius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alvredus",
|
||||||
|
"email": "Alvredus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Alvredus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amadeus",
|
||||||
|
"email": "Amadeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amadeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amaliricus",
|
||||||
|
"email": "Amaliricus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amaliricus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amandus",
|
||||||
|
"email": "Amandus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amandus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amantius",
|
||||||
|
"email": "Amantius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amantius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amarandus",
|
||||||
|
"email": "Amarandus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amarandus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amaranthus",
|
||||||
|
"email": "Amaranthus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amaranthus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amatus",
|
||||||
|
"email": "Amatus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amatus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ambrosianus",
|
||||||
|
"email": "Ambrosianus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ambrosianus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ambrosius",
|
||||||
|
"email": "Ambrosius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ambrosius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amedeus",
|
||||||
|
"email": "Amedeus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amedeus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Americus",
|
||||||
|
"email": "Americus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Americus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amlethus",
|
||||||
|
"email": "Amlethus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amlethus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amletus",
|
||||||
|
"email": "Amletus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amletus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amor",
|
||||||
|
"email": "Amor.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ampelius",
|
||||||
|
"email": "Ampelius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Ampelius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Amphion",
|
||||||
|
"email": "Amphion.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Amphion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anacletus",
|
||||||
|
"email": "Anacletus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anacletus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anastasius",
|
||||||
|
"email": "Anastasius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anastasius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anastatius",
|
||||||
|
"email": "Anastatius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anastatius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anastius",
|
||||||
|
"email": "Anastius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anastius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anatolius",
|
||||||
|
"email": "Anatolius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anatolius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Androcles",
|
||||||
|
"email": "Androcles.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Androcles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Andronicus",
|
||||||
|
"email": "Andronicus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Andronicus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anencletus",
|
||||||
|
"email": "Anencletus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anencletus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Angelicus",
|
||||||
|
"email": "Angelicus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Angelicus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Angelus",
|
||||||
|
"email": "Angelus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Angelus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Anicetus",
|
||||||
|
"email": "Anicetus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Anicetus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antigonus",
|
||||||
|
"email": "Antigonus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antigonus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antipater",
|
||||||
|
"email": "Antipater.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antipater"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antoninus",
|
||||||
|
"email": "Antoninus.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antoninus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Antonius",
|
||||||
|
"email": "Antonius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Antonius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Aphrodisius",
|
||||||
|
"email": "Aphrodisius.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Aphrodisius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Apollinaris",
|
||||||
|
"email": "Apollinaris.Romain@ens.fr",
|
||||||
|
"last_name": "Romain",
|
||||||
|
"first_name": "Apollinaris"
|
||||||
|
}
|
||||||
|
]
|
333
gestioncof/migrations/0001_initial.py
Normal file
333
gestioncof/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Clipper',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('username', models.CharField(max_length=20, verbose_name=b'Identifiant')),
|
||||||
|
('fullname', models.CharField(max_length=200, verbose_name=b'Nom complet')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Club',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name=b'Nom')),
|
||||||
|
('description', models.TextField(verbose_name=b'Description')),
|
||||||
|
('membres', models.ManyToManyField(related_name='clubs', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('respos', models.ManyToManyField(related_name='clubs_geres', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CofProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('login_clipper', models.CharField(max_length=8, verbose_name=b'Login clipper', blank=True)),
|
||||||
|
('is_cof', models.BooleanField(default=False, verbose_name=b'Membre du COF')),
|
||||||
|
('num', models.IntegerField(default=0, verbose_name=b"Num\xc3\xa9ro d'adh\xc3\xa9rent", blank=True)),
|
||||||
|
('phone', models.CharField(max_length=20, verbose_name=b'T\xc3\xa9l\xc3\xa9phone', blank=True)),
|
||||||
|
('occupation', models.CharField(default=b'1A', max_length=9, verbose_name='Occupation', choices=[(b'exterieur', 'Ext\xe9rieur'), (b'1A', '1A'), (b'2A', '2A'), (b'3A', '3A'), (b'4A', '4A'), (b'archicube', 'Archicube'), (b'doctorant', 'Doctorant'), (b'CST', 'CST')])),
|
||||||
|
('departement', models.CharField(max_length=50, verbose_name='D\xe9partement', blank=True)),
|
||||||
|
('type_cotiz', models.CharField(default=b'normalien', max_length=9, verbose_name='Type de cotisation', choices=[(b'etudiant', 'Normalien \xe9tudiant'), (b'normalien', 'Normalien \xe9l\xe8ve'), (b'exterieur', 'Ext\xe9rieur')])),
|
||||||
|
('mailing_cof', models.BooleanField(default=False, verbose_name=b'Recevoir les mails COF')),
|
||||||
|
('mailing_bda', models.BooleanField(default=False, verbose_name=b'Recevoir les mails BdA')),
|
||||||
|
('mailing_bda_revente', models.BooleanField(default=False, verbose_name=b'Recevoir les mails de revente de places BdA')),
|
||||||
|
('comments', models.TextField(verbose_name=b'Commentaires visibles uniquement par le Buro', blank=True)),
|
||||||
|
('is_buro', models.BooleanField(default=False, verbose_name=b'Membre du Bur\xc3\xb4')),
|
||||||
|
('petits_cours_accept', models.BooleanField(default=False, verbose_name=b'Recevoir des petits cours')),
|
||||||
|
('petits_cours_remarques', models.TextField(default=b'', verbose_name='Remarques et pr\xe9cisions pour les petits cours', blank=True)),
|
||||||
|
('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Profil COF',
|
||||||
|
'verbose_name_plural': 'Profils COF',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomMail',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('shortname', models.SlugField()),
|
||||||
|
('title', models.CharField(max_length=200, verbose_name=b'Titre')),
|
||||||
|
('content', models.TextField(verbose_name=b'Contenu')),
|
||||||
|
('comments', models.TextField(verbose_name=b'Informations contextuelles sur le mail', blank=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Mails personnalisables',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Event',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('title', models.CharField(max_length=200, verbose_name=b'Titre')),
|
||||||
|
('location', models.CharField(max_length=200, verbose_name=b'Lieu')),
|
||||||
|
('start_date', models.DateField(null=True, verbose_name=b'Date de d\xc3\xa9but', blank=True)),
|
||||||
|
('end_date', models.DateField(null=True, verbose_name=b'Date de fin', blank=True)),
|
||||||
|
('description', models.TextField(verbose_name=b'Description', blank=True)),
|
||||||
|
('registration_open', models.BooleanField(default=True, verbose_name=b'Inscriptions ouvertes')),
|
||||||
|
('old', models.BooleanField(default=False, verbose_name=b'Archiver (\xc3\xa9v\xc3\xa9nement fini)')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '\xc9v\xe9nement',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventCommentField',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name=b'Champ')),
|
||||||
|
('fieldtype', models.CharField(default=b'text', max_length=10, verbose_name=b'Type', choices=[(b'text', 'Texte long'), (b'char', 'Texte court')])),
|
||||||
|
('default', models.TextField(verbose_name=b'Valeur par d\xc3\xa9faut', blank=True)),
|
||||||
|
('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Champ',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventCommentValue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('content', models.TextField(null=True, verbose_name=b'Contenu', blank=True)),
|
||||||
|
('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventOption',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name=b'Option')),
|
||||||
|
('multi_choices', models.BooleanField(default=False, verbose_name=b'Choix multiples')),
|
||||||
|
('event', models.ForeignKey(related_name='options', to='gestioncof.Event')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Option',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventOptionChoice',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('value', models.CharField(max_length=200, verbose_name=b'Valeur')),
|
||||||
|
('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Choix',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventRegistration',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('paid', models.BooleanField(default=False, verbose_name=b'A pay\xc3\xa9')),
|
||||||
|
('event', models.ForeignKey(to='gestioncof.Event')),
|
||||||
|
('filledcomments', models.ManyToManyField(to='gestioncof.EventCommentField', through='gestioncof.EventCommentValue')),
|
||||||
|
('options', models.ManyToManyField(to='gestioncof.EventOptionChoice')),
|
||||||
|
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Inscription',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PetitCoursAbility',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('niveau', models.CharField(max_length=12, verbose_name='Niveau', choices=[(b'college', 'Coll\xe8ge'), (b'lycee', 'Lyc\xe9e'), (b'prepa1styear', 'Pr\xe9pa 1\xe8re ann\xe9e / L1'), (b'prepa2ndyear', 'Pr\xe9pa 2\xe8me ann\xe9e / L2'), (b'licence3', 'Licence 3'), (b'other', 'Autre (pr\xe9ciser dans les commentaires)')])),
|
||||||
|
('agrege', models.BooleanField(default=False, verbose_name='Agr\xe9g\xe9')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Comp\xe9tence petits cours',
|
||||||
|
'verbose_name_plural': 'Comp\xe9tences des petits cours',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PetitCoursAttribution',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('date', models.DateTimeField(auto_now_add=True, verbose_name="Date d'attribution")),
|
||||||
|
('rank', models.IntegerField(verbose_name=b"Rang dans l'email")),
|
||||||
|
('selected', models.BooleanField(default=False, verbose_name='S\xe9lectionn\xe9 par le demandeur')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Attribution de petits cours',
|
||||||
|
'verbose_name_plural': 'Attributions de petits cours',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PetitCoursAttributionCounter',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('count', models.IntegerField(default=0, verbose_name=b"Nombre d'envois")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': "Compteur d'attribution de petits cours",
|
||||||
|
'verbose_name_plural': "Compteurs d'attributions de petits cours",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PetitCoursDemande',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Nom/pr\xe9nom')),
|
||||||
|
('email', models.CharField(max_length=300, verbose_name='Adresse email')),
|
||||||
|
('phone', models.CharField(max_length=20, verbose_name='T\xe9l\xe9phone (facultatif)', blank=True)),
|
||||||
|
('quand', models.CharField(help_text='Indiquez ici la p\xe9riode d\xe9sir\xe9e pour les petits cours (vacances scolaires, semaine, week-end).', max_length=300, verbose_name='Quand ?', blank=True)),
|
||||||
|
('freq', models.CharField(help_text='Indiquez ici la fr\xe9quence envisag\xe9e (hebdomadaire, 2 fois par semaine, ...)', max_length=300, verbose_name='Fr\xe9quence', blank=True)),
|
||||||
|
('lieu', models.CharField(help_text='Si vous avez avez une pr\xe9f\xe9rence sur le lieu.', max_length=300, verbose_name='Lieu (si pr\xe9f\xe9rence)', blank=True)),
|
||||||
|
('agrege_requis', models.BooleanField(default=False, verbose_name='Agr\xe9g\xe9 requis')),
|
||||||
|
('niveau', models.CharField(default=b'', max_length=12, verbose_name='Niveau', choices=[(b'college', 'Coll\xe8ge'), (b'lycee', 'Lyc\xe9e'), (b'prepa1styear', 'Pr\xe9pa 1\xe8re ann\xe9e / L1'), (b'prepa2ndyear', 'Pr\xe9pa 2\xe8me ann\xe9e / L2'), (b'licence3', 'Licence 3'), (b'other', 'Autre (pr\xe9ciser dans les commentaires)')])),
|
||||||
|
('remarques', models.TextField(verbose_name='Remarques et pr\xe9cisions', blank=True)),
|
||||||
|
('traitee', models.BooleanField(default=False, verbose_name='Trait\xe9e')),
|
||||||
|
('processed', models.DateTimeField(verbose_name='Date de traitement', blank=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='Date de cr\xe9ation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Demande de petits cours',
|
||||||
|
'verbose_name_plural': 'Demandes de petits cours',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PetitCoursSubject',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('name', models.CharField(max_length=30, verbose_name='Mati\xe8re')),
|
||||||
|
('users', models.ManyToManyField(related_name='petits_cours_matieres', through='gestioncof.PetitCoursAbility', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Mati\xe8re de petits cours',
|
||||||
|
'verbose_name_plural': 'Mati\xe8res des petits cours',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Survey',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('title', models.CharField(max_length=200, verbose_name=b'Titre')),
|
||||||
|
('details', models.TextField(verbose_name=b'D\xc3\xa9tails', blank=True)),
|
||||||
|
('survey_open', models.BooleanField(default=True, verbose_name=b'Sondage ouvert')),
|
||||||
|
('old', models.BooleanField(default=False, verbose_name=b'Archiver (sondage fini)')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Sondage',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SurveyAnswer',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'R\xe9ponses',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SurveyQuestion',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('question', models.CharField(max_length=200, verbose_name=b'Question')),
|
||||||
|
('multi_answers', models.BooleanField(default=False, verbose_name=b'Choix multiples')),
|
||||||
|
('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Question',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SurveyQuestionAnswer',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('answer', models.CharField(max_length=200, verbose_name=b'R\xc3\xa9ponse')),
|
||||||
|
('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'R\xe9ponse',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='surveyanswer',
|
||||||
|
name='answers',
|
||||||
|
field=models.ManyToManyField(related_name='selected_by', to='gestioncof.SurveyQuestionAnswer'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='surveyanswer',
|
||||||
|
name='survey',
|
||||||
|
field=models.ForeignKey(to='gestioncof.Survey'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='surveyanswer',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursdemande',
|
||||||
|
name='matieres',
|
||||||
|
field=models.ManyToManyField(related_name='demandes', verbose_name='Mati\xe8res', to='gestioncof.PetitCoursSubject'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursdemande',
|
||||||
|
name='traitee_par',
|
||||||
|
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursattributioncounter',
|
||||||
|
name='matiere',
|
||||||
|
field=models.ForeignKey(verbose_name='Matiere', to='gestioncof.PetitCoursSubject'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursattributioncounter',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursattribution',
|
||||||
|
name='demande',
|
||||||
|
field=models.ForeignKey(verbose_name='Demande', to='gestioncof.PetitCoursDemande'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursattribution',
|
||||||
|
name='matiere',
|
||||||
|
field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursattribution',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursability',
|
||||||
|
name='matiere',
|
||||||
|
field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='petitcoursability',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventcommentvalue',
|
||||||
|
name='registration',
|
||||||
|
field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='surveyanswer',
|
||||||
|
unique_together=set([('user', 'survey')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='eventregistration',
|
||||||
|
unique_together=set([('user', 'event')]),
|
||||||
|
),
|
||||||
|
]
|
19
gestioncof/migrations/0002_enable_unprocessed_demandes.py
Normal file
19
gestioncof/migrations/0002_enable_unprocessed_demandes.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gestioncof', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='petitcoursdemande',
|
||||||
|
name='processed',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='Date de traitement', blank=True),
|
||||||
|
),
|
||||||
|
]
|
19
gestioncof/migrations/0003_event_image.py
Normal file
19
gestioncof/migrations/0003_event_image.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gestioncof', '0002_enable_unprocessed_demandes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(upload_to=b'imgs/events/', null=True, verbose_name=b'Image', blank=True),
|
||||||
|
),
|
||||||
|
]
|
34
gestioncof/migrations/0004_registration_mail.py
Normal file
34
gestioncof/migrations/0004_registration_mail.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_mail(apps, schema_editor):
|
||||||
|
CustomMail = apps.get_model("gestioncof", "CustomMail")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
if CustomMail.objects.filter(shortname="bienvenue").count() == 0:
|
||||||
|
CustomMail.objects.using(db_alias).bulk_create([
|
||||||
|
CustomMail(
|
||||||
|
shortname="bienvenue",
|
||||||
|
title="Bienvenue au COF",
|
||||||
|
content="Mail de bienvenue au COF, envoyé automatiquement à " \
|
||||||
|
+ "l'inscription.\n\n" \
|
||||||
|
+ "Les balises {{ ... }} sont interprétées comme expliqué " \
|
||||||
|
+ "ci-dessous à l'envoi.",
|
||||||
|
comments="{{ nom }} \t fullname de la personne.\n"\
|
||||||
|
+ "{{ prenom }} \t prénom de la personne.")
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gestioncof', '0003_event_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Pas besoin de supprimer le mail lors de la migration dans l'autre
|
||||||
|
# sens.
|
||||||
|
migrations.RunPython(create_mail, migrations.RunPython.noop),
|
||||||
|
]
|
67
gestioncof/migrations/0005_encoding.py
Normal file
67
gestioncof/migrations/0005_encoding.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gestioncof', '0004_registration_mail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='custommail',
|
||||||
|
options={'verbose_name': 'Mail personnalisable', 'verbose_name_plural': 'Mails personnalisables'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='eventoptionchoice',
|
||||||
|
options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='is_buro',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Membre du Bur\xf4'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='num',
|
||||||
|
field=models.IntegerField(default=0, verbose_name="Num\xe9ro d'adh\xe9rent", blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='phone',
|
||||||
|
field=models.CharField(max_length=20, verbose_name='T\xe9l\xe9phone', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='old',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Archiver (\xe9v\xe9nement fini)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateField(null=True, verbose_name='Date de d\xe9but', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventcommentfield',
|
||||||
|
name='default',
|
||||||
|
field=models.TextField(verbose_name='Valeur par d\xe9faut', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventregistration',
|
||||||
|
name='paid',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='A pay\xe9'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='survey',
|
||||||
|
name='details',
|
||||||
|
field=models.TextField(verbose_name='D\xe9tails', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='surveyquestionanswer',
|
||||||
|
name='answer',
|
||||||
|
field=models.CharField(max_length=200, verbose_name='R\xe9ponse'),
|
||||||
|
),
|
||||||
|
]
|
50
gestioncof/migrations/0006_add_calendar.py
Normal file
50
gestioncof/migrations/0006_add_calendar.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0004_mails-rappel'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('gestioncof', '0005_encoding'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CalendarSubscription',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False,
|
||||||
|
auto_created=True, primary_key=True)),
|
||||||
|
('token', models.UUIDField()),
|
||||||
|
('subscribe_to_events', models.BooleanField(default=True)),
|
||||||
|
('subscribe_to_my_shows', models.BooleanField(default=True)),
|
||||||
|
('other_shows', models.ManyToManyField(to='bda.Spectacle')),
|
||||||
|
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='custommail',
|
||||||
|
options={'verbose_name': 'Mail personnalisable',
|
||||||
|
'verbose_name_plural': 'Mails personnalisables'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='eventoptionchoice',
|
||||||
|
options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='end_date',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name=b'Date de fin',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateTimeField(
|
||||||
|
null=True, verbose_name=b'Date de d\xc3\xa9but', blank=True),
|
||||||
|
),
|
||||||
|
]
|
47
gestioncof/migrations/0007_alter_club.py
Normal file
47
gestioncof/migrations/0007_alter_club.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gestioncof', '0006_add_calendar'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(unique=True, max_length=200,
|
||||||
|
verbose_name='Nom')
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(verbose_name='Description', blank=True)
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='membres',
|
||||||
|
field=models.ManyToManyField(related_name='clubs',
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='respos',
|
||||||
|
field=models.ManyToManyField(related_name='clubs_geres',
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateTimeField(null=True,
|
||||||
|
verbose_name='Date de d\xe9but',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
]
|
253
gestioncof/migrations/0008_py3.py
Normal file
253
gestioncof/migrations/0008_py3.py
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forwards(apps, schema_editor):
|
||||||
|
Profile = apps.get_model("gestioncof", "CofProfile")
|
||||||
|
Profile.objects.update(comments="")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gestioncof', '0007_alter_club'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='clipper',
|
||||||
|
name='fullname',
|
||||||
|
field=models.CharField(verbose_name='Nom complet', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='clipper',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(verbose_name='Identifiant', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(
|
||||||
|
verbose_name="Commentaires visibles par l'utilisateur",
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='is_cof',
|
||||||
|
field=models.BooleanField(verbose_name='Membre du COF',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='login_clipper',
|
||||||
|
field=models.CharField(verbose_name='Login clipper', max_length=8,
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='mailing_bda',
|
||||||
|
field=models.BooleanField(verbose_name='Recevoir les mails BdA',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='mailing_bda_revente',
|
||||||
|
field=models.BooleanField(
|
||||||
|
verbose_name='Recevoir les mails de revente de places BdA',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='mailing_cof',
|
||||||
|
field=models.BooleanField(verbose_name='Recevoir les mails COF',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='occupation',
|
||||||
|
field=models.CharField(verbose_name='Occupation',
|
||||||
|
choices=[('exterieur', 'Extérieur'),
|
||||||
|
('1A', '1A'),
|
||||||
|
('2A', '2A'),
|
||||||
|
('3A', '3A'),
|
||||||
|
('4A', '4A'),
|
||||||
|
('archicube', 'Archicube'),
|
||||||
|
('doctorant', 'Doctorant'),
|
||||||
|
('CST', 'CST')],
|
||||||
|
max_length=9, default='1A'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='petits_cours_accept',
|
||||||
|
field=models.BooleanField(verbose_name='Recevoir des petits cours',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='petits_cours_remarques',
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Remarques et précisions pour les petits cours',
|
||||||
|
default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cofprofile',
|
||||||
|
name='type_cotiz',
|
||||||
|
field=models.CharField(
|
||||||
|
verbose_name='Type de cotisation',
|
||||||
|
choices=[('etudiant', 'Normalien étudiant'),
|
||||||
|
('normalien', 'Normalien élève'),
|
||||||
|
('exterieur', 'Extérieur')],
|
||||||
|
max_length=9, default='normalien'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custommail',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(
|
||||||
|
verbose_name='Informations contextuelles sur le mail',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custommail',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(verbose_name='Contenu'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custommail',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(verbose_name='Titre', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(verbose_name='Description', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='end_date',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='Date de fin',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(upload_to='imgs/events/', null=True,
|
||||||
|
verbose_name='Image', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='location',
|
||||||
|
field=models.CharField(verbose_name='Lieu', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='registration_open',
|
||||||
|
field=models.BooleanField(verbose_name='Inscriptions ouvertes',
|
||||||
|
default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(verbose_name='Titre', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventcommentfield',
|
||||||
|
name='fieldtype',
|
||||||
|
field=models.CharField(verbose_name='Type',
|
||||||
|
choices=[('text', 'Texte long'),
|
||||||
|
('char', 'Texte court')],
|
||||||
|
max_length=10, default='text'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventcommentfield',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(verbose_name='Champ', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventcommentvalue',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(null=True, verbose_name='Contenu',
|
||||||
|
blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventoption',
|
||||||
|
name='multi_choices',
|
||||||
|
field=models.BooleanField(verbose_name='Choix multiples',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventoption',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(verbose_name='Option', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventoptionchoice',
|
||||||
|
name='value',
|
||||||
|
field=models.CharField(verbose_name='Valeur', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='petitcoursability',
|
||||||
|
name='niveau',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('college', 'Collège'), ('lycee', 'Lycée'),
|
||||||
|
('prepa1styear', 'Prépa 1ère année / L1'),
|
||||||
|
('prepa2ndyear', 'Prépa 2ème année / L2'),
|
||||||
|
('licence3', 'Licence 3'),
|
||||||
|
('other', 'Autre (préciser dans les commentaires)')],
|
||||||
|
max_length=12, verbose_name='Niveau'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='petitcoursattribution',
|
||||||
|
name='rank',
|
||||||
|
field=models.IntegerField(verbose_name="Rang dans l'email"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='petitcoursattributioncounter',
|
||||||
|
name='count',
|
||||||
|
field=models.IntegerField(verbose_name="Nombre d'envois",
|
||||||
|
default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='petitcoursdemande',
|
||||||
|
name='niveau',
|
||||||
|
field=models.CharField(
|
||||||
|
verbose_name='Niveau',
|
||||||
|
choices=[('college', 'Collège'), ('lycee', 'Lycée'),
|
||||||
|
('prepa1styear', 'Prépa 1ère année / L1'),
|
||||||
|
('prepa2ndyear', 'Prépa 2ème année / L2'),
|
||||||
|
('licence3', 'Licence 3'),
|
||||||
|
('other', 'Autre (préciser dans les commentaires)')],
|
||||||
|
max_length=12, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='survey',
|
||||||
|
name='old',
|
||||||
|
field=models.BooleanField(verbose_name='Archiver (sondage fini)',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='survey',
|
||||||
|
name='survey_open',
|
||||||
|
field=models.BooleanField(verbose_name='Sondage ouvert',
|
||||||
|
default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='survey',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(verbose_name='Titre', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='surveyquestion',
|
||||||
|
name='multi_answers',
|
||||||
|
field=models.BooleanField(verbose_name='Choix multiples',
|
||||||
|
default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='surveyquestion',
|
||||||
|
name='question',
|
||||||
|
field=models.CharField(verbose_name='Question', max_length=200),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards, migrations.RunPython.noop),
|
||||||
|
]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue