Compare commits

..

79 commits

Author SHA1 Message Date
Martin Pépin ac4dc0c192 tmp 2017-07-23 19:57:18 +01:00
Martin Pépin 75bfe98006 Add the COF registration view 2017-06-23 00:10:46 +01:00
Martin Pépin 0f491ef396 Generic registration view 2017-06-23 00:10:38 +01:00
Martin Pépin f93a68b942 Remove the old user registration material 2017-06-23 00:10:38 +01:00
Martin Pépin 8a346bf834 Reformat code in 79 columns 2017-04-01 18:29:36 +01:00
Aurélien Delobelle 95b96d470f Merge branch 'Kerl/1_11_urls' into 'supportBDS'
Properly include ddt's urls using `include`

See merge request !190
2017-03-26 15:41:12 +02:00
Martin Pepin 33dedc7474 Merge branch 'aureplop/1_11/fix_querysets' into 'supportBDS'
Aureplop/1 11/fix querysets

See merge request !183
2017-03-19 16:16:05 +01:00
Martin Pépin 1a107be4ba Properly include ddt's urls using include 2017-03-18 19:14:51 +00:00
Aurélien Delobelle e2c4214efc fix history kpsul view 2017-02-25 01:36:53 +01:00
Aurélien Delobelle a98c6b233e fix checkout data error on k-psul 2017-02-25 01:30:26 +01:00
Qwann e1713a1d4f Merge branch 'Kerl/datetime_warnings' into supportBDS 2017-02-24 15:02:27 +01:00
Qwann 9c6f5533ec Merge branch 'supportBDS' of git.eleves.ens.fr:cof-geek/gestioCOF into supportBDS 2017-02-23 19:00:58 +01:00
Qwann 486d3c4ced Merge branch 'Kerl/fix_autocomplete' into supportBDS 2017-02-23 18:53:55 +01:00
Martin Pépin 80f1514d39 handle timezones in petits_cours_views 2017-02-23 18:35:38 +01:00
Martin Pépin 1663a03a33 Use timezones everywhere in migrations 2017-02-23 18:33:44 +01:00
Martin Pepin 0e4cfc5121 Merge branch 'Kerl/django111-packages' into 'supportBDS'
Upgrade to Django 1.11

- We upgrade our django packages to 1.11 (beta 1)
- We apply some necessary changes in the settings file
- We prepare to upgrade Django-autocomplete-light
- We upgrade some other packages

See merge request !179
2017-02-23 17:58:28 +01:00
Martin Pépin 856faf2b73 Merge branch 'supportBDS' into Kerl/django111-packages 2017-02-23 17:56:12 +01:00
Martin Pepin 646b213d97 Merge branch 'Kerl/django111-urls' into 'supportBDS'
Write modern-style urls

- Proper use of include
- Defining namespaces (I do not use them for now because many urls are
  going to change)
- Do not try to reverse with old-style references: 'cof.views.XXX'

See merge request !178
2017-02-23 17:32:05 +01:00
Martin Pepin cc25685aa3 Merge branch 'Kerl/bds_groups' into 'supportBDS'
BDS groups and permissions

Creates groups and permissions for the BDS members and staff.

Fixes #136 

See merge request !176
2017-02-23 12:58:44 +01:00
Martin Pépin 5ce4809f06 Merge branch 'supportBDS' into Kerl/bds_groups 2017-02-23 12:58:10 +01:00
Martin Pepin 4d825b485d Merge branch 'Kerl/clubs_support' into 'supportBDS'
Kerl/clubs support

- Add the club-related models
- Register them into the admin site in order to be able to play with them

See #133 

See merge request !175
2017-02-23 12:46:27 +01:00
Martin Pépin 7988fb24a0 Merge branch 'supportBDS' into Kerl/clubs_support 2017-02-23 12:35:37 +01:00
Martin Pepin 4b4d570e07 Merge branch 'Kerl/fix_generic_team_ath' into 'supportBDS'
Change CofProfile to Profile in kfet/backends.py

- K-Fêt accounts are now linked to profiles
- There is no need to perform the `get_or_create` as long as the profile
  creation has been automated.
- This file is now PEP8 compliant

See merge request !180
2017-02-23 12:31:55 +01:00
Martin Pépin 83e73376ad Change CofProfile to Profile in kfet/backends.py
- K-Fêt accounts are now linked to profiles
- There is no need to perform the `get_or_create` as long as the profile
  creation has been automated.
- This file is now PEP8 compliant
2017-02-23 12:04:33 +01:00
Martin Pépin 7742ad999f Typo in url reverse name
`liste_bda_diff` -> `liste_bdadiff`
2017-02-23 11:05:53 +01:00
Martin Pepin 6444ae3b92 Merge branch 'Kerl/django111-models' into 'supportBDS'
Specify `on_delete`

Two kind of files are affected: 
- Models
- Migrations

You can use the `-Wall` flag to check that the warning `RemovedInDjango20Warning: on_delete…` is not raised:

    python -Wall manage.py runserver 0.0.0.0:8000

See merge request !177
2017-02-23 11:04:26 +01:00
Martin Pépin 213c11721e Prevent petits cours demandes deletion 2017-02-23 11:03:28 +01:00
Martin Pépin 7d1c1fc868 Specify the on_delete strategy
- Remove an absurd debug line in the migration
- Specify the on_delete strategy for the club-related models
2017-02-23 10:56:36 +01:00
Martin Pépin 6c34742cc4 Remove an erroneous RegistrationUserForm
This form appeared twice in `cof/forms.py` and the second occurrence
(which was used by the views) was erroneous.
2017-02-23 10:36:25 +01:00
Martin Pépin 7abcf28666 Move the registration_form template 2017-02-23 10:35:46 +01:00
Martin Pépin 3f52af8ca0 Upgrade requirements
- Upgrade some dependencies
- We do not specify the version for some packages: we use them in a very
  simple way so we may not be affected by upgrades.
2017-02-23 02:03:15 +01:00
Martin Pépin 6bf16441e6 Drop old context processors
Some context processors disappear in Django 1.11, we have to drop them
2017-02-23 02:02:22 +01:00
Martin Pépin 69f748acbd Django1.11-style MiddleWares
The design of middlewares has changed in Django 1.11
2017-02-23 02:00:34 +01:00
Martin Pépin 8b905f66dc Remove dependencies of an old version of dal
Django-autocomplete-light does not support the `modelform_factory`
anymore in recent versions. We are actually using an old version of dal
because of this.

This had to be dropped at some point… So now is a good time
2017-02-23 01:57:50 +01:00
Martin Pépin e1bab7e4ed Use the AppConfig class 2017-02-23 01:56:59 +01:00
Martin Pépin 68c0ff559d Drop Grappelli
It's ugly and does not really improve the admin site
2017-02-23 01:55:43 +01:00
Martin Pépin 2dcc17298a Use Django 1.11 (beta 1)
Starting to use Django 1.11.
The final version will be released before we push this to production.
2017-02-23 01:55:26 +01:00
Martin Pépin 1aed36330f Write modern-style urls
- Proper use of include
- Defining namespaces (I do not use them for now because many urls are
  going to change)
- Do not try to reverse with old-style references: 'cof.views.XXX'
2017-02-23 01:52:55 +01:00
Martin Pépin 9f401b66e9 Specify the on_delete attribute everywhere
- Models
- Migrations
2017-02-23 01:40:25 +01:00
Martin Pépin c81b849785 Prevent conflicts in COF perm migration
There may be a conflict during the migration cof 0009 if the permissions
are referenced only by their codename.
2017-02-22 18:21:23 +01:00
Martin Pépin d36d69238d Add groups and permissions for BDS staff & members 2017-02-22 18:19:49 +01:00
Martin Pépin f8a8465630 Remove useless imports 2017-02-22 15:23:37 +01:00
Martin Pépin 5632cdaa22 Uses the cof_members groups in kfet's autocomplete
The is_cof attribute cannot be used in database queries anymore
2017-02-22 15:03:34 +01:00
Martin Pépin 68b38228a9 Apply the new db structure to autocomplete in cof
- The autocompletion feature works again
- The template is a bit more readable (indentation)
- The `options` variable in the template is no longer a integer but a
  boolean.
2017-02-22 14:49:34 +01:00
Martin Pépin 1f85f75896 Include the Clubs into the admin site 2017-02-20 01:16:50 +01:00
Martin Pépin 669129e30d Move the club model to the gestion app
- Move the model
- Add some BDS-related fields
- Add an `associations` fields to be able to separate the clubs between
  the different associations using groups
2017-02-20 01:12:06 +01:00
Martin Pépin 859f191894 Simpler admin interface 2017-02-18 19:06:43 +01:00
Martin Pépin 52bdd9824a Merge branch 'supportBDS' of git.eleves.ens.fr:michele.orru/gestioCOF into michele 2017-02-18 13:00:21 +01:00
Martin Pépin d7a13229ad Fix CI config
- The setting file has moved to `gestioCOF/`
2017-02-13 13:41:34 +01:00
Martin Pépin 7f5132961f Add the cof_members group
GestioCOF cannot run without it.
There is no permission associated to it: this has to been thought about
2017-02-12 19:49:30 +01:00
Martin Pépin 659c6e720a Merge branch 'master' into supportBDS 2017-02-12 19:36:17 +01:00
Martin Pépin e1a8c0e8dd Add missing files 2017-02-12 19:21:53 +01:00
Martin Pépin 1d7499d3b2 simplify profile edition and test it 2017-02-12 19:21:53 +01:00
Martin Pépin 0420839b20 Test the authentication
- An "outsider" must use django's authentication backend
- A user with a login_clipper should use CAS
2017-02-12 15:38:20 +01:00
Martin Pépin 8b620a5319 PEP8 in gestion/tests.py 2017-02-12 15:38:20 +01:00
Martin Pépin a28c00e474 Move the auth stuff to gestion/
- The login views are in `gestion/`
- The templates are under `gestion/templates/gestion/`
- `cof/shared.py` moves to `gestion/` and is splitted into 3 files:
    - The auth backends are in `backends.py`.
    - The context_processor is in `context_processor.py`
    - The LOCK/UNLOCK functions remain in `shared.py`
