Compare commits

..

1 commit

Author SHA1 Message Date
Aurélien Delobelle 83faf417c9 core -- Fix pre-commit hook, cmd see many args 2018-10-06 23:19:41 +02:00
591 changed files with 62261 additions and 62662 deletions

1
.envrc
View file

@ -1 +0,0 @@
use nix

6
.gitignore vendored
View file

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

View file

@ -1,7 +1,12 @@
image: "python:3.7"
image: "python:3.5"
services:
- postgres:latest
- redis:latest
variables:
# GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
DBHOST: "postgres"
REDIS_HOST: "redis"
REDIS_PASSWD: "dummy"
@ -17,24 +22,21 @@ variables:
# 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"
.test_template:
test:
stage: test
before_script:
- mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client 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
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade -r requirements-prod.txt coverage tblib
- pip install --upgrade -r requirements.txt coverage
- python --version
script:
- coverage run manage.py test
after_script:
- coverage report
services:
- postgres:11.7
- redis:latest
cache:
key: test
paths:
@ -43,53 +45,18 @@ variables:
# Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
coftest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
bdstest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod"
script:
- coverage run manage.py test bds clubs events --parallel
linters:
image: python:3.6
stage: test
before_script:
- mkdir -p vendor/pip
- pip install --upgrade black isort flake8
script:
- black --check .
- isort --check --diff .
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
# Print errors only
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
cache:
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/

View file

@ -12,7 +12,6 @@ checker_dirty=0
# 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 ... "
@ -24,8 +23,8 @@ if type black &>/dev/null; then
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
if ! black --check "$STAGED_PYTHON_FILES" &>$BLACK_OUTPUT; then
black $STAGED_PYTHON_FILES &>$BLACK_OUTPUT
tail -1 $BLACK_OUTPUT
formatter_updated=1
else
@ -48,8 +47,8 @@ if type isort &>/dev/null; then
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
if ! isort --check-only "$STAGED_PYTHON_FILES" &>$ISORT_OUTPUT; then
isort $STAGED_PYTHON_FILES &>$ISORT_OUTPUT
printf "Reformatted.\n"
formatter_updated=1
else
@ -72,7 +71,7 @@ if type flake8 &>/dev/null; then
FLAKE8_OUTPUT="/tmp/gc-flake8-output.log"
touch $FLAKE8_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then
if ! flake8 $STAGED_PYTHON_FILES &>$FLAKE8_OUTPUT; then
printf "FAIL\n"
cat $FLAKE8_OUTPUT
checker_dirty=1

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

View file