2017-02-12 15:38:14 +01:00
Martin Pépin 50b667993f Merge branch 'master' into supportBDS
- Mise en page
- Cleanup des petits cours
- Utilisation de custommail
- Utilisation du ldap du SPI pour fetch les nouveaux comptes
2017-02-12 04:26:43 +01:00
Michele Orrù 94937fc7cd Add groups cof_members and cof_buro.
- remove is_buro from the database in the same way we did for is_cof
- make a decent migration that *SHOULD* take into account is_cof for old
  databases. note, this has been tested only through unittests.
- make unittests pass again accordin=gly fixing views.

Note: we should make a method for filtering with specific group members,
something like
map(lambda x: x.profile.cof, filter… group…)
2017-02-11 23:05:51 +01:00
Qwann b5037329dd small fixes + migration
enable admin interface for bds
2017-02-11 20:36:16 +01:00
Qwann d46ab87e9b Merge branch 'michele.orru/gestioCOF-supportBDS' into supportBDS 2017-02-11 20:17:23 +01:00
Qwann f53ced6a33 Merge branch 'supportBDS' into michele.orru/gestioCOF-supportBDS 2017-02-11 19:21:23 +01:00
Michele Orrù ee1f29b17d Make is_cof a property and start developing permissions.
Rmeove the is_cof flag to check permission and start implementing
a group and a permission for the respective cof members.
XXX. note: migrations do NOT add old is_cof members to the new group
(as actually they're just on the tests…)
2017-02-11 18:48:13 +01:00
Qwann b9ed7320ec Fix user registration 2017-02-11 18:41:47 +01:00
Qwann fcf392f40d Merge remote-tracking branch 'origin/tmp' into michele.orru/gestioCOF-supportBDS 2017-02-11 18:02:05 +01:00
Martin Pépin 3365d7b9a1 Updates the decorators 2017-02-11 18:00:01 +01:00
Qwann b16219f8ee fixing profile view 2017-02-11 17:57:37 +01:00
Martin Pépin 6c3e1bd2db Fix the loaddevdata script 2017-02-11 17:18:42 +01:00
Michele Orrù a2b8dee022 Fix #134.
Fill bds.models with the required fields; add migration scripts, and a stupid
unittests that checks the model really works.
Note: old fields will migrate to datetime.now().
2017-02-11 17:13:48 +01:00
Michele Orrù f0c3def935 Make the test really works.
This fixes the proble with debug_toolbar,
the fucking toolbar that messes all your tests.
2017-02-11 15:47:31 +01:00
Michele Orrù 376e829502 Reaching a point where I can query /k-fet.
Edit forms and views in app kfet to make the depend on gestion.Profile and not
on cof.CofProfile.
2017-02-11 15:07:45 +01:00
Michele Orrù f50ef1d51a Merge remote-tracking branch 'origin/supportBDS-fixes1' into supportBDS 2017-02-11 14:26:55 +01:00
Martin Pépin b639c04549 Fix the registration forms
- The former `RegistrationUserProfileForm` is splitted in two.
- There is a new form: `RegistrationCofProfileForm`
2017-02-11 12:42:36 +01:00
Martin Pépin b1cf96d0ae Move profile editing to gestion 2017-02-11 01:43:17 +00:00
Michele Orrù 815a5f274c Fix some reviewing considerations.
- appropriate naming for migration
- remove __future__ imports.
- remove "CofProfile" left in kfet/models.py
2017-02-11 00:33:46 +01:00
Michele Orrù 25c3106168 Add some tests about how profiles types should relate to each other. 2017-02-11 00:33:36 +01:00
Michele Orrù 22da04c3e2 s/cofprofile/profile/g into k-fêt.
This commit also restores the only unittest present.
2017-02-11 00:32:58 +01:00
Martin Pépin 58d708b791 Move profile editing to gestion 2017-02-10 23:50:19 +01:00
Ubuntu f39d1545f0 Generic profiles and migrations.
Creating profiles for BDS, COF and K-Fêt.
2017-02-10 22:12:03 +01:00
Martin Pépin 5aff771d9c Set the new structure of gestioCOF
- `cof` is renamed `gestioCOF`
- `gestioncof` become `cof` (yes it looks pretty stupid but it is not)
- `bds` is created
2017-02-09 21:28:36 +01:00
628 changed files with 29423 additions and 88737 deletions

1
.envrc
View file

@ -1 +0,0 @@
use nix

12
.gitignore vendored
View file

@ -5,19 +5,7 @@ cof/settings.py
settings.py
*~
venv/
.venv/
.vagrant
/src
media/
*.log
.sass-cache/
*.sqlite3
.coverage
# PyCharm
.idea
.cache
# VSCode
.vscode/
.direnv

View file

@ -1,95 +1,40 @@
image: "python:3.7"
services:
- mysql:latest
- redis:latest
variables:
# GestioCOF settings
DBHOST: "postgres"
DJANGO_SETTINGS_MODULE: "gestioCOF.settings_dev"
DBNAME: "cof_gestion"
DBUSER: "cof_gestion"
DBPASSWD: "cof_password"
DBHOST: "mysql"
REDIS_HOST: "redis"
REDIS_PASSWD: "dummy"
# Cached packages
PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
# postgres service configuration
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
POSTGRES_USER: "cof_gestion"
POSTGRES_DB: "cof_gestion"
# mysql service configuration
MYSQL_DATABASE: "$DBNAME"
MYSQL_USER: "$DBUSER"
MYSQL_PASSWORD: "$DBPASSWD"
MYSQL_ROOT_PASSWORD: "root_password"
# psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD
# apps to check migrations for
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
cache:
paths:
- vendor/python
- vendor/pip
- vendor/apt
.test_template:
before_script:
- mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade -r requirements-prod.txt coverage tblib
- python --version
after_script:
- coverage report
services:
- postgres:11.7
- redis:latest
cache:
key: test
paths:
- vendor/
# For GitLab CI to get coverage from build.
# Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
before_script:
- mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq mysql-client
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST"
-e "GRANT ALL ON test_$DBNAME.* TO '$DBUSER'@'%'"
- pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt
coftest:
test:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
bdstest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod"
script:
- coverage run manage.py test bds clubs events --parallel
linters:
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/
- python manage.py test

View file

@ -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

View file

@ -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

242
README.md
View file

@ -1,86 +1,17 @@
# GestioCOF / GestioBDS
[![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)
# GestioCOF
## 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
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
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
le monde à la même configuration de développement (même sous Windows !), et
l'installation se fait en une commande.
Pour utiliser Vagrant, il faut le
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
Si vous êtes sous Linux, votre distribution propose probablement des paquets
Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui
@ -135,72 +66,137 @@ car par défaut Django n'écoute que sur l'adresse locale de la machine virtuell
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
entrer dans le navigateur est `localhost:8000`.
#### Serveur de développement type production
Juste histoire de jouer, pas indispensable pour développer :
La VM Vagrant héberge en plus un serveur nginx configuré pour servir GestioCOF
comme en production : on utilise
Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de
façon similaire à la version en production : on utilise
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
derrière un reverse-proxy nginx.
derrière un reverse-proxy apache. Le tout est monitoré par
[supervisor](http://supervisord.org/).
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
`localhost:8080/gestion/`. Toutefois il ne se recharge pas tout seul lorsque le
code change, il faut relancer le worker avec `sudo systemctl restart
worker.service` pour visualiser la dernière version du code.
`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code
change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour
visualiser la dernière version du code.
### Installation manuelle
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il
est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python, un
client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées
(Ubuntu, ...) :
sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
virtualenv env -p $(which python3)
L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir
python3, si c'est la version de python par défaut sur votre système, ceci n'est
pas nécessaire. Pour l'activer, il faut faire
. env/bin/activate
dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -r requirements-devel.txt
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.
#### Installation avec MySQL
Il faut maintenant installer MySQL. Si vous n'avez pas déjà MySQL installé sur
votre serveur, il faut l'installer ; sous Debian et dérivées (Ubuntu, ...) :
sudo apt-get install mysql-server
Il vous demandera un mot de passe pour le compte d'administration MySQL,
notez-le quelque part (ou n'en mettez pas, le serveur n'est accessible que
localement par défaut). Si vous utilisez une autre distribution, consultez la
documentation de votre distribution pour savoir comment changer ce mot de passe
et démarrer le serveur MySQL (c'est automatique sous Ubuntu).
Vous devez alors créer un utilisateur local et une base `cof_gestion`, avec le
mot de passe de votre choix (remplacez `mot_de_passe`) :
mysql -uroot -e "CREATE DATABASE cof_gestion; GRANT ALL PRIVILEGES ON cof_gestion.* TO 'cof_gestion'@'localhost' IDENTIFIER BY 'mot_de_passe'"
Éditez maintenant le fichier `cof/settings.py` pour y intégrer ces changements ;
la définition de `DATABASES` doit ressembler à (à nouveau, remplacez
`mot_de_passe` de façon appropriée) :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'cof_gestion',
'USER': 'cof_gestion',
'PASSWORD': 'mot_de_passe',
}
}
#### Installation avec SQLite
GestioCOF est installé avec MySQL sur la VM COF, et afin d'avoir un
environnement de développement aussi proche que possible de ce qui tourne en
vrai pour éviter les mauvaises surprises, il est conseillé d'utiliser MySQL sur
votre machine de développement également. Toutefois, GestioCOF devrait
fonctionner avec d'autres moteurs SQL, et certains préfèrent utiliser SQLite
pour sa légèreté et facilité d'installation.
Si vous décidez d'utiliser SQLite, il faut l'installer ; sous Debian et dérivées :
sudo apt-get install sqlite3
puis éditer le fichier `cof/settings.py` pour que la définition de `DATABASES`
ressemble à :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande suivante :
python manage.py migrate
Charger les mails indispensables au bon fonctionnement de GestioCOF :
python manage.py syncmails
Une base de donnée pré-remplie est disponible en lançant les commandes :
python manage.py loaddata gestion sites accounts groups articles
python manage.py loaddevdata
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
### Mise à jour
Pour mettre à jour les paquets Python, utiliser la commande suivante :
pip install --upgrade -r requirements-devel.txt
pip install --upgrade -r requirements.txt -r requirements-devel.txt
Pour mettre à jour les modèles après une migration, il faut ensuite faire :
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
Une brève documentation utilisateur est accessible sur le
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home) pour avoir une
idée de la façon dont le COF utilise GestioCOF.
Une brève documentation utilisateur pour se familiariser plus vite avec l'outil
est accessible sur le
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home).

42
Vagrantfile vendored
View file

@ -1,19 +1,47 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Configuration de base pour GestioCOF.
# Voir https://docs.vagrantup.com pour plus d'informations.
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# 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|
# On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en
# production.
config.vm.box = "debian/contrib-buster64"
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.box = "ubuntu/xenial64"
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
# ordinateur, et le port 8000 avec le port 8000.
config.vm.network :forwarded_port, guest: 80, host: 8080
config.vm.network :forwarded_port, guest: 8000, host: 8000
# Le restes de la configuration (installation de paquets, etc) est géré un
# script shell.
# Create a private network, which allows host-only access to the machine
# 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"
end

231
bda/admin.py Normal file
View file

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
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 ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle
sortable_field_name = "priority"
class AttributionInline(admin.TabularInline):
model = Attribution
extra = 0
def get_queryset(self, request):
qs = super(AttributionInline, self).get_queryset(request)
return qs.filter(spectacle__listing=False)
class AttributionInlineListing(admin.TabularInline):
model = Attribution
exclude = ('given', )
extra = 0
def get_queryset(self, request):
qs = super(AttributionInlineListing, self).get_queryset(request)
return qs.filter(spectacle__listing=True)
class ParticipantAdmin(admin.ModelAdmin):
inlines = [AttributionInline, AttributionInlineListing]
def get_queryset(self, request):
return Participant.objects.annotate(nb_places=Count('attributions'),
total=Sum('attributions__price'))
def nb_places(self, obj):
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",)
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 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(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
class ChoixSpectacleAdmin(admin.ModelAdmin):
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 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
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)

107
bda/algorithm.py Normal file
View file

@ -0,0 +1,107 @@
# -*- 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 * choices.aggregate(Max('priority'))['priority__max']
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

6
bda/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BdAConfig(AppConfig):
name = "bda"
verbose_name = "Gestion des tirages du BdA"

83
bda/forms.py Normal file
View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from datetime import timedelta
from django import forms
from django.forms.models import BaseInlineFormSet
from django.utils import timezone
from bda.models import Attribution, Spectacle
class BaseBdaFormSet(BaseInlineFormSet):
def clean(self):
"""Checks that no two articles have the same title."""
super(BaseBdaFormSet, self).clean()
if any(self.errors):
# Don't bother validating the formset unless each form is valid on
# its own
return
spectacles = []
for i in range(0, self.total_form_count()):
form = self.forms[i]
if not form.cleaned_data:
continue
spectacle = form.cleaned_data['spectacle']
delete = form.cleaned_data['DELETE']
if not delete and spectacle in spectacles:
raise forms.ValidationError(
"Vous ne pouvez pas vous inscrire deux fois pour le "
"même spectacle.")
spectacles.append(spectacle)
class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea())
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return "%s" % obj.spectacle
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
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)
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
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__date__gt=timezone.now()-timedelta(hours=1),
revente__soldTo__isnull=True)
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.filter(
date__gte=timezone.now())

View file

@ -0,0 +1,108 @@
"""
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 Group
from cof.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 = []
cof_members = Group.objects.get(name="cof_members")
for user in cof_members.user_set.all():
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")

View 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")

View 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.')

View 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"
}
]

View 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
}
]

View file

@ -0,0 +1,122 @@
# -*- 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(
on_delete=models.CASCADE,
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(
on_delete=models.CASCADE,
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='choixspectacle',
name='participant',
field=models.ForeignKey(
on_delete=models.CASCADE,
to='bda.Participant'),
),
migrations.AddField(
model_name='choixspectacle',
name='spectacle',
field=models.ForeignKey(
on_delete=models.CASCADE,
related_name='participants',
to='bda.Spectacle'),
),
migrations.AddField(
model_name='attribution',
name='participant',
field=models.ForeignKey(
on_delete=models.CASCADE,
to='bda.Participant'),
),
migrations.AddField(
model_name='attribution',
name='spectacle',
field=models.ForeignKey(
related_name='attribues',
on_delete=models.CASCADE,
to='bda.Spectacle'),
),
migrations.AlterUniqueTogether(
name='choixspectacle',
unique_together=set([('participant', 'spectacle')]),
),
]

View file

@ -0,0 +1,64 @@
# -*- 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 forwards_func(apps, schema_editor):
Tirage = apps.get_model("bda", "Tirage")
db_alias = schema_editor.connection.alias
Tirage.objects.using(db_alias).bulk_create([
Tirage(
id=1,
title="Tirage de test (migration)",
active=False,
ouverture=timezone.now(),
fermeture=timezone.now()),
])
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.RunPython(forwards_func, migrations.RunPython.noop),
migrations.AlterField(
model_name='participant',
name='user',
field=models.ForeignKey(
on_delete=models.CASCADE,
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='participant',
name='tirage',
field=models.ForeignKey(
on_delete=models.CASCADE,
default=1,
to='bda.Tirage'),
preserve_default=False,
),
migrations.AddField(
model_name='spectacle',
name='tirage',
field=models.ForeignKey(
on_delete=models.CASCADE,
default=1,
to='bda.Tirage'),
preserve_default=False,
),
]

View 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'),
),
]

View 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),
),
]

View 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),
),
]

View 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),
]

View file

@ -0,0 +1,94 @@
# -*- 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(
on_delete=models.CASCADE,
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(
on_delete=models.CASCADE,
to='bda.Spectacle'),
),
]

103
bda/migrations/0008_py3.py Normal file
View 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),
),
]

View file

@ -0,0 +1,73 @@
# -*- 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',
on_delete=models.CASCADE,
related_name='revente'),
),
migrations.AddField(
model_name='spectaclerevente',
name='seller',
field=models.ForeignKey(
on_delete=models.CASCADE,
to='bda.Participant',
verbose_name='Vendeur',
related_name='original_shows'),
),
migrations.AddField(
model_name='spectaclerevente',
name='soldTo',
field=models.ForeignKey(
on_delete=models.CASCADE,
to='bda.Participant',
verbose_name='Vendue à',
null=True,
blank=True),
),
]

View 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),
]

359
bda/models.py Normal file
View file

@ -0,0 +1,359 @@
# -*- 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.contrib.auth.models import User
from django.conf import settings
from django.utils import timezone, formats
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)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False)
def __str__(self):
return "%s - %s" % (self.title, formats.localize(
timezone.template_localtime(self.fermeture)))
class Salle(models.Model):
name = models.CharField("Nom", max_length=300)
address = models.TextField("Adresse")
def __str__(self):
return self.name
class CategorieSpectacle(models.Model):
name = models.CharField('Nom', max_length=100, unique=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Catégorie"
class Spectacle(models.Model):
title = models.CharField("Titre", max_length=300)
category = models.ForeignKey(
CategorieSpectacle,
on_delete=models.CASCADE,
blank=True,
null=True
)
date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle, on_delete=models.CASCADE)
vips = models.TextField('Personnalités', blank=True)
description = models.TextField("Description", blank=True)
slots_description = models.TextField("Description des places", blank=True)
image = models.ImageField('Image', blank=True, null=True,
upload_to='imgs/shows/')
ext_link = models.CharField('Lien vers le site du spectacle', blank=True,
max_length=500)
price = models.FloatField("Prix d'une place")
slots = models.IntegerField("Places")
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
listing = models.BooleanField("Les places sont sur listing")
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True,
null=True)
class Meta:
verbose_name = "Spectacle"
ordering = ("date", "title",)
def timestamp(self):
return "%d" % calendar.timegm(self.date.utctimetuple())
def __str__(self):
return "%s - %s, %s, %.02f" % (
self.title,
formats.localize(timezone.template_localtime(self.date)),
self.location,
self.price
)
def 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
members = {}
for attr in Attribution.objects.filter(spectacle=self).all():
member = attr.participant.user
if member.id in members:
members[member.id][1] = 2
else:
members[member.id] = [member, 1]
# FIXME : faire quelque chose de ça, un utilisateur bda_generic ?
# # Pour le BdA
# members[0] = ['BdA', 1, 'bda@ens.fr']
# members[-1] = ['BdA', 2, 'bda@ens.fr']
# On écrit un mail personnalisé à chaque participant
datatuple = [(
'bda-rappel',
{'member': member[0], 'nb_attr': member[1], 'show': self},
settings.MAIL_DATA['rappels']['FROM'],
[member[0].email])
for member in members.values()
]
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.values()
@property
def is_past(self):
return self.date < timezone.now()
class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
text = models.TextField('Citation')
author = models.CharField('Auteur', max_length=200)
PAYMENT_TYPES = (
("cash", "Cash"),
("cb", "CB"),
("cheque", "Chèque"),
("autre", "Autre"),
)
class Participant(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(Spectacle,
through="ChoixSpectacle",
related_name="chosen_by")
attributions = models.ManyToManyField(Spectacle,
through="Attribution",
related_name="attributed_to")
paid = models.BooleanField("A payé", default=False)
paymenttype = models.CharField("Moyen de paiement",
max_length=6, choices=PAYMENT_TYPES,
blank=True)
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed",
blank=True)
def __str__(self):
return "%s - %s" % (self.user, self.tirage.title)
DOUBLE_CHOICES = (
("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"),
("double", "2 places sinon rien"),
)
class ChoixSpectacle(models.Model):
participant = models.ForeignKey(
Participant,
on_delete=models.CASCADE
)
spectacle = models.ForeignKey(
Spectacle,
on_delete=models.CASCADE,
related_name="participants"
)
priority = models.PositiveIntegerField("Priorité")
double_choice = models.CharField("Nombre de places",
default="1", choices=DOUBLE_CHOICES,
max_length=10)
def get_double(self):
return self.double_choice != "1"
double = property(get_double)
def get_autoquit(self):
return self.double_choice == "autoquit"
autoquit = property(get_autoquit)
def __str__(self):
return "Vœux de %s pour %s" % (
self.participant.user.get_full_name(),
self.spectacle.title)
class Meta:
ordering = ("priority",)
unique_together = (("participant", "spectacle",),)
verbose_name = "voeu"
verbose_name_plural = "voeux"
class Attribution(models.Model):
participant = models.ForeignKey(
Participant,
on_delete=models.CASCADE
)
spectacle = models.ForeignKey(
Spectacle,
on_delete=models.CASCADE,
related_name="attribues"
)
given = models.BooleanField("Donnée", default=False)
def __str__(self):
return "%s -- %s, %s" % (self.participant.user, self.spectacle.title,
self.spectacle.date)
class SpectacleRevente(models.Model):
attribution = models.OneToOneField(
Attribution,
on_delete=models.CASCADE,
related_name="revente"
)
date = models.DateTimeField("Date de mise en vente",
default=timezone.now)
answered_mail = models.ManyToManyField(Participant,
related_name="wanted",
blank=True)
seller = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
related_name="original_shows",
verbose_name="Vendeur"
)
soldTo = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
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:
context['acheteur'] = inscrit.user
datatuple.append((
'bda-revente-loser',
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
View 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;
}

Binary file not shown.

View 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 %}

View 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 }} &mdash; 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 }} &mdash; rang {{ rank }})</span></li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
{% block extracontent %}
{% endblock %}
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View file

@ -0,0 +1,64 @@
{% 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>
<br>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
<pre id="export-mails" style="display:none">
{%for participant in participants %}{{participant.email}}, {%endfor%}
</pre>
<br>
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
<pre id="export-salle" style="display:none">
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
{% endfor %}
</pre>
<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 %}

View file

@ -0,0 +1,73 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Revente de place</h2>
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
<div class="form-group">
<div class="multiple-checkbox">
<ul>
{% for box in resellform.attributions %}
<li>
{{box.tag}}
{{box.choice_label}}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
<br>
{% if annulform.attributions or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<div class="multiple-checkbox">
<ul>
{% for box in annulform.attributions %}
<li>
{{box.tag}}
{{box.choice_label}}
</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{attrib.spectacle}}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if annulform.attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
{% endif %}
<br>
{% if sold %}
<h3>Places revendues</h3>
<table class="table">
{% for attrib in sold %}
<tr>
<form action="" method="post">
{% csrf_token %}
<td>{{attrib.spectacle}}</td>
<td>{{attrib.revente.soldTo.user.get_full_name}}</td>
<td><button type="submit" class="btn btn-primary" name="transfer"
value="{{attrib.revente.id}}">Transférer</button></td>
<td><button type="submit" class="btn btn-primary" name="reinit"
value="{{attrib.revente.id}}">Réinitialiser</button></td>
</form>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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&nbsp: {{ unpaid|length }}</h3>
{% endblock %}

View 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 %}

View 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&nbsp;: {{ 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 %}

View 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>

View 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'>&nbsp;</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&oelig;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&oelig;ux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque v&oelig;u.<br />
</p>
</div>
</form>
{% endblock %}

View 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 %}

View file

@ -0,0 +1,43 @@
{% 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 }}&nbsp;?</h3>
{% if show.rappel_sent %}
<p class="error">Attention, les mails ont déjà été envoyés le
{{ show.rappel_sent }}</p>
{% endif %}
{% endif %}
{% 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 %}
<br/>
<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 %}

View file

@ -0,0 +1,14 @@
{% extends "base_title.html" %}
{% block realcontent %}
{% if choices %}
<h3>Vos v&oelig;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&oelig;u pour le tirage au sort</h3>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2><strong>Places attribuées</strong></h3>
{% if places %}
<table class="table table-striped">
{% for place in places %}
<tr>
<td>{{place.spectacle.title}}</td>
<td>{{place.spectacle.location}}</td>
<td>{{place.spectacle.date}}</td>
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
</tr>
{% endfor %}
</table>
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
<br/>
<p>Ne manque pas un spectacle avec le
<a href="{% url "calendar" %}">calendrier
automatique&#8239;!</a></p>
{% else %}
<h3>Vous n'avez aucune place :(</h3>
{% endif %}
{% endblock %}

View 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>

View 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%}

View 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 %}

View file

@ -0,0 +1,10 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Raté, le tirage ne peut pas être lancé&#8239;!</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 %}

22
bda/tests.py Normal file
View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

50
bda/urls.py Normal file
View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.conf.urls import url
from cof.decorators import buro_required
from bda.views import SpectacleListView
from bda 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),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'),
]

653
bda/views.py Normal file
View file

@ -0,0 +1,653 @@
# -*- coding: utf-8 -*-
import random
import hashlib
import time
from datetime import timedelta
from custommail.shortcuts import (
send_mass_custom_mail, send_custom_mail, render_custom_mail
)
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 models, transaction
from django.core import serializers
from django.db.models import Count, Q, Sum
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect
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 cof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
Tirage, SpectacleRevente
from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
InscriptionReventeForm
@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)
spectacles1 = ChoixSpectacle.objects \
.filter(spectacle__tirage=tirage) \
.filter(double_choice="1") \
.all() \
.values('spectacle', 'spectacle__title') \
.annotate(total=models.Count('spectacle'))
spectacles2 = ChoixSpectacle.objects \
.filter(spectacle__tirage=tirage) \
.exclude(double_choice="1") \
.all() \
.values('spectacle', 'spectacle__title') \
.annotate(total=models.Count('spectacle'))
spectacles = tirage.spectacle_set.all()
spectacles_dict = {}
total = 0
for spectacle in spectacles:
spectacle.total = 0
spectacle.ratio = 0.0
spectacles_dict[spectacle.id] = spectacle
for spectacle in spectacles1:
spectacles_dict[spectacle["spectacle"]].total += spectacle["total"]
spectacles_dict[spectacle["spectacle"]].ratio = \
spectacles_dict[spectacle["spectacle"]].total / \
spectacles_dict[spectacle["spectacle"]].slots
total += spectacle["total"]
for spectacle in spectacles2:
spectacles_dict[spectacle["spectacle"]].total += 2*spectacle["total"]
spectacles_dict[spectacle["spectacle"]].ratio = \
spectacles_dict[spectacle["spectacle"]].total / \
spectacles_dict[spectacle["spectacle"]].slots
total += 2*spectacle["total"]
context = {
"proposed": tirage.spectacle_set.aggregate(Sum('slots'))['slots__sum'],
"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, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
places = participant.attribution_set.order_by(
"spectacle__date", "spectacle").all()
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 le
client demande le formulaire et le moment 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', {})
if timezone.now() > tirage.fermeture:
# Le tirage est fermé.
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
choices = participant.choixspectacle_set.order_by("priority").all()
messages.error(request,
" C'est fini : tirage au sort dans la journée !")
return render(request, "bda/resume-inscription-tirage.html",
{"choices": choices})
def formfield_callback(f, **kwargs):
"""
Fonction utilisée par inlineformset_factory ci dessous.
Restreint les spectacles proposés aux spectacles du bo tirage.
"""
if f.name == "spectacle":
kwargs['queryset'] = tirage.spectacle_set
return f.formfield(**kwargs)
BdaFormSet = inlineformset_factory(
Participant,
ChoixSpectacle,
fields=("spectacle", "double_choice", "priority"),
formset=BaseBdaFormSet,
formfield_callback=formfield_callback)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
success = False
stateerror = False
if request.method == "POST":
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)
dbstate = _hash_queryset(participant.choixspectacle_set.all())
total_price = 0
for choice in participant.choixspectacle_set.all():
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().all(),
'token': token,
'members': tirage_elt.participant_set.all(),
'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().all()
)
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
for (show, _, losers) in results:
for (loser, _, _, _) in losers:
loser.choicesrevente.add(show)
loser.save()
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", {})
if request.method == 'POST':
# On met en vente une place
if 'resell' in request.POST:
resellform = ResellForm(participant, request.POST, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
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')
resellform = ResellForm(participant, prefix='resell')
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:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['transfer'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
attrib = revente.attribution
attrib.participant = revente.soldTo
attrib.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:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['reinit'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
if revente.attribution.spectacle.date > timezone.now():
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
revente.shotgun = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
else:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
else:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
overdue = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__seller=participant,
revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
sold = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__soldTo__isnull=False)
return render(request, "bda-revente.html",
{'tirage': tirage, 'overdue': overdue, "sold": sold,
"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)
min_resell.save()
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.
if reventes.filter(seller=participant).exists():
revente = reventes.filter(seller=participant)[0]
revente.delete()
return HttpResponseRedirect(reverse("bda-shotgun",
args=[tirage.id]))
reventes_shotgun = list(reventes.filter(shotgun=True).all())
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())
shotgun = []
for spectacle in spectacles:
reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
shotgun=True,
soldTo__isnull=True)
if reventes.exists():
shotgun.append(spectacle)
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.all()
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.all()
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).all()
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
exemple_mail_1place = render_custom_mail('bda-rappel', {
'member': request.user,
'show': show,
'nb_attr': 1
})
exemple_mail_2places = render_custom_mail('bda-rappel', {
'member': request.user,
'show': show,
'nb_attr': 2
})
# Contexte
ctxt = {'show': show,
'exemple_mail_1place': exemple_mail_1place,
'exemple_mail_2places': exemple_mail_2places}
# Envoi confirmé
if request.method == 'POST':
members = show.send_rappel()
ctxt['sent'] = True
ctxt['members'] = members
# Demande de confirmation
else:
ctxt['sent'] = False
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
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.all()})

4
bds/admin.py Normal file
View file

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import BdsProfile
admin.site.register(BdsProfile)

6
bds/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BDSConfig(AppConfig):
name = "bds"
verbose_name = "Application de gestion du BDS"

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import bds.models
class Migration(migrations.Migration):
dependencies = [
('gestion', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='BdsProfile',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
('ASPSL_number', models.CharField(null=True, blank=True, verbose_name='Numéro AS PSL', max_length=50)),
('FFSU_number', models.CharField(null=True, blank=True, verbose_name='Numéro FFSU', max_length=50)),
('have_certificate', models.BooleanField(verbose_name='Certificat médical', default=False)),
('certificate_file', models.FileField(blank=True, upload_to=bds.models.BdsProfile.issue_file_name, verbose_name='Fichier de certificat médical')),
('cotisation_period', models.CharField(choices=[('ANN', 'Année'), ('SE1', 'Premier semestre'), ('SE2', 'Deuxième semestre')], verbose_name='Inscription', max_length=3, default='ANN')),
('registration_date', models.DateField(verbose_name="Date d'inscription", auto_now_add=True)),
('payment_method', models.CharField(choices=[('CASH', 'Liquide'), ('BANK', 'Transfer bancaire'), ('CHEQUE', 'Cheque'), ('OTHER', 'Autre')], verbose_name='Methode de paiement', max_length=6, default='CASH')),
('profile', models.OneToOneField(
related_name='bds',
on_delete=models.CASCADE,
to='gestion.Profile')),
],
),
]

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_groups(apps, schema_editor):
"""
Creates the groups for BDS members and staff
"""
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="bds_members")
Group.objects.get_or_create(name="bds_buro")
class Migration(migrations.Migration):
dependencies = [
('bds', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bdsprofile',
options={
'permissions': [
('member', 'Is a BDS member'),
('buro', 'Is part of the BDS staff')
],
'verbose_name': 'Profil BDS',
'verbose_name_plural': 'Profils BDS'
},
),
migrations.RunPython(create_groups, migrations.RunPython.noop),
]

65
bds/models.py Normal file
View file

@ -0,0 +1,65 @@
import os.path
from django.utils import timezone
from django.db import models
from gestion.models import Profile
class BdsProfile(models.Model):
profile = models.OneToOneField(Profile,
related_name='bds',
on_delete=models.CASCADE)
def issue_file_name(sportif, filename):
fn, extension = os.path.splitext(filename)
year = timezone.now().year
return "certifs/{!s}-{:d}{:s}".format(sportif, year, extension)
COTIZ_DURATION_CHOICES = (
('ANN', 'Année'),
('SE1', 'Premier semestre'),
('SE2', 'Deuxième semestre'),
)
PAYMENT_METHOD_CHOICES = (
('CASH', 'Liquide'),
('BANK', 'Transfer bancaire'),
('CHEQUE', 'Cheque'),
('OTHER', 'Autre'),
)
ASPSL_number = models.CharField("Numéro AS PSL",
max_length=50,
blank=True,
null=True)
FFSU_number = models.CharField("Numéro FFSU",
max_length=50,
blank=True,
null=True)
have_certificate = models.BooleanField("Certificat médical",
default=False)
certificate_file = models.FileField("Fichier de certificat médical",
upload_to=issue_file_name,
blank=True)
cotisation_period = models.CharField("Inscription",
default="ANN",
choices=COTIZ_DURATION_CHOICES,
max_length=3)
registration_date = models.DateField(auto_now_add=True,
verbose_name="Date d'inscription")
payment_method = models.CharField('Methode de paiement',
default='CASH',
choices=PAYMENT_METHOD_CHOICES,
max_length=6)
class Meta:
verbose_name = "Profil BDS"
verbose_name_plural = "Profils BDS"
permissions = [
("member", "Is a BDS member"),
("buro", "Is part of the BDS staff")
]

12
bds/tests.py Normal file
View file

@ -0,0 +1,12 @@
from django.test import TestCase
from gestion.tests import create_profile
from .models import BdsProfile
class TestBdsProfile(TestCase):
def test_profile(self):
# each bdspofile should have an associated profile
p = create_profile('foo')
bdsp = BdsProfile.objects.create(profile=p)
self.assertEqual(p.bds, bdsp)

3
bds/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
cof/__init__.py Normal file
View file

145
cof/admin.py Normal file
View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
import django.utils.six as six
from .petits_cours_models import PetitCoursDemande, \
PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
PetitCoursAttributionCounter
from .models import (
SurveyQuestionAnswer, SurveyQuestion, CofProfile, EventOption,
EventOptionChoice, Event, EventCommentField, EventRegistration,
Survey
)
def add_link_field(target_model='', field='', link_text=six.text_type,
desc_text=six.text_type):
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 EventRegistrationAdmin(admin.ModelAdmin):
list_display = ('__unicode__' if six.PY2 else '__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')
admin.site.register(Survey, SurveyAdmin)
admin.site.register(SurveyQuestion, SurveyQuestionAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(EventOption, EventOptionAdmin)
admin.site.register(CofProfile)
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)

6
cof/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class COFConfig(AppConfig):
name = "cof"
verbose_name = "Application de gestion du COF"

78
cof/autocomplete.py Normal file
View file

@ -0,0 +1,78 @@
# -*- 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, Group
from .decorators import buro_required
from django.conf import settings
class Clipper(object):
def __init__(self, clipper, fullname):
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}
cof_members = Group.objects.get(name="cof_members")
queries = {}
bits = q.split()
# Fetching data from User and Profile tables
queries['members'] = User.objects.filter(groups=cof_members)
queries['users'] = User.objects.exclude(groups=cof_members)
for bit in bits:
queries['members'] = queries['members'].filter(
Q(first_name__icontains=bit)
| Q(last_name__icontains=bit)
| Q(username__icontains=bit)
| Q(profile__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('profile__login_clipper',
flat='True'))
| set(queries['users'].values_list('profile__login_clipper',
flat='True'))
)
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(bit=bit)
for bit in bits]
))
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
]
# Resulting data
data.update(queries)
data['options'] = any(bool(query) for query in queries.values())
return shortcuts.render(request, "cof/autocomplete_user.html", data)

75
cof/csv_views.py Normal file
View 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 %}
"""

8
cof/decorators.py Normal file
View file

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import permission_required
cof_required = permission_required('cof.member')
cof_required_customdenied = permission_required('cof.member',
login_url="cof-denied")
buro_required = permission_required('cof.buro')

199
cof/fixtures/gestion.json Normal file
View 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": "cof.survey",
"pk": 1
},
{
"fields": {
"question": "Sanction s'il chante",
"survey": 1,
"multi_answers": true
},
"model": "cof.surveyquestion",
"pk": 1
},
{
"fields": {
"question": "Est-ce qu'on le garde\u00a0?",
"survey": 1,
"multi_answers": false
},
"model": "cof.surveyquestion",
"pk": 2
},
{
"fields": {
"answer": "On l'ernestise",
"survey_question": 1
},
"model": "cof.surveyquestionanswer",
"pk": 1
},
{
"fields": {
"answer": "On ligote",
"survey_question": 1
},
"model": "cof.surveyquestionanswer",
"pk": 2
},
{
"fields": {
"answer": "On le prive de banquet",
"survey_question": 1
},
"model": "cof.surveyquestionanswer",
"pk": 3
},
{
"fields": {
"answer": "Oui",
"survey_question": 2
},
"model": "cof.surveyquestionanswer",
"pk": 4
},
{
"fields": {
"answer": "Non",
"survey_question": 2
},
"model": "cof.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": "cof.event",
"pk": 1
},
{
"fields": {
"default": "",
"event": 1,
"fieldtype": "text",
"name": "Commentaires"
},
"model": "cof.eventcommentfield",
"pk": 1
},
{
"fields": {
"multi_choices": true,
"event": 1,
"name": "Potion magique"
},
"model": "cof.eventoption",
"pk": 1
},
{
"fields": {
"event_option": 1,
"value": "Je suis alergique"
},
"model": "cof.eventoptionchoice",
"pk": 1
},
{
"fields": {
"event_option": 1,
"value": "J'en veux"
},
"model": "cof.eventoptionchoice",
"pk": 2
},
{
"fields": {
"event_option": 1,
"value": "Je suis tomb\u00e9 dans la marmite quand j'\u00e9tais petit"
},
"model": "cof.eventoptionchoice",
"pk": 3
},
{
"fields": {
"name": "Bagarre"
},
"model": "cof.petitcourssubject",
"pk": 1
},
{
"fields": {
"name": "Lancer de menhir"
},
"model": "cof.petitcourssubject",
"pk": 2
},
{
"fields": {
"name": "Pr\u00e9paration de potions"
},
"model": "cof.petitcourssubject",
"pk": 3
},
{
"fields": {
"name": "Chant"
},
"model": "cof.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": "cof.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": "cof.petitcoursdemande",
"pk": 2
}
]

10
cof/fixtures/sites.json Normal file
View file

@ -0,0 +1,10 @@
[
{
"fields": {
"domain": "localhost",
"name": "GestioCOF - dev - local"
},
"model": "sites.site",
"pk": 1
}
]

297
cof/forms.py Normal file
View file

@ -0,0 +1,297 @@
from django import forms
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory
from django.utils.translation import ugettext_lazy as _
from bda.models import Spectacle
from .models import CofProfile, EventCommentValue, CalendarSubscription
from .widgets import TriStateCheckbox
class EventForm(forms.Form):
def __init__(self, *args, **kwargs):
event = kwargs.pop("event")
self.event = event
current_choices = kwargs.pop("current_choices", None)
super(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 COFProfileRegistrationForm(forms.ModelForm):
member = forms.BooleanField(
label=_("Membre du COF"),
required=False,
)
def save(self, **kwargs):
super().save()
self.instance.is_cof = True
class Meta:
model = CofProfile
fields = [
"type_cotiz", "mailing", "mailing_bda", "mailing_bda_revente",
]
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']

41
cof/management/base.py Normal file
View 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

View file

@ -0,0 +1,121 @@
"""
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 cof.management.base import MyBaseCommand
from cof.petits_cours_models import (
PetitCoursAbility, PetitCoursSubject, LEVELS_CHOICES,
PetitCoursAttributionCounter
)
from cof.models import CofProfile
# 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:
cofprofile = CofProfile.objects.create(
profile=user.profile,
)
cofprofile.is_cof = True
# 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.save()
CofProfile.objects.create(
profile=root.profile,
is_cof=True,
is_buro=True
)
# ---
# 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')

View 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"
}
]

View 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"
}
]

View file

@ -0,0 +1,387 @@
# -*- 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',
on_delete=models.CASCADE,
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',
on_delete=models.CASCADE,
to='cof.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',
on_delete=models.CASCADE,
to='cof.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',
on_delete=models.CASCADE,
to='cof.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',
on_delete=models.CASCADE,
to='cof.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(
on_delete=models.CASCADE,
to='cof.Event')),
('filledcomments', models.ManyToManyField(to='cof.EventCommentField', through='cof.EventCommentValue')),
('options', models.ManyToManyField(to='cof.EventOptionChoice')),
('user', models.ForeignKey(
on_delete=models.CASCADE,
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='cof.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(
on_delete=models.CASCADE,
related_name='questions',
to='cof.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',
on_delete=models.CASCADE,
to='cof.SurveyQuestion')),
],
options={
'verbose_name': 'R\xe9ponse',
},
),
migrations.AddField(
model_name='surveyanswer',
name='answers',
field=models.ManyToManyField(related_name='selected_by', to='cof.SurveyQuestionAnswer'),
),
migrations.AddField(
model_name='surveyanswer',
name='survey',
field=models.ForeignKey(
on_delete=models.CASCADE,
to='cof.Survey'),
),
migrations.AddField(
model_name='surveyanswer',
name='user',
field=models.ForeignKey(
on_delete=models.CASCADE,
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='petitcoursdemande',
name='matieres',
field=models.ManyToManyField(related_name='demandes', verbose_name='Mati\xe8res', to='cof.PetitCoursSubject'),
),
migrations.AddField(
model_name='petitcoursdemande',
name='traitee_par',
field=models.ForeignKey(
on_delete=models.PROTECT,
blank=True,
to=settings.AUTH_USER_MODEL,
null=True),
),
migrations.AddField(
model_name='petitcoursattributioncounter',
name='matiere',
field=models.ForeignKey(
on_delete=models.PROTECT,
verbose_name='Matiere',
to='cof.PetitCoursSubject'),
),
migrations.AddField(
model_name='petitcoursattributioncounter',
name='user',
field=models.ForeignKey(
on_delete=models.PROTECT,
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='petitcoursattribution',
name='demande',
field=models.ForeignKey(
on_delete=models.CASCADE,
verbose_name='Demande',
to='cof.PetitCoursDemande'),
),
migrations.AddField(
model_name='petitcoursattribution',
name='matiere',
field=models.ForeignKey(
on_delete=models.PROTECT,
verbose_name='Mati\xe8re',
to='cof.PetitCoursSubject'),
),
migrations.AddField(
model_name='petitcoursattribution',
name='user',
field=models.ForeignKey(
on_delete=models.CASCADE,
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='petitcoursability',
name='matiere',
field=models.ForeignKey(
on_delete=models.CASCADE,
verbose_name='Mati\xe8re',
to='cof.PetitCoursSubject'),
),
migrations.AddField(
model_name='petitcoursability',
name='user',
field=models.ForeignKey(
on_delete=models.CASCADE,
to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='eventcommentvalue',
name='registration',
field=models.ForeignKey(
on_delete=models.CASCADE,
related_name='comments',
to='cof.EventRegistration'),
),
migrations.AlterUniqueTogether(
name='surveyanswer',
unique_together=set([('user', 'survey')]),
),
migrations.AlterUniqueTogether(
name='eventregistration',
unique_together=set([('user', 'event')]),
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cof', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='petitcoursdemande',
name='processed',
field=models.DateTimeField(null=True, verbose_name='Date de traitement', blank=True),
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cof', '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),
),
]

View 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("cof", "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 = [
('cof', '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),
]

View file

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cof', '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'),
),
]

View file

@ -0,0 +1,52 @@
# -*- 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),
('cof', '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(
on_delete=models.CASCADE,
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),
),
]

View 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 = [
('cof', '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
cof/migrations/0008_py3.py Normal file
View 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("cof", "CofProfile")
Profile.objects.update(comments="")
class Migration(migrations.Migration):
dependencies = [
('cof', '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),
]

View file

@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.auth.models import Group
from django.db import migrations, models
def create_profile(apps, schema_editor):
CofProfile = apps.get_model("cof", "CofProfile")
Profile = apps.get_model("gestion", "Profile")
for p in CofProfile.objects.all():
profile = Profile.objects.create(
id=p.id,
user=p.user,
login_clipper=p.login_clipper,
phone=p.phone,
occupation=p.occupation,
departement=p.departement,
comments=p.comments
)
p.profile = profile
p.save()
def preserve_perms(apps, schema_editor):
# from django.contrib.auth.management import create_permissions
# apps.models_module = True
# create_permissions(apps, verbosity=0)
# apps.models_module = None
CofProfile = apps.get_model("cof", "CofProfile")
# memberp = Permission.objects.get(codename='member')
# burop = Permission.objects.get(codename='buro')
# creates the groups for COF members and
member = Group.objects.create(name='cof_members')
buro = Group.objects.create(name='cof_buro')
# associate permissions to the respective groups.
# buro.permissions = [burop, memberp]
# member.permissions = [memberp]
for cofp in CofProfile.objects.filter(is_cof=True):
cofp.profile.user.groups.add(member)
for cofp in CofProfile.objects.filter(is_buro=True):
cofp.profile.user.groups.add(buro)
def remove_profile(apps, schema_editor):
raise NotImplementedError
fuckup_perms = remove_profile
class Migration(migrations.Migration):
dependencies = [
('gestion', '0001_initial'),
('cof', '0008_py3'),
]
operations = [
migrations.RenameField(
model_name='cofprofile',
old_name='mailing_cof',
new_name='mailing',
),
migrations.AddField(
model_name='cofprofile',
name='profile',
field=models.OneToOneField(
on_delete=models.CASCADE,
to='gestion.Profile',
null=True,
related_name='cof'
),
preserve_default=False,
),
migrations.AlterModelOptions(
name='cofprofile',
options={
'permissions': (('member', 'Is a COF member'),
('buro', 'Is part of COF staff')),
'verbose_name': 'Profil COF',
'verbose_name_plural': 'Profils COF'},
),
migrations.RunPython(create_profile, remove_profile),
migrations.AlterField(
model_name='cofprofile',
name='profile',
field=models.OneToOneField(
on_delete=models.CASCADE,
to='gestion.Profile',
related_name='cof'
),
),
migrations.RemoveField(
model_name='cofprofile',
name='comments',
),
migrations.RemoveField(
model_name='cofprofile',
name='departement',
),
migrations.RemoveField(
model_name='cofprofile',
name='login_clipper',
),
migrations.RemoveField(
model_name='cofprofile',
name='occupation',
),
migrations.RemoveField(
model_name='cofprofile',
name='phone',
),
migrations.RunPython(preserve_perms, fuckup_perms),
migrations.RemoveField(
model_name='cofprofile',
name='is_cof',
),
migrations.RemoveField(
model_name='cofprofile',
name='is_buro',
),
migrations.RemoveField(
model_name='cofprofile',
name='user',
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_cof_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="cof_members")
def create_buro_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="cof_buro")
class Migration(migrations.Migration):
dependencies = [
('cof', '0009_generic_profiles'),
]
operations = [
migrations.RunPython(create_cof_group, migrations.RunPython.noop),
migrations.RunPython(create_buro_group, migrations.RunPython.noop),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cof', '0010_create_cof_group'),
]
operations = [
migrations.DeleteModel(
name='Clipper',
),
migrations.DeleteModel(
name='CustomMail',
),
]

View file

@ -0,0 +1,23 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cof', '0011_delete_clipper_and_custommail'),
('gestion', '0002_club_support')
]
operations = [
migrations.RemoveField(
model_name='club',
name='membres',
),
migrations.RemoveField(
model_name='club',
name='respos',
),
migrations.DeleteModel(
name='Club',
),
]

View file

279
cof/models.py Normal file
View file

@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import Group, User
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
import django.utils.six as six
from gestion.models import Profile
from bda.models import Spectacle
from .petits_cours_models import choices_length
TYPE_COTIZ_CHOICES = (
('etudiant', _("Normalien étudiant")),
('normalien', _("Normalien élève")),
('exterieur', _("Extérieur")),
)
TYPE_COMMENT_FIELD = (
('text', _("Texte long")),
('char', _("Texte court")),
)
class CofProfile(models.Model):
profile = models.OneToOneField(Profile,
on_delete=models.CASCADE,
related_name="cof")
num = models.IntegerField("Numéro d'adhérent", blank=True, default=0)
type_cotiz = models.CharField(_("Type de cotisation"),
default="normalien",
choices=TYPE_COTIZ_CHOICES,
max_length=choices_length(
TYPE_COTIZ_CHOICES))
mailing = models.BooleanField("Recevoir les mails COF", default=False)
# XXX. remove the following and put in a BDA profile
mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False)
mailing_bda_revente = models.BooleanField(
"Recevoir les mails de revente de places BdA", default=False)
petits_cours_accept = models.BooleanField(
"Recevoir des petits cours", default=False)
petits_cours_remarques = models.TextField(
_("Remarques et précisions pour les petits cours"),
blank=True, default="")
class Meta:
verbose_name = "Profil COF"
verbose_name_plural = "Profils COF"
permissions = (
('member', 'Is a COF member'),
('buro', 'Is part of COF staff'),
)
@property
def is_cof(self):
return self.profile.user.has_perm('cof.member')
@is_cof.setter
def is_cof(self, really):
if really:
g = Group.objects.get(name='cof_members')
self.profile.user.groups.add(g)
# XXX. remove the following and use django auth.
@property
def is_buro(self):
return self.profile.user.has_perm('cof.buro')
@is_buro.setter
def is_buro(self, really):
if really:
g = Group.objects.get(name='cof_buro')
self.profile.user.groups.add(g)
def __str__(self):
return self.profile.user.username
@python_2_unicode_compatible
class Event(models.Model):
title = models.CharField("Titre", max_length=200)
location = models.CharField("Lieu", max_length=200)
start_date = models.DateTimeField("Date de début", blank=True, null=True)
end_date = models.DateTimeField("Date de fin", blank=True, null=True)
description = models.TextField("Description", blank=True)
image = models.ImageField("Image", blank=True, null=True,
upload_to="imgs/events/")
registration_open = models.BooleanField("Inscriptions ouvertes",
default=True)
old = models.BooleanField("Archiver (événement fini)", default=False)
class Meta:
verbose_name = "Événement"
def __str__(self):
return six.text_type(self.title)
@python_2_unicode_compatible
class EventCommentField(models.Model):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="commentfields"
)
name = models.CharField("Champ", max_length=200)
fieldtype = models.CharField("Type", max_length=10,
choices=TYPE_COMMENT_FIELD, default="text")
default = models.TextField("Valeur par défaut", blank=True)
class Meta:
verbose_name = "Champ"
def __str__(self):
return six.text_type(self.name)
@python_2_unicode_compatible
class EventCommentValue(models.Model):
commentfield = models.ForeignKey(
EventCommentField,
on_delete=models.CASCADE,
related_name="values"
)
registration = models.ForeignKey(
"EventRegistration",
on_delete=models.CASCADE,
related_name="comments"
)
content = models.TextField("Contenu", blank=True, null=True)
def __str__(self):
return "Commentaire de %s" % self.commentfield
@python_2_unicode_compatible
class EventOption(models.Model):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="options"
)
name = models.CharField("Option", max_length=200)
multi_choices = models.BooleanField("Choix multiples", default=False)
class Meta:
verbose_name = "Option"
def __str__(self):
return six.text_type(self.name)
@python_2_unicode_compatible
class EventOptionChoice(models.Model):
event_option = models.ForeignKey(
EventOption,
on_delete=models.CASCADE,
related_name="choices"
)
value = models.CharField("Valeur", max_length=200)
class Meta:
verbose_name = "Choix"
verbose_name_plural = "Choix"
def __str__(self):
return six.text_type(self.value)
@python_2_unicode_compatible
class EventRegistration(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
event = models.ForeignKey(
Event,
on_delete=models.CASCADE
)
options = models.ManyToManyField(EventOptionChoice)
filledcomments = models.ManyToManyField(EventCommentField,
through=EventCommentValue)
paid = models.BooleanField("A payé", default=False)
class Meta:
verbose_name = "Inscription"
unique_together = ("user", "event")
def __str__(self):
return "Inscription de %s à %s" % (six.text_type(self.user),
six.text_type(self.event.title))
@python_2_unicode_compatible
class Survey(models.Model):
title = models.CharField("Titre", max_length=200)
details = models.TextField("Détails", blank=True)
survey_open = models.BooleanField("Sondage ouvert", default=True)
old = models.BooleanField("Archiver (sondage fini)", default=False)
class Meta:
verbose_name = "Sondage"
def __str__(self):
return six.text_type(self.title)
@python_2_unicode_compatible
class SurveyQuestion(models.Model):
survey = models.ForeignKey(
Survey,
on_delete=models.CASCADE,
related_name="questions"
)
question = models.CharField("Question", max_length=200)
multi_answers = models.BooleanField("Choix multiples", default=False)
class Meta:
verbose_name = "Question"
def __str__(self):
return six.text_type(self.question)
@python_2_unicode_compatible
class SurveyQuestionAnswer(models.Model):
survey_question = models.ForeignKey(
SurveyQuestion,
on_delete=models.CASCADE,
related_name="answers"
)
answer = models.CharField("Réponse", max_length=200)
class Meta:
verbose_name = "Réponse"
def __str__(self):
return six.text_type(self.answer)
@python_2_unicode_compatible
class SurveyAnswer(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
survey = models.ForeignKey(
Survey,
on_delete=models.CASCADE
)
answers = models.ManyToManyField(SurveyQuestionAnswer,
related_name="selected_by")
class Meta:
verbose_name = "Réponses"
unique_together = ("user", "survey")
def __str__(self):
return "Réponse de %s sondage %s" % (
self.user.get_full_name(),
self.survey.title)
@python_2_unicode_compatible
class CalendarSubscription(models.Model):
token = models.UUIDField()
user = models.OneToOneField(
User,
on_delete=models.CASCADE
)
other_shows = models.ManyToManyField(Spectacle)
subscribe_to_events = models.BooleanField(default=True)
subscribe_to_my_shows = models.BooleanField(default=True)
def __str__(self):
return "Calendrier de %s" % self.user.get_full_name()

54
cof/petits_cours_forms.py Normal file
View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from captcha.fields import ReCaptchaField
from django import forms
from django.forms import ModelForm
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User
from .petits_cours_models import PetitCoursDemande, PetitCoursAbility
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super(BaseMatieresFormSet, self).clean()
if any(self.errors):
# Don't bother validating the formset unless each form is
# valid on its own
return
matieres = []
for i in range(0, self.total_form_count()):
form = self.forms[i]
if not form.cleaned_data:
continue
matiere = form.cleaned_data['matiere']
niveau = form.cleaned_data['niveau']
delete = form.cleaned_data['DELETE']
if not delete and (matiere, niveau) in matieres:
raise forms.ValidationError(
"Vous ne pouvez pas vous inscrire deux fois pour la "
"même matiere avec le même niveau.")
matieres.append((matiere, niveau))
class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
def __init__(self, *args, **kwargs):
super(DemandeForm, self).__init__(*args, **kwargs)
self.fields['matieres'].help_text = ''
class Meta:
model = PetitCoursDemande
fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu',
'matieres', 'agrege_requis', 'niveau', 'remarques')
widgets = {'matieres': forms.CheckboxSelectMultiple}
MatieresFormSet = inlineformset_factory(
User,
PetitCoursAbility,
fields=("matiere", "niveau", "agrege"),
formset=BaseMatieresFormSet
)

207
cof/petits_cours_models.py Normal file
View file

@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
from functools import reduce
from django.db import models
from django.db.models import Min
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
LEVELS_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)")),
)
class PetitCoursSubject(models.Model):
name = models.CharField(_("Matière"), max_length=30)
users = models.ManyToManyField(User, related_name="petits_cours_matieres",
through="PetitCoursAbility")
class Meta:
verbose_name = "Matière de petits cours"
verbose_name_plural = "Matières des petits cours"
def __str__(self):
return self.name
class PetitCoursAbility(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
matiere = models.ForeignKey(
PetitCoursSubject,
on_delete=models.CASCADE,
verbose_name=_("Matière")
)
niveau = models.CharField(_("Niveau"),
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES))
agrege = models.BooleanField(_("Agrégé"), default=False)
class Meta:
verbose_name = "Compétence petits cours"
verbose_name_plural = "Compétences des petits cours"
def __str__(self):
return "{:s} - {!s} - {:s}".format(
self.user.username, self.matiere, self.niveau
)
class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300)
phone = models.CharField(_("Téléphone (facultatif)"),
max_length=20, blank=True)
quand = models.CharField(
_("Quand ?"),
help_text=_("Indiquez ici la période désirée pour les petits"
" cours (vacances scolaires, semaine, week-end)."),
max_length=300, blank=True)
freq = models.CharField(
_("Fréquence"),
help_text=_("Indiquez ici la fréquence envisagée "
"(hebdomadaire, 2 fois par semaine, ...)"),
max_length=300, blank=True)
lieu = models.CharField(
_("Lieu (si préférence)"),
help_text=_("Si vous avez avez une préférence sur le lieu."),
max_length=300, blank=True)
matieres = models.ManyToManyField(
PetitCoursSubject, verbose_name=_("Matières"),
related_name="demandes")
agrege_requis = models.BooleanField(_("Agrégé requis"), default=False)
niveau = models.CharField(_("Niveau"),
default="",
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES))
remarques = models.TextField(_("Remarques et précisions"), blank=True)
traitee = models.BooleanField(_("Traitée"), default=False)
traitee_par = models.ForeignKey(
User,
on_delete=models.CASCADE,
blank=True, null=True
)
processed = models.DateTimeField(_("Date de traitement"),
blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=True)
def get_candidates(self, redo=False):
"""
Donne la liste des profs disponibles pour chaque matière de la demande.
- On ne donne que les agrégés si c'est demandé
- Si ``redo`` vaut ``True``, cela signifie qu'on retraite la demande et
il ne faut pas proposer à nouveau des noms qui ont déjà été proposés
"""
for matiere in self.matieres.all():
candidates = PetitCoursAbility.objects.filter(
matiere=matiere,
niveau=self.niveau,
user__profile__is_cof=True,
user__profile__petits_cours_accept=True
)
if self.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attrs = self.petitcoursattribution_set.filter(matiere=matiere)
already_proposed = [
attr.user
for attr in attrs
]
candidates = candidates.exclude(user__in=already_proposed)
candidates = candidates.order_by('?').select_related().all()
yield (matiere, candidates)
class Meta:
verbose_name = "Demande de petits cours"
verbose_name_plural = "Demandes de petits cours"
def __str__(self):
return "Demande {:d} du {:s}".format(
self.id, self.created.strftime("%d %b %Y")
)
class PetitCoursAttribution(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
demande = models.ForeignKey(
PetitCoursDemande,
on_delete=models.CASCADE,
verbose_name=_("Demande")
)
matiere = models.ForeignKey(
PetitCoursSubject,
on_delete=models.CASCADE,
verbose_name=_("Matière")
)
date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True)
rank = models.IntegerField("Rang dans l'email")
selected = models.BooleanField(_("Sélectionné par le demandeur"),
default=False)
class Meta:
verbose_name = "Attribution de petits cours"
verbose_name_plural = "Attributions de petits cours"
def __str__(self):
return "Attribution de la demande {:d} à {:s} pour {!s}".format(
self.demande.id, self.user.username, self.matiere
)
class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
matiere = models.ForeignKey(
PetitCoursSubject,
on_delete=models.CASCADE,
verbose_name=_("Matiere")
)
count = models.IntegerField("Nombre d'envois", default=0)
@classmethod
def get_uptodate(cls, user, matiere):
"""
Donne le compteur de l'utilisateur pour cette matière. Si le compteur
n'existe pas encore, il est initialisé avec le minimum des valeurs des
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(
user=user, matiere=matiere)
if created:
mincount = (
cls.objects.filter(matiere=matiere).exclude(user=user)
.aggregate(Min('count'))
['count__min']
)
counter.count = mincount
counter.save()
return counter
class Meta:
verbose_name = "Compteur d'attribution de petits cours"
verbose_name_plural = "Compteurs d'attributions de petits cours"
def __str__(self):
return "{:d} demandes envoyées à {:s} pour {!s}".format(
self.count, self.user.username, self.matiere
)

350
cof/petits_cours_views.py Normal file
View file

@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
import json
from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail
from django.contrib.auth.models import User
from django.views.generic import ListView, DetailView
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils import timezone
from .models import CofProfile
from .petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject
)
from .decorators import buro_required
from .petits_cours_forms import DemandeForm, MatieresFormSet
from gestion.shared import lock_table, unlock_tables
class DemandeListView(ListView):
model = PetitCoursDemande
template_name = "petits_cours_demandes_list.html"
paginate_by = 20
def get_queryset(self):
return PetitCoursDemande.objects.order_by('traitee', '-id').all()
class DemandeDetailView(DetailView):
model = PetitCoursDemande
template_name = "cof/details_demande_petit_cours.html"
context_object_name = "demande"
def get_context_data(self, **kwargs):
context = super(DemandeDetailView, self).get_context_data(**kwargs)
obj = self.object
context['attributions'] = obj.petitcoursattribution_set.all()
return context
@buro_required
def traitement(request, demande_id, redo=False):
demande = get_object_or_404(PetitCoursDemande, id=demande_id)
if demande.niveau == "other":
return _traitement_other(request, demande, redo)
if request.method == "POST":
return _traitement_post(request, demande)
proposals = {}
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
user = candidate.user
tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))]
attribdata[matiere.id] = []
proposals[matiere] = []
for candidate in candidates:
user = candidate.user
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
else:
unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals,
proposed_for, unsatisfied, attribdata, redo)
@buro_required
def retraitement(request, demande_id):
return traitement(request, demande_id, redo=True)
def _finalize_traitement(request, demande, proposals, proposed_for,
unsatisfied, attribdata, redo=False, errors=None):
proposals = proposals.items()
proposed_for = proposed_for.items()
attribdata = list(attribdata.items())
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals,
"unsatisfied": unsatisfied,
"extra":
'<textarea name="extra" '
'style="width:99%; height: 90px;">'
'</textarea>'
})
for error in errors:
messages.error(request, error)
return render(request, "cof/traitement_demande_petit_cours.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
"proposed_mails": proposed_mails,
"mainmail": mainmail,
"attribdata": json.dumps(attribdata),
"redo": redo,
})
def _generate_eleve_email(demande, proposed_for):
return [
(
user,
render_custom_mail('petit-cours-mail-eleve', {
"demande": demande,
"matieres": matieres
})
)
for user, matieres in proposed_for
]
def _traitement_other_preparing(request, demande):
redo = "redo" in request.POST
unsatisfied = []
proposals = {}
proposed_for = {}
attribdata = {}
errors = []
for matiere, candidates in demande.get_candidates(redo):
if candidates:
candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates])
attribdata[matiere.id] = []
proposals[matiere] = []
for choice_id in range(min(3, len(candidates))):
choice = int(
request.POST["proposal-{:d}-{:d}"
.format(matiere.id, choice_id)]
)
if choice == -1:
continue
if choice not in candidates:
errors.append("Choix invalide pour la proposition {:d}"
"en {!s}".format(choice_id + 1, matiere))
continue
user = candidates[choice]
if user in proposals[matiere]:
errors.append("La proposition {:d} en {!s} est un doublon"
.format(choice_id + 1, matiere))
continue
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
if not proposals[matiere]:
errors.append("Aucune proposition pour {!s}".format(matiere))
elif len(proposals[matiere]) < 3:
errors.append("Seulement {:d} proposition{:s} pour {!s}"
.format(
len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "",
matiere))
else:
unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals, proposed_for,
unsatisfied, attribdata, errors=errors)
def _traitement_other(request, demande, redo):
if request.method == "POST":
if "preparing" in request.POST:
return _traitement_other_preparing(request, demande)
else:
return _traitement_post(request, demande)
proposals = {}
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
user = candidate.user
tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples)
attribdata[matiere.id] = []
proposals[matiere] = []
for candidate in candidates:
user = candidate.user
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
else:
unsatisfied.append(matiere)
proposals = proposals.items()
proposed_for = proposed_for.items()
return render(request,
"gestiocof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
})
def _traitement_post(request, demande):
proposals = {}
proposed_for = {}
unsatisfied = []
extra = request.POST["extra"].strip()
redo = "redo" in request.POST
attribdata = request.POST["attribdata"]
attribdata = dict(json.loads(attribdata))
for matiere in demande.matieres.all():
if matiere.id not in attribdata:
unsatisfied.append(matiere)
else:
proposals[matiere] = []
for user_id in attribdata[matiere.id]:
user = User.objects.get(pk=user_id)
proposals[matiere].append(user)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
proposals_list = proposals.items()
proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra,
})
frommail = settings.MAIL_DATA['petits_cours']['FROM']
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
mails_to_send = []
for (user, msg) in proposed_mails:
msg = mail.EmailMessage("Petits cours ENS par le COF", msg,
frommail, [user.email],
[bccaddress], headers={'Reply-To': replyto})
mails_to_send.append(msg)
mails_to_send.append(mail.EmailMessage("Cours particuliers ENS", mainmail,
frommail, [demande.email],
[bccaddress],
headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=True)
connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User)
for matiere in proposals:
for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(user=user,
matiere=matiere)
counter.count += 1
counter.save()
attrib = PetitCoursAttribution(user=user, matiere=matiere,
demande=demande, rank=rank + 1)
attrib.save()
unlock_tables()
demande.traitee = True
demande.traitee_par = request.user
demande.processed = timezone.now()
demande.save()
return render(request,
"cof/traitement_demande_petit_cours_success.html",
{"demande": demande,
"redo": redo,
})
@login_required
def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user)
if not profile.is_cof:
return redirect("cof-denied")
success = False
if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user)
if formset.is_valid():
formset.save()
profile.petits_cours_accept = "receive_proposals" in request.POST
profile.petits_cours_remarques = request.POST["remarques"]
profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject)
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
unlock_tables()
success = True
formset = MatieresFormSet(instance=request.user)
else:
formset = MatieresFormSet(instance=request.user)
return render(request, "inscription-petit-cours.html",
{"formset": formset, "success": success,
"receive_proposals": profile.petits_cours_accept,
"remarques": profile.petits_cours_remarques})
@csrf_exempt
def demande(request):
success = False
if request.method == "POST":
form = DemandeForm(request.POST)
if form.is_valid():
form.save()
success = True
else:
form = DemandeForm()
return render(request, "demande-petit-cours.html", {"form": form,
"success": success})
@csrf_exempt
def demande_raw(request):
success = False
if request.method == "POST":
form = DemandeForm(request.POST)
if form.is_valid():
form.save()
success = True
else:
form = DemandeForm()
return render(request, "demande-petit-cours-raw.html",
{"form": form, "success": success})

6
cof/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more