@ -1,4 +1,4 @@
# GestioCOF / GestioBDS
# GestioCOF
[![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
@ -38,11 +38,11 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier
pip install -U pip # parfois nécessaire la première fois
pip install -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 terminer, copier le fichier `cof/settings/secret_example.py` vers
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py gestioasso/settings/secret.py
ln -s secret_example.py cof/settings/secret.py
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
@ -186,18 +186,6 @@ Pour lancer les tests :
python manage.py test
```
### 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

1
TODO_PROD.md Normal file
View file

@ -0,0 +1 @@
- Changer les urls dans les mails "bda-revente" et "bda-shotgun"

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

328
bda/admin.py Normal file
View file

@ -0,0 +1,328 @@
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin
from django.db.models import Count, Sum
from django.template.defaultfilters import pluralize
from django.utils import timezone
from bda.models import (
Attribution,
CategorieSpectacle,
ChoixSpectacle,
Participant,
Quote,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
class ReadOnlyMixin(object):
readonly_fields_update = ()
def get_readonly_fields(self, request, obj=None):
readonly_fields = super().get_readonly_fields(request, obj)
if obj is None:
return readonly_fields
else:
return readonly_fields + self.readonly_fields_update
class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle
form = ChoixSpectacleAdminForm
sortable_field_name = "priority"
class AttributionTabularAdminForm(forms.ModelForm):
listing = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
spectacles = Spectacle.objects.select_related("location")
if self.listing is not None:
spectacles = spectacles.filter(listing=self.listing)
self.fields["spectacle"].queryset = spectacles
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
listing = False
class WithListingAttributionTabularAdminForm(AttributionTabularAdminForm):
listing = True
class AttributionInline(admin.TabularInline):
model = Attribution
extra = 0
listing = None
def get_queryset(self, request):
qs = super().get_queryset(request)
if self.listing is not None:
qs = qs.filter(spectacle__listing=self.listing)
return qs
class WithListingAttributionInline(AttributionInline):
exclude = ("given",)
form = WithListingAttributionTabularAdminForm
listing = True
class WithoutListingAttributionInline(AttributionInline):
form = WithoutListingAttributionTabularAdminForm
listing = False
class ParticipantAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
"location"
)
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
def get_queryset(self, request):
return Participant.objects.annotate(
nb_places=Count("attributions"), total=Sum("attributions__price")
)
def nb_places(self, obj):
return obj.nb_places
nb_places.admin_order_field = "nb_places"
nb_places.short_description = "Nombre de places"
def total(self, obj):
tot = obj.total
if tot:
return "%.02f" % tot
else:
return "0 €"
total.admin_order_field = "total"
total.short_description = "Total à payer"
list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage")
list_filter = ("paid", "tirage")
search_fields = ("user__username", "user__first_name", "user__last_name")
actions = ["send_attribs"]
actions_on_bottom = True
list_per_page = 400
readonly_fields = ("total",)
readonly_fields_update = ("user", "tirage")
form = ParticipantAdminForm
def send_attribs(self, request, queryset):
datatuple = []
for member in queryset.all():
attribs = member.attributions.all()
context = {"member": member.user}
shortname = ""
if len(attribs) == 0:
shortname = "bda-attributions-decus"
else:
shortname = "bda-attributions"
context["places"] = attribs
print(context)
datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
send_mass_custom_mail(datatuple)
count = len(queryset.all())
if count == 1:
message_bit = "1 membre a"
plural = ""
else:
message_bit = "%d membres ont" % count
plural = "s"
self.message_user(
request, "%s été informé%s avec succès." % (message_bit, plural)
)
send_attribs.short_description = "Envoyer les résultats par mail"
class AttributionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "spectacle" in self.fields:
self.fields["spectacle"].queryset = Spectacle.objects.select_related(
"location"
)
if "participant" in self.fields:
self.fields["participant"].queryset = Participant.objects.select_related(
"user", "tirage"
)
def clean(self):
cleaned_data = super().clean()
participant = cleaned_data.get("participant")
spectacle = cleaned_data.get("spectacle")
if participant and spectacle:
if participant.tirage != spectacle.tirage:
raise forms.ValidationError(
"Erreur : le participant et le spectacle n'appartiennent"
"pas au même tirage"
)
return cleaned_data
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
def paid(self, obj):
return obj.participant.paid
paid.short_description = "A payé"
paid.boolean = True
list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = (
"spectacle__title",
"participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
)
form = AttributionAdminForm
readonly_fields_update = ("spectacle", "participant")
class ChoixSpectacleAdmin(admin.ModelAdmin):
form = ChoixSpectacleAdminForm
def tirage(self, obj):
return obj.participant.tirage
list_display = ("participant", "tirage", "spectacle", "priority", "double_choice")
list_filter = ("double_choice", "participant__tirage")
search_fields = (
"participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
"spectacle__title",
)
class QuoteInline(admin.TabularInline):
model = Quote
class SpectacleAdmin(admin.ModelAdmin):
inlines = [QuoteInline]
model = Spectacle
list_display = ("title", "date", "tirage", "location", "slots", "price", "listing")
list_filter = ("location", "tirage")
search_fields = ("title", "location__name")
readonly_fields = ("rappel_sent",)
class TirageAdmin(admin.ModelAdmin):
model = Tirage
list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage")
readonly_fields = ("tokens",)
list_filter = ("active",)
search_fields = ("title",)
class SalleAdmin(admin.ModelAdmin):
model = Salle
search_fields = ("name", "address")
class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["confirmed_entry"].queryset = Participant.objects.select_related(
"user", "tirage"
)
self.fields["seller"].queryset = Participant.objects.select_related(
"user", "tirage"
)
self.fields["soldTo"].queryset = Participant.objects.select_related(
"user", "tirage"
)
class SpectacleReventeAdmin(admin.ModelAdmin):
"""
Administration des reventes de spectacles
"""
model = SpectacleRevente
def spectacle(self, obj):
"""
Raccourci vers le spectacle associé à la revente.
"""
return obj.attribution.spectacle
list_display = ("spectacle", "seller", "date", "soldTo")
raw_id_fields = ("attribution",)
readonly_fields = ("date_tirage",)
search_fields = [
"attribution__spectacle__title",
"seller__user__username",
"seller__user__first_name",
"seller__user__last_name",
]
actions = ["transfer", "reinit"]
actions_on_bottom = True
form = SpectacleReventeAdminForm
def transfer(self, request, queryset):
"""
Effectue le transfert des reventes pour lesquels on connaît l'acheteur.
"""
reventes = queryset.exclude(soldTo__isnull=True).all()
count = reventes.count()
for revente in reventes:
attrib = revente.attribution
attrib.participant = revente.soldTo
attrib.save()
self.message_user(
request,
"%d attribution%s %s été transférée%s avec succès."
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
)
transfer.short_description = "Transférer les reventes sélectionnées"
def reinit(self, request, queryset):
"""
Réinitialise les reventes.
"""
count = queryset.count()
for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()
):
revente.reset(new_date=timezone.now() - timedelta(hours=1))
self.message_user(
request,
"%d attribution%s %s été réinitialisée%s avec succès."
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
)
reinit.short_description = "Réinitialiser les reventes sélectionnées"
admin.site.register(CategorieSpectacle)
admin.site.register(Spectacle, SpectacleAdmin)
admin.site.register(Salle, SalleAdmin)
admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Attribution, AttributionAdmin)
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
admin.site.register(Tirage, TirageAdmin)
admin.site.register(SpectacleRevente, SpectacleReventeAdmin)

103
bda/algorithm.py Normal file
View file

@ -0,0 +1,103 @@
import random
class Algorithm(object):
shows = None
ranks = None
origranks = None
double = None
def __init__(self, shows, members, choices):
"""Initialisation :
- on aggrège toutes les demandes pour chaque spectacle dans
show.requests
- on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings"""
self.max_group = 2 * max(choice.priority for choice in choices)
self.shows = []
showdict = {}
for show in shows:
show.nrequests = 0
showdict[show] = show
show.requests = []
self.shows.append(show)
self.ranks = {}
self.origranks = {}
self.choices = {}
next_rank = {}
member_shows = {}
for member in members:
self.ranks[member] = {}
self.choices[member] = {}
next_rank[member] = 1
member_shows[member] = {}
for choice in choices:
member = choice.participant
if choice.spectacle in member_shows[member]:
continue
else:
member_shows[member][choice.spectacle] = True
showdict[choice.spectacle].requests.append(member)
showdict[choice.spectacle].nrequests += 2 if choice.double else 1
self.ranks[member][choice.spectacle] = next_rank[member]
next_rank[member] += 2 if choice.double else 1
self.choices[member][choice.spectacle] = choice
for member in members:
self.origranks[member] = dict(self.ranks[member])
def IncrementRanks(self, member, currank, increment=1):
for show in self.ranks[member]:
if self.ranks[member][show] > currank:
self.ranks[member][show] -= increment
def appendResult(self, l, member, show):
l.append(
(
member,
self.ranks[member][show],
self.origranks[member][show],
self.choices[member][show].double,
)
)
def __call__(self, seed):
random.seed(seed)
results = []
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True)
for show in shows:
# On regroupe tous les gens ayant le même rang
groups = dict([(i, []) for i in range(1, self.max_group + 1)])
for member in show.requests:
if self.ranks[member][show] == 0:
raise RuntimeError(member, show.title)
groups[self.ranks[member][show]].append(member)
# On passe à l'attribution
winners = []
losers = []
for i in range(1, self.max_group + 1):
group = list(groups[i])
random.shuffle(group)
for member in group:
if self.choices[member][show].double: # double
if len(winners) + 1 < show.slots:
self.appendResult(winners, member, show)
self.appendResult(winners, member, show)
elif (
not self.choices[member][show].autoquit
and len(winners) < show.slots
):
self.appendResult(winners, member, show)
self.appendResult(losers, member, show)
else:
self.appendResult(losers, member, show)
self.appendResult(losers, member, show)
self.IncrementRanks(member, i, 2)
else: # simple
if len(winners) < show.slots:
self.appendResult(winners, member, show)
else:
self.appendResult(losers, member, show)
self.IncrementRanks(member, i)
results.append((show, winners, losers))
return results

171
bda/forms.py Normal file
View file

@ -0,0 +1,171 @@
from django import forms
from django.forms.models import BaseInlineFormSet
from django.utils import timezone
from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# self.instance is a Participant object
tirage = self.instance.tirage
# set once for all "spectacle" field choices
# - restrict choices to the spectacles of this tirage
# - force_choices avoid many db requests
spectacles = tirage.spectacle_set.select_related("location")
choices = [(sp.pk, str(sp)) for sp in spectacles]
self.force_choices("spectacle", choices)
def force_choices(self, name, choices):
"""Set choices of a field.
As ModelChoiceIterator (default use to get choices of a
ModelChoiceField), it appends an empty selection if requested.
"""
for form in self.forms:
field = form.fields[name]
if field.empty_label is not None:
field.choices = [("", field.empty_label)] + choices
else:
field.choices = choices
class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea())
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return str(obj.spectacle)
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, *args, own=True, **kwargs):
super().__init__(*args, **kwargs)
self.own = own
def label_from_instance(self, obj):
label = "{show}{suffix}"
suffix = ""
if self.own:
# C'est notre propre revente : pas besoin de spécifier le vendeur
if obj.soldTo is not None:
suffix = " -- Vendue à {firstname} {lastname}".format(
firstname=obj.soldTo.user.first_name,
lastname=obj.soldTo.user.last_name,
)
else:
# Ce n'est pas à nous : on ne voit jamais l'acheteur
suffix = " -- Vendue par {firstname} {lastname}".format(
firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name
)
return label.format(show=str(obj.attribution.spectacle), suffix=suffix)
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["attributions"].queryset = (
participant.attribution_set.filter(spectacle__date__gte=timezone.now())
.exclude(revente__seller=participant)
.select_related("spectacle", "spectacle__location", "participant__user")
)
class AnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=True,
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = participant.original_shows.filter(
attribution__spectacle__date__gte=timezone.now(),
notif_sent=False,
soldTo__isnull=True,
).select_related("attribution__spectacle", "attribution__spectacle__location")
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, tirage, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["spectacles"].queryset = tirage.spectacle_set.select_related(
"location"
).filter(date__gte=timezone.now())
class ReventeTirageAnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
label="",
queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = participant.entered.filter(
soldTo__isnull=True
).select_related("attribution__spectacle", "seller__user")
class ReventeTirageForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
label="",
queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = (
SpectacleRevente.objects.filter(
notif_sent=True, shotgun=False, tirage_done=False
)
.exclude(confirmed_entry=participant)
.select_related("attribution__spectacle")
)
class SoldForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=True,
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = (
participant.original_shows.filter(soldTo__isnull=False)
.exclude(soldTo=participant)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
)
)

View file

View file

@ -0,0 +1,101 @@
"""
Crée deux tirages de test et y inscrit les utilisateurs
"""
import os
import random
from django.contrib.auth.models import User
from django.utils import timezone
from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage
from bda.views import do_tirage
from gestioncof.management.base import MyBaseCommand
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
class Command(MyBaseCommand):
help = "Crée deux tirages de test et y inscrit les utilisateurs."
def handle(self, *args, **options):
# ---
# Tirages
# ---
Tirage.objects.all().delete()
Tirage.objects.bulk_create(
[
Tirage(
title="Tirage de test 1",
ouverture=timezone.now() - timezone.timedelta(days=7),
fermeture=timezone.now(),
active=True,
),
Tirage(
title="Tirage de test 2",
ouverture=timezone.now(),
fermeture=timezone.now() + timezone.timedelta(days=60),
active=True,
),
]
)
tirages = Tirage.objects.all()
# ---
# Salles
# ---
locations = self.from_json("locations.json", DATA_DIR, Salle)
# ---
# Spectacles
# ---
def show_callback(show):
"""
Assigne un tirage, une date et un lieu à un spectacle et décide si
les places sont sur listing.
"""
show.tirage = random.choice(tirages)
show.listing = bool(random.randint(0, 1))
show.date = show.tirage.fermeture + timezone.timedelta(
days=random.randint(60, 90)
)
show.location = random.choice(locations)
return show
shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback)
# ---
# Inscriptions
# ---
self.stdout.write("Inscription des utilisateurs aux tirages")
ChoixSpectacle.objects.all().delete()
choices = []
for user in User.objects.filter(profile__is_cof=True):
for tirage in tirages:
part, _ = Participant.objects.get_or_create(user=user, tirage=tirage)
shows = random.sample(
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
)
for (rank, show) in enumerate(shows):
choices.append(
ChoixSpectacle(
participant=part,
spectacle=show,
priority=rank + 1,
double_choice=random.choice(["1", "double", "autoquit"]),
)
)
ChoixSpectacle.objects.bulk_create(choices)
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))
# ---
# On lance le premier tirage
# ---
self.stdout.write("Lancement du premier tirage")
do_tirage(tirages[0], "dummy_token")

View file

@ -0,0 +1,49 @@
"""
Gestion en ligne de commande des reventes.
"""
from django.core.management import BaseCommand
from django.utils import timezone
from bda.models import SpectacleRevente
class Command(BaseCommand):
help = (
"Envoie les mails de notification et effectue les tirages au sort des reventes"
)
leave_locale_alone = True
def handle(self, *args, **options):
now = timezone.now()
reventes = SpectacleRevente.objects.all()
for revente in reventes:
# Le spectacle est bientôt et on a pas encore envoyé de mail :
# on met la place au shotgun et on prévient.
if revente.is_urgent and not revente.notif_sent:
if revente.can_notif:
self.stdout.write(str(now))
revente.mail_shotgun()
self.stdout.write(
"Mails de disponibilité immédiate envoyés "
"pour la revente [%s]" % revente
)
# Le spectacle est dans plus longtemps : on prévient
elif revente.can_notif and not revente.notif_sent:
self.stdout.write(str(now))
revente.send_notif()
self.stdout.write(
"Mails d'inscription à la revente [%s] envoyés" % revente
)
# On fait le tirage
elif now >= revente.date_tirage and not revente.tirage_done:
self.stdout.write(str(now))
winner = revente.tirage()
self.stdout.write("Tirage effectué pour la revente [%s]" % revente)
if winner:
self.stdout.write("Gagnant : %s" % winner.user)
else:
self.stdout.write("Pas de gagnant ; place au shotgun")

View file

@ -0,0 +1,33 @@
"""
Gestion en ligne de commande des mails de rappel.
"""
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from bda.models import Spectacle
class Command(BaseCommand):
help = (
"Envoie les mails de rappel des spectacles dont la date approche.\n"
"Ne renvoie pas les mails déjà envoyés."
)
leave_locale_alone = True
def handle(self, *args, **options):
now = timezone.now()
delay = timedelta(days=4)
shows = (
Spectacle.objects.filter(date__range=(now, now + delay))
.filter(tirage__active=True)
.filter(rappel_sent__isnull=True)
.all()
)
for show in shows:
show.send_rappel()
self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show)
if not shows:
self.stdout.write("Aucun mail à envoyer.")

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,206 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name="Attribution",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("given", models.BooleanField(default=False, verbose_name="Donn\xe9e")),
],
),
migrations.CreateModel(
name="ChoixSpectacle",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"priority",
models.PositiveIntegerField(verbose_name=b"Priorit\xc3\xa9"),
),
(
"double_choice",
models.CharField(
default=b"1",
max_length=10,
verbose_name=b"Nombre de places",
choices=[
(b"1", b"1 place"),
(b"autoquit", b"2 places si possible, 1 sinon"),
(b"double", b"2 places sinon rien"),
],
),
),
],
options={
"ordering": ("priority",),
"verbose_name": "voeu",
"verbose_name_plural": "voeux",
},
),
migrations.CreateModel(
name="Participant",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("paid", models.BooleanField(default=False, verbose_name="A pay\xe9")),
(
"paymenttype",
models.CharField(
blank=True,
max_length=6,
verbose_name="Moyen de paiement",
choices=[
(b"cash", "Cash"),
(b"cb", b"CB"),
(b"cheque", "Ch\xe8que"),
(b"autre", "Autre"),
],
),
),
],
),
migrations.CreateModel(
name="Salle",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=300, verbose_name=b"Nom")),
("address", models.TextField(verbose_name=b"Adresse")),
],
),
migrations.CreateModel(
name="Spectacle",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("title", models.CharField(max_length=300, verbose_name=b"Titre")),
("date", models.DateTimeField(verbose_name=b"Date & heure")),
(
"description",
models.TextField(verbose_name=b"Description", blank=True),
),
(
"slots_description",
models.TextField(
verbose_name=b"Description des places", blank=True
),
),
(
"price",
models.FloatField(verbose_name=b"Prix d'une place", blank=True),
),
("slots", models.IntegerField(verbose_name=b"Places")),
(
"priority",
models.IntegerField(default=1000, verbose_name=b"Priorit\xc3\xa9"),
),
(
"location",
models.ForeignKey(to="bda.Salle", on_delete=models.CASCADE),
),
],
options={
"ordering": ("priority", "date", "title"),
"verbose_name": "Spectacle",
},
),
migrations.AddField(
model_name="participant",
name="attributions",
field=models.ManyToManyField(
related_name="attributed_to",
through="bda.Attribution",
to="bda.Spectacle",
),
),
migrations.AddField(
model_name="participant",
name="choices",
field=models.ManyToManyField(
related_name="chosen_by",
through="bda.ChoixSpectacle",
to="bda.Spectacle",
),
),
migrations.AddField(
model_name="participant",
name="user",
field=models.OneToOneField(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
migrations.AddField(
model_name="choixspectacle",
name="participant",
field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
),
migrations.AddField(
model_name="choixspectacle",
name="spectacle",
field=models.ForeignKey(
related_name="participants",
to="bda.Spectacle",
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name="attribution",
name="participant",
field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
),
migrations.AddField(
model_name="attribution",
name="spectacle",
field=models.ForeignKey(
related_name="attribues", to="bda.Spectacle", on_delete=models.CASCADE
),
),
migrations.AlterUniqueTogether(
name="choixspectacle", unique_together=set([("participant", "spectacle")])
),
]

View file

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
from django.utils import timezone
def fill_tirage_fields(apps, schema_editor):
"""
Create a `Tirage` to fill new field `tirage` of `Participant`
and `Spectacle` already existing.
"""
Participant = apps.get_model("bda", "Participant")
Spectacle = apps.get_model("bda", "Spectacle")
Tirage = apps.get_model("bda", "Tirage")
# These querysets only contains instances not linked to any `Tirage`.
participants = Participant.objects.filter(tirage=None)
spectacles = Spectacle.objects.filter(tirage=None)
if not participants.count() and not spectacles.count():
# No need to create a "trash" tirage.
return
tirage = Tirage.objects.create(
title="Tirage de test (migration)",
active=False,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
participants.update(tirage=tirage)
spectacles.update(tirage=tirage)
class Migration(migrations.Migration):
dependencies = [("bda", "0001_initial")]
operations = [
migrations.CreateModel(
name="Tirage",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("title", models.CharField(max_length=300, verbose_name=b"Titre")),
(
"ouverture",
models.DateTimeField(
verbose_name=b"Date et heure d'ouverture du tirage"
),
),
(
"fermeture",
models.DateTimeField(
verbose_name=b"Date et heure de fermerture du tirage"
),
),
(
"token",
models.TextField(verbose_name=b"Graine du tirage", blank=True),
),
(
"active",
models.BooleanField(default=True, verbose_name=b"Tirage actif"),
),
],
),
migrations.AlterField(
model_name="participant",
name="user",
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
# Create fields `spectacle` for `Participant` and `Spectacle` models.
# These fields are not nullable, but we first create them as nullable
# to give a default value for existing instances of these models.
migrations.AddField(
model_name="participant",
name="tirage",
field=models.ForeignKey(
to="bda.Tirage", null=True, on_delete=models.CASCADE
),
),
migrations.AddField(
model_name="spectacle",
name="tirage",
field=models.ForeignKey(
to="bda.Tirage", null=True, on_delete=models.CASCADE
),
),
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
migrations.AlterField(
model_name="participant",
name="tirage",
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
),
migrations.AlterField(
model_name="spectacle",
name="tirage",
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0002_add_tirage")]
operations = [
migrations.AlterField(
model_name="spectacle",
name="price",
field=models.FloatField(verbose_name=b"Prix d'une place"),
),
migrations.AlterField(
model_name="tirage",
name="active",
field=models.BooleanField(default=False, verbose_name=b"Tirage actif"),
),
]

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0003_update_tirage_and_spectacle")]
operations = [
migrations.AddField(
model_name="spectacle",
name="listing",
field=models.BooleanField(
default=False, verbose_name=b"Les places sont sur listing"
),
preserve_default=False,
),
migrations.AddField(
model_name="spectacle",
name="rappel_sent",
field=models.DateTimeField(
null=True, verbose_name=b"Mail de rappel envoy\xc3\xa9", blank=True
),
),
]

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,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.utils import timezone
def forwards_func(apps, schema_editor):
Tirage = apps.get_model("bda", "Tirage")
db_alias = schema_editor.connection.alias
for tirage in Tirage.objects.using(db_alias).all():
if tirage.tokens:
tirage.tokens = 'Before %s\n"""%s"""\n' % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
tirage.tokens,
)
tirage.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0005_encoding")]
operations = [
migrations.RenameField("tirage", "token", "tokens"),
migrations.AddField(
model_name="tirage",
name="enable_do_tirage",
field=models.BooleanField(
default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9"
),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0006_add_tirage_switch")]
operations = [
migrations.CreateModel(
name="CategorieSpectacle",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"name",
models.CharField(max_length=100, verbose_name="Nom", unique=True),
),
],
options={"verbose_name": "Cat\xe9gorie"},
),
migrations.CreateModel(
name="Quote",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("text", models.TextField(verbose_name="Citation")),
("author", models.CharField(max_length=200, verbose_name="Auteur")),
],
),
migrations.AlterModelOptions(
name="spectacle",
options={"ordering": ("date", "title"), "verbose_name": "Spectacle"},
),
migrations.RemoveField(model_name="spectacle", name="priority"),
migrations.AddField(
model_name="spectacle",
name="ext_link",
field=models.CharField(
max_length=500,
verbose_name="Lien vers le site du spectacle",
blank=True,
),
),
migrations.AddField(
model_name="spectacle",
name="image",
field=models.ImageField(
upload_to="imgs/shows/", null=True, verbose_name="Image", blank=True
),
),
migrations.AlterField(
model_name="tirage",
name="enable_do_tirage",
field=models.BooleanField(
default=False, verbose_name="Le tirage peut \xeatre lanc\xe9"
),
),
migrations.AlterField(
model_name="tirage",
name="tokens",
field=models.TextField(verbose_name="Graine(s) du tirage", blank=True),
),
migrations.AddField(
model_name="spectacle",
name="category",
field=models.ForeignKey(
blank=True,
to="bda.CategorieSpectacle",
on_delete=models.CASCADE,
null=True,
),
),
migrations.AddField(
model_name="spectacle",
name="vips",
field=models.TextField(verbose_name="Personnalit\xe9s", blank=True),
),
migrations.AddField(
model_name="quote",
name="spectacle",
field=models.ForeignKey(to="bda.Spectacle", on_delete=models.CASCADE),
),
]

110
bda/migrations/0008_py3.py Normal file
View file

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0007_extends_spectacle")]
operations = [
migrations.AlterField(
model_name="choixspectacle",
name="double_choice",
field=models.CharField(
verbose_name="Nombre de places",
choices=[
("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"),
("double", "2 places sinon rien"),
],
max_length=10,
default="1",
),
),
migrations.AlterField(
model_name="participant",
name="paymenttype",
field=models.CharField(
blank=True,
choices=[
("cash", "Cash"),
("cb", "CB"),
("cheque", "Chèque"),
("autre", "Autre"),
],
max_length=6,
verbose_name="Moyen de paiement",
),
),
migrations.AlterField(
model_name="salle",
name="address",
field=models.TextField(verbose_name="Adresse"),
),
migrations.AlterField(
model_name="salle",
name="name",
field=models.CharField(verbose_name="Nom", max_length=300),
),
migrations.AlterField(
model_name="spectacle",
name="date",
field=models.DateTimeField(verbose_name="Date & heure"),
),
migrations.AlterField(
model_name="spectacle",
name="description",
field=models.TextField(verbose_name="Description", blank=True),
),
migrations.AlterField(
model_name="spectacle",
name="listing",
field=models.BooleanField(verbose_name="Les places sont sur listing"),
),
migrations.AlterField(
model_name="spectacle",
name="price",
field=models.FloatField(verbose_name="Prix d'une place"),
),
migrations.AlterField(
model_name="spectacle",
name="slots",
field=models.IntegerField(verbose_name="Places"),
),
migrations.AlterField(
model_name="spectacle",
name="slots_description",
field=models.TextField(verbose_name="Description des places", blank=True),
),
migrations.AlterField(
model_name="spectacle",
name="title",
field=models.CharField(verbose_name="Titre", max_length=300),
),
migrations.AlterField(
model_name="tirage",
name="active",
field=models.BooleanField(verbose_name="Tirage actif", default=False),
),
migrations.AlterField(
model_name="tirage",
name="fermeture",
field=models.DateTimeField(
verbose_name="Date et heure de fermerture du tirage"
),
),
migrations.AlterField(
model_name="tirage",
name="ouverture",
field=models.DateTimeField(
verbose_name="Date et heure d'ouverture du tirage"
),
),
migrations.AlterField(
model_name="tirage",
name="title",
field=models.CharField(verbose_name="Titre", max_length=300),
),
]

View file

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0008_py3")]
operations = [
migrations.CreateModel(
name="SpectacleRevente",
fields=[
(
"id",
models.AutoField(
serialize=False,
primary_key=True,
auto_created=True,
verbose_name="ID",
),
),
(
"date",
models.DateTimeField(
verbose_name="Date de mise en vente",
default=django.utils.timezone.now,
),
),
(
"notif_sent",
models.BooleanField(
verbose_name="Notification envoyée", default=False
),
),
(
"tirage_done",
models.BooleanField(verbose_name="Tirage effectué", default=False),
),
],
options={"verbose_name": "Revente"},
),
migrations.AddField(
model_name="participant",
name="choicesrevente",
field=models.ManyToManyField(
to="bda.Spectacle", related_name="subscribed", blank=True
),
),
migrations.AddField(
model_name="spectaclerevente",
name="answered_mail",
field=models.ManyToManyField(
to="bda.Participant", related_name="wanted", blank=True
),
),
migrations.AddField(
model_name="spectaclerevente",
name="attribution",
field=models.OneToOneField(
to="bda.Attribution", on_delete=models.CASCADE, related_name="revente"
),
),
migrations.AddField(
model_name="spectaclerevente",
name="seller",
field=models.ForeignKey(
to="bda.Participant",
on_delete=models.CASCADE,
verbose_name="Vendeur",
related_name="original_shows",
),
),
migrations.AddField(
model_name="spectaclerevente",
name="soldTo",
field=models.ForeignKey(
to="bda.Participant",
on_delete=models.CASCADE,
verbose_name="Vendue à",
null=True,
blank=True,
),
),
]

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import timedelta
from django.db import migrations, models
from django.utils import timezone
def forwards_func(apps, schema_editor):
SpectacleRevente = apps.get_model("bda", "SpectacleRevente")
for revente in SpectacleRevente.objects.all():
is_expired = timezone.now() > revente.date_tirage()
is_direct = revente.attribution.spectacle.date >= revente.date and timezone.now() > revente.date + timedelta(
minutes=15
)
revente.shotgun = is_expired or is_direct
revente.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0009_revente")]
operations = [
migrations.AddField(
model_name="spectaclerevente",
name="shotgun",
field=models.BooleanField(
default=False, verbose_name="Disponible imm\xe9diatement"
),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]

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 = [("bda", "0010_spectaclerevente_shotgun")]
operations = [
migrations.AddField(
model_name="tirage",
name="appear_catalogue",
field=models.BooleanField(
default=False, verbose_name="Tirage à afficher dans le catalogue"
),
)
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [
migrations.RenameField(
model_name="spectaclerevente",
old_name="answered_mail",
new_name="confirmed_entry",
),
migrations.AlterField(
model_name="spectaclerevente",
name="confirmed_entry",
field=models.ManyToManyField(
blank=True, related_name="entered", to="bda.Participant"
),
),
migrations.AddField(
model_name="spectaclerevente",
name="notif_time",
field=models.DateTimeField(
blank=True, verbose_name="Moment d'envoi de la notification", null=True
),
),
]

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def swap_double_choice(apps, schema_editor):
choices = apps.get_model("bda", "ChoixSpectacle").objects
choices.filter(double_choice="double").update(double_choice="tmp")
choices.filter(double_choice="autoquit").update(double_choice="double")
choices.filter(double_choice="tmp").update(double_choice="autoquit")
class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [
# Temporarily allow an extra "tmp" value for the `double_choice` field
migrations.AlterField(
model_name="choixspectacle",
name="double_choice",
field=models.CharField(
verbose_name="Nombre de places",
max_length=10,
default="1",
choices=[
("tmp", "tmp"),
("1", "1 place"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
],
),
),
migrations.RunPython(swap_double_choice, migrations.RunPython.noop),
migrations.AlterField(
model_name="choixspectacle",
name="double_choice",
field=models.CharField(
verbose_name="Nombre de places",
max_length=10,
default="1",
choices=[
("1", "1 place"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
],
),
),
]

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-05-24 19:23
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
operations = []

View file

450
bda/models.py Normal file
View file

@ -0,0 +1,450 @@
import calendar
import random
from datetime import timedelta
from custommail.models import CustomMail
from custommail.shortcuts import send_mass_custom_mail
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core import mail
from django.db import models
from django.db.models import Count
from django.utils import formats, timezone
def get_generic_user():
generic, _ = User.objects.get_or_create(
username="bda_generic",
defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"},
)
return generic
class Tirage(models.Model):
title = models.CharField("Titre", max_length=300)
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
tokens = models.TextField("Graine(s) du tirage", blank=True)
active = models.BooleanField("Tirage actif", default=False)
appear_catalogue = models.BooleanField(
"Tirage à afficher dans le catalogue", default=False
)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
def __str__(self):
return "%s - %s" % (
self.title,
formats.localize(timezone.template_localtime(self.fermeture)),
)
class Salle(models.Model):
name = models.CharField("Nom", max_length=300)
address = models.TextField("Adresse")
def __str__(self):
return self.name
class CategorieSpectacle(models.Model):
name = models.CharField("Nom", max_length=100, unique=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Catégorie"
class Spectacle(models.Model):
title = models.CharField("Titre", max_length=300)
category = models.ForeignKey(
CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True
)
date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle, on_delete=models.CASCADE)
vips = models.TextField("Personnalités", blank=True)
description = models.TextField("Description", blank=True)
slots_description = models.TextField("Description des places", blank=True)
image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/")
ext_link = models.CharField(
"Lien vers le site du spectacle", blank=True, max_length=500
)
price = models.FloatField("Prix d'une place")
slots = models.IntegerField("Places")
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
listing = models.BooleanField("Les places sont sur listing")
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True)
class Meta:
verbose_name = "Spectacle"
ordering = ("date", "title")
def timestamp(self):
return "%d" % calendar.timegm(self.date.utctimetuple())
def __str__(self):
return "%s - %s, %s, %.02f" % (
self.title,
formats.localize(timezone.template_localtime(self.date)),
self.location,
self.price,
)
def getImgUrl(self):
"""
Cette fonction permet d'obtenir l'URL de l'image, si elle existe
"""
try:
return self.image.url
except Exception:
return None
def send_rappel(self):
"""
Envoie un mail de rappel à toutes les personnes qui ont une place pour
ce spectacle.
"""
# On récupère la liste des participants + le BdA
members = list(
User.objects.filter(participant__attributions=self)
.annotate(nb_attr=Count("id"))
.order_by()
)
bda_generic = get_generic_user()
bda_generic.nb_attr = 1
members.append(bda_generic)
# On écrit un mail personnalisé à chaque participant
datatuple = [
(
"bda-rappel",
{"member": member, "nb_attr": member.nb_attr, "show": self},
settings.MAIL_DATA["rappels"]["FROM"],
[member.email],
)
for member in members
]
send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now()
self.save()
# On renvoie la liste des destinataires
return members
@property
def is_past(self):
return self.date < timezone.now()
class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
text = models.TextField("Citation")
author = models.CharField("Auteur", max_length=200)
PAYMENT_TYPES = (
("cash", "Cash"),
("cb", "CB"),
("cheque", "Chèque"),
("autre", "Autre"),
)
class Participant(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(
Spectacle, through="ChoixSpectacle", related_name="chosen_by"
)
attributions = models.ManyToManyField(
Spectacle, through="Attribution", related_name="attributed_to"
)
paid = models.BooleanField("A payé", default=False)
paymenttype = models.CharField(
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
)
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(
Spectacle, related_name="subscribed", blank=True
)
def __str__(self):
return "%s - %s" % (self.user, self.tirage.title)
DOUBLE_CHOICES = (
("1", "1 place"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
)
class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="participants"
)
priority = models.PositiveIntegerField("Priorité")
double_choice = models.CharField(
"Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10
)
def get_double(self):
return self.double_choice != "1"
double = property(get_double)
def get_autoquit(self):
return self.double_choice == "autoquit"
autoquit = property(get_autoquit)
def __str__(self):
return "Vœux de %s pour %s" % (
self.participant.user.get_full_name(),
self.spectacle.title,
)
class Meta:
ordering = ("priority",)
unique_together = (("participant", "spectacle"),)
verbose_name = "voeu"
verbose_name_plural = "voeux"
class Attribution(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="attribues"
)
given = models.BooleanField("Donnée", default=False)
def __str__(self):
return "%s -- %s, %s" % (
self.participant.user,
self.spectacle.title,
self.spectacle.date,
)
class SpectacleRevente(models.Model):
attribution = models.OneToOneField(
Attribution, on_delete=models.CASCADE, related_name="revente"
)
date = models.DateTimeField("Date de mise en vente", default=timezone.now)
confirmed_entry = models.ManyToManyField(
Participant, related_name="entered", blank=True
)
seller = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
verbose_name="Vendeur",
related_name="original_shows",
)
soldTo = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
verbose_name="Vendue à",
blank=True,
null=True,
)
notif_sent = models.BooleanField("Notification envoyée", default=False)
notif_time = models.DateTimeField(
"Moment d'envoi de la notification", blank=True, null=True
)
tirage_done = models.BooleanField("Tirage effectué", default=False)
shotgun = models.BooleanField("Disponible immédiatement", default=False)
####
# Some class attributes
###
# TODO : settings ?
# Temps minimum entre le tirage et le spectacle
min_margin = timedelta(days=5)
# Temps entre la création d'une revente et l'envoi du mail
remorse_time = timedelta(hours=1)
# Temps min/max d'attente avant le tirage
max_wait_time = timedelta(days=3)
min_wait_time = timedelta(days=1)
@property
def real_notif_time(self):
if self.notif_time:
return self.notif_time
else:
return self.date + self.remorse_time
@property
def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente."""
remaining_time = (
self.attribution.spectacle.date - self.real_notif_time - self.min_margin
)
delay = min(remaining_time, self.max_wait_time)
return self.real_notif_time + delay
@property
def is_urgent(self):
"""
Renvoie True iff la revente doit être mise au shotgun directement.
Plus précisément, on doit avoir min_margin + min_wait_time de marge.
"""
spectacle_date = self.attribution.spectacle.date
return spectacle_date <= timezone.now() + self.min_margin + self.min_wait_time
@property
def can_notif(self):
return timezone.now() >= self.date + self.remorse_time
def __str__(self):
return "%s -- %s" % (self.seller, self.attribution.spectacle.title)
class Meta:
verbose_name = "Revente"
def reset(self, new_date=timezone.now()):
"""Réinitialise la revente pour permettre une remise sur le marché"""
self.seller = self.attribution.participant
self.date = new_date
self.confirmed_entry.clear()
self.soldTo = None
self.notif_sent = False
self.notif_time = None
self.tirage_done = False
self.shotgun = False
self.save()
def send_notif(self):
"""
Envoie une notification pour indiquer la mise en vente d'une place sur
BdA-Revente à tous les intéressés.
"""
inscrits = self.attribution.spectacle.subscribed.select_related("user")
datatuple = [
(
"bda-revente",
{
"member": participant.user,
"show": self.attribution.spectacle,
"revente": self,
"site": Site.objects.get_current(),
},
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
for participant in inscrits
]
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
self.save()
def mail_shotgun(self):
"""
Envoie un mail à toutes les personnes intéréssées par le spectacle pour
leur indiquer qu'il est désormais disponible au shotgun.
"""
inscrits = self.attribution.spectacle.subscribed.select_related("user")
datatuple = [
(
"bda-shotgun",
{
"member": participant.user,
"show": self.attribution.spectacle,
"site": Site.objects.get_current(),
},
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
for participant in inscrits
]
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True
self.shotgun = True
self.save()
def tirage(self, send_mails=True):
"""
Lance le tirage au sort associé à la revente. Un gagnant est choisi
parmis les personnes intéressées par le spectacle. Les personnes sont
ensuites prévenues par mail du résultat du tirage.
"""
inscrits = list(self.confirmed_entry.all())
spectacle = self.attribution.spectacle
seller = self.seller
winner = None
if inscrits:
# Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits)
self.soldTo = winner
if send_mails:
mails = []
context = {
"acheteur": winner.user,
"vendeur": seller.user,
"show": spectacle,
}
c_mails_qs = CustomMail.objects.filter(
shortname__in=[
"bda-revente-winner",
"bda-revente-loser",
"bda-revente-seller",
]
)
c_mails = {cm.shortname: cm for cm in c_mails_qs}
mails.append(
c_mails["bda-revente-winner"].get_message(
context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[winner.user.email],
)
)
mails.append(
c_mails["bda-revente-seller"].get_message(
context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[seller.user.email],
reply_to=[winner.user.email],
)
)
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context["acheteur"] = inscrit.user
mails.append(
c_mails["bda-revente-loser"].get_message(
new_context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[inscrit.user.email],
)
)
mail_conn = mail.get_connection()
mail_conn.send_messages(mails)
# Si personne ne veut de la place, elle part au shotgun
else:
self.shotgun = True
self.tirage_done = True
self.save()
return winner

48
bda/static/css/bda.css Normal file
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,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,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,123 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block extra_head %}
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}
<script type="text/javascript">
var django = {
"jQuery": jQuery.noConflict(true)
};
(function($) {
cloneMore = function(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
// Cloning <select> element doesn't properly propagate the default
// selected <option>, so we set it manually.
newElement.find('select').each(function (index, select) {
var defaultValue = $(select).find('option[selected]').val();
if (typeof defaultValue !== 'undefined') {
$(select).val(defaultValue);
}
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
deleteButtonHandler = function(elem) {
elem.bind("click", function() {
var deleteInput = $(this).prev().prev(),
form = $(this).parents(".dynamic-form").first();
// callback
// toggle options.predeleteCssClass and toggle checkbox
if (form.hasClass("has_original")) {
form.toggleClass("predelete");
if (deleteInput.attr("checked")) {
deleteInput.attr("checked", false);
} else {
deleteInput.attr("checked", true);
}
}
// callback
});
};
$(document).ready(function($) {
deleteButtonHandler($("table#bda_formset tbody.bda_formset_content").find("a.remove-btn"));
$("table#bda_formset tbody.bda_formset_content").sortable({
handle: "a.drag-btn",
items: "tr",
axis: "y",
appendTo: 'body',
forceHelperSize: true,
placeholder: 'ui-sortable-placeholder',
forcePlaceholderSize: true,
containment: 'form#bda_form',
tolerance: 'pointer',
start: function(evt, ui) {
var template = "",
len = ui.item.children("td").length;
for (var i = 0; i < len; i++) {
template += "<td style='height:" + (ui.item.outerHeight() + 12 ) + "px' class='placeholder-cell'>&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,48 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Mails de rappels</h2>
{% if sent %}
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
<ul>
{% for member in members %}
<li>{{ member.get_full_name }} ({{ member.email }})</li>
{% endfor %}
</ul>
{% else %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }}&nbsp;?</h3>
{% endif %}
<div class="empty-form">
{% if not sent %}
<form action="" method="post">
{% csrf_token %}
<div class="pull-right">
<input class="btn btn-primary" type="submit" value="Envoyer" />
</div>
</form>
{% endif %}
</div>
<hr \>
<p>
<em>Note :</em> le template de ce mail peut être modifié à
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
</p>
<hr \>
<h3>Forme des mails</h3>
<h4>Une seule place</h4>
{% for part in exemple_mail_1place %}
<pre>{{ part }}</pre>
{% endfor %}
<h4>Deux places</h4>
{% for part in exemple_mail_2places %}
<pre>{{ part }}</pre>
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,73 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>{{ spectacle }}</h2>
<table class='table table-striped etat-bda'>
<thead>
<tr>
<th data-sort="string">Nom</th>
<th data-sort="int">Places</th>
<th data-sort="string">Adresse Mail</th>
<th data-sort="string">Payé</th>
<th data-sort="string">Donné</th>
</tr>
</thead>
<tbody>
{% for participant in participants %}
<tr>
<td data-sort-value="{{ participan.name}}">{{participant.name}}</td>
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
<td data-sort-value="{{participant.email}}">{{participant.email}}</td>
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
{% if participant.paid %}Oui{% else %}Non{%endif%}
</td>
<td data-sort-value="{{participant.given}}" class={%if participant.given == participant.nb_places %}"greenratio"
{%elif participant.given == 0%}"redratio"
{%else%}"orangeratio"
{%endif%}>
{% if participant.given == participant.nb_places %}Oui
{% elif participant.given == 0 %}Non
{% else %}{{participant.given}}/{{participant.nb_places}}
{%endif%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
<div>
<div>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
<pre id="export-mails" style="display:none">{% spaceless %}
{% for participant in participants %}{{ participant.email }}, {% endfor %}
{% endspaceless %}</pre>
</div>
<div>
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
<pre id="export-salle" style="display:none">{% spaceless %}
{% for participant in participants %}{{ participant.name }} : {{ participant.nb_places }} place{{ participant.nb_places|pluralize }}
{% endfor %}
{% endspaceless %}</pre>
</div>
<div>
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
</div>
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script>
function toggle(id) {
var pre = document.getElementById(id) ;
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
}
</script>
<script type="text/javascript">
$(function(){
$("table.etat-bda").stupidtable();
});
</script>
{% endblock %}

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,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,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,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,90 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Gestion des places que je revends</h2>
{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %}
{% if resellform.attributions %}
<br />
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les places que vous souhaitez revendre, et validez. Vous aurez
ensuite 1h pour changer d'avis avant que la revente soit confirmée et
que les notifications soient envoyées aux intéressé·e·s.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ resellform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
<hr />
{% endif %}
{% if annul_reventes or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% if annul_reventes %}
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez annuler les places mises en vente il y a moins d'une heure.
</div>
{% endif %}
{% csrf_token %}
<div class='form-group'>
<div class='multiple-checkbox'>
<ul>
{% for revente in annul_reventes %}
<li>{{ revente.tag }} {{ revente.choice_label }}</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{ attrib.spectacle }}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if annul_reventes %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
<hr />
{% endif %}
{% if sold_reventes %}
<h3>Places revendues</h3>
<form action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Pour chaque revente, vous devez soit l'annuler soit la confirmer pour
transférer la place la place à la personne tirée au sort.
L'annulation sert par exemple à pouvoir remettre la place en jeu si
vous ne parvenez pas à entrer en contact avec la personne tirée au
sort.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ soldform|bootstrap }}
</div>
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_attributions and not annul_attributions and not overdue and not sold_reventes %}
<p>Plus de reventes possibles !</p>
{% endif %}
{% endwith %}
{% endblock %}

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,14 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Places disponibles immédiatement</h2>
{% if shotgun %}
<ul class="list-unstyled">
{% for spectacle in shotgun %}
<li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %}
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,46 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les spectacles pour lesquels vous souhaitez recevoir un
notification quand une place est disponible en revente. <br />
Lorsque vous validez vos choix, si un tirage au sort est en cours pour
un des spectacles que vous avez sélectionné, vous serez automatiquement
inscrit à ce tirage.
</div>
<br />
{% csrf_token %}
<div class="form-group">
<button type="button"
class="btn btn-primary"
onClick="select(true)">Tout sélectionner</button>
<button type="button"
class="btn btn-primary"
onClick="select(false)">Tout désélectionner</button>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{ checkbox }}</li>
{% endfor %}
</ul>
</div>
</div>
<input type="submit"
class="btn btn-primary"
value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length; i < n; i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -0,0 +1,52 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Tirages au sort de reventes</h2>
{% if annulform.reventes %}
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous désinscrire des reventes suivantes tant que le tirage n'a
pas eu lieu.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ annulform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="annul"
value="Se désinscrire des tirages sélectionnés">
</div>
</form>
<hr />
{% endif %}
{% if subform.reventes %}
<h3>Tirages en cours</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous inscrire aux tirage en cours suivants.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ subform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="subscribe"
value="S'inscrire aux tirages sélectionnés">
</div>
</form>
{% endif %}
{% endblock %}

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-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
{% else %}
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
{% endif %}
{% endblock %}

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

0
bda/tests/__init__.py Normal file
View file

102
bda/tests/test_models.py Normal file
View file

@ -0,0 +1,102 @@
from datetime import timedelta
from unittest import mock
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import TestCase
from django.utils import timezone
from bda.models import (
Attribution,
Participant,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
User = get_user_model()
class SpectacleReventeTests(TestCase):
fixtures = ["gestioncof/management/data/custommail.json"]
def setUp(self):
now = timezone.now()
self.t = Tirage.objects.create(
title="Tirage",
ouverture=now - timedelta(days=7),
fermeture=now - timedelta(days=3),
active=True,
)
self.s = Spectacle.objects.create(
title="Spectacle",
date=now + timedelta(days=20),
location=Salle.objects.create(name="Salle", address="Address"),
price=10.5,
slots=5,
tirage=self.t,
listing=False,
)
self.seller = Participant.objects.create(
user=User.objects.create(username="seller", email="seller@mail.net"),
tirage=self.t,
)
self.p1 = Participant.objects.create(
user=User.objects.create(username="part1", email="part1@mail.net"),
tirage=self.t,
)
self.p2 = Participant.objects.create(
user=User.objects.create(username="part2", email="part2@mail.net"),
tirage=self.t,
)
self.p3 = Participant.objects.create(
user=User.objects.create(username="part3", email="part3@mail.net"),
tirage=self.t,
)
self.attr = Attribution.objects.create(
participant=self.seller, spectacle=self.s
)
self.rev = SpectacleRevente.objects.create(
attribution=self.attr, seller=self.seller
)
def test_tirage(self):
revente = self.rev
wanted_by = [self.p1, self.p2, self.p3]
revente.confirmed_entry = wanted_by
with mock.patch("bda.models.random.choice") as mc:
# Set winner to self.p1.
mc.return_value = self.p1
revente.tirage()
# Call to random.choice used participants in wanted_by.
mc_args, _ = mc.call_args
self.assertEqual(set(mc_args[0]), set(wanted_by))
self.assertEqual(revente.soldTo, self.p1)
self.assertTrue(revente.tirage_done)
mails = {m.to[0]: m for m in mail.outbox}
self.assertEqual(len(mails), 4)
m_seller = mails["seller@mail.net"]
self.assertListEqual(m_seller.to, ["seller@mail.net"])
self.assertListEqual(m_seller.reply_to, ["part1@mail.net"])
m_winner = mails["part1@mail.net"]
self.assertListEqual(m_winner.to, ["part1@mail.net"])
self.assertCountEqual(
[mails["part2@mail.net"].to, mails["part3@mail.net"].to],
[["part2@mail.net"], ["part3@mail.net"]],
)

79
bda/tests/test_revente.py Normal file
View file

@ -0,0 +1,79 @@
from datetime import timedelta
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone
from bda.models import (
Attribution,
CategorieSpectacle,
Participant,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
class TestModels(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="Tirage test",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
self.spectacle_soon = Spectacle.objects.create(
title="foo",
date=timezone.now() + timedelta(days=1),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.spectacle_later = Spectacle.objects.create(
title="bar",
date=timezone.now() + timedelta(days=30),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
user_buyer = User.objects.create_user(
username="bda_buyer", password="testbuyer"
)
user_seller = User.objects.create_user(
username="bda_seller", password="testseller"
)
self.buyer = Participant.objects.create(user=user_buyer, tirage=self.tirage)
self.seller = Participant.objects.create(user=user_seller, tirage=self.tirage)
self.attr_soon = Attribution.objects.create(
participant=self.seller, spectacle=self.spectacle_soon
)
self.attr_later = Attribution.objects.create(
participant=self.seller, spectacle=self.spectacle_later
)
self.revente_soon = SpectacleRevente.objects.create(
seller=self.seller, attribution=self.attr_soon
)
self.revente_later = SpectacleRevente.objects.create(
seller=self.seller, attribution=self.attr_later
)
def test_urgent(self):
self.assertTrue(self.revente_soon.is_urgent)
self.assertFalse(self.revente_later.is_urgent)
def test_tirage(self):
self.revente_soon.confirmed_entry.add(self.buyer)
self.assertEqual(self.revente_soon.tirage(send_mails=False), self.buyer)
self.assertIsNone(self.revente_later.tirage(send_mails=False))

243
bda/tests/test_views.py Normal file
View file

@ -0,0 +1,243 @@
import json
from datetime import timedelta
from unittest import mock
from urllib.parse import urlencode
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.utils import timezone
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
def create_user(username, is_cof=False, is_buro=False):
user = User.objects.create_user(username=username, password=username)
user.profile.is_cof = is_cof
user.profile.is_buro = is_buro
user.profile.save()
return user
def user_is_cof(user):
return (user is not None) and user.profile.is_cof
def user_is_staff(user):
return (user is not None) and user.profile.is_buro
class BdATestHelpers:
def setUp(self):
# Some user with different access privileges
staff = create_user(username="bda_staff", is_cof=True, is_buro=True)
staff_c = Client()
staff_c.force_login(staff)
member = create_user(username="bda_member", is_cof=True)
member_c = Client()
member_c.force_login(member)
other = create_user(username="bda_other")
other_c = Client()
other_c.force_login(other)
self.client_matrix = [
(staff, staff_c),
(member, member_c),
(other, other_c),
(None, Client()),
]
def require_custommails(self):
from django.core.management import call_command
call_command("syncmails", verbosity=0)
def check_restricted_access(
self, url, validate_user=user_is_cof, redirect_url=None
):
def craft_redirect_url(user):
if redirect_url:
return redirect_url
elif user is None:
# client is not logged in
login_url = "/login"
if url:
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
return login_url
else:
return "/"
for (user, client) in self.client_matrix:
resp = client.get(url, follow=True)
if validate_user(user):
self.assertEqual(200, resp.status_code)
else:
self.assertRedirects(resp, craft_redirect_url(user))
class TestBdAViews(BdATestHelpers, TestCase):
def setUp(self):
# Signals handlers on login/logout send messages.
# Due to the way the Django' test Client performs login, this raise an
# error. As workaround, we mock the Django' messages module.
patcher_messages = mock.patch("gestioncof.signals.messages")
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
# Set up the helpers
super().setUp()
# Some BdA stuff
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
Spectacle.objects.bulk_create(
[
Spectacle(
title="foo",
date=timezone.now(),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
),
Spectacle(
title="bar",
date=timezone.now(),
location=self.location,
price=1,
slots=142,
tirage=self.tirage,
listing=False,
category=self.category,
),
Spectacle(
title="baz",
date=timezone.now(),
location=self.location,
price=2,
slots=242,
tirage=self.tirage,
listing=False,
category=self.category,
),
]
)
def test_bda_inscriptions(self):
# TODO: test the form
url = "/bda/inscription/{}".format(self.tirage.id)
self.check_restricted_access(url)
def test_bda_places(self):
url = "/bda/places/{}".format(self.tirage.id)
self.check_restricted_access(url)
def test_etat_places(self):
url = "/bda/etat-places/{}".format(self.tirage.id)
self.check_restricted_access(url)
def test_perform_tirage(self):
# Only staff member can perform a tirage
url = "/bda/tirage/{}".format(self.tirage.id)
self.check_restricted_access(url, validate_user=user_is_staff)
_, staff_c = self.client_matrix[0]
# Cannot be performed if disabled
self.tirage.enable_do_tirage = False
self.tirage.save()
resp = staff_c.get(url)
self.assertTemplateUsed(resp, "tirage-failed.html")
# Cannot be performed if registrations are still open
self.tirage.enable_do_tirage = True
self.tirage.fermeture = timezone.now() + timedelta(seconds=3600)
self.tirage.save()
resp = staff_c.get(url)
self.assertTemplateUsed(resp, "tirage-failed.html")
# Otherwise, perform the tirage
self.tirage.fermeture = timezone.now()
self.tirage.save()
resp = staff_c.get(url)
self.assertTemplateNotUsed(resp, "tirage-failed.html")
def test_spectacles_list(self):
url = "/bda/spectacles/{}".format(self.tirage.id)
self.check_restricted_access(url, validate_user=user_is_staff)
def test_spectacle_detail(self):
show = self.tirage.spectacle_set.first()
url = "/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
self.check_restricted_access(url, validate_user=user_is_staff)
def test_tirage_unpaid(self):
url = "/bda/spectacles/unpaid/{}".format(self.tirage.id)
self.check_restricted_access(url, validate_user=user_is_staff)
def test_send_reminders(self):
self.require_custommails()
# Just get the page
show = self.tirage.spectacle_set.first()
url = "/bda/mails-rappel/{}".format(show.id)
self.check_restricted_access(url, validate_user=user_is_staff)
# Actually send the reminder emails
_, staff_c = self.client_matrix[0]
resp = staff_c.post(url)
self.assertEqual(200, resp.status_code)
# TODO: check that emails are sent
def test_catalogue_api(self):
url_list = "/bda/catalogue/list"
url_details = "/bda/catalogue/details?id={}".format(self.tirage.id)
url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id)
# Anyone can get
def anyone_can_get(url):
self.check_restricted_access(url, validate_user=lambda user: True)
anyone_can_get(url_list)
anyone_can_get(url_details)
anyone_can_get(url_descriptions)
# The resulting JSON contains the information
_, client = self.client_matrix[0]
# List
resp = client.get(url_list)
self.assertJSONEqual(
resp.content.decode("utf-8"),
[{"id": self.tirage.id, "title": self.tirage.title}],
)
# Details
resp = client.get(url_details)
self.assertJSONEqual(
resp.content.decode("utf-8"),
{
"categories": [{"id": self.category.id, "name": self.category.name}],
"locations": [{"id": self.location.id, "name": self.location.name}],
},
)
# Descriptions
resp = client.get(url_descriptions)
raw = resp.content.decode("utf-8")
try:
results = json.loads(raw)
except ValueError:
self.fail("Not valid JSON: {}".format(raw))
self.assertEqual(len(results), 3)
self.assertEqual(
{(s["title"], s["price"], s["slots"]) for s in results},
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)},
)
class TestBdaRevente:
pass
# TODO

75
bda/urls.py Normal file
View file

@ -0,0 +1,75 @@
from django.conf.urls import url
from bda import views
from bda.views import SpectacleListView
from gestioncof.decorators import buro_required
urlpatterns = [
url(
r"^inscription/(?P<tirage_id>\d+)$",
views.inscription,
name="bda-tirage-inscription",
),
url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage),
url(
r"^spectacles/(?P<tirage_id>\d+)$",
buro_required(SpectacleListView.as_view()),
name="bda-liste-spectacles",
),
url(
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
views.spectacle,
name="bda-spectacle",
),
url(r"^spectacles/unpaid/(?P<tirage_id>\d+)$", views.unpaid, name="bda-unpaid"),
url(
r"^spectacles/autocomplete$",
views.spectacle_autocomplete,
name="bda-spectacle-autocomplete",
),
url(
r"^participants/autocomplete$",
views.participant_autocomplete,
name="bda-participant-autocomplete",
),
# Urls BdA-Revente
url(
r"^revente/(?P<tirage_id>\d+)/manage$",
views.revente_manage,
name="bda-revente-manage",
),
url(
r"^revente/(?P<tirage_id>\d+)/subscribe$",
views.revente_subscribe,
name="bda-revente-subscribe",
),
url(
r"^revente/(?P<tirage_id>\d+)/tirages$",
views.revente_tirages,
name="bda-revente-tirages",
),
url(
r"^revente/(?P<spectacle_id>\d+)/buy$",
views.revente_buy,
name="bda-revente-buy",
),
url(
r"^revente/(?P<revente_id>\d+)/confirm$",
views.revente_confirm,
name="bda-revente-confirm",
),
url(
r"^revente/(?P<tirage_id>\d+)/shotgun$",
views.revente_shotgun,
name="bda-revente-shotgun",
),
url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
url(
r"^descriptions/(?P<tirage_id>\d+)$",
views.descriptions_spectacles,
name="bda-descriptions",
),
url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"),
]

910
bda/views.py Normal file
View file

@ -0,0 +1,910 @@
import hashlib
import json
import random
import time
from collections import defaultdict
from custommail.models import CustomMail
from custommail.shortcuts import send_custom_mail, send_mass_custom_mail
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core import serializers
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Count, Prefetch, Q
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import pluralize
from django.utils import formats, timezone
from django.views.generic.list import ListView
from bda.algorithm import Algorithm
from bda.forms import (
AnnulForm,
InscriptionInlineFormSet,
InscriptionReventeForm,
ResellForm,
ReventeTirageAnnulForm,
ReventeTirageForm,
SoldForm,
TokenForm,
)
from bda.models import (
Attribution,
CategorieSpectacle,
ChoixSpectacle,
Participant,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
from gestioncof.decorators import buro_required, cof_required
from utils.views.autocomplete import Select2QuerySetView
@cof_required
def etat_places(request, tirage_id):
"""
Résumé des spectacles d'un tirage avec pour chaque spectacle :
- Le nombre de places en jeu
- Le nombre de demandes
- Le ratio demandes/places
Et le total de toutes les demandes
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = tirage.spectacle_set.select_related("location")
spectacles_dict = {} # index of spectacle by id
for spectacle in spectacles:
spectacle.total = 0 # init total requests
spectacles_dict[spectacle.id] = spectacle
choices = (
ChoixSpectacle.objects.filter(spectacle__in=spectacles)
.values("spectacle")
.annotate(total=Count("spectacle"))
)
# choices *by spectacles* whose only 1 place is requested
choices1 = choices.filter(double_choice="1")
# choices *by spectacles* whose 2 places is requested
choices2 = choices.exclude(double_choice="1")
for spectacle in choices1:
pk = spectacle["spectacle"]
spectacles_dict[pk].total += spectacle["total"]
for spectacle in choices2:
pk = spectacle["spectacle"]
spectacles_dict[pk].total += 2 * spectacle["total"]
# here, each spectacle.total contains the number of requests
slots = 0 # proposed slots
total = 0 # requests
for spectacle in spectacles:
slots += spectacle.slots
total += spectacle.total
spectacle.ratio = spectacle.total / spectacle.slots
context = {
"proposed": slots,
"spectacles": spectacles,
"total": total,
"tirage": tirage,
}
return render(request, "bda/etat-places.html", context)
def _hash_queryset(queryset):
data = serializers.serialize("json", queryset).encode("utf-8")
hasher = hashlib.sha256()
hasher.update(data)
return hasher.hexdigest()
@cof_required
def places(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
places = participant.attribution_set.order_by(
"spectacle__date", "spectacle"
).select_related("spectacle", "spectacle__location")
total = sum(place.spectacle.price for place in places)
filtered_places = []
places_dict = {}
spectacles = []
dates = []
warning = False
for place in places:
if place.spectacle in spectacles:
places_dict[place.spectacle].double = True
else:
place.double = False
places_dict[place.spectacle] = place
spectacles.append(place.spectacle)
filtered_places.append(place)
date = place.spectacle.date.date()
if date in dates:
warning = True
else:
dates.append(date)
# On prévient l'utilisateur s'il a deux places à la même date
if warning:
messages.warning(
request,
"Attention, vous avez reçu des places pour "
"des spectacles différents à la même date.",
)
return render(
request,
"bda/resume_places.html",
{
"participant": participant,
"places": filtered_places,
"tirage": tirage,
"total": total,
},
)
@cof_required
def inscription(request, tirage_id):
"""
Vue d'inscription à un tirage BdA.
- On vérifie qu'on se situe bien entre la date d'ouverture et la date de
fermeture des inscriptions.
- On vérifie que l'inscription n'a pas été modifiée entre le moment 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", {})
participant, _ = Participant.objects.select_related("tirage").get_or_create(
user=request.user, tirage=tirage
)
if timezone.now() > tirage.fermeture:
# Le tirage est fermé.
choices = participant.choixspectacle_set.order_by("priority")
messages.error(request, " C'est fini : tirage au sort dans la journée !")
return render(
request, "bda/resume-inscription-tirage.html", {"choices": choices}
)
BdaFormSet = inlineformset_factory(
Participant,
ChoixSpectacle,
fields=("spectacle", "double_choice", "priority"),
formset=InscriptionInlineFormSet,
)
success = False
stateerror = False
if request.method == "POST":
# use *this* queryset
dbstate = _hash_queryset(participant.choixspectacle_set.all())
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
stateerror = True
formset = BdaFormSet(instance=participant)
else:
formset = BdaFormSet(request.POST, instance=participant)
if formset.is_valid():
formset.save()
success = True
formset = BdaFormSet(instance=participant)
else:
formset = BdaFormSet(instance=participant)
# use *this* queryset
dbstate = _hash_queryset(participant.choixspectacle_set.all())
total_price = 0
choices = participant.choixspectacle_set.select_related("spectacle")
for choice in choices:
total_price += choice.spectacle.price
if choice.double:
total_price += choice.spectacle.price
# Messages
if success:
messages.success(
request, "Votre inscription a été mise à jour avec " "succès !"
)
if stateerror:
messages.error(
request,
"Impossible d'enregistrer vos modifications "
": vous avez apporté d'autres modifications "
"entre temps.",
)
return render(
request,
"bda/inscription-tirage.html",
{
"formset": formset,
"total_price": total_price,
"dbstate": dbstate,
"tirage": tirage,
},
)
def do_tirage(tirage_elt, token):
"""
Fonction auxiliaire à la vue ``tirage`` qui lance effectivement le tirage
après qu'on a vérifié que c'est légitime et que le token donné en argument
est correct.
Rend les résultats
"""
# Initialisation du dictionnaire data qui va contenir les résultats
start = time.time()
data = {
"shows": tirage_elt.spectacle_set.select_related("location"),
"token": token,
"members": tirage_elt.participant_set.select_related("user"),
"total_slots": 0,
"total_losers": 0,
"total_sold": 0,
"total_deficit": 0,
"opera_deficit": 0,
}
# On lance le tirage
choices = (
ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt)
.order_by("participant", "priority")
.select_related("participant", "participant__user", "spectacle")
)
results = Algorithm(data["shows"], data["members"], choices)(token)
# On compte les places attribuées et les déçus
for (_, members, losers) in results:
data["total_slots"] += len(members)
data["total_losers"] += len(losers)
# On calcule le déficit et les bénéfices pour le BdA
# FIXME: le traitement de l'opéra est sale
for (show, members, _) in results:
deficit = (show.slots - len(members)) * show.price
data["total_sold"] += show.slots * show.price
if deficit >= 0:
if "Opéra" in show.location.name:
data["opera_deficit"] += deficit
data["total_deficit"] += deficit
data["total_sold"] -= data["total_deficit"]
# Participant objects are not shared accross spectacle results,
# so assign a single object for each Participant id
members_uniq = {}
members2 = {}
for (show, members, _) in results:
for (member, _, _, _) in members:
if member.id not in members_uniq:
members_uniq[member.id] = member
members2[member] = []
member.total = 0
member = members_uniq[member.id]
members2[member].append(show)
member.total += show.price
members2 = members2.items()
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
# ---
# À partir d'ici, le tirage devient effectif
# ---
# On suppression les vieilles attributions, on sauvegarde le token et on
# désactive le tirage
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
tirage_elt.tokens += '{:s}\n"""{:s}"""\n'.format(
timezone.now().strftime("%y-%m-%d %H:%M:%S"), token
)
tirage_elt.enable_do_tirage = False
tirage_elt.save()
# On enregistre les nouvelles attributions
Attribution.objects.bulk_create(
[
Attribution(spectacle=show, participant=member)
for show, members, _ in results
for member, _, _, _ in members
]
)
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
ChoixRevente = Participant.choicesrevente.through
# Suppression des reventes demandées/enregistrées
# (si le tirage est relancé)
(ChoixRevente.objects.filter(spectacle__tirage=tirage_elt).delete())
(
SpectacleRevente.objects.filter(
attribution__spectacle__tirage=tirage_elt
).delete()
)
lost_by = defaultdict(set)
for show, _, losers in results:
for loser, _, _, _ in losers:
lost_by[loser].add(show)
ChoixRevente.objects.bulk_create(
ChoixRevente(participant=member, spectacle=show)
for member, shows in lost_by.items()
for show in shows
)
data["duration"] = time.time() - start
data["results"] = results
return data
@buro_required
def tirage(request, tirage_id):
tirage_elt = get_object_or_404(Tirage, id=tirage_id)
if not (tirage_elt.enable_do_tirage and tirage_elt.fermeture < timezone.now()):
return render(request, "tirage-failed.html", {"tirage": tirage_elt})
if request.POST:
form = TokenForm(request.POST)
if form.is_valid():
results = do_tirage(tirage_elt, form.cleaned_data["token"])
return render(request, "bda-attrib-extra.html", results)
else:
form = TokenForm()
return render(request, "bda-token.html", {"form": form})
@login_required
def revente_manage(request, tirage_id):
"""
Gestion de ses propres reventes :
- Création d'une revente
- Annulation d'une revente
- Confirmation d'une revente = transfert de la place à la personne qui
rachète
- Annulation d'une revente après que le tirage a eu lieu
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage
)
if not participant.paid:
return render(request, "bda/revente/notpaid.html", {})
resellform = ResellForm(participant, prefix="resell")
annulform = AnnulForm(participant, prefix="annul")
soldform = SoldForm(participant, prefix="sold")
if request.method == "POST":
# On met en vente une place
if "resell" in request.POST:
resellform = ResellForm(participant, request.POST, prefix="resell")
if resellform.is_valid():
datatuple = []
attributions = resellform.cleaned_data["attributions"]
with transaction.atomic():
for attribution in attributions:
revente, created = SpectacleRevente.objects.get_or_create(
attribution=attribution, defaults={"seller": participant}
)
if not created:
revente.reset()
context = {
"vendeur": participant.user,
"show": attribution.spectacle,
"revente": revente,
}
datatuple.append(
(
"bda-revente-new",
context,
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
)
revente.save()
send_mass_custom_mail(datatuple)
# On annule une revente
elif "annul" in request.POST:
annulform = AnnulForm(participant, request.POST, prefix="annul")
if annulform.is_valid():
reventes = annulform.cleaned_data["reventes"]
for revente in reventes:
revente.delete()
# On confirme une vente en transférant la place à la personne qui a
# gagné le tirage
elif "transfer" in request.POST:
soldform = SoldForm(participant, request.POST, prefix="sold")
if soldform.is_valid():
reventes = soldform.cleaned_data["reventes"]
for revente in reventes:
revente.attribution.participant = revente.soldTo
revente.attribution.save()
# On annule la revente après le tirage au sort (par exemple si
# la personne qui a gagné le tirage ne se manifeste pas). La place est
# alors remise en vente
elif "reinit" in request.POST:
soldform = SoldForm(participant, request.POST, prefix="sold")
if soldform.is_valid():
reventes = soldform.cleaned_data["reventes"]
for revente in reventes:
if revente.attribution.spectacle.date > timezone.now():
# On antidate pour envoyer le mail plus vite
new_date = timezone.now() - SpectacleRevente.remorse_time
revente.reset(new_date=new_date)
overdue = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__seller=participant,
revente__notif_sent=True,
).filter(Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
return render(
request,
"bda/revente/manage.html",
{
"tirage": tirage,
"overdue": overdue,
"soldform": soldform,
"annulform": annulform,
"resellform": resellform,
},
)
@login_required
def revente_tirages(request, tirage_id):
"""
Affiche à un participant la liste de toutes les reventes en cours (pour un
tirage donné) et lui permet de s'inscrire et se désinscrire à ces reventes.
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
subform = ReventeTirageForm(participant, prefix="subscribe")
annulform = ReventeTirageAnnulForm(participant, prefix="annul")
if request.method == "POST":
if "subscribe" in request.POST:
subform = ReventeTirageForm(participant, request.POST, prefix="subscribe")
if subform.is_valid():
reventes = subform.cleaned_data["reventes"]
count = reventes.count()
for revente in reventes:
revente.confirmed_entry.add(participant)
if count > 0:
messages.success(
request,
"Tu as bien été inscrit à {} revente{}".format(
count, pluralize(count)
),
)
elif "annul" in request.POST:
annulform = ReventeTirageAnnulForm(
participant, request.POST, prefix="annul"
)
if annulform.is_valid():
reventes = annulform.cleaned_data["reventes"]
count = reventes.count()
for revente in reventes:
revente.confirmed_entry.remove(participant)
if count > 0:
messages.success(
request,
"Tu as bien été désinscrit de {} revente{}".format(
count, pluralize(count)
),
)
return render(
request,
"bda/revente/tirages.html",
{"annulform": annulform, "subform": subform},
)
@login_required
def revente_confirm(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage
)
if not revente.notif_sent or revente.shotgun:
return render(request, "bda/revente/wrongtime.html", {"revente": revente})
revente.confirmed_entry.add(participant)
return render(
request,
"bda/revente/confirmed.html",
{"spectacle": revente.attribution.spectacle, "date": revente.date_tirage},
)
@login_required
def revente_subscribe(request, tirage_id):
"""
Permet à un participant de sélectionner ses préférences pour les reventes.
Il recevra des notifications pour les spectacles qui l'intéressent et il
est automatiquement inscrit aux reventes en cours au moment il ajoute un
spectacle à la liste des spectacles qui l'intéressent.
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
deja_revente = False
success = False
inscrit_revente = []
if request.method == "POST":
form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid():
choices = form.cleaned_data["spectacles"]
participant.choicesrevente = choices
participant.save()
for spectacle in choices:
qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle)
if qset.filter(shotgun=True, soldTo__isnull=True).exists():
# Une place est disponible au shotgun, on suggère à
# l'utilisateur d'aller la récupérer
deja_revente = True
else:
# La place n'est pas disponible au shotgun, si des reventes
# pour ce spectacle existent déjà, on inscrit la personne à
# la revente ayant le moins d'inscrits
min_resell = (
qset.filter(shotgun=False)
.annotate(nb_subscribers=Count("confirmed_entry"))
.order_by("nb_subscribers")
.first()
)
if min_resell is not None:
min_resell.confirmed_entry.add(participant)
inscrit_revente.append(spectacle)
success = True
else:
form = InscriptionReventeForm(
tirage, initial={"spectacles": participant.choicesrevente.all()}
)
# Messages
if success:
messages.success(request, "Ton inscription a bien été prise en compte")
if deja_revente:
messages.info(
request,
"Des reventes existent déjà pour certains de "
"ces spectacles, vérifie les places "
"disponibles sans tirage !",
)
if inscrit_revente:
shows = map("<li>{!s}</li>".format, inscrit_revente)
msg = (
"Tu as été inscrit à des reventes en cours pour les spectacles "
"<ul>{:s}</ul>".format("\n".join(shows))
)
messages.info(request, msg, extra_tags="safe")
return render(request, "bda/revente/subscribe.html", {"form": form})
@login_required
def revente_buy(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle, soldTo__isnull=True
)
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
# on supprime la revente en question.
own_reventes = reventes.filter(seller=participant)
if len(own_reventes) > 0:
own_reventes[0].delete()
return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id]))
reventes_shotgun = reventes.filter(shotgun=True)
if not reventes_shotgun:
return render(request, "bda/revente/none.html", {})
if request.POST:
revente = random.choice(reventes_shotgun)
revente.soldTo = participant
revente.save()
context = {
"show": spectacle,
"acheteur": request.user,
"vendeur": revente.seller.user,
}
send_custom_mail(
"bda-buy-shotgun",
"bda@ens.fr",
[revente.seller.user.email],
context=context,
)
return render(
request,
"bda/revente/mail-success.html",
{"seller": revente.attribution.participant.user, "spectacle": spectacle},
)
return render(
request,
"bda/revente/confirm-shotgun.html",
{"spectacle": spectacle, "user": request.user},
)
@login_required
def revente_shotgun(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = (
tirage.spectacle_set.filter(date__gte=timezone.now())
.select_related("location")
.prefetch_related(
Prefetch(
"attribues",
queryset=(
Attribution.objects.filter(
revente__shotgun=True, revente__soldTo__isnull=True
)
),
to_attr="shotguns",
)
)
)
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun})
@buro_required
def spectacle(request, tirage_id, spectacle_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
attributions = spectacle.attribues.select_related(
"participant", "participant__user"
)
participants = {}
for attrib in attributions:
participant = attrib.participant
participant_info = {
"lastname": participant.user.last_name,
"name": participant.user.get_full_name,
"username": participant.user.username,
"email": participant.user.email,
"given": int(attrib.given),
"paid": participant.paid,
"nb_places": 1,
}
if participant.id in participants:
participants[participant.id]["nb_places"] += 1
participants[participant.id]["given"] += attrib.given
else:
participants[participant.id] = participant_info
participants_info = sorted(participants.values(), key=lambda part: part["lastname"])
return render(
request,
"bda/participants.html",
{"spectacle": spectacle, "participants": participants_info},
)
class SpectacleListView(ListView):
model = Spectacle
template_name = "spectacle_list.html"
def get_queryset(self):
self.tirage = get_object_or_404(Tirage, id=self.kwargs["tirage_id"])
categories = self.tirage.spectacle_set.select_related("location")
return categories
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["tirage_id"] = self.tirage.id
context["tirage_name"] = self.tirage.title
return context
@buro_required
def unpaid(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
unpaid = (
tirage.participant_set.annotate(nb_attributions=Count("attribution"))
.filter(paid=False, nb_attributions__gt=0)
.select_related("user")
)
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
@buro_required
def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples
custommail = CustomMail.objects.get(shortname="bda-rappel")
exemple_mail_1place = custommail.render(
{"member": request.user, "show": show, "nb_attr": 1}
)
exemple_mail_2places = custommail.render(
{"member": request.user, "show": show, "nb_attr": 2}
)
# Contexte
ctxt = {
"show": show,
"exemple_mail_1place": exemple_mail_1place,
"exemple_mail_2places": exemple_mail_2places,
"custommail": custommail,
}
# Envoi confirmé
if request.method == "POST":
members = show.send_rappel()
ctxt["sent"] = True
ctxt["members"] = members
# Demande de confirmation
else:
ctxt["sent"] = False
if show.rappel_sent:
messages.warning(
request,
"Attention, un mail de rappel pour ce spectale a déjà été "
"envoyé le {}".format(
formats.localize(timezone.template_localtime(show.rappel_sent))
),
)
return render(request, "bda/mails-rappel.html", ctxt)
def descriptions_spectacles(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
"quote_set"
)
category_name = request.GET.get("category", "")
location_id = request.GET.get("location", "")
if category_name:
shows_qs = shows_qs.filter(category__name=category_name)
if location_id:
try:
shows_qs = shows_qs.filter(location__id=int(location_id))
except ValueError:
return HttpResponseBadRequest(
"La variable GET 'location' doit contenir un entier"
)
return render(request, "descriptions.html", {"shows": shows_qs})
def catalogue(request, request_type):
"""
Vue destinée à communiquer avec un client AJAX, fournissant soit :
- la liste des tirages
- les catégories et salles d'un tirage
- les descriptions d'un tirage (filtrées selon la catégorie et la salle)
"""
if request_type == "list":
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
data_return = list(
Tirage.objects.filter(appear_catalogue=True).values("id", "title")
)
return JsonResponse(data_return, safe=False)
if request_type == "details":
# Dans ce cas on retourne une liste des catégories et des salles
tirage_id = request.GET.get("id", None)
if tirage_id is None:
return HttpResponseBadRequest("Missing GET parameter: id <int>")
try:
tirage = get_object_or_404(Tirage, id=int(tirage_id))
except ValueError:
return HttpResponseBadRequest("Bad format: int expected for `id`")
shows = tirage.spectacle_set.values_list("id", flat=True)
categories = list(
CategorieSpectacle.objects.filter(spectacle__in=shows)
.distinct()
.values("id", "name")
)
locations = list(
Salle.objects.filter(spectacle__in=shows).distinct().values("id", "name")
)
data_return = {"categories": categories, "locations": locations}
return JsonResponse(data_return, safe=False)
if request_type == "descriptions":
# Ici on retourne les descriptions correspondant à la catégorie et
# à la salle spécifiées
tirage_id = request.GET.get("id", "")
categories = request.GET.get("category", "[]")
locations = request.GET.get("location", "[]")
try:
tirage_id = int(tirage_id)
categories_id = json.loads(categories)
locations_id = json.loads(locations)
# Integers expected
if not all(isinstance(id, int) for id in categories_id):
raise ValueError
if not all(isinstance(id, int) for id in locations_id):
raise ValueError
except ValueError: # Contient JSONDecodeError
return HttpResponseBadRequest(
"Parse error, please ensure the GET parameters have the "
"following types:\n"
"id: int, category: [int], location: [int]\n"
"Data received:\n"
"id = {}, category = {}, locations = {}".format(
request.GET.get("id", ""),
request.GET.get("category", "[]"),
request.GET.get("location", "[]"),
)
)
tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
"quote_set"
)
if categories_id and 0 not in categories_id:
shows_qs = shows_qs.filter(category__id__in=categories_id)
if locations_id and 0 not in locations_id:
shows_qs = shows_qs.filter(location__id__in=locations_id)
# On convertit les descriptions à envoyer en une liste facilement
# JSONifiable (il devrait y avoir un moyen plus efficace en
# redéfinissant le serializer de JSON)
data_return = [
{
"title": spectacle.title,
"category": str(spectacle.category),
"date": str(
formats.date_format(
timezone.localtime(spectacle.date), "SHORT_DATETIME_FORMAT"
)
),
"location": str(spectacle.location),
"vips": spectacle.vips,
"description": spectacle.description,
"slots_description": spectacle.slots_description,
"quotes": [
dict(author=quote.author, text=quote.text)
for quote in spectacle.quote_set.all()
],
"image": spectacle.getImgUrl(),
"ext_link": spectacle.ext_link,
"price": spectacle.price,
"slots": spectacle.slots,
}
for spectacle in shows_qs
]
return JsonResponse(data_return, safe=False)
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
return HttpResponseBadRequest()
##
# Autocomplete views
#
# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view
##
class ParticipantAutocomplete(Select2QuerySetView):
model = Participant
search_fields = ("user__username", "user__first_name", "user__last_name")
participant_autocomplete = buro_required(ParticipantAutocomplete.as_view())
class SpectacleAutocomplete(Select2QuerySetView):
model = Spectacle
search_fields = ("title",)
spectacle_autocomplete = buro_required(SpectacleAutocomplete.as_view())

0
cof/__init__.py Normal file
View file

8
cof/asgi.py Normal file
View file

@ -0,0 +1,8 @@
import os
from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
channel_layer = get_channel_layer()

0
cof/locale/__init__.py Normal file
View file

View file

5
cof/locale/fr/formats.py Normal file
View file

@ -0,0 +1,5 @@
"""
Formats français.
"""
DATETIME_FORMAT = r"l j F Y \à H:i"

3
cof/routing.py Normal file
View file

@ -0,0 +1,3 @@
from channels.routing import include
routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")]

1
cof/settings/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
secret.py

0
cof/settings/__init__.py Normal file
View file

252
cof/settings/common.py Normal file
View file

@ -0,0 +1,252 @@
"""
Django common settings for cof project.
Everything which is supposed to be identical between the production server and
the local development server should be here.
"""
import os
import sys
try:
from . import secret
except ImportError:
raise ImportError(
"The secret.py file is missing.\n"
"For a development environment, simply copy secret_example.py"
)
def import_secret(name):
"""
Shorthand for importing a value from the secret module and raising an
informative exception if a secret is missing.
"""
try:
return getattr(secret, name)
except AttributeError:
raise RuntimeError("Secret missing: {}".format(name))
SECRET_KEY = import_secret("SECRET_KEY")
ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL")
EMAIL_HOST = import_secret("EMAIL_HOST")
DBNAME = import_secret("DBNAME")
DBUSER = import_secret("DBUSER")
DBPASSWD = import_secret("DBPASSWD")
REDIS_PASSWD = import_secret("REDIS_PASSWD")
REDIS_DB = import_secret("REDIS_DB")
REDIS_HOST = import_secret("REDIS_HOST")
REDIS_PORT = import_secret("REDIS_PORT")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TESTING = sys.argv[1] == "test"
# Application definition
INSTALLED_APPS = [
"shared",
"gestioncof",
# Must be before 'django.contrib.admin'.
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
"dal",
"dal_select2",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.admin",
"django.contrib.admindocs",
"bda",
"captcha",
"django_cas_ng",
"bootstrapform",
"kfet",
"kfet.open",
"channels",
"widget_tweaks",
"custommail",
"djconfig",
"wagtail.wagtailforms",
"wagtail.wagtailredirects",
"wagtail.wagtailembeds",
"wagtail.wagtailsites",
"wagtail.wagtailusers",
"wagtail.wagtailsnippets",
"wagtail.wagtaildocs",
"wagtail.wagtailimages",
"wagtail.wagtailsearch",
"wagtail.wagtailadmin",
"wagtail.wagtailcore",
"wagtail.contrib.modeladmin",
"wagtailmenus",
"modelcluster",
"taggit",
"kfet.auth",
"kfet.cms",
"corsheaders",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.auth.middleware.SessionAuthenticationMiddleware",
"kfet.auth.middleware.TemporaryAuthMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware",
"djconfig.middleware.DjConfigMiddleware",
"wagtail.wagtailcore.middleware.SiteMiddleware",
"wagtail.wagtailredirects.middleware.RedirectMiddleware",
]
ROOT_URLCONF = "cof.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"wagtailmenus.context_processors.wagtailmenus",
"djconfig.context_processors.config",
"gestioncof.shared.context_processor",
"kfet.auth.context_processors.temporary_auth",
"kfet.context_processors.config",
]
},
}
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": DBNAME,
"USER": DBUSER,
"PASSWORD": DBPASSWD,
"HOST": os.environ.get("DBHOST", "localhost"),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = "fr-fr"
TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Various additional settings
SITE_ID = 1
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
GRAPPELLI_ADMIN_TITLE = '<a href="/">GestioCOF</a>'
MAIL_DATA = {
"petits_cours": {
"FROM": "Le COF <cof@ens.fr>",
"BCC": "archivescof@gmail.com",
"REPLYTO": "cof@ens.fr",
},
"rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"},
"revente": {
"FROM": "BdA-Revente <bda-revente@ens.fr>",
"REPLYTO": "BdA-Revente <bda-revente@ens.fr>",
},
}
LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home"
CAS_SERVER_URL = "https://cas.eleves.ens.fr/"
CAS_VERSION = "3"
CAS_LOGIN_MSG = None
CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = "/"
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"gestioncof.shared.COFCASBackend",
"kfet.auth.backends.GenericBackend",
)
# reCAPTCHA settings
# https://github.com/praekelt/django-recaptcha
#
# Default settings authorize reCAPTCHA usage for local developement.
# Public and private keys are appended in the 'prod' module settings.
NOCAPTCHA = True
RECAPTCHA_USE_SSL = True
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
# Cache settings
CACHES = {
"default": {
"BACKEND": "redis_cache.RedisCache",
"LOCATION": "redis://:{passwd}@{host}:{port}/db".format(
passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB
),
}
}
# Channels settings
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": {
"hosts": [
(
"redis://:{passwd}@{host}:{port}/{db}".format(
passwd=REDIS_PASSWD,
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
)
)
]
},
"ROUTING": "cof.routing.routing",
}
}
FORMAT_MODULE_PATH = "cof.locale"
# Wagtail settings
WAGTAIL_SITE_NAME = "GestioCOF"
WAGTAIL_ENABLE_UPDATE_CHECK = False
TAGGIT_CASE_INSENSITIVE = True

48
cof/settings/dev.py Normal file
View file

@ -0,0 +1,48 @@
"""
Django development settings for the cof project.
The settings that are not listed here are imported from .common
"""
from .common import * # NOQA
from .common import INSTALLED_APPS, MIDDLEWARE, TESTING
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEBUG = True
if TESTING:
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# ---
# Apache static/media config
# ---
STATIC_URL = "/static/"
STATIC_ROOT = "/srv/gestiocof/static/"
MEDIA_ROOT = "/srv/gestiocof/media/"
MEDIA_URL = "/media/"
# ---
# Debug tool bar
# ---
def show_toolbar(request):
"""
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
machine physique n'est pas forcément connue, et peut difficilement être
mise dans les INTERNAL_IPS.
"""
return DEBUG
if not TESTING:
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE = ["debug_panel.middleware.DebugPanelMiddleware"] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}

31
cof/settings/local.py Normal file
View file

@ -0,0 +1,31 @@
"""
Django local settings for the cof project.
The settings that are not listed here are imported from .common
"""
import os
from .dev import * # NOQA
from .dev import BASE_DIR
# Use sqlite for local development
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
# Use the default cache backend for local development
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
# Use the default in memory asgi backend for local development
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "cof.routing.routing",
}
}
# No need to run collectstatic -> unset STATIC_ROOT
STATIC_ROOT = None

26
cof/settings/prod.py Normal file
View file

@ -0,0 +1,26 @@
"""
Django development settings for the cof project.
The settings that are not listed here are imported from .common
"""
import os
from .common import * # NOQA
from .common import BASE_DIR, import_secret
DEBUG = False
ALLOWED_HOSTS = ["cof.ens.fr", "www.cof.ens.fr", "dev.cof.ens.fr"]
STATIC_ROOT = os.path.join(
os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static"
)
STATIC_URL = "/gestion/static/"
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
MEDIA_URL = "/gestion/media/"
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")

View file

@ -0,0 +1,21 @@
SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah"
ADMINS = None
SERVER_EMAIL = "root@vagrant"
EMAIL_HOST = "localhost"
DBUSER = "cof_gestion"
DBNAME = "cof_gestion"
DBPASSWD = "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
REDIS_PASSWD = "dummy"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_HOST = "127.0.0.1"
RECAPTCHA_PUBLIC_KEY = "DUMMY"
RECAPTCHA_PRIVATE_KEY = "DUMMY"
EMAIL_HOST = None
KFETOPEN_TOKEN = "plop"
LDAP_SERVER_URL = None

135
cof/urls.py Normal file
View file

@ -0,0 +1,135 @@
"""
Fichier principal de configuration des urls du projet GestioCOF
"""
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as django_views
from django.views.generic.base import TemplateView
from django_cas_ng import views as django_cas_views
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from gestioncof import csv_views, views as gestioncof_views
from gestioncof.autocomplete import autocomplete
from gestioncof.urls import (
calendar_patterns,
clubs_patterns,
events_patterns,
export_patterns,
petitcours_patterns,
surveys_patterns,
)
admin.autodiscover()
urlpatterns = [
# Page d'accueil
url(r"^$", gestioncof_views.home, name="home"),
# Le BdA
url(r"^bda/", include("bda.urls")),
# Les exports
url(r"^export/", include(export_patterns)),
# Les petits cours
url(r"^petitcours/", include(petitcours_patterns)),
# Les sondages
url(r"^survey/", include(surveys_patterns)),
# Evenements
url(r"^event/", include(events_patterns)),
# Calendrier
url(r"^calendar/", include(calendar_patterns)),
# Clubs
url(r"^clubs/", include(clubs_patterns)),
# Authentification
url(
r"^cof/denied$",
TemplateView.as_view(template_name="cof-denied.html"),
name="cof-denied",
),
url(r"^cas/login$", django_cas_views.login, name="cas_login_view"),
url(r"^cas/logout$", django_cas_views.logout),
url(r"^outsider/login$", gestioncof_views.login_ext, name="ext_login_view"),
url(r"^outsider/logout$", django_views.logout, {"next_page": "home"}),
url(r"^login$", gestioncof_views.login, name="cof-login"),
url(r"^logout$", gestioncof_views.logout, name="cof-logout"),
# Infos persos
url(r"^profile$", gestioncof_views.profile, name="profile"),
url(
r"^outsider/password-change$",
django_views.password_change,
name="password_change",
),
url(
r"^outsider/password-change-done$",
django_views.password_change_done,
name="password_change_done",
),
# Inscription d'un nouveau membre
url(r"^registration$", gestioncof_views.registration, name="registration"),
url(
r"^registration/clipper/(?P<login_clipper>[\w-]+)/" r"(?P<fullname>.*)$",
gestioncof_views.registration_form2,
name="clipper-registration",
),
url(
r"^registration/user/(?P<username>.+)$",
gestioncof_views.registration_form2,
name="user-registration",
),
url(
r"^registration/empty$",
gestioncof_views.registration_form2,
name="empty-registration",
),
# Autocompletion
url(
r"^autocomplete/registration$",
autocomplete,
name="cof.registration.autocomplete",
),
url(
r"^user/autocomplete$",
gestioncof_views.user_autocomplete,
name="cof-user-autocomplete",
),
# Interface admin
url(r"^admin/logout/", gestioncof_views.logout),
url(r"^admin/doc/", include("django.contrib.admindocs.urls")),
url(
r"^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/",
csv_views.admin_list_export,
{"fields": ["username"]},
),
url(r"^admin/", include(admin.site.urls)),
# Liens utiles du COF et du BdA
url(r"^utile_cof$", gestioncof_views.utile_cof, name="utile_cof"),
url(r"^utile_bda$", gestioncof_views.utile_bda, name="utile_bda"),
url(r"^utile_bda/bda_diff$", gestioncof_views.liste_bdadiff, name="ml_diffbda"),
url(r"^utile_cof/diff_cof$", gestioncof_views.liste_diffcof, name="ml_diffcof"),
url(
r"^utile_bda/bda_revente$",
gestioncof_views.liste_bdarevente,
name="ml_bda_revente",
),
url(r"^k-fet/", include("kfet.urls")),
url(r"^cms/", include(wagtailadmin_urls)),
url(r"^documents/", include(wagtaildocs_urls)),
# djconfig
url(r"^config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"),
]
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))]
if settings.DEBUG:
# Si on est en production, MEDIA_ROOT est servi par Apache.
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Wagtail for uncatched
urlpatterns += [url(r"", include(wagtail_urls))]

1
gestioncof/__init__.py Normal file
View file

@ -0,0 +1 @@
default_app_config = "gestioncof.apps.GestioncofConfig"

333
gestioncof/admin.py Normal file
View file

@ -0,0 +1,333 @@
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group, Permission, User
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from gestioncof.models import (
Club,
CofProfile,
Event,
EventCommentField,
EventOption,
EventOptionChoice,
EventRegistration,
Survey,
SurveyQuestion,
SurveyQuestionAnswer,
)
from gestioncof.petits_cours_models import (
PetitCoursAbility,
PetitCoursAttribution,
PetitCoursAttributionCounter,
PetitCoursDemande,
PetitCoursSubject,
)
def add_link_field(target_model="", field="", link_text=str, desc_text=str):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
if not link_obj.id:
return ""
url = reverse(reverse_path, args=(link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = desc_text(reverse_name + " link")
cls.link = link
cls.readonly_fields = list(getattr(cls, "readonly_fields", [])) + ["link"]
return cls
return add_link
class SurveyQuestionAnswerInline(admin.TabularInline):
model = SurveyQuestionAnswer
@add_link_field(
desc_text=lambda x: "Réponses", link_text=lambda x: "Éditer les réponses"
)
class SurveyQuestionInline(admin.TabularInline):
model = SurveyQuestion
class SurveyQuestionAdmin(admin.ModelAdmin):
search_fields = ("survey__title", "answer")
inlines = [SurveyQuestionAnswerInline]
class SurveyAdmin(admin.ModelAdmin):
search_fields = ("title", "details")
inlines = [SurveyQuestionInline]
class EventOptionChoiceInline(admin.TabularInline):
model = EventOptionChoice
@add_link_field(desc_text=lambda x: "Choix", link_text=lambda x: "Éditer les choix")
class EventOptionInline(admin.TabularInline):
model = EventOption
class EventCommentFieldInline(admin.TabularInline):
model = EventCommentField
class EventOptionAdmin(admin.ModelAdmin):
search_fields = ("event__title", "name")
inlines = [EventOptionChoiceInline]
class EventAdmin(admin.ModelAdmin):
search_fields = ("title", "location", "description")
inlines = [EventOptionInline, EventCommentFieldInline]
class CofProfileInline(admin.StackedInline):
model = CofProfile
inline_classes = ("collapse open",)
class FkeyLookup(object):
def __init__(self, fkeydecl, short_description=None, admin_order_field=None):
self.fk, fkattrs = fkeydecl.split("__", 1)
self.fkattrs = fkattrs.split("__")
self.short_description = short_description or self.fkattrs[-1]
self.admin_order_field = admin_order_field or fkeydecl
def __get__(self, obj, klass):
if obj is None:
"""
hack required to make Django validate (if obj is
None, then we're a class, and classes are callable
<wink>)
"""
return self
item = getattr(obj, self.fk)
for attr in self.fkattrs:
item = getattr(item, attr)
return item
def ProfileInfo(field, short_description, boolean=False):
def getter(self):
try:
return getattr(self.profile, field)
except CofProfile.DoesNotExist:
return ""
getter.short_description = short_description
getter.boolean = boolean
return getter
User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper")
User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement")
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True)
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True)
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente", "ML BdA-R", True)
class UserProfileAdmin(UserAdmin):
def is_buro(self, obj):
try:
return obj.profile.is_buro
except CofProfile.DoesNotExist:
return False
is_buro.short_description = "Membre du Buro"
is_buro.boolean = True
def is_cof(self, obj):
try:
return obj.profile.is_cof
except CofProfile.DoesNotExist:
return False
is_cof.short_description = "Membre du COF"
is_cof.boolean = True
list_display = UserAdmin.list_display + (
"profile_login_clipper",
"profile_phone",
"profile_occupation",
"profile_mailing_cof",
"profile_mailing_bda",
"profile_mailing_bda_revente",
"is_cof",
"is_buro",
)
list_display_links = ("username", "email", "first_name", "last_name")
list_filter = UserAdmin.list_filter + (
"profile__is_cof",
"profile__is_buro",
"profile__mailing_cof",
"profile__mailing_bda",
)
search_fields = UserAdmin.search_fields + ("profile__phone",)
inlines = [CofProfileInline]
staff_fieldsets = [
(None, {"fields": ["username", "password"]}),
(_("Personal info"), {"fields": ["first_name", "last_name", "email"]}),
]
def get_fieldsets(self, request, user=None):
if not request.user.is_superuser:
return self.staff_fieldsets
return super().get_fieldsets(request, user)
def save_model(self, request, user, form, change):
cof_group, created = Group.objects.get_or_create(name="COF")
if created:
# Si le groupe COF n'était pas déjà dans la bdd
# On lui assigne les bonnes permissions
perms = Permission.objects.filter(
Q(content_type__app_label="gestioncof")
| Q(content_type__app_label="bda")
| (Q(content_type__app_label="auth") & Q(content_type__model="user"))
)
cof_group.permissions = perms
# On y associe les membres du Burô
cof_group.user_set = User.objects.filter(profile__is_buro=True)
# Sauvegarde
cof_group.save()
# le Burô est staff et appartient au groupe COF
if user.profile.is_buro:
user.is_staff = True
user.groups.add(cof_group)
else:
user.is_staff = False
user.groups.remove(cof_group)
user.save()
# FIXME: This is absolutely horrible.
def user_str(self):
if self.first_name and self.last_name:
return "{} ({})".format(self.get_full_name(), self.username)
else:
return self.username
User.__str__ = user_str
class EventRegistrationAdminForm(forms.ModelForm):
class Meta:
widgets = {"user": ModelSelect2(url="cof-user-autocomplete")}
class EventRegistrationAdmin(admin.ModelAdmin):
form = EventRegistrationAdminForm
list_display = ("__str__", "event", "user", "paid")
list_filter = ("paid",)
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"event__title",
)
class PetitCoursAbilityAdmin(admin.ModelAdmin):
list_display = ("user", "matiere", "niveau", "agrege")
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"matiere__name",
"niveau",
)
list_filter = ("matiere", "niveau", "agrege")
class PetitCoursAttributionAdmin(admin.ModelAdmin):
list_display = ("user", "demande", "matiere", "rank")
search_fields = ("user__username", "matiere__name")
class PetitCoursAttributionCounterAdmin(admin.ModelAdmin):
list_display = ("user", "matiere", "count")
list_filter = ("matiere",)
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"matiere__name",
)
actions = ["reset"]
actions_on_bottom = True
def reset(self, request, queryset):
queryset.update(count=0)
reset.short_description = "Remise à zéro du compteur"
class PetitCoursDemandeAdmin(admin.ModelAdmin):
list_display = (
"name",
"email",
"agrege_requis",
"niveau",
"created",
"traitee",
"processed",
)
list_filter = ("traitee", "niveau")
search_fields = ("name", "email", "phone", "lieu", "remarques")
class ClubAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
respos = cleaned_data.get("respos")
members = cleaned_data.get("membres")
for respo in respos.all():
if respo not in members:
raise forms.ValidationError(
"Erreur : le respo %s n'est pas membre du club."
% respo.get_full_name()
)
return cleaned_data
class ClubAdmin(admin.ModelAdmin):
list_display = ["name"]
form = ClubAdminForm
admin.site.register(Survey, SurveyAdmin)
admin.site.register(SurveyQuestion, SurveyQuestionAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(EventOption, EventOptionAdmin)
admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
admin.site.register(CofProfile)
admin.site.register(Club, ClubAdmin)
admin.site.register(PetitCoursSubject)
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)
admin.site.register(PetitCoursAttributionCounter, PetitCoursAttributionCounterAdmin)
admin.site.register(PetitCoursDemande, PetitCoursDemandeAdmin)
admin.site.register(EventRegistration, EventRegistrationAdmin)

17
gestioncof/apps.py Normal file
View file

@ -0,0 +1,17 @@
from django.apps import AppConfig
class GestioncofConfig(AppConfig):
name = "gestioncof"
verbose_name = "Gestion des adhérents du COF"
def ready(self):
from . import signals # noqa
self.register_config()
def register_config(self):
import djconfig
from .forms import GestioncofConfigForm
djconfig.register(GestioncofConfigForm)

View file

@ -0,0 +1,88 @@
from django import shortcuts
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Q
from django.http import Http404
from ldap3 import Connection
from gestioncof.decorators import buro_required
from gestioncof.models import CofProfile
class Clipper(object):
def __init__(self, clipper, fullname):
if fullname is None:
fullname = ""
assert isinstance(clipper, str)
assert isinstance(fullname, str)
self.clipper = clipper
self.fullname = fullname
def __str__(self):
return "{} ({})".format(self.clipper, self.fullname)
def __eq__(self, other):
return self.clipper == other.clipper and self.fullname == other.fullname
@buro_required
def autocomplete(request):
if "q" not in request.GET:
raise Http404
q = request.GET["q"]
data = {"q": q}
queries = {}
bits = q.split()
# Fetching data from User and CofProfile tables
queries["members"] = CofProfile.objects.filter(is_cof=True)
queries["users"] = User.objects.filter(profile__is_cof=False)
for bit in bits:
queries["members"] = queries["members"].filter(
Q(user__first_name__icontains=bit)
| Q(user__last_name__icontains=bit)
| Q(user__username__icontains=bit)
| Q(login_clipper__icontains=bit)
)
queries["users"] = queries["users"].filter(
Q(first_name__icontains=bit)
| Q(last_name__icontains=bit)
| Q(username__icontains=bit)
)
queries["members"] = queries["members"].distinct()
queries["users"] = queries["users"].distinct()
# Clearing redundancies
usernames = set(queries["members"].values_list("login_clipper", flat="True")) | set(
queries["users"].values_list("profile__login_clipper", flat="True")
)
# Fetching data from the SPI
if getattr(settings, "LDAP_SERVER_URL", None):
# Fetching
ldap_query = "(&{:s})".format(
"".join(
"(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=bit)
for bit in bits
if bit.isalnum()
)
)
if ldap_query != "(&)":
# If none of the bits were legal, we do not perform the query
entries = None
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"])
entries = conn.entries
# Clearing redundancies
queries["clippers"] = [
Clipper(entry.uid.value, entry.cn.value)
for entry in entries
if entry.uid.value and entry.uid.value not in usernames
]
# Resulting data
data.update(queries)
data["options"] = sum(len(query) for query in queries)
return shortcuts.render(request, "autocomplete_user.html", data)

72
gestioncof/csv_views.py Normal file
View file

@ -0,0 +1,72 @@
import csv
from django.apps import apps
from django.http import HttpResponse, HttpResponseForbidden
from django.template.defaultfilters import slugify
def export(qs, fields=None):
model = qs.model
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = "attachment; filename=%s.csv" % slugify(
model.__name__
)
writer = csv.writer(response)
# Write headers to CSV file
if fields:
headers = fields
else:
headers = []
for field in model._meta.fields:
headers.append(field.name)
writer.writerow(headers)
# Write data to CSV file
for obj in qs:
row = []
for field in headers:
if field in headers:
val = getattr(obj, field)
if callable(val):
val = val()
row.append(val)
writer.writerow(row)
# Return CSV file to browser as download
return response
def admin_list_export(
request, model_name, app_label, queryset=None, fields=None, list_display=True
):
"""
Put the following line in your urls.py BEFORE your admin include
(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',
'util.csv_view.admin_list_export'),
"""
if not request.user.is_staff:
return HttpResponseForbidden()
if not queryset:
model = apps.get_model(app_label, model_name)
queryset = model.objects.all()
queryset = queryset.filter(profile__is_cof=True)
if not fields:
if list_display and len(queryset.model._meta.admin.list_display) > 1:
fields = queryset.model._meta.admin.list_display
else:
fields = None
return export(queryset, fields)
"""
Create your own change_list.html for your admin view and put something
like this in it:
{% block object-tools %}
<ul class="object-tools">
<li><a href="csv/{%if request.GET%}?{{request.GET.urlencode}}
{%endif%}" class="addlink">Export to CSV</a></li>
{% if has_add_permission %}
<li><a href="add/{% if is_popup %}?_popup=1{% endif %}"
class="addlink">
{% blocktrans with cl.opts.verbose_name|escape as name %}
Add {{ name }}{% endblocktrans %}</a></li>
{% endif %}
</ul>
{% endblock %}
"""

23
gestioncof/decorators.py Normal file
View file

@ -0,0 +1,23 @@
from django.contrib.auth.decorators import user_passes_test
def is_cof(user):
try:
profile = user.profile
return profile.is_cof
except Exception:
return False
cof_required = user_passes_test(is_cof)
def is_buro(user):
try:
profile = user.profile
return profile.is_buro
except Exception:
return False
buro_required = user_passes_test(is_buro)

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": "gestioncof.survey",
"pk": 1
},
{
"fields": {
"question": "Sanction s'il chante",
"survey": 1,
"multi_answers": true
},
"model": "gestioncof.surveyquestion",
"pk": 1
},
{
"fields": {
"question": "Est-ce qu'on le garde\u00a0?",
"survey": 1,
"multi_answers": false
},
"model": "gestioncof.surveyquestion",
"pk": 2
},
{
"fields": {
"answer": "On l'ernestise",
"survey_question": 1
},
"model": "gestioncof.surveyquestionanswer",
"pk": 1
},
{
"fields": {
"answer": "On ligote",
"survey_question": 1
},
"model": "gestioncof.surveyquestionanswer",
"pk": 2
},
{
"fields": {
"answer": "On le prive de banquet",
"survey_question": 1
},
"model": "gestioncof.surveyquestionanswer",
"pk": 3
},
{
"fields": {
"answer": "Oui",
"survey_question": 2
},
"model": "gestioncof.surveyquestionanswer",
"pk": 4
},
{
"fields": {
"answer": "Non",
"survey_question": 2
},
"model": "gestioncof.surveyquestionanswer",
"pk": 5
},
{
"fields": {
"old": false,
"description": "On va casser du romain.",
"end_date": "2016-09-12T00:00:00Z",
"title": "Bataille de Gergovie",
"image": "",
"location": "Gergovie",
"registration_open": true,
"start_date": "2016-09-09T00:00:00Z"
},
"model": "gestioncof.event",
"pk": 1
},
{
"fields": {
"default": "",
"event": 1,
"fieldtype": "text",
"name": "Commentaires"
},
"model": "gestioncof.eventcommentfield",
"pk": 1
},
{
"fields": {
"multi_choices": true,
"event": 1,
"name": "Potion magique"
},
"model": "gestioncof.eventoption",
"pk": 1
},
{
"fields": {
"event_option": 1,
"value": "Je suis alergique"
},
"model": "gestioncof.eventoptionchoice",
"pk": 1
},
{
"fields": {
"event_option": 1,
"value": "J'en veux"
},
"model": "gestioncof.eventoptionchoice",
"pk": 2
},
{
"fields": {
"event_option": 1,
"value": "Je suis tomb\u00e9 dans la marmite quand j'\u00e9tais petit"
},
"model": "gestioncof.eventoptionchoice",
"pk": 3
},
{
"fields": {
"name": "Bagarre"
},
"model": "gestioncof.petitcourssubject",
"pk": 1
},
{
"fields": {
"name": "Lancer de menhir"
},
"model": "gestioncof.petitcourssubject",
"pk": 2
},
{
"fields": {
"name": "Pr\u00e9paration de potions"
},
"model": "gestioncof.petitcourssubject",
"pk": 3
},
{
"fields": {
"name": "Chant"
},
"model": "gestioncof.petitcourssubject",
"pk": 4
},
{
"fields": {
"traitee": false,
"remarques": "En grande difficult\u00e9",
"quand": "weekend (dimanche) / soir apr\u00e8s les cours",
"name": "Jules C\u00e9sar",
"created": "2016-07-15T11:12:35Z",
"niveau": "prepa1styear",
"agrege_requis": false,
"phone": "",
"traitee_par": null,
"matieres": [
1
],
"lieu": "Al\u00e9sia",
"freq": "3 fois / semaine",
"email": "jules.cesar@polytechnique.edu",
"processed": null
},
"model": "gestioncof.petitcoursdemande",
"pk": 1
},
{
"fields": {
"traitee": false,
"remarques": "",
"quand": "Weekends",
"name": "Jules C\u00e9sar",
"created": "2016-07-15T11:13:26Z",
"niveau": "lycee",
"agrege_requis": true,
"phone": "",
"traitee_par": null,
"matieres": [
3
],
"lieu": "\u00e0 domicile",
"freq": "toutes les semaines",
"email": "jules.cesar@polytechnique.edu",
"processed": null
},
"model": "gestioncof.petitcoursdemande",
"pk": 2
}
]

View file

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

419
gestioncof/forms.py Normal file
View file

@ -0,0 +1,419 @@
from django import forms
from django.contrib.auth.models import User
from django.forms.formsets import BaseFormSet, formset_factory
from django.forms.widgets import CheckboxSelectMultiple, RadioSelect
from django.utils.translation import ugettext_lazy as _
from djconfig.forms import ConfigForm
from bda.models import Spectacle
from gestioncof.models import CalendarSubscription, Club, CofProfile, EventCommentValue
from gestioncof.widgets import TriStateCheckbox
class EventForm(forms.Form):
def __init__(self, *args, **kwargs):
event = kwargs.pop("event")
self.event = event
current_choices = kwargs.pop("current_choices", None)
super().__init__(*args, **kwargs)
choices = {}
if current_choices:
for choice in current_choices.all():
if choice.event_option.id not in choices:
choices[choice.event_option.id] = [choice.id]
else:
choices[choice.event_option.id].append(choice.id)
all_choices = choices
for option in event.options.all():
choices = [(choice.id, choice.value) for choice in option.choices.all()]
if option.multi_choices:
initial = [] if option.id not in all_choices else all_choices[option.id]
field = forms.MultipleChoiceField(
label=option.name,
choices=choices,
widget=CheckboxSelectMultiple,
required=False,
initial=initial,
)
else:
initial = (
None if option.id not in all_choices else all_choices[option.id][0]
)
field = forms.ChoiceField(
label=option.name,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial,
)
field.option_id = option.id
self.fields["option_%d" % option.id] = field
def choices(self):
for name, value in self.cleaned_data.items():
if name.startswith("option_"):
yield (self.fields[name].option_id, value)
class SurveyForm(forms.Form):
def __init__(self, *args, **kwargs):
survey = kwargs.pop("survey")
current_answers = kwargs.pop("current_answers", None)
super().__init__(*args, **kwargs)
answers = {}
if current_answers:
for answer in current_answers.all():
if answer.survey_question.id not in answers:
answers[answer.survey_question.id] = [answer.id]
else:
answers[answer.survey_question.id].append(answer.id)
for question in survey.questions.all():
choices = [(answer.id, answer.answer) for answer in question.answers.all()]
if question.multi_answers:
initial = [] if question.id not in answers else answers[question.id]
field = forms.MultipleChoiceField(
label=question.question,
choices=choices,
widget=CheckboxSelectMultiple,
required=False,
initial=initial,
)
else:
initial = (
None if question.id not in answers else answers[question.id][0]
)
field = forms.ChoiceField(
label=question.question,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial,
)
field.question_id = question.id
self.fields["question_%d" % question.id] = field
def answers(self):
for name, value in self.cleaned_data.items():
if name.startswith("question_"):
yield (self.fields[name].question_id, value)
class SurveyStatusFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
survey = kwargs.pop("survey")
super().__init__(*args, **kwargs)
for question in survey.questions.all():
for answer in question.answers.all():
name = "question_%d_answer_%d" % (question.id, answer.id)
if self.is_bound and self.data.get(self.add_prefix(name), None):
initial = self.data.get(self.add_prefix(name), None)
else:
initial = "none"
field = forms.ChoiceField(
label="%s : %s" % (question.question, answer.answer),
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial,
)
field.question_id = question.id
field.answer_id = answer.id
self.fields[name] = field
def filters(self):
for name, value in self.cleaned_data.items():
if name.startswith("question_"):
yield (
self.fields[name].question_id,
self.fields[name].answer_id,
value,
)
class EventStatusFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
event = kwargs.pop("event")
super().__init__(*args, **kwargs)
for option in event.options.all():
for choice in option.choices.all():
name = "option_%d_choice_%d" % (option.id, choice.id)
if self.is_bound and self.data.get(self.add_prefix(name), None):
initial = self.data.get(self.add_prefix(name), None)
else:
initial = "none"
field = forms.ChoiceField(
label="%s : %s" % (option.name, choice.value),
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial,
)
field.option_id = option.id
field.choice_id = choice.id
self.fields[name] = field
# has_paid
name = "event_has_paid"
if self.is_bound and self.data.get(self.add_prefix(name), None):
initial = self.data.get(self.add_prefix(name), None)
else:
initial = "none"
field = forms.ChoiceField(
label="Événement payé",
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial,
)
self.fields[name] = field
def filters(self):
for name, value in self.cleaned_data.items():
if name.startswith("option_"):
yield (self.fields[name].option_id, self.fields[name].choice_id, value)
elif name == "event_has_paid":
yield ("has_paid", None, value)
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ["first_name", "last_name", "email"]
class ProfileForm(forms.ModelForm):
class Meta:
model = CofProfile
fields = [
"phone",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
]
class RegistrationUserForm(forms.ModelForm):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
self.fields["username"].help_text = ""
class Meta:
model = User
fields = ("username", "first_name", "last_name", "email")
class RegistrationPassUserForm(RegistrationUserForm):
"""
Formulaire pour changer le mot de passe d'un utilisateur.
"""
password1 = forms.CharField(label=_("Mot de passe"), widget=forms.PasswordInput)
password2 = forms.CharField(
label=_("Confirmation du mot de passe"), widget=forms.PasswordInput
)
def clean_password2(self):
pass1 = self.cleaned_data["password1"]
pass2 = self.cleaned_data["password2"]
if pass1 and pass2:
if pass1 != pass2:
raise forms.ValidationError(_("Mots de passe non identiques."))
return pass2
def save(self, commit=True, *args, **kwargs):
user = super().save(commit, *args, **kwargs)
user.set_password(self.cleaned_data["password2"])
if commit:
user.save()
return user
class RegistrationProfileForm(forms.ModelForm):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
self.fields["mailing_cof"].initial = True
self.fields["mailing_bda"].initial = True
self.fields["mailing_bda_revente"].initial = True
self.fields["mailing_unernestaparis"].initial = True
self.fields.keyOrder = [
"login_clipper",
"phone",
"occupation",
"departement",
"is_cof",
"type_cotiz",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
"comments",
]
class Meta:
model = CofProfile
fields = (
"login_clipper",
"phone",
"occupation",
"departement",
"is_cof",
"type_cotiz",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
"comments",
)
STATUS_CHOICES = (
("no", "Non"),
("wait", "Oui mais attente paiement"),
("paid", "Oui payé"),
)
class AdminEventForm(forms.Form):
status = forms.ChoiceField(
label="Inscription", initial="no", choices=STATUS_CHOICES, widget=RadioSelect
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event")
registration = kwargs.pop("current_registration", None)
current_choices, paid = (
(registration.options.all(), registration.paid)
if registration is not None
else ([], None)
)
if paid is True:
kwargs["initial"] = {"status": "paid"}
elif paid is False:
kwargs["initial"] = {"status": "wait"}
else:
kwargs["initial"] = {"status": "no"}
super().__init__(*args, **kwargs)
choices = {}
for choice in current_choices:
if choice.event_option.id not in choices:
choices[choice.event_option.id] = [choice.id]
else:
choices[choice.event_option.id].append(choice.id)
all_choices = choices
for option in self.event.options.all():
choices = [(choice.id, choice.value) for choice in option.choices.all()]
if option.multi_choices:
initial = [] if option.id not in all_choices else all_choices[option.id]
field = forms.MultipleChoiceField(
label=option.name,
choices=choices,
widget=CheckboxSelectMultiple,
required=False,
initial=initial,
)
else:
initial = (
None if option.id not in all_choices else all_choices[option.id][0]
)
field = forms.ChoiceField(
label=option.name,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial,
)
field.option_id = option.id
self.fields["option_%d" % option.id] = field
for commentfield in self.event.commentfields.all():
initial = commentfield.default
if registration is not None:
try:
initial = registration.comments.get(
commentfield=commentfield
).content
except EventCommentValue.DoesNotExist:
pass
widget = (
forms.Textarea if commentfield.fieldtype == "text" else forms.TextInput
)
field = forms.CharField(
label=commentfield.name, widget=widget, required=False, initial=initial
)
field.comment_id = commentfield.id
self.fields["comment_%d" % commentfield.id] = field
def choices(self):
for name, value in self.cleaned_data.items():
if name.startswith("option_"):
yield (self.fields[name].option_id, value)
def comments(self):
for name, value in self.cleaned_data.items():
if name.startswith("comment_"):
yield (self.fields[name].comment_id, value)
class BaseEventRegistrationFormset(BaseFormSet):
def __init__(self, *args, **kwargs):
self.events = kwargs.pop("events")
self.current_registrations = kwargs.pop("current_registrations", None)
self.extra = len(self.events)
super().__init__(*args, **kwargs)
def _construct_form(self, index, **kwargs):
kwargs["event"] = self.events[index]
if self.current_registrations is not None:
kwargs["current_registration"] = self.current_registrations[index]
return super()._construct_form(index, **kwargs)
EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
class CalendarForm(forms.ModelForm):
subscribe_to_events = forms.BooleanField(
initial=True, label="Événements du COF", required=False
)
subscribe_to_my_shows = forms.BooleanField(
initial=True,
label="Les spectacles pour lesquels j'ai obtenu une place",
required=False,
)
other_shows = forms.ModelMultipleChoiceField(
label="Spectacles supplémentaires",
queryset=Spectacle.objects.filter(tirage__active=True),
widget=forms.CheckboxSelectMultiple,
required=False,
)
class Meta:
model = CalendarSubscription
fields = ["subscribe_to_events", "subscribe_to_my_shows", "other_shows"]
class ClubsForm(forms.Form):
"""
Formulaire d'inscription d'un membre à plusieurs clubs du COF.
"""
clubs = forms.ModelMultipleChoiceField(
label="Inscriptions aux clubs du COF",
queryset=Club.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
# ---
# Announcements banner
# TODO: move this to the `gestion` app once the supportBDS branch is merged
# ---
class GestioncofConfigForm(ConfigForm):
gestion_banner = forms.CharField(
label=_("Announcements banner"),
help_text=_("An empty banner disables annoucements"),
max_length=2048,
)

View file

View file

@ -0,0 +1,41 @@
"""
Un mixin à utiliser avec BaseCommand pour charger des objets depuis un json
"""
import json
import os
from django.core.management.base import BaseCommand
class MyBaseCommand(BaseCommand):
"""
Ajoute une méthode ``from_json`` qui charge des objets à partir d'un json.
"""
def from_json(self, filename, data_dir, klass, callback=lambda obj: obj):
"""
Charge les objets contenus dans le fichier json référencé par
``filename`` dans la base de donnée. La fonction callback est appelées
sur chaque objet avant enregistrement.
"""
self.stdout.write("Chargement de {:s}".format(filename))
with open(os.path.join(data_dir, filename), "r") as file:
descriptions = json.load(file)
objects = []
nb_new = 0
for description in descriptions:
qset = klass.objects.filter(**description)
try:
objects.append(qset.get())
except klass.DoesNotExist:
obj = klass(**description)
obj = callback(obj)
obj.save()
objects.append(obj)
nb_new += 1
self.stdout.write("- {:d} objets créés".format(nb_new))
self.stdout.write(
"- {:d} objets gardés en l'état".format(len(objects) - nb_new)
)
return objects

View file

@ -0,0 +1,116 @@
"""
Charge des données de test dans la BDD
- Utilisateurs
- Sondage
- Événement
- Petits cours
"""
import os
import random
from django.contrib.auth.models import User
from django.core.management import call_command
from gestioncof.management.base import MyBaseCommand
from gestioncof.petits_cours_models import (
LEVELS_CHOICES,
PetitCoursAbility,
PetitCoursAttributionCounter,
PetitCoursSubject,
)
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
class Command(MyBaseCommand):
help = "Charge des données de test dans la BDD"
def add_arguments(self, parser):
"""
Permet de ne pas créer l'utilisateur "root".
"""
parser.add_argument(
"--no-root",
action="store_true",
dest="no-root",
default=False,
help='Ne crée pas l\'utilisateur "root"',
)
def handle(self, *args, **options):
# ---
# Utilisateurs
# ---
# Gaulois
gaulois = self.from_json("gaulois.json", DATA_DIR, User)
for user in gaulois:
user.profile.is_cof = True
user.profile.save()
# Romains
self.from_json("romains.json", DATA_DIR, User)
# Root
no_root = options.get("no-root", False)
if not no_root:
self.stdout.write("Création de l'utilisateur root")
root, _ = User.objects.get_or_create(
username="root",
first_name="super",
last_name="user",
email="root@localhost",
)
root.set_password("root")
root.is_staff = True
root.is_superuser = True
root.profile.is_cof = True
root.profile.is_buro = True
root.profile.save()
root.save()
# ---
# Petits cours
# ---
self.stdout.write("Inscriptions au système des petits cours")
levels = [id for (id, verbose) in LEVELS_CHOICES]
subjects = list(PetitCoursSubject.objects.all())
nb_of_teachers = 0
for user in gaulois:
if random.randint(0, 1):
nb_of_teachers += 1
# L'utilisateur reçoit les demandes de petits cours
user.profile.petits_cours_accept = True
user.save()
# L'utilisateur est compétent dans une matière
subject = random.choice(subjects)
if not PetitCoursAbility.objects.filter(
user=user, matiere=subject
).exists():
PetitCoursAbility.objects.create(
user=user,
matiere=subject,
niveau=random.choice(levels),
agrege=bool(random.randint(0, 1)),
)
# On initialise son compteur d'attributions
PetitCoursAttributionCounter.objects.get_or_create(
user=user, matiere=subject
)
self.stdout.write("- {:d} inscriptions".format(nb_of_teachers))
# ---
# Le BdA
# ---
call_command("loadbdadevdata")
# ---
# La K-Fêt
# ---
call_command("loadkfetdevdata")

View file

@ -0,0 +1,89 @@
"""
Import des mails de GestioCOF dans la base de donnée
"""
import json
import os
from custommail.models import CustomMail, Type, Variable
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
DATA_LOCATION = os.path.join(os.path.dirname(__file__), "..", "data", "custommail.json")
def dummy_log(__):
pass
# XXX. this should probably be in the custommail package
def load_from_file(log=dummy_log, verbosity=1):
with open(DATA_LOCATION, "r") as jsonfile:
mail_data = json.load(jsonfile)
# On se souvient à quel objet correspond quel pk du json
assoc = {"types": {}, "mails": {}}
status = {"synced": 0, "unchanged": 0}
for obj in mail_data:
fields = obj["fields"]
# Pour les trois types d'objets :
# - On récupère les objets référencés par les clefs étrangères
# - On crée l'objet si nécessaire
# - On le stocke éventuellement dans les deux dictionnaires définis
# plus haut
# Variable types
if obj["model"] == "custommail.variabletype":
fields["inner1"] = assoc["types"].get(fields["inner1"])
fields["inner2"] = assoc["types"].get(fields["inner2"])
if fields["kind"] == "model":
fields["content_type"] = ContentType.objects.get_by_natural_key(
*fields["content_type"]
)
var_type, _ = Type.objects.get_or_create(**fields)
assoc["types"][obj["pk"]] = var_type
# Custom mails
if obj["model"] == "custommail.custommail":
mail = None
try:
mail = CustomMail.objects.get(shortname=fields["shortname"])
status["unchanged"] += 1
except CustomMail.DoesNotExist:
mail = CustomMail.objects.create(**fields)
status["synced"] += 1
if verbosity:
log("SYNCED {:s}".format(fields["shortname"]))
assoc["mails"][obj["pk"]] = mail
# Variables
if obj["model"] == "custommail.custommailvariable":
fields["custommail"] = assoc["mails"].get(fields["custommail"])
fields["type"] = assoc["types"].get(fields["type"])
try:
Variable.objects.get(
custommail=fields["custommail"], name=fields["name"]
)
except Variable.DoesNotExist:
Variable.objects.create(**fields)
if verbosity:
log("{synced:d} mails synchronized {unchanged:d} unchanged".format(**status))
class Command(BaseCommand):
help = (
"Va chercher les données mails de GestioCOF stocké au format json "
"dans /gestioncof/management/data/custommails.json. Le format des "
"données est celui donné par la commande :"
" `python manage.py dumpdata custommail --natural-foreign` "
"La bonne façon de mettre à jour ce fichier est donc de le "
"charger à l'aide de syncmails, le faire les modifications à "
"l'aide de l'interface administration et/ou du shell puis de le "
"remplacer par le nouveau résultat de la commande précédente."
)
def handle(self, *args, **options):
load_from_file(log=self.stdout.write)

View file

@ -0,0 +1,600 @@
[
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"auth",
"user"
],
"inner1": null,
"inner2": null
},
"pk": 1
},
{
"model": "custommail.type",
"fields": {
"kind": "int",
"content_type": null,
"inner1": null,
"inner2": null
},
"pk": 2
},
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"bda",
"spectacle"
],
"inner1": null,
"inner2": null
},
"pk": 3
},
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"bda",
"spectaclerevente"
],
"inner1": null,
"inner2": null
},
"pk": 4
},
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"sites",
"site"
],
"inner1": null,
"inner2": null
},
"pk": 5
},
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"gestioncof",
"petitcoursdemande"
],
"inner1": null,
"inner2": null
},
"pk": 6
},
{
"model": "custommail.type",
"fields": {
"kind": "list",
"content_type": null,
"inner1": 12,
"inner2": null
},
"pk": 7
},
{
"model": "custommail.type",
"fields": {
"kind": "list",
"content_type": null,
"inner1": 1,
"inner2": null
},
"pk": 8
},
{
"model": "custommail.type",
"fields": {
"kind": "pair",
"content_type": null,
"inner1": 12,
"inner2": 8
},
"pk": 9
},
{
"model": "custommail.type",
"fields": {
"kind": "list",
"content_type": null,
"inner1": 9,
"inner2": null
},
"pk": 10
},
{
"model": "custommail.type",
"fields": {
"kind": "list",
"content_type": null,
"inner1": 3,
"inner2": null
},
"pk": 11
},
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"gestioncof",
"petitcourssubject"
],
"inner1": null,
"inner2": null
},
"pk": 12
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "welcome",
"subject": "Bienvenue au COF",
"body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime.",
"description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre"
},
"pk": 1
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-rappel",
"subject": "{{ show }}",
"body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts",
"description": "Mail de rappel pour les spectacles BdA"
},
"pk": 2
},
{
"model": "custommail.custommail",
"pk": 3,
"fields": {
"shortname": "bda-revente",
"subject": "{{ show }}",
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour leur signaler qu'une place vient d'\u00eatre mise en vente.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 4,
"fields": {
"shortname": "bda-shotgun",
"subject": "{{ show }}",
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-revente-winner",
"subject": "BdA-Revente : {{ show.title }}",
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA",
"description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente"
},
"pk": 5
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-revente-loser",
"subject": "BdA-Revente : {{ show.title }}",
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts",
"description": "Notification envoy\u00e9e aux perdants d'un tirage de revente."
},
"pk": 6
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-revente-seller",
"subject": "BdA-Revente : {{ show.title }}",
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA",
"description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e"
},
"pk": 7
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-revente-new",
"subject": "BdA-Revente : {{ show.title }}",
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts",
"description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires."
},
"pk": 8
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-buy-shotgun",
"subject": "BdA-Revente : {{ show.title }}",
"body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})",
"description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun."
},
"pk": 9
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "petit-cours-mail-eleve",
"subject": "Petits cours ENS par le COF",
"body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours",
"description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours"
},
"pk": 10
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "petits-cours-mail-demandeur",
"subject": "Cours particuliers ENS",
"body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS",
"description": "Mail envoy\u00e9 aux personnes qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})"
},
"pk": 11
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-attributions",
"subject": "R\u00e9sultats du tirage au sort",
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts",
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places"
},
"pk": 12
},
{
"model": "custommail.custommail",
"fields": {
"shortname": "bda-attributions-decus",
"subject": "R\u00e9sultats du tirage au sort",
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts",
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place"
},
"pk": 13
},
{
"model": "custommail.variable",
"fields": {
"custommail": 1,
"type": 1,
"name": "member",
"description": "Utilisateur de GestioCOF"
},
"pk": 1
},
{
"model": "custommail.variable",
"fields": {
"custommail": 2,
"type": 1,
"name": "member",
"description": "Utilisateur ayant eu une place pour ce spectacle"
},
"pk": 2
},
{
"model": "custommail.variable",
"fields": {
"custommail": 2,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 3
},
{
"model": "custommail.variable",
"fields": {
"custommail": 2,
"type": 2,
"name": "nb_attr",
"description": "Nombre de places obtenues"
},
"pk": 4
},
{
"model": "custommail.variable",
"fields": {
"custommail": 3,
"type": 4,
"name": "revente",
"description": "Revente mentionn\u00e9e dans le mail"
},
"pk": 5
},
{
"model": "custommail.variable",
"fields": {
"custommail": 3,
"type": 1,
"name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place"
},
"pk": 6
},
{
"model": "custommail.variable",
"fields": {
"custommail": 3,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 7
},
{
"model": "custommail.variable",
"fields": {
"custommail": 3,
"type": 5,
"name": "site",
"description": "Site web (gestioCOF)"
},
"pk": 8
},
{
"model": "custommail.variable",
"fields": {
"custommail": 4,
"type": 5,
"name": "site",
"description": "Site web (gestioCOF)"
},
"pk": 9
},
{
"model": "custommail.variable",
"fields": {
"custommail": 4,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 10
},
{
"model": "custommail.variable",
"fields": {
"custommail": 4,
"type": 1,
"name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place"
},
"pk": 11
},
{
"model": "custommail.variable",
"fields": {
"custommail": 5,
"type": 1,
"name": "acheteur",
"description": "Gagnant-e du tirage"
},
"pk": 12
},
{
"model": "custommail.variable",
"fields": {
"custommail": 5,
"type": 1,
"name": "vendeur",
"description": "Personne qui vend une place"
},
"pk": 13
},
{
"model": "custommail.variable",
"fields": {
"custommail": 5,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 14
},
{
"model": "custommail.variable",
"fields": {
"custommail": 6,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 15
},
{
"model": "custommail.variable",
"fields": {
"custommail": 6,
"type": 1,
"name": "vendeur",
"description": "Personne qui vend une place"
},
"pk": 16
},
{
"model": "custommail.variable",
"fields": {
"custommail": 6,
"type": 1,
"name": "acheteur",
"description": "Personne inscrite au tirage qui n'a pas eu la place"
},
"pk": 17
},
{
"model": "custommail.variable",
"fields": {
"custommail": 7,
"type": 1,
"name": "acheteur",
"description": "Gagnant-e du tirage"
},
"pk": 18
},
{
"model": "custommail.variable",
"fields": {
"custommail": 7,
"type": 1,
"name": "vendeur",
"description": "Personne qui vend une place"
},
"pk": 19
},
{
"model": "custommail.variable",
"fields": {
"custommail": 7,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 20
},
{
"model": "custommail.variable",
"fields": {
"custommail": 8,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 21
},
{
"model": "custommail.variable",
"fields": {
"custommail": 8,
"type": 1,
"name": "vendeur",
"description": "Personne qui vend la place"
},
"pk": 22
},
{
"model": "custommail.variable",
"fields": {
"custommail": 8,
"type": 4,
"name": "revente",
"description": "Revente mentionn\u00e9e dans le mail"
},
"pk": 23
},
{
"model": "custommail.variable",
"fields": {
"custommail": 9,
"type": 1,
"name": "vendeur",
"description": "Personne qui vend la place"
},
"pk": 24
},
{
"model": "custommail.variable",
"fields": {
"custommail": 9,
"type": 3,
"name": "show",
"description": "Spectacle"
},
"pk": 25
},
{
"model": "custommail.variable",
"fields": {
"custommail": 9,
"type": 1,
"name": "acheteur",
"description": "Personne qui prend la place au shotgun"
},
"pk": 26
},
{
"model": "custommail.variable",
"fields": {
"custommail": 10,
"type": 6,
"name": "demande",
"description": "Demande de petit cours"
},
"pk": 27
},
{
"model": "custommail.variable",
"fields": {
"custommail": 10,
"type": 7,
"name": "matieres",
"description": "Liste des mati\u00e8res concern\u00e9es par la demande"
},
"pk": 28
},
{
"model": "custommail.variable",
"fields": {
"custommail": 11,
"type": 10,
"name": "proposals",
"description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re"
},
"pk": 29
},
{
"model": "custommail.variable",
"fields": {
"custommail": 11,
"type": 7,
"name": "unsatisfied",
"description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer"
},
"pk": 30
},
{
"model": "custommail.variable",
"fields": {
"custommail": 12,
"type": 11,
"name": "places",
"description": "Places de spectacle du participant"
},
"pk": 31
},
{
"model": "custommail.variable",
"fields": {
"custommail": 12,
"type": 1,
"name": "member",
"description": "Participant du tirage au sort"
},
"pk": 32
},
{
"model": "custommail.variable",
"fields": {
"custommail": 13,
"type": 1,
"name": "member",
"description": "Participant du tirage au sort"
},
"pk": 33
}
]

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,856 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name="Clipper",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"username",
models.CharField(max_length=20, verbose_name=b"Identifiant"),
),
(
"fullname",
models.CharField(max_length=200, verbose_name=b"Nom complet"),
),
],
),
migrations.CreateModel(
name="Club",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=200, verbose_name=b"Nom")),
("description", models.TextField(verbose_name=b"Description")),
(
"membres",
models.ManyToManyField(
related_name="clubs", to=settings.AUTH_USER_MODEL
),
),
(
"respos",
models.ManyToManyField(
related_name="clubs_geres", to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.CreateModel(
name="CofProfile",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"login_clipper",
models.CharField(
max_length=8, verbose_name=b"Login clipper", blank=True
),
),
(
"is_cof",
models.BooleanField(default=False, verbose_name=b"Membre du COF"),
),
(
"num",
models.IntegerField(
default=0,
verbose_name=b"Num\xc3\xa9ro d'adh\xc3\xa9rent",
blank=True,
),
),
(
"phone",
models.CharField(
max_length=20,
verbose_name=b"T\xc3\xa9l\xc3\xa9phone",
blank=True,
),
),
(
"occupation",
models.CharField(
default=b"1A",
max_length=9,
verbose_name="Occupation",
choices=[
(b"exterieur", "Ext\xe9rieur"),
(b"1A", "1A"),
(b"2A", "2A"),
(b"3A", "3A"),
(b"4A", "4A"),
(b"archicube", "Archicube"),
(b"doctorant", "Doctorant"),
(b"CST", "CST"),
],
),
),
(
"departement",
models.CharField(
max_length=50, verbose_name="D\xe9partement", blank=True
),
),
(
"type_cotiz",
models.CharField(
default=b"normalien",
max_length=9,
verbose_name="Type de cotisation",
choices=[
(b"etudiant", "Normalien \xe9tudiant"),
(b"normalien", "Normalien \xe9l\xe8ve"),
(b"exterieur", "Ext\xe9rieur"),
],
),
),
(
"mailing_cof",
models.BooleanField(
default=False, verbose_name=b"Recevoir les mails COF"
),
),
(
"mailing_bda",
models.BooleanField(
default=False, verbose_name=b"Recevoir les mails BdA"
),
),
(
"mailing_bda_revente",
models.BooleanField(
default=False,
verbose_name=b"Recevoir les mails de revente de places BdA",
),
),
(
"comments",
models.TextField(
verbose_name=b"Commentaires visibles uniquement par le Buro",
blank=True,
),
),
(
"is_buro",
models.BooleanField(
default=False, verbose_name=b"Membre du Bur\xc3\xb4"
),
),
(
"petits_cours_accept",
models.BooleanField(
default=False, verbose_name=b"Recevoir des petits cours"
),
),
(
"petits_cours_remarques",
models.TextField(
default=b"",
verbose_name="Remarques et pr\xe9cisions pour les petits cours",
blank=True,
),
),
(
"user",
models.OneToOneField(
related_name="profile",
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
),
),
],
options={
"verbose_name": "Profil COF",
"verbose_name_plural": "Profils COF",
},
),
migrations.CreateModel(
name="CustomMail",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("shortname", models.SlugField()),
("title", models.CharField(max_length=200, verbose_name=b"Titre")),
("content", models.TextField(verbose_name=b"Contenu")),
(
"comments",
models.TextField(
verbose_name=b"Informations contextuelles sur le mail",
blank=True,
),
),
],
options={"verbose_name": "Mails personnalisables"},
),
migrations.CreateModel(
name="Event",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("title", models.CharField(max_length=200, verbose_name=b"Titre")),
("location", models.CharField(max_length=200, verbose_name=b"Lieu")),
(
"start_date",
models.DateField(
null=True, verbose_name=b"Date de d\xc3\xa9but", blank=True
),
),
(
"end_date",
models.DateField(
null=True, verbose_name=b"Date de fin", blank=True
),
),
(
"description",
models.TextField(verbose_name=b"Description", blank=True),
),
(
"registration_open",
models.BooleanField(
default=True, verbose_name=b"Inscriptions ouvertes"
),
),
(
"old",
models.BooleanField(
default=False,
verbose_name=b"Archiver (\xc3\xa9v\xc3\xa9nement fini)",
),
),
],
options={"verbose_name": "\xc9v\xe9nement"},
),
migrations.CreateModel(
name="EventCommentField",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=200, verbose_name=b"Champ")),
(
"fieldtype",
models.CharField(
default=b"text",
max_length=10,
verbose_name=b"Type",
choices=[(b"text", "Texte long"), (b"char", "Texte court")],
),
),
(
"default",
models.TextField(
verbose_name=b"Valeur par d\xc3\xa9faut", blank=True
),
),
(
"event",
models.ForeignKey(
related_name="commentfields",
to="gestioncof.Event",
on_delete=models.CASCADE,
),
),
],
options={"verbose_name": "Champ"},
),
migrations.CreateModel(
name="EventCommentValue",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"content",
models.TextField(null=True, verbose_name=b"Contenu", blank=True),
),
(
"commentfield",
models.ForeignKey(
related_name="values",
to="gestioncof.EventCommentField",
on_delete=models.CASCADE,
),
),
],
),
migrations.CreateModel(
name="EventOption",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=200, verbose_name=b"Option")),
(
"multi_choices",
models.BooleanField(default=False, verbose_name=b"Choix multiples"),
),
(
"event",
models.ForeignKey(
related_name="options",
to="gestioncof.Event",
on_delete=models.CASCADE,
),
),
],
options={"verbose_name": "Option"},
),
migrations.CreateModel(
name="EventOptionChoice",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("value", models.CharField(max_length=200, verbose_name=b"Valeur")),
(
"event_option",
models.ForeignKey(
related_name="choices",
to="gestioncof.EventOption",
on_delete=models.CASCADE,
),
),
],
options={"verbose_name": "Choix"},
),
migrations.CreateModel(
name="EventRegistration",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"paid",
models.BooleanField(default=False, verbose_name=b"A pay\xc3\xa9"),
),
(
"event",
models.ForeignKey(to="gestioncof.Event", on_delete=models.CASCADE),
),
(
"filledcomments",
models.ManyToManyField(
to="gestioncof.EventCommentField",
through="gestioncof.EventCommentValue",
),
),
("options", models.ManyToManyField(to="gestioncof.EventOptionChoice")),
(
"user",
models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
],
options={"verbose_name": "Inscription"},
),
migrations.CreateModel(
name="PetitCoursAbility",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"niveau",
models.CharField(
max_length=12,
verbose_name="Niveau",
choices=[
(b"college", "Coll\xe8ge"),
(b"lycee", "Lyc\xe9e"),
(b"prepa1styear", "Pr\xe9pa 1\xe8re ann\xe9e / L1"),
(b"prepa2ndyear", "Pr\xe9pa 2\xe8me ann\xe9e / L2"),
(b"licence3", "Licence 3"),
(b"other", "Autre (pr\xe9ciser dans les commentaires)"),
],
),
),
(
"agrege",
models.BooleanField(default=False, verbose_name="Agr\xe9g\xe9"),
),
],
options={
"verbose_name": "Comp\xe9tence petits cours",
"verbose_name_plural": "Comp\xe9tences des petits cours",
},
),
migrations.CreateModel(
name="PetitCoursAttribution",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"date",
models.DateTimeField(
auto_now_add=True, verbose_name="Date d'attribution"
),
),
("rank", models.IntegerField(verbose_name=b"Rang dans l'email")),
(
"selected",
models.BooleanField(
default=False, verbose_name="S\xe9lectionn\xe9 par le demandeur"
),
),
],
options={
"verbose_name": "Attribution de petits cours",
"verbose_name_plural": "Attributions de petits cours",
},
),
migrations.CreateModel(
name="PetitCoursAttributionCounter",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"count",
models.IntegerField(default=0, verbose_name=b"Nombre d'envois"),
),
],
options={
"verbose_name": "Compteur d'attribution de petits cours",
"verbose_name_plural": "Compteurs d'attributions de petits cours",
},
),
migrations.CreateModel(
name="PetitCoursDemande",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"name",
models.CharField(max_length=200, verbose_name="Nom/pr\xe9nom"),
),
(
"email",
models.CharField(max_length=300, verbose_name="Adresse email"),
),
(
"phone",
models.CharField(
max_length=20,
verbose_name="T\xe9l\xe9phone (facultatif)",
blank=True,
),
),
(
"quand",
models.CharField(
help_text="Indiquez ici la p\xe9riode d\xe9sir\xe9e pour les petits cours (vacances scolaires, semaine, week-end).",
max_length=300,
verbose_name="Quand ?",
blank=True,
),
),
(
"freq",
models.CharField(
help_text="Indiquez ici la fr\xe9quence envisag\xe9e (hebdomadaire, 2 fois par semaine, ...)",
max_length=300,
verbose_name="Fr\xe9quence",
blank=True,
),
),
(
"lieu",
models.CharField(
help_text="Si vous avez avez une pr\xe9f\xe9rence sur le lieu.",
max_length=300,
verbose_name="Lieu (si pr\xe9f\xe9rence)",
blank=True,
),
),
(
"agrege_requis",
models.BooleanField(
default=False, verbose_name="Agr\xe9g\xe9 requis"
),
),
(
"niveau",
models.CharField(
default=b"",
max_length=12,
verbose_name="Niveau",
choices=[
(b"college", "Coll\xe8ge"),
(b"lycee", "Lyc\xe9e"),
(b"prepa1styear", "Pr\xe9pa 1\xe8re ann\xe9e / L1"),
(b"prepa2ndyear", "Pr\xe9pa 2\xe8me ann\xe9e / L2"),
(b"licence3", "Licence 3"),
(b"other", "Autre (pr\xe9ciser dans les commentaires)"),
],
),
),
(
"remarques",
models.TextField(
verbose_name="Remarques et pr\xe9cisions", blank=True
),
),
(
"traitee",
models.BooleanField(default=False, verbose_name="Trait\xe9e"),
),
(
"processed",
models.DateTimeField(verbose_name="Date de traitement", blank=True),
),
(
"created",
models.DateTimeField(
auto_now_add=True, verbose_name="Date de cr\xe9ation"
),
),
],
options={
"verbose_name": "Demande de petits cours",
"verbose_name_plural": "Demandes de petits cours",
},
),
migrations.CreateModel(
name="PetitCoursSubject",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=30, verbose_name="Mati\xe8re")),
(
"users",
models.ManyToManyField(
related_name="petits_cours_matieres",
through="gestioncof.PetitCoursAbility",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Mati\xe8re de petits cours",
"verbose_name_plural": "Mati\xe8res des petits cours",
},
),
migrations.CreateModel(
name="Survey",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("title", models.CharField(max_length=200, verbose_name=b"Titre")),
(
"details",
models.TextField(verbose_name=b"D\xc3\xa9tails", blank=True),
),
(
"survey_open",
models.BooleanField(default=True, verbose_name=b"Sondage ouvert"),
),
(
"old",
models.BooleanField(
default=False, verbose_name=b"Archiver (sondage fini)"
),
),
],
options={"verbose_name": "Sondage"},
),
migrations.CreateModel(
name="SurveyAnswer",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
)
],
options={"verbose_name": "R\xe9ponses"},
),
migrations.CreateModel(
name="SurveyQuestion",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"question",
models.CharField(max_length=200, verbose_name=b"Question"),
),
(
"multi_answers",
models.BooleanField(default=False, verbose_name=b"Choix multiples"),
),
(
"survey",
models.ForeignKey(
related_name="questions",
to="gestioncof.Survey",
on_delete=models.CASCADE,
),
),
],
options={"verbose_name": "Question"},
),
migrations.CreateModel(
name="SurveyQuestionAnswer",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"answer",
models.CharField(max_length=200, verbose_name=b"R\xc3\xa9ponse"),
),
(
"survey_question",
models.ForeignKey(
related_name="answers",
to="gestioncof.SurveyQuestion",
on_delete=models.CASCADE,
),
),
],
options={"verbose_name": "R\xe9ponse"},
),
migrations.AddField(
model_name="surveyanswer",
name="answers",
field=models.ManyToManyField(
related_name="selected_by", to="gestioncof.SurveyQuestionAnswer"
),
),
migrations.AddField(
model_name="surveyanswer",
name="survey",
field=models.ForeignKey(to="gestioncof.Survey", on_delete=models.CASCADE),
),
migrations.AddField(
model_name="surveyanswer",
name="user",
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
migrations.AddField(
model_name="petitcoursdemande",
name="matieres",
field=models.ManyToManyField(
related_name="demandes",
verbose_name="Mati\xe8res",
to="gestioncof.PetitCoursSubject",
),
),
migrations.AddField(
model_name="petitcoursdemande",
name="traitee_par",
field=models.ForeignKey(
blank=True,
to=settings.AUTH_USER_MODEL,
null=True,
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name="petitcoursattributioncounter",
name="matiere",
field=models.ForeignKey(
verbose_name="Matiere",
to="gestioncof.PetitCoursSubject",
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name="petitcoursattributioncounter",
name="user",
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
migrations.AddField(
model_name="petitcoursattribution",
name="demande",
field=models.ForeignKey(
verbose_name="Demande",
to="gestioncof.PetitCoursDemande",
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name="petitcoursattribution",
name="matiere",
field=models.ForeignKey(
verbose_name="Mati\xe8re",
to="gestioncof.PetitCoursSubject",
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name="petitcoursattribution",
name="user",
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
migrations.AddField(
model_name="petitcoursability",
name="matiere",
field=models.ForeignKey(
verbose_name="Mati\xe8re",
to="gestioncof.PetitCoursSubject",
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name="petitcoursability",
name="user",
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
migrations.AddField(
model_name="eventcommentvalue",
name="registration",
field=models.ForeignKey(
related_name="comments",
to="gestioncof.EventRegistration",
on_delete=models.CASCADE,
),
),
migrations.AlterUniqueTogether(
name="surveyanswer", unique_together=set([("user", "survey")])
),
migrations.AlterUniqueTogether(
name="eventregistration", unique_together=set([("user", "event")])
),
]

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