Merge branch 'master' into qwann/k-fet/graphs

This commit is contained in:
Qwann 2018-10-11 23:06:01 +02:00
commit bff8f84c8a
275 changed files with 22142 additions and 4457 deletions

5
.gitignore vendored
View file

@ -9,3 +9,8 @@ venv/
/src
media/
*.log
*.sqlite3
# PyCharm
.idea
.cache

View file

@ -1,25 +1,24 @@
services:
- mysql:latest
- postgres:latest
- redis:latest
variables:
# GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings_dev"
DBNAME: "cof_gestion"
DBUSER: "cof_gestion"
DBPASSWD: "cof_password"
DBHOST: "mysql"
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
DBHOST: "postgres"
REDIS_HOST: "redis"
REDIS_PASSWD: "dummy"
# Cached packages
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
# mysql service configuration
MYSQL_DATABASE: "$DBNAME"
MYSQL_USER: "$DBUSER"
MYSQL_PASSWORD: "$DBPASSWD"
MYSQL_ROOT_PASSWORD: "root_password"
# postgres service configuration
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
POSTGRES_USER: "cof_gestion"
POSTGRES_DB: "cof_gestion"
# psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD
cache:
paths:
@ -29,13 +28,12 @@ cache:
before_script:
- mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq mysql-client
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST"
-e "GRANT ALL ON test_$DBNAME.* TO '$DBUSER'@'%'"
- 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
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST"
-e "DROP DATABASE test_$DBNAME" || true
- pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
test:
stage: test

217
README.md
View file

@ -1,17 +1,75 @@
# GestioCOF
![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg)
## Installation
Il est possible d'installer GestioCOF sur votre machine de deux façons différentes :
- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère
- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde
### Installation manuelle
Il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python ainsi
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python3-pip python3-dev python3-venv sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
python3 -m venv venv
Pour l'activer, il faut taper
. venv/bin/activate
depuis le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -U pip # parfois nécessaire la première fois
pip install -r requirements-devel.txt
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py cof/settings/secret.py
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
données bidons bien pratiques pour développer avec la commande suivante :
bash provisioning/prepare_django.sh
Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas
pour plus de détails.
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
### Vagrant
La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser
Une autre façon d'installer GestioCOF sur votre machine est d'utiliser
[Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
le monde à la même configuration de développement (même sous Windows !), et
l'installation se fait en une commande.
Pour utiliser Vagrant, il faut le
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
Si vous êtes sous Linux, votre distribution propose probablement des paquets
Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui
@ -66,137 +124,60 @@ car par défaut Django n'écoute que sur l'adresse locale de la machine virtuell
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
entrer dans le navigateur est `localhost:8000`.
#### Serveur de développement type production
Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de
façon similaire à la version en production : on utilise
Juste histoire de jouer, pas indispensable pour développer :
La VM Vagrant héberge en plus un serveur nginx configuré pour servir GestioCOF
comme en production : on utilise
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
derrière un reverse-proxy apache. Le tout est monitoré par
[supervisor](http://supervisord.org/).
derrière un reverse-proxy nginx.
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code
change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour
visualiser la dernière version du code.
### Installation manuelle
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il
est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python, un
client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées
(Ubuntu, ...) :
sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
virtualenv env -p $(which python3)
L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir
python3, si c'est la version de python par défaut sur votre système, ceci n'est
pas nécessaire. Pour l'activer, il faut faire
. env/bin/activate
dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -r requirements-devel.txt
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.
#### Installation avec MySQL
Il faut maintenant installer MySQL. Si vous n'avez pas déjà MySQL installé sur
votre serveur, il faut l'installer ; sous Debian et dérivées (Ubuntu, ...) :
sudo apt-get install mysql-server
Il vous demandera un mot de passe pour le compte d'administration MySQL,
notez-le quelque part (ou n'en mettez pas, le serveur n'est accessible que
localement par défaut). Si vous utilisez une autre distribution, consultez la
documentation de votre distribution pour savoir comment changer ce mot de passe
et démarrer le serveur MySQL (c'est automatique sous Ubuntu).
Vous devez alors créer un utilisateur local et une base `cof_gestion`, avec le
mot de passe de votre choix (remplacez `mot_de_passe`) :
mysql -uroot -e "CREATE DATABASE cof_gestion; GRANT ALL PRIVILEGES ON cof_gestion.* TO 'cof_gestion'@'localhost' IDENTIFIER BY 'mot_de_passe'"
Éditez maintenant le fichier `cof/settings.py` pour y intégrer ces changements ;
la définition de `DATABASES` doit ressembler à (à nouveau, remplacez
`mot_de_passe` de façon appropriée) :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'cof_gestion',
'USER': 'cof_gestion',
'PASSWORD': 'mot_de_passe',
}
}
#### Installation avec SQLite
GestioCOF est installé avec MySQL sur la VM COF, et afin d'avoir un
environnement de développement aussi proche que possible de ce qui tourne en
vrai pour éviter les mauvaises surprises, il est conseillé d'utiliser MySQL sur
votre machine de développement également. Toutefois, GestioCOF devrait
fonctionner avec d'autres moteurs SQL, et certains préfèrent utiliser SQLite
pour sa légèreté et facilité d'installation.
Si vous décidez d'utiliser SQLite, il faut l'installer ; sous Debian et dérivées :
sudo apt-get install sqlite3
puis éditer le fichier `cof/settings.py` pour que la définition de `DATABASES`
ressemble à :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande suivante :
python manage.py migrate
Charger les mails indispensables au bon fonctionnement de GestioCOF :
python manage.py syncmails
Une base de donnée pré-remplie est disponible en lançant les commandes :
python manage.py loaddata gestion sites accounts groups articles
python manage.py loaddevdata
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
`localhost:8080/gestion/`. Toutefois il ne se recharge pas tout seul lorsque le
code change, il faut relancer le worker avec `sudo systemctl restart
worker.service` pour visualiser la dernière version du code.
### Mise à jour
Pour mettre à jour les paquets Python, utiliser la commande suivante :
pip install --upgrade -r requirements.txt -r requirements-devel.txt
pip install --upgrade -r requirements-devel.txt
Pour mettre à jour les modèles après une migration, il faut ensuite faire :
python manage.py migrate
## Outils pour développer
### Base de donnée
Quelle que soit la méthode d'installation choisie, la base de donnée locale est
peuplée avec des données artificielles pour faciliter le développement.
- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez
vous sur ce compte pour accéder à tout GestioCOF.
- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques
spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA.
- À chaque compte est associé un trigramme K-Fêt
- Un certain nombre d'articles K-Fêt sont renseignés.
### Tests unitaires
On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab
à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante.
Pour lancer les tests :
```
python manage.py test
```
## Documentation utilisateur
Une brève documentation utilisateur pour se familiariser plus vite avec l'outil
est accessible sur le
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home).
Une brève documentation utilisateur est accessible sur le
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home) pour avoir une
idée de la façon dont le COF utilise GestioCOF.

1
TODO_PROD.md Normal file
View file

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

View file

@ -0,0 +1 @@

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
import autocomplete_light
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
@ -9,6 +6,9 @@ from django.db.models import Sum, Count
from django.template.defaultfilters import pluralize
from django.utils import timezone
from django import forms
from dal.autocomplete import ModelSelect2
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
@ -24,8 +24,17 @@ class ReadOnlyMixin(object):
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"
@ -56,17 +65,17 @@ class AttributionInline(admin.TabularInline):
def get_queryset(self, request):
qs = super().get_queryset(request)
if self.listing is not None:
qs.filter(spectacle__listing=self.listing)
qs = qs.filter(spectacle__listing=self.listing)
return qs
class WithListingAttributionInline(AttributionInline):
exclude = ('given', )
form = WithListingAttributionTabularAdminForm
listing = True
class WithoutListingAttributionInline(AttributionInline):
exclude = ('given', )
form = WithoutListingAttributionTabularAdminForm
listing = False
@ -155,7 +164,7 @@ class AttributionAdminForm(forms.ModelForm):
)
def clean(self):
cleaned_data = super(AttributionAdminForm, self).clean()
cleaned_data = super().clean()
participant = cleaned_data.get("participant")
spectacle = cleaned_data.get("spectacle")
if participant and spectacle:
@ -180,7 +189,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
class ChoixSpectacleAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(ChoixSpectacle, exclude=[])
form = ChoixSpectacleAdminForm
def tirage(self, obj):
return obj.participant.tirage
@ -225,7 +234,7 @@ class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['answered_mail'].queryset = (
self.fields['confirmed_entry'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
@ -288,13 +297,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
count = queryset.count()
for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()):
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
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." % (

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db.models import Max
import random

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light
from bda.models import Participant, Spectacle
autocomplete_light.register(
Participant, search_fields=('user__username', 'user__first_name',
'user__last_name'),
autocomplete_js_attributes={'placeholder': 'participant...'})
autocomplete_light.register(
Spectacle, search_fields=('title', ),
autocomplete_js_attributes={'placeholder': 'spectacle...'})

View file

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
from django import forms
from django.forms.models import BaseInlineFormSet
from django.utils import timezone
from bda.models import Attribution, Spectacle
from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet):
@ -43,7 +41,33 @@ class TokenForm(forms.Form):
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return "%s" % str(obj.spectacle)
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):
@ -54,7 +78,7 @@ class ResellForm(forms.Form):
required=False)
def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now())
@ -65,22 +89,22 @@ class ResellForm(forms.Form):
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
reventes = ReventeModelMultipleChoiceField(
own=True,
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs):
super(AnnulForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__notif_sent=False,
revente__soldTo__isnull=True)
.select_related('spectacle', 'spectacle__location',
'participant__user')
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')
)
@ -91,7 +115,7 @@ class InscriptionReventeForm(forms.Form):
required=False)
def __init__(self, tirage, *args, **kwargs):
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.fields['spectacles'].queryset = (
tirage.spectacle_set
.select_related('location')
@ -99,19 +123,58 @@ class InscriptionReventeForm(forms.Form):
)
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):
attributions = AttributionModelMultipleChoiceField(
reventes = ReventeModelMultipleChoiceField(
own=True,
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple)
def __init__(self, participant, *args, **kwargs):
super(SoldForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(revente__isnull=False,
revente__soldTo__isnull=False)
.exclude(revente__soldTo=participant)
.select_related('spectacle', 'spectacle__location',
'participant__user')
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

@ -1,12 +1,7 @@
# -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des reventes.
"""
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management import BaseCommand
from django.utils import timezone
from bda.models import SpectacleRevente
@ -21,23 +16,36 @@ class Command(BaseCommand):
now = timezone.now()
reventes = SpectacleRevente.objects.all()
for revente in reventes:
# Check si < 24h
if (revente.attribution.spectacle.date <=
revente.date + timedelta(days=1)) and \
now >= revente.date + timedelta(minutes=15) and \
not revente.notif_sent:
self.stdout.write(str(now))
revente.mail_shotgun()
self.stdout.write("Mail de disponibilité immédiate envoyé")
# Check si délai de retrait dépassé
elif (now >= revente.date + timedelta(hours=1) and
not revente.notif_sent):
# 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("Mail d'inscription à une revente envoyé")
# Check si tirage à faire
elif (now >= revente.date_tirage and
not revente.tirage_done):
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))
revente.tirage()
self.stdout.write("Tirage effectué, mails envoyés")
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

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des mails de rappel.
"""
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone

View file

@ -59,7 +59,7 @@ class Migration(migrations.Migration):
('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')),
('location', models.ForeignKey(to='bda.Salle', on_delete=models.CASCADE)),
],
options={
'ordering': ('priority', 'date', 'title'),
@ -79,27 +79,27 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='participant',
name='user',
field=models.OneToOneField(to=settings.AUTH_USER_MODEL),
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'),
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'),
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'),
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'),
field=models.ForeignKey(related_name='attribues', to='bda.Spectacle', on_delete=models.CASCADE),
),
migrations.AlterUniqueTogether(
name='choixspectacle',

View file

@ -5,17 +5,34 @@ from django.db import migrations, models
from django.conf import settings
from django.utils import timezone
def forwards_func(apps, schema_editor):
def fill_tirage_fields(apps, schema_editor):
"""
Create a `Tirage` to fill new field `tirage` of `Participant`
and `Spectacle` already existing.
"""
Participant = apps.get_model("bda", "Participant")
Spectacle = apps.get_model("bda", "Spectacle")
Tirage = apps.get_model("bda", "Tirage")
db_alias = schema_editor.connection.alias
Tirage.objects.using(db_alias).bulk_create([
Tirage(
id=1,
title="Tirage de test (migration)",
active=False,
ouverture=timezone.now(),
fermeture=timezone.now()),
])
# 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):
@ -35,22 +52,33 @@ class Migration(migrations.Migration):
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
],
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
migrations.AlterField(
model_name='participant',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
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(default=1, to='bda.Tirage'),
preserve_default=False,
field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE),
),
migrations.AddField(
model_name='spectacle',
name='tirage',
field=models.ForeignKey(default=1, to='bda.Tirage'),
preserve_default=False,
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

@ -73,6 +73,7 @@ class Migration(migrations.Migration):
model_name='spectacle',
name='category',
field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle',
on_delete=models.CASCADE,
null=True),
),
migrations.AddField(
@ -84,6 +85,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='quote',
name='spectacle',
field=models.ForeignKey(to='bda.Spectacle'),
field=models.ForeignKey(to='bda.Spectacle',
on_delete=models.CASCADE),
),
]

View file

@ -47,12 +47,14 @@ class Migration(migrations.Migration):
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'),
),
@ -60,6 +62,7 @@ class Migration(migrations.Migration):
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,29 @@
# -*- 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,53 @@
# -*- 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,16 @@
# -*- 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

@ -1,16 +1,26 @@
# -*- coding: utf-8 -*-
import calendar
import random
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from django.contrib.sites.models import Site
from django.core import mail
from django.db import models
from django.db.models import Count
from django.contrib.auth.models import User
from django.conf import settings
from django.utils import timezone, formats
from custommail.models import CustomMail
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)
@ -50,9 +60,12 @@ class CategorieSpectacle(models.Model):
class Spectacle(models.Model):
title = models.CharField("Titre", max_length=300)
category = models.ForeignKey(CategorieSpectacle, blank=True, null=True)
category = models.ForeignKey(
CategorieSpectacle, on_delete=models.CASCADE,
blank=True, null=True,
)
date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle)
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)
@ -62,7 +75,7 @@ class Spectacle(models.Model):
max_length=500)
price = models.FloatField("Prix d'une place")
slots = models.IntegerField("Places")
tirage = models.ForeignKey(Tirage)
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)
@ -96,32 +109,29 @@ class Spectacle(models.Model):
Envoie un mail de rappel à toutes les personnes qui ont une place pour
ce spectacle.
"""
# On récupère la liste des participants
members = {}
for attr in Attribution.objects.filter(spectacle=self).all():
member = attr.participant.user
if member.id in members:
members[member.id][1] = 2
else:
members[member.id] = [member, 1]
# FIXME : faire quelque chose de ça, un utilisateur bda_generic ?
# # Pour le BdA
# members[0] = ['BdA', 1, 'bda@ens.fr']
# members[-1] = ['BdA', 2, 'bda@ens.fr']
# On 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[0], 'nb_attr': member[1], 'show': self},
{'member': member, "nb_attr": member.nb_attr, 'show': self},
settings.MAIL_DATA['rappels']['FROM'],
[member[0].email])
for member in members.values()
[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.values()
return members
@property
def is_past(self):
@ -129,7 +139,7 @@ class Spectacle(models.Model):
class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle)
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
text = models.TextField('Citation')
author = models.CharField('Auteur', max_length=200)
@ -143,7 +153,7 @@ PAYMENT_TYPES = (
class Participant(models.Model):
user = models.ForeignKey(User)
user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(Spectacle,
through="ChoixSpectacle",
related_name="chosen_by")
@ -154,7 +164,7 @@ class Participant(models.Model):
paymenttype = models.CharField("Moyen de paiement",
max_length=6, choices=PAYMENT_TYPES,
blank=True)
tirage = models.ForeignKey(Tirage)
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed",
blank=True)
@ -162,16 +172,20 @@ class Participant(models.Model):
def __str__(self):
return "%s - %s" % (self.user, self.tirage.title)
DOUBLE_CHOICES = (
("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"),
("double", "2 places sinon rien"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
)
class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant)
spectacle = models.ForeignKey(Spectacle, related_name="participants")
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,
@ -198,8 +212,11 @@ class ChoixSpectacle(models.Model):
class Attribution(models.Model):
participant = models.ForeignKey(Participant)
spectacle = models.ForeignKey(Spectacle, related_name="attribues")
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):
@ -208,36 +225,83 @@ class Attribution(models.Model):
class SpectacleRevente(models.Model):
attribution = models.OneToOneField(Attribution,
related_name="revente")
attribution = models.OneToOneField(
Attribution, on_delete=models.CASCADE,
related_name="revente",
)
date = models.DateTimeField("Date de mise en vente",
default=timezone.now)
answered_mail = models.ManyToManyField(Participant,
related_name="wanted",
blank=True)
seller = models.ForeignKey(Participant,
related_name="original_shows",
verbose_name="Vendeur")
soldTo = models.ForeignKey(Participant, blank=True, null=True,
verbose_name="Vendue à")
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."""
# L'acheteur doit être connu au plus 12h avant le spectacle
remaining_time = (self.attribution.spectacle.date
- self.date - timedelta(hours=13))
# Au minimum, on attend 2 jours avant le tirage
delay = min(remaining_time, timedelta(days=2))
# Le vendeur a aussi 1h pour changer d'avis
return self.date + delay + timedelta(hours=1)
- 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,
@ -246,6 +310,18 @@ class SpectacleRevente(models.Model):
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
@ -266,6 +342,7 @@ class SpectacleRevente(models.Model):
]
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
self.save()
def mail_shotgun(self):
@ -287,58 +364,79 @@ class SpectacleRevente(models.Model):
]
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):
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.answered_mail.all())
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
datatuple = []
context = {
'acheteur': winner.user,
'vendeur': seller.user,
'show': spectacle,
}
datatuple.append((
'bda-revente-winner',
context,
settings.MAIL_DATA['revente']['FROM'],
[winner.user.email],
))
datatuple.append((
'bda-revente-seller',
context,
settings.MAIL_DATA['revente']['FROM'],
[seller.user.email]
))
if send_mails:
mails = []
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context['acheteur'] = inscrit.user
datatuple.append((
'bda-revente-loser',
new_context,
settings.MAIL_DATA['revente']['FROM'],
[inscrit.user.email]
))
send_mass_custom_mail(datatuple)
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

View file

@ -14,7 +14,7 @@
</tr></thead>
<tbody class="bda_formset_content">
{% endif %}
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
{% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}">

View file

@ -27,6 +27,14 @@ var django = {
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);

View file

@ -1,33 +0,0 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
{% csrf_token %}
<div class="form-group">
<h3>Spectacles</h3>
<br/>
<button type="button" class="btn btn-primary" onClick="select(true)">Tout sélectionner</button>
<button type="button" class="btn btn-primary" onClick="select(false)">Tout désélectionner</button>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{checkbox}}</li>
{%endfor%}
</ul>
</div>
</div>
<input type="submit" class="btn btn-primary" value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -3,41 +3,46 @@
{% 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>
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
<ul>
{% for member in members %}
<li>{{ member.get_full_name }} ({{ member.email }})</li>
{% endfor %}
</ul>
{% else %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle
{{ show.title }}&nbsp;?</h3>
{% if show.rappel_sent %}
<p class="error">Attention, les mails ont déjà été envoyés le
{{ show.rappel_sent }}</p>
{% endif %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }}&nbsp;?</h3>
{% endif %}
{% if not sent %}
<form action="" method="post">
{% csrf_token %}
<div class="pull-right">
<input class="btn btn-primary" type="submit" value="Envoyer" />
</div>
</form>
{% endif %}
<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 \>
<br/>
<hr/>
<h3>Forme des mails</h3>
<h4>Une seule place</h4>
{% for part in exemple_mail_1place %}
<pre>{{ part }}</pre>
{% endfor %}
{% for part in exemple_mail_1place %}
<pre>{{ part }}</pre>
{% endfor %}
<h4>Deux places</h4>
{% for part in exemple_mail_2places %}
<pre>{{ part }}</pre>
<pre>{{ part }}</pre>
{% endfor %}
{% endblock %}

View file

@ -36,17 +36,26 @@
</tbody>
</table>
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
<br>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
<pre id="export-mails" style="display:none">
{%for participant in participants %}{{participant.email}}, {%endfor%}
</pre>
<br>
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
<pre id="export-salle" style="display:none">
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
<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 %}
</pre>
{% 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>

View file

@ -16,7 +16,7 @@
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
<br/>
<p>Ne manque pas un spectacle avec le
<a href="{% url "gestioncof.views.calendar" %}">calendrier
<a href="{% url "calendar" %}">calendrier
automatique&#8239;!</a></p>
{% else %}
<h3>Vous n'avez aucune place :(</h3>

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

@ -5,7 +5,7 @@
{% if shotgun %}
<ul class="list-unstyled">
{% for spectacle in shotgun %}
<li><a href="{% url "bda-buy-revente" spectacle.id %}">{{spectacle}}</a></li>
<li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %}
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>

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

@ -6,7 +6,7 @@
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
<p>Si personne n'était intéressé, elle est maintenant disponible
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
<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 %}

View file

@ -1,56 +0,0 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Revente de place</h2>
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
{% if resellform.attributions %}
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{{resellform|bootstrap}}
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
{% endif %}
<br>
{% if annul_attributions or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% csrf_token %}
<div class='form-group'>
<div class='multiple-checkbox'>
<ul>
{% for attrib in annul_attributions %}
<li>{{attrib.tag}} {{attrib.choice_label}}</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{attrib.spectacle}}
</li>
{% endfor %}
{% if annul_attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
{% endif %}
<br>
{% if sold_attributions %}
<h3>Places revendues</h3>
<form action="" method="post">
{% csrf_token %}
{{soldform|bootstrap}}
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}
<p>Plus de reventes possibles !</p>
{% endif %}
{% endwith %}
{% endblock %}

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

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

@ -0,0 +1,100 @@
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']],
)

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

@ -0,0 +1,69 @@
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.utils import timezone
from datetime import timedelta
from bda.models import (Tirage, Spectacle, Salle, CategorieSpectacle,
SpectacleRevente, Attribution, Participant)
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))

View file

@ -1,9 +1,10 @@
import json
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.utils import timezone
from .models import Tirage, Spectacle, Salle, CategorieSpectacle
from bda.models import Tirage, Spectacle, Salle, CategorieSpectacle
class TestBdAViews(TestCase):
@ -34,11 +35,36 @@ class TestBdAViews(TestCase):
),
])
self.bda_user = User.objects.create_user(
username="bda_user", password="bda4ever"
)
self.bda_user.profile.is_cof = True
self.bda_user.profile.is_buro = True
self.bda_user.profile.save()
def bda_participants(self):
"""The BdA participants views can be queried"""
client = Client()
show = self.tirage.spectacle_set.first()
client.login(self.bda_user.username, "bda4ever")
tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id))
show_resp = client.get(
"/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
)
reminder_url = "/bda/mails-rappel/{}".format(show.id)
reminder_get_resp = client.get(reminder_url)
reminder_post_resp = client.post(reminder_url)
self.assertEqual(200, tirage_resp.status_code)
self.assertEqual(200, show_resp.status_code)
self.assertEqual(200, reminder_get_resp.status_code)
self.assertEqual(200, reminder_post_resp.status_code)
def test_catalogue(self):
"""Test the catalogue JSON API"""
client = Client()
# The `list` hooh
# The `list` hook
resp = client.get("/bda/catalogue/list")
self.assertJSONEqual(
resp.content.decode("utf-8"),

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.conf.urls import url
from gestioncof.decorators import buro_required
from bda.views import SpectacleListView
@ -16,9 +10,6 @@ urlpatterns = [
url(r'^places/(?P<tirage_id>\d+)$',
views.places,
name="bda-places-attribuees"),
url(r'^revente/(?P<tirage_id>\d+)$',
views.revente,
name='bda-revente'),
url(r'^etat-places/(?P<tirage_id>\d+)$',
views.etat_places,
name='bda-etat-places'),
@ -32,19 +23,38 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
views.unpaid,
name="bda-unpaid"),
url(r'^liste-revente/(?P<tirage_id>\d+)$',
views.list_revente,
name="bda-liste-revente"),
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
views.buy_revente,
name="bda-buy-revente"),
url(r'^revente-interested/(?P<revente_id>\d+)$',
views.revente_interested,
name='bda-revente-interested'),
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
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-shotgun"),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
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,

View file

@ -1,21 +1,17 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from functools import partial
import random
import hashlib
import time
import json
from datetime import timedelta
from custommail.shortcuts import (
send_mass_custom_mail, send_custom_mail, render_custom_mail
)
from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
from custommail.models import CustomMail
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db import transaction
from django.core import serializers
from django.db.models import Count, Q, Prefetch
from django.template.defaultfilters import pluralize
from django.forms.models import inlineformset_factory
from django.http import (
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
@ -27,14 +23,16 @@ from django.views.generic.list import ListView
from gestioncof.decorators import cof_required, buro_required
from bda.models import (
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
SpectacleRevente, Salle, Quote, CategorieSpectacle
SpectacleRevente, Salle, CategorieSpectacle
)
from bda.algorithm import Algorithm
from bda.forms import (
TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
InscriptionInlineFormSet,
InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm
)
from utils.views.autocomplete import Select2QuerySetView
@cof_required
def etat_places(request, tirage_id):
@ -305,7 +303,8 @@ def do_tirage(tirage_elt, token):
# 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é)
# Suppression des reventes demandées/enregistrées
# (si le tirage est relancé)
(
ChoixRevente.objects
.filter(spectacle__tirage=tirage_elt)
@ -350,13 +349,21 @@ def tirage(request, tirage_id):
@login_required
def revente(request, tirage_id):
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-notpaid.html", {})
return render(request, "bda/revente/notpaid.html", {})
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
@ -376,12 +383,8 @@ def revente(request, tirage_id):
attribution=attribution,
defaults={'seller': participant})
if not created:
revente.seller = participant
revente.date = timezone.now()
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
revente.shotgun = False
revente.reset()
context = {
'vendeur': participant.user,
'show': attribution.spectacle,
@ -398,18 +401,18 @@ def revente(request, tirage_id):
elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul')
if annulform.is_valid():
attributions = annulform.cleaned_data["attributions"]
for attribution in attributions:
attribution.revente.delete()
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():
attributions = soldform.cleaned_data['attributions']
for attribution in attributions:
attribution.participant = attribution.revente.soldTo
attribution.save()
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
@ -417,18 +420,13 @@ def revente(request, tirage_id):
elif 'reinit' in request.POST:
soldform = SoldForm(participant, request.POST, prefix='sold')
if soldform.is_valid():
attributions = soldform.cleaned_data['attributions']
for attribution in attributions:
if attribution.spectacle.date > timezone.now():
revente = attribution.revente
revente.date = timezone.now() - timedelta(minutes=65)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
revente.shotgun = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
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(),
@ -438,28 +436,80 @@ def revente(request, tirage_id):
.filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
return render(request, "bda/reventes.html",
return render(request, "bda/revente/manage.html",
{'tirage': tirage, 'overdue': overdue, "soldform": soldform,
"annulform": annulform, "resellform": resellform})
@login_required
def revente_interested(request, revente_id):
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 (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
return render(request, "bda-wrongtime.html",
if not revente.notif_sent or revente.shotgun:
return render(request, "bda/revente/wrongtime.html",
{"revente": revente})
revente.answered_mail.add(participant)
return render(request, "bda-interested.html",
revente.confirmed_entry.add(participant)
return render(request, "bda/revente/confirmed.html",
{"spectacle": revente.attribution.spectacle,
"date": revente.date_tirage})
@login_required
def list_revente(request, tirage_id):
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)
@ -485,12 +535,12 @@ def list_revente(request, tirage_id):
# la revente ayant le moins d'inscrits
min_resell = (
qset.filter(shotgun=False)
.annotate(nb_subscribers=Count('answered_mail'))
.annotate(nb_subscribers=Count('confirmed_entry'))
.order_by('nb_subscribers')
.first()
)
if min_resell is not None:
min_resell.answered_mail.add(participant)
min_resell.confirmed_entry.add(participant)
inscrit_revente.append(spectacle)
success = True
else:
@ -513,11 +563,11 @@ def list_revente(request, tirage_id):
)
messages.info(request, msg, extra_tags="safe")
return render(request, "bda/liste-reventes.html", {"form": form})
return render(request, "bda/revente/subscribe.html", {"form": form})
@login_required
def buy_revente(request, spectacle_id):
def revente_buy(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
participant, _ = Participant.objects.get_or_create(
@ -531,13 +581,13 @@ def buy_revente(request, spectacle_id):
own_reventes = reventes.filter(seller=participant)
if len(own_reventes) > 0:
own_reventes[0].delete()
return HttpResponseRedirect(reverse("bda-shotgun",
return HttpResponseRedirect(reverse("bda-revente-shotgun",
args=[tirage.id]))
reventes_shotgun = reventes.filter(shotgun=True)
if not reventes_shotgun:
return render(request, "bda-no-revente.html", {})
return render(request, "bda/revente/none.html", {})
if request.POST:
revente = random.choice(reventes_shotgun)
@ -554,11 +604,11 @@ def buy_revente(request, spectacle_id):
[revente.seller.user.email],
context=context,
)
return render(request, "bda-success.html",
return render(request, "bda/revente/mail-success.html",
{"seller": revente.attribution.participant.user,
"spectacle": spectacle})
return render(request, "revente-confirm.html",
return render(request, "bda/revente/confirm-shotgun.html",
{"spectacle": spectacle,
"user": request.user})
@ -582,7 +632,7 @@ def revente_shotgun(request, tirage_id):
)
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
return render(request, "bda-shotgun.html",
return render(request, "bda/revente/shotgun.html",
{"shotgun": shotgun})
@ -612,7 +662,7 @@ def spectacle(request, tirage_id, spectacle_id):
participants_info = sorted(participants.values(),
key=lambda part: part['lastname'])
return render(request, "bda-participants.html",
return render(request, "bda/participants.html",
{"spectacle": spectacle, "participants": participants_info})
@ -629,7 +679,7 @@ class SpectacleListView(ListView):
return categories
def get_context_data(self, **kwargs):
context = super(SpectacleListView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['tirage_id'] = self.tirage.id
context['tirage_name'] = self.tirage.title
return context
@ -651,20 +701,24 @@ def unpaid(request, tirage_id):
def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples
exemple_mail_1place = render_custom_mail('bda-rappel', {
custommail = CustomMail.objects.get(shortname="bda-rappel")
exemple_mail_1place = custommail.render({
'member': request.user,
'show': show,
'nb_attr': 1
})
exemple_mail_2places = render_custom_mail('bda-rappel', {
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}
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()
@ -673,6 +727,14 @@ def send_rappel(request, spectacle_id):
# 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)
@ -771,9 +833,9 @@ def catalogue(request, request_type):
.select_related('location')
.prefetch_related('quote_set')
)
if categories_id:
if categories_id and 0 not in categories_id:
shows_qs = shows_qs.filter(category__id__in=categories_id)
if locations_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
@ -802,3 +864,26 @@ def catalogue(request, request_type):
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())

View file

@ -1,9 +1,5 @@
# -*- encoding: utf-8 -*-
"""
Formats français.
"""
from __future__ import unicode_literals
DATETIME_FORMAT = r'l j F Y \à H:i'

View file

@ -1,3 +1,6 @@
from kfet.routing import channel_routing as kfet_channel_routings
from channels.routing import include
channel_routing = kfet_channel_routings
routing = [
include('kfet.routing.routing', path=r'^/ws/k-fet'),
]

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Django common settings for cof project.
@ -7,68 +6,117 @@ the local development server should be here.
"""
import os
import sys
# Database credentials
try:
from .secret import DBNAME, DBUSER, DBPASSWD
from . import secret
except ImportError:
# On the local development VM, theses credentials are in the environment
DBNAME = os.environ["DBNAME"]
DBUSER = os.environ["DBUSER"]
DBPASSWD = os.environ["DBPASSWD"]
except KeyError:
raise RuntimeError("Secrets missing")
# Other secrets
try:
from .secret import (
SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS,
REDIS_PASSWD, REDIS_DB, REDIS_HOST, REDIS_PORT
raise ImportError(
"The secret.py file is missing.\n"
"For a development environment, simply copy secret_example.py"
)
except ImportError:
raise RuntimeError("Secrets missing")
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',
'grappelli',
'django.contrib.admin',
'django.contrib.admindocs',
'bda',
'autocomplete_light',
'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_CLASSES = [
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.middleware.KFetAuthenticationMiddleware',
'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'
@ -84,12 +132,13 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'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.context_processors.auth',
'kfet.auth.context_processors.temporary_auth',
'kfet.context_processors.config',
],
},
@ -98,7 +147,7 @@ TEMPLATES = [
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': DBNAME,
'USER': DBUSER,
'PASSWORD': DBPASSWD,
@ -143,17 +192,47 @@ 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.backends.GenericTeamBackend',
'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 = {
@ -166,8 +245,14 @@ CHANNEL_LAYERS = {
port=REDIS_PORT, db=REDIS_DB)
)],
},
"ROUTING": "cof.routing.channel_routing",
"ROUTING": "cof.routing.routing",
}
}
FORMAT_MODULE_PATH = 'cof.locale'
# Wagtail settings
WAGTAIL_SITE_NAME = 'GestioCOF'
WAGTAIL_ENABLE_UPDATE_CHECK = False
TAGGIT_CASE_INSENSITIVE = True

View file

@ -3,24 +3,28 @@ Django development settings for the cof project.
The settings that are not listed here are imported from .common
"""
import os
from .common import *
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 = '/var/www/static/'
STATIC_ROOT = '/srv/gestiocof/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_ROOT = '/srv/gestiocof/media/'
MEDIA_URL = '/media/'
@ -37,11 +41,13 @@ def show_toolbar(request):
"""
return DEBUG
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE_CLASSES = (
["debug_panel.middleware.DebugPanelMiddleware"]
+ MIDDLEWARE_CLASSES
)
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
}
if not TESTING:
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE = [
"debug_panel.middleware.DebugPanelMiddleware"
] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
}

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

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

View file

@ -5,7 +5,8 @@ The settings that are not listed here are imported from .common
import os
from .common import *
from .common import * # NOQA
from .common import BASE_DIR, import_secret
DEBUG = False
@ -16,11 +17,18 @@ ALLOWED_HOSTS = [
"dev.cof.ens.fr"
]
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static")
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/"
LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636"
EMAIL_HOST = "nef.ens.fr"
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")

View file

@ -1,8 +1,21 @@
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
RECAPTCHA_PUBLIC_KEY = "DUMMY"
RECAPTCHA_PRIVATE_KEY = "DUMMY"
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"
ADMINS = None
RECAPTCHA_PUBLIC_KEY = "DUMMY"
RECAPTCHA_PRIVATE_KEY = "DUMMY"
EMAIL_HOST = None
KFETOPEN_TOKEN = "plop"
LDAP_SERVER_URL = None

View file

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
"""
Fichier principal de configuration des urls du projet GestioCOF
"""
import autocomplete_light
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
@ -14,13 +10,16 @@ from django.views.generic.base import TemplateView
from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from gestioncof import views as gestioncof_views, csv_views
from gestioncof.urls import export_patterns, petitcours_patterns, \
surveys_patterns, events_patterns, calendar_patterns, \
clubs_patterns
from gestioncof.autocomplete import autocomplete
autocomplete_light.autodiscover()
admin.autodiscover()
urlpatterns = [
@ -45,18 +44,22 @@ urlpatterns = [
name="cof-denied"),
url(r'^cas/login$', django_cas_views.login, name="cas_login_view"),
url(r'^cas/logout$', django_cas_views.logout),
url(r'^outsider/login$', gestioncof_views.login_ext),
url(r'^outsider/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),
url(r'^logout$', gestioncof_views.logout, name="cof-logout"),
# Infos persos
url(r'^profile$', gestioncof_views.profile),
url(r'^outsider/password-change$', django_views.password_change),
url(r'^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),
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"),
@ -65,8 +68,10 @@ urlpatterns = [
url(r'^registration/empty$', gestioncof_views.registration_form2,
name="empty-registration"),
# Autocompletion
url(r'^autocomplete/registration$', autocomplete),
url(r'^autocomplete/', include('autocomplete_light.urls')),
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')),
@ -74,14 +79,23 @@ urlpatterns = [
csv_views.admin_list_export,
{'fields': ['username', ]}),
url(r'^admin/', include(admin.site.urls)),
url(r'^grappelli/', include('grappelli.urls')),
# Liens utiles du COF et du BdA
url(r'^utile_cof$', gestioncof_views.utile_cof),
url(r'^utile_bda$', gestioncof_views.utile_bda),
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^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:
@ -90,7 +104,13 @@ if 'debug_toolbar' in settings.INSTALLED_APPS:
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)),
]

View file

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

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
@ -18,13 +12,12 @@ from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.db.models import Q
import django.utils.six as six
import autocomplete_light
from dal.autocomplete import ModelSelect2
def add_link_field(target_model='', field='', link_text=six.text_type,
desc_text=six.text_type):
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()
@ -139,7 +132,6 @@ def ProfileInfo(field, short_description, boolean=False):
User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper")
User.profile_num = FkeyLookup("profile__num", "Numéro")
User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement")
@ -166,10 +158,12 @@ class UserProfileAdmin(UserAdmin):
is_cof.short_description = 'Membre du COF'
is_cof.boolean = True
list_display = ('profile_num',) + UserAdmin.list_display \
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',
@ -187,7 +181,7 @@ class UserProfileAdmin(UserAdmin):
def get_fieldsets(self, request, user=None):
if not request.user.is_superuser:
return self.staff_fieldsets
return super(UserProfileAdmin, self).get_fieldsets(request, user)
return super().get_fieldsets(request, user)
def save_model(self, request, user, form, change):
cof_group, created = Group.objects.get_or_create(name='COF')
@ -215,21 +209,25 @@ class UserProfileAdmin(UserAdmin):
# FIXME: This is absolutely horrible.
def user_unicode(self):
def user_str(self):
if self.first_name and self.last_name:
return "%s %s (%s)" % (self.first_name, self.last_name, self.username)
return "{} ({})".format(self.get_full_name(), self.username)
else:
return self.username
if six.PY2:
User.__unicode__ = user_unicode
else:
User.__str__ = user_unicode
User.__str__ = user_str
class EventRegistrationAdminForm(forms.ModelForm):
class Meta:
widgets = {
'user': ModelSelect2(url='cof-user-autocomplete'),
}
class EventRegistrationAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[])
list_display = ('__unicode__' if six.PY2 else '__str__', 'event', 'user',
'paid')
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')
@ -269,7 +267,7 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin):
class ClubAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = super(ClubAdminForm, self).clean()
cleaned_data = super().clean()
respos = cleaned_data.get('respos')
members = cleaned_data.get('membres')
for respo in respos.all():

15
gestioncof/apps.py Normal file
View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from ldap3 import Connection
from django import shortcuts
@ -58,7 +56,7 @@ def autocomplete(request):
)
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching
ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit)

View file

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
import autocomplete_light
from django.contrib.auth.models import User
autocomplete_light.register(
User, search_fields=('username', 'first_name', 'last_name'),
attrs={'placeholder': 'membre...'}
)

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import csv
from django.http import HttpResponse, HttpResponseForbidden
from django.template.defaultfilters import slugify

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import user_passes_test

View file

@ -1,21 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory
from django.db.models import Max
from django.core.validators import MinLengthValidator
from djconfig.forms import ConfigForm
from gestioncof.models import CofProfile, EventCommentValue, \
CalendarSubscription, Club
from gestioncof.widgets import TriStateCheckbox
from gestioncof.shared import lock_table, unlock_table
from bda.models import Spectacle
@ -25,7 +18,7 @@ class EventForm(forms.Form):
event = kwargs.pop("event")
self.event = event
current_choices = kwargs.pop("current_choices", None)
super(EventForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
choices = {}
if current_choices:
for choice in current_choices.all():
@ -67,7 +60,7 @@ class SurveyForm(forms.Form):
def __init__(self, *args, **kwargs):
survey = kwargs.pop("survey")
current_answers = kwargs.pop("current_answers", None)
super(SurveyForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
answers = {}
if current_answers:
for answer in current_answers.all():
@ -107,7 +100,7 @@ class SurveyForm(forms.Form):
class SurveyStatusFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
survey = kwargs.pop("survey")
super(SurveyStatusFilterForm, self).__init__(*args, **kwargs)
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)
@ -136,7 +129,7 @@ class SurveyStatusFilterForm(forms.Form):
class EventStatusFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
event = kwargs.pop("event")
super(EventStatusFilterForm, self).__init__(*args, **kwargs)
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)
@ -182,12 +175,12 @@ class UserProfileForm(forms.ModelForm):
last_name = forms.CharField(label=_('Nom'), max_length=30)
def __init__(self, *args, **kw):
super(UserProfileForm, self).__init__(*args, **kw)
super().__init__(*args, **kw)
self.fields['first_name'].initial = self.instance.user.first_name
self.fields['last_name'].initial = self.instance.user.last_name
def save(self, *args, **kw):
super(UserProfileForm, self).save(*args, **kw)
super().save(*args, **kw)
self.instance.user.first_name = self.cleaned_data.get('first_name')
self.instance.user.last_name = self.cleaned_data.get('last_name')
self.instance.user.save()
@ -200,12 +193,9 @@ class UserProfileForm(forms.ModelForm):
class RegistrationUserForm(forms.ModelForm):
def __init__(self, *args, **kw):
super(RegistrationUserForm, self).__init__(*args, **kw)
super().__init__(*args, **kw)
self.fields['username'].help_text = ""
def force_long_username(self):
self.fields['username'].validators = [MinLengthValidator(9)]
class Meta:
model = User
fields = ("username", "first_name", "last_name", "email")
@ -229,8 +219,7 @@ class RegistrationPassUserForm(RegistrationUserForm):
return pass2
def save(self, commit=True, *args, **kwargs):
user = super(RegistrationPassUserForm, self).save(commit, *args,
**kwargs)
user = super().save(commit, *args, **kwargs)
user.set_password(self.cleaned_data['password2'])
if commit:
user.save()
@ -239,11 +228,10 @@ class RegistrationPassUserForm(RegistrationUserForm):
class RegistrationProfileForm(forms.ModelForm):
def __init__(self, *args, **kw):
super(RegistrationProfileForm, self).__init__(*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['num'].widget.attrs['readonly'] = True
self.fields.keyOrder = [
'login_clipper',
@ -251,7 +239,6 @@ class RegistrationProfileForm(forms.ModelForm):
'occupation',
'departement',
'is_cof',
'num',
'type_cotiz',
'mailing_cof',
'mailing_bda',
@ -259,24 +246,9 @@ class RegistrationProfileForm(forms.ModelForm):
'comments'
]
def save(self, *args, **kw):
instance = super(RegistrationProfileForm, self).save(*args, **kw)
if instance.is_cof and not instance.num:
# Generate new number
try:
lock_table(CofProfile)
aggregate = CofProfile.objects.aggregate(Max('num'))
instance.num = aggregate['num__max'] + 1
instance.save()
self.cleaned_data['num'] = instance.num
self.data['num'] = instance.num
finally:
unlock_table(CofProfile)
return instance
class Meta:
model = CofProfile
fields = ("login_clipper", "num", "phone", "occupation",
fields = ("login_clipper", "phone", "occupation",
"departement", "is_cof", "type_cotiz", "mailing_cof",
"mailing_bda", "mailing_bda_revente", "comments")
@ -301,7 +273,7 @@ class AdminEventForm(forms.Form):
kwargs["initial"] = {"status": "wait"}
else:
kwargs["initial"] = {"status": "no"}
super(AdminEventForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
choices = {}
for choice in current_choices:
if choice.event_option.id not in choices:
@ -364,24 +336,27 @@ class BaseEventRegistrationFormset(BaseFormSet):
self.events = kwargs.pop('events')
self.current_registrations = kwargs.pop('current_registrations', None)
self.extra = len(self.events)
super(BaseEventRegistrationFormset, self).__init__(*args, **kwargs)
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(BaseEventRegistrationFormset, self)._construct_form(
index, **kwargs)
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")
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")
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),
@ -403,3 +378,16 @@ class ClubsForm(forms.Form):
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

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Import des mails de GestioCOF dans la base de donnée
"""
@ -63,8 +62,9 @@ class Command(BaseCommand):
except CustomMail.DoesNotExist:
mail = CustomMail.objects.create(**fields)
status['synced'] += 1
self.stdout.write(
'SYNCED {:s}'.format(fields['shortname']))
if options['verbosity']:
self.stdout.write(
'SYNCED {:s}'.format(fields['shortname']))
assoc['mails'][obj['pk']] = mail
# Variables
@ -79,8 +79,9 @@ class Command(BaseCommand):
except Variable.DoesNotExist:
Variable.objects.create(**fields)
# C'est agréable d'avoir le résultat affiché
self.stdout.write(
'{synced:d} mails synchronized {unchanged:d} unchanged'
.format(**status)
)
if options['verbosity']:
# C'est agréable d'avoir le résultat affiché
self.stdout.write(
'{synced:d} mails synchronized {unchanged:d} unchanged'
.format(**status)
)

View file

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

View file

@ -48,7 +48,7 @@ class Migration(migrations.Migration):
('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)),
('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Profil COF',
@ -91,7 +91,7 @@ class Migration(migrations.Migration):
('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')),
('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event', on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Champ',
@ -102,7 +102,7 @@ class Migration(migrations.Migration):
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')),
('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField', on_delete=models.CASCADE)),
],
),
migrations.CreateModel(
@ -111,7 +111,7 @@ class Migration(migrations.Migration):
('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')),
('event', models.ForeignKey(related_name='options', to='gestioncof.Event', on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Option',
@ -122,7 +122,7 @@ class Migration(migrations.Migration):
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')),
('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption', on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Choix',
@ -133,10 +133,10 @@ class Migration(migrations.Migration):
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')),
('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)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Inscription',
@ -240,7 +240,7 @@ class Migration(migrations.Migration):
('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')),
('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey', on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Question',
@ -251,7 +251,7 @@ class Migration(migrations.Migration):
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')),
('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion', on_delete=models.CASCADE)),
],
options={
'verbose_name': 'R\xe9ponse',
@ -265,12 +265,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='surveyanswer',
name='survey',
field=models.ForeignKey(to='gestioncof.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),
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
),
migrations.AddField(
model_name='petitcoursdemande',
@ -280,47 +280,47 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='petitcoursdemande',
name='traitee_par',
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True),
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'),
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),
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'),
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'),
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),
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'),
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),
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'),
field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration', on_delete=models.CASCADE),
),
migrations.AlterUniqueTogether(
name='surveyanswer',

View file

@ -23,7 +23,8 @@ class Migration(migrations.Migration):
('subscribe_to_events', models.BooleanField(default=True)),
('subscribe_to_my_shows', models.BooleanField(default=True)),
('other_shows', models.ManyToManyField(to='bda.Spectacle')),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
],
),
migrations.AlterModelOptions(

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 = [
('gestioncof', '0010_delete_custommail'),
]
operations = [
migrations.AlterField(
model_name='cofprofile',
name='login_clipper',
field=models.CharField(verbose_name='Login clipper', blank=True, max_length=32),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0010_delete_custommail'),
]
operations = [
migrations.RemoveField(
model_name='cofprofile',
name='num',
),
]

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0011_remove_cofprofile_num'),
('gestioncof', '0011_longer_clippers'),
]
operations = [
]

View file

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0012_merge'),
]
operations = [
migrations.AlterField(
model_name='cofprofile',
name='occupation',
field=models.CharField(
verbose_name='Occupation',
max_length=9,
default='1A',
choices=[
('exterieur', 'Extérieur'),
('1A', '1A'),
('2A', '2A'),
('3A', '3A'),
('4A', '4A'),
('archicube', 'Archicube'),
('doctorant', 'Doctorant'),
('CST', 'CST'),
('PEI', 'PEI')
]),
),
migrations.AlterField(
model_name='cofprofile',
name='type_cotiz',
field=models.CharField(
verbose_name='Type de cotisation',
max_length=9,
default='normalien',
choices=[
('etudiant', 'Normalien étudiant'),
('normalien', 'Normalien élève'),
('exterieur', 'Extérieur'),
('gratis', 'Gratuit')
]),
),
]

View file

@ -1,46 +1,62 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
import django.utils.six as six
from django.db.models.signals import post_save, post_delete
from gestioncof.petits_cours_models import choices_length
from bda.models import Spectacle
OCCUPATION_CHOICES = (
('exterieur', _("Extérieur")),
('1A', _("1A")),
('2A', _("2A")),
('3A', _("3A")),
('4A', _("4A")),
('archicube', _("Archicube")),
('doctorant', _("Doctorant")),
('CST', _("CST")),
)
TYPE_COTIZ_CHOICES = (
('etudiant', _("Normalien étudiant")),
('normalien', _("Normalien élève")),
('exterieur', _("Extérieur")),
)
TYPE_COMMENT_FIELD = (
('text', _("Texte long")),
('char', _("Texte court")),
)
@python_2_unicode_compatible
class CofProfile(models.Model):
user = models.OneToOneField(User, related_name="profile")
login_clipper = models.CharField("Login clipper", max_length=8, blank=True)
STATUS_EXTE = "exterieur"
STATUS_1A = "1A"
STATUS_2A = "2A"
STATUS_3A = "3A"
STATUS_4A = "4A"
STATUS_ARCHI = "archicube"
STATUS_DOCTORANT = "doctorant"
STATUS_CST = "CST"
STATUS_PEI = "PEI"
OCCUPATION_CHOICES = (
(STATUS_EXTE, _("Extérieur")),
(STATUS_1A, _("1A")),
(STATUS_2A, _("2A")),
(STATUS_3A, _("3A")),
(STATUS_4A, _("4A")),
(STATUS_ARCHI, _("Archicube")),
(STATUS_DOCTORANT, _("Doctorant")),
(STATUS_CST, _("CST")),
(STATUS_PEI, _("PEI")),
)
COTIZ_ETUDIANT = "etudiant"
COTIZ_NORMALIEN = "normalien"
COTIZ_EXTE = "exterieur"
COTIZ_GRATIS = "gratis"
TYPE_COTIZ_CHOICES = (
(COTIZ_ETUDIANT, _("Normalien étudiant")),
(COTIZ_NORMALIEN, _("Normalien élève")),
(COTIZ_EXTE, _("Extérieur")),
(COTIZ_GRATIS, _("Gratuit")),
)
user = models.OneToOneField(
User, on_delete=models.CASCADE,
related_name="profile",
)
login_clipper = models.CharField(
"Login clipper", max_length=32, blank=True
)
is_cof = models.BooleanField("Membre du COF", default=False)
num = models.IntegerField("Numéro d'adhérent", blank=True, default=0)
phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField(_("Occupation"),
default="1A",
@ -72,7 +88,7 @@ class CofProfile(models.Model):
verbose_name_plural = "Profils COF"
def __str__(self):
return six.text_type(self.user.username)
return self.user.username
@receiver(post_save, sender=User)
@ -86,7 +102,6 @@ def post_delete_user(sender, instance, *args, **kwargs):
instance.user.delete()
@python_2_unicode_compatible
class Club(models.Model):
name = models.CharField("Nom", max_length=200, unique=True)
description = models.TextField("Description", blank=True)
@ -98,7 +113,6 @@ class Club(models.Model):
return self.name
@python_2_unicode_compatible
class Event(models.Model):
title = models.CharField("Titre", max_length=200)
location = models.CharField("Lieu", max_length=200)
@ -115,12 +129,14 @@ class Event(models.Model):
verbose_name = "Événement"
def __str__(self):
return six.text_type(self.title)
return self.title
@python_2_unicode_compatible
class EventCommentField(models.Model):
event = models.ForeignKey(Event, related_name="commentfields")
event = models.ForeignKey(
Event, on_delete=models.CASCADE,
related_name="commentfields",
)
name = models.CharField("Champ", max_length=200)
fieldtype = models.CharField("Type", max_length=10,
choices=TYPE_COMMENT_FIELD, default="text")
@ -130,23 +146,29 @@ class EventCommentField(models.Model):
verbose_name = "Champ"
def __str__(self):
return six.text_type(self.name)
return self.name
@python_2_unicode_compatible
class EventCommentValue(models.Model):
commentfield = models.ForeignKey(EventCommentField, related_name="values")
registration = models.ForeignKey("EventRegistration",
related_name="comments")
commentfield = models.ForeignKey(
EventCommentField, on_delete=models.CASCADE,
related_name="values",
)
registration = models.ForeignKey(
"EventRegistration", on_delete=models.CASCADE,
related_name="comments",
)
content = models.TextField("Contenu", blank=True, null=True)
def __str__(self):
return "Commentaire de %s" % self.commentfield
@python_2_unicode_compatible
class EventOption(models.Model):
event = models.ForeignKey(Event, related_name="options")
event = models.ForeignKey(
Event, on_delete=models.CASCADE,
related_name="options",
)
name = models.CharField("Option", max_length=200)
multi_choices = models.BooleanField("Choix multiples", default=False)
@ -154,12 +176,14 @@ class EventOption(models.Model):
verbose_name = "Option"
def __str__(self):
return six.text_type(self.name)
return self.name
@python_2_unicode_compatible
class EventOptionChoice(models.Model):
event_option = models.ForeignKey(EventOption, related_name="choices")
event_option = models.ForeignKey(
EventOption, on_delete=models.CASCADE,
related_name="choices",
)
value = models.CharField("Valeur", max_length=200)
class Meta:
@ -167,13 +191,12 @@ class EventOptionChoice(models.Model):
verbose_name_plural = "Choix"
def __str__(self):
return six.text_type(self.value)
return self.value
@python_2_unicode_compatible
class EventRegistration(models.Model):
user = models.ForeignKey(User)
event = models.ForeignKey(Event)
user = models.ForeignKey(User, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
options = models.ManyToManyField(EventOptionChoice)
filledcomments = models.ManyToManyField(EventCommentField,
through=EventCommentValue)
@ -184,11 +207,9 @@ class EventRegistration(models.Model):
unique_together = ("user", "event")
def __str__(self):
return "Inscription de %s à %s" % (six.text_type(self.user),
six.text_type(self.event.title))
return "Inscription de {} à {}".format(self.user, self.event.title)
@python_2_unicode_compatible
class Survey(models.Model):
title = models.CharField("Titre", max_length=200)
details = models.TextField("Détails", blank=True)
@ -199,12 +220,14 @@ class Survey(models.Model):
verbose_name = "Sondage"
def __str__(self):
return six.text_type(self.title)
return self.title
@python_2_unicode_compatible
class SurveyQuestion(models.Model):
survey = models.ForeignKey(Survey, related_name="questions")
survey = models.ForeignKey(
Survey, on_delete=models.CASCADE,
related_name="questions",
)
question = models.CharField("Question", max_length=200)
multi_answers = models.BooleanField("Choix multiples", default=False)
@ -212,25 +235,26 @@ class SurveyQuestion(models.Model):
verbose_name = "Question"
def __str__(self):
return six.text_type(self.question)
return self.question
@python_2_unicode_compatible
class SurveyQuestionAnswer(models.Model):
survey_question = models.ForeignKey(SurveyQuestion, related_name="answers")
survey_question = models.ForeignKey(
SurveyQuestion, on_delete=models.CASCADE,
related_name="answers",
)
answer = models.CharField("Réponse", max_length=200)
class Meta:
verbose_name = "Réponse"
def __str__(self):
return six.text_type(self.answer)
return self.answer
@python_2_unicode_compatible
class SurveyAnswer(models.Model):
user = models.ForeignKey(User)
survey = models.ForeignKey(Survey)
user = models.ForeignKey(User, on_delete=models.CASCADE)
survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
answers = models.ManyToManyField(SurveyQuestionAnswer,
related_name="selected_by")
@ -244,10 +268,9 @@ class SurveyAnswer(models.Model):
self.survey.title)
@python_2_unicode_compatible
class CalendarSubscription(models.Model):
token = models.UUIDField()
user = models.OneToOneField(User)
user = models.OneToOneField(User, on_delete=models.CASCADE)
other_shows = models.ManyToManyField(Spectacle)
subscribe_to_events = models.BooleanField(default=True)
subscribe_to_my_shows = models.BooleanField(default=True)

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from captcha.fields import ReCaptchaField
from django import forms
@ -12,7 +10,7 @@ from gestioncof.petits_cours_models import PetitCoursDemande, PetitCoursAbility
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super(BaseMatieresFormSet, self).clean()
super().clean()
if any(self.errors):
# Don't bother validating the formset unless each form is
# valid on its own
@ -36,7 +34,7 @@ class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
def __init__(self, *args, **kwargs):
super(DemandeForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.fields['matieres'].help_text = ''
class Meta:

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from functools import reduce
from django.db import models
@ -35,8 +33,11 @@ class PetitCoursSubject(models.Model):
class PetitCoursAbility(models.Model):
user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
user = models.ForeignKey(User, on_delete=models.CASCADE)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE,
verbose_name=_("Matière"),
)
niveau = models.CharField(_("Niveau"),
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES))
@ -84,7 +85,10 @@ class PetitCoursDemande(models.Model):
remarques = models.TextField(_("Remarques et précisions"), blank=True)
traitee = models.BooleanField(_("Traitée"), default=False)
traitee_par = models.ForeignKey(User, blank=True, null=True)
traitee_par = models.ForeignKey(
User, on_delete=models.CASCADE,
blank=True, null=True,
)
processed = models.DateTimeField(_("Date de traitement"),
blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=True)
@ -126,9 +130,15 @@ class PetitCoursDemande(models.Model):
class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User)
demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande"))
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
user = models.ForeignKey(User, on_delete=models.CASCADE)
demande = models.ForeignKey(
PetitCoursDemande, on_delete=models.CASCADE,
verbose_name=_("Demande"),
)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE,
verbose_name=_("Matière"),
)
date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True)
rank = models.IntegerField("Rang dans l'email")
selected = models.BooleanField(_("Sélectionné par le demandeur"),
@ -145,8 +155,11 @@ class PetitCoursAttribution(models.Model):
class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere"))
user = models.ForeignKey(User, on_delete=models.CASCADE)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE,
verbose_name=_("Matiere"),
)
count = models.IntegerField("Nombre d'envois", default=0)
@classmethod
@ -157,14 +170,16 @@ class PetitCoursAttributionCounter(models.Model):
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(
user=user, matiere=matiere)
user=user,
matiere=matiere,
)
if created:
mincount = (
cls.objects.filter(matiere=matiere).exclude(user=user)
.aggregate(Min('count'))
['count__min']
)
counter.count = mincount
counter.count = mincount or 0
counter.save()
return counter

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
import json
from datetime import datetime
from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect
@ -12,15 +9,16 @@ from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db import transaction
from django.utils import timezone
from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject
PetitCoursAbility
)
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables
class DemandeListView(ListView):
@ -44,7 +42,7 @@ class DemandeDetailView(DetailView):
context_object_name = "demande"
def get_context_data(self, **kwargs):
context = super(DemandeDetailView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
obj = self.object
context['attributions'] = obj.petitcoursattribution_set.all()
return context
@ -274,20 +272,20 @@ def _traitement_post(request, demande):
headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User)
for matiere in proposals:
for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(user=user,
matiere=matiere)
counter.count += 1
counter.save()
attrib = PetitCoursAttribution(user=user, matiere=matiere,
demande=demande, rank=rank + 1)
attrib.save()
unlock_tables()
with transaction.atomic():
for matiere in proposals:
for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(
user=user, matiere=matiere
)
counter.count += 1
counter.save()
attrib = PetitCoursAttribution(user=user, matiere=matiere,
demande=demande, rank=rank + 1)
attrib.save()
demande.traitee = True
demande.traitee_par = request.user
demande.processed = datetime.now()
demande.processed = timezone.now()
demande.save()
return render(request,
"gestioncof/traitement_demande_petit_cours_success.html",
@ -309,17 +307,15 @@ def inscription(request):
profile.petits_cours_accept = "receive_proposals" in request.POST
profile.petits_cours_remarques = request.POST["remarques"]
profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject)
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
with transaction.atomic():
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
unlock_tables()
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
success = True
formset = MatieresFormSet(instance=request.user)
else:

View file

@ -1,69 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.contrib.sites.models import Site
from django.conf import settings
from django.contrib.sites.models import Site
from django_cas_ng.backends import CASBackend
from django_cas_ng.utils import get_cas_client
from django.contrib.auth import get_user_model
from django.db import connection
from gestioncof.models import CofProfile
User = get_user_model()
class COFCASBackend(CASBackend):
def authenticate_cas(self, ticket, service, request):
"""Verifies CAS ticket and gets or creates User object"""
client = get_cas_client(service_url=service)
username, attributes, _ = client.verify_ticket(ticket)
if attributes:
request.session['attributes'] = attributes
if not username:
return None
def clean_username(self, username):
# Le CAS de l'ENS accepte les logins avec des espaces au début
# et à la fin, ainsi quavec une casse variable. On normalise pour
# éviter les doublons.
username = username.strip().lower()
return username.strip().lower()
profiles = CofProfile.objects.filter(login_clipper=username)
if len(profiles) > 0:
profile = profiles.order_by('-is_cof')[0]
user = profile.user
return user
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
# user will have an "unusable" password
user = User.objects.create_user(username, '')
user.save()
return user
def authenticate(self, ticket, service, request):
"""Authenticates CAS ticket and retrieves user data"""
user = self.authenticate_cas(ticket, service, request)
if user is None:
return user
try:
profile = user.profile
except CofProfile.DoesNotExist:
profile, created = CofProfile.objects.get_or_create(user=user)
profile.save()
if not profile.login_clipper:
profile.login_clipper = user.username
profile.save()
if not user.email:
user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper
user.save()
if profile.is_buro and not user.is_staff:
user.is_staff = True
user.save()
def configure_user(self, user):
clipper = user.username
user.profile.login_clipper = clipper
user.profile.save()
user.email = settings.CAS_EMAIL_FORMAT % clipper
user.save()
return user
@ -74,25 +30,3 @@ def context_processor(request):
"site": Site.objects.get_current(),
}
return data
def lock_table(*models):
query = "LOCK TABLES "
for i, model in enumerate(models):
table = model._meta.db_table
if i > 0:
query += ", "
query += "%s WRITE" % table
cursor = connection.cursor()
cursor.execute(query)
row = cursor.fetchone()
return row
def unlock_tables(*models):
cursor = connection.cursor()
cursor.execute("UNLOCK TABLES")
row = cursor.fetchone()
return row
unlock_table = unlock_tables

23
gestioncof/signals.py Normal file
View file

@ -0,0 +1,23 @@
from django.contrib import messages
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django_cas_ng.signals import cas_user_authenticated
@receiver(user_logged_in)
def messages_on_out_login(request, user, **kwargs):
if user.backend.startswith('django.contrib.auth'):
msg = _('Connexion à GestioCOF réussie. Bienvenue {}.').format(
user.get_short_name(),
)
messages.success(request, msg)
@receiver(cas_user_authenticated)
def mesagges_on_cas_login(request, user, **kwargs):
msg = _('Connexion à GestioCOF par CAS réussie. Bienvenue {}.').format(
user.get_short_name(),
)
messages.success(request, msg)

View file

@ -40,8 +40,9 @@ a {
background: transparent;
}
div.empty-form {
padding-bottom: 2em;
}
a:hover {
color: #444;
@ -341,10 +342,12 @@ fieldset legend {
font-weight: 700;
font-size: large;
color:#DE826B;
padding-bottom: .5em;
}
#main-content h4 {
color:#DE826B;
padding-bottom: .5em;
}
#main-content h2,
@ -778,6 +781,17 @@ header .open > .dropdown-toggle.btn-default {
border-color: white;
}
/* Announcements banner ------------------ */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* FORMS --------------------------------- */
@ -1126,3 +1140,14 @@ p.help-block {
margin: 5px auto;
width: 90%;
}
div.bg-info {
border-radius: 3px;
padding: 0.3em 1em;
margin-left: 1em;
margin-right: 1em;
}
.bootstrap-form-reduce > .form-group {
margin-top: -16px;
}

View file

@ -1,6 +0,0 @@
{% extends "admin/base.html" %}
{% block extrahead %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
{% include 'autocomplete_light/static.html' %}
{% endblock %}

View file

@ -1,78 +0,0 @@
{% extends "admin/base_site.html" %}
<!-- LOADING -->
{% load i18n grp_tags log %}
<!-- JAVASCRIPTS -->
{% block javascripts %}
{{ block.super }}
{% endblock %}
<!-- COLTYPE/BODYCLASS-- >
{% block bodyclass %}dashboard{% endblock %}
{% block content-class %}content-grid{% endblock %}
<!-- BREADCRUMBS -->
{% block breadcrumbs %}
<ul class="grp-horizontal-list">
<li>{% trans "Home" %}</li>
</ul>
{% endblock %}
{% block content_title %}
{% if title %}
<header><h1>{{ title }}</h1></header>
{% endif %}
{% endblock %}
<!-- CONTENT -->
{% block content %}
<div class="g-d-c">
<div class="g-d-12 g-d-f">
{% for app in app_list %}
<div class="grp-module" id="app_{{ app.name|lower }}">
<h2><a href="{{ app.app_url }}" class="grp-section">{% trans app.name %}</a></h2>
{% for model in app.models %}
<div class="grp-row">
{% if model.perms.change %}<a href="{{ model.admin_url }}"><strong>{{ model.name }}</strong></a>{% else %}<span><strong>{{ model.name }}</strong></span>{% endif %}
{% if model.perms.add or model.perms.change %}
<ul class="grp-actions">
{% if model.perms.add %}<li class="grp-add-link"><a href="{{ model.admin_url }}add/">{% trans 'Add' %}</a></li>{% endif %}
{% if model.perms.change %}<li class="grp-change-link"><a href="{{ model.admin_url }}">{% trans 'Change' %}</a></li>{% endif %}
</ul>
{% endif %}
</div>
{% endfor %}
</div>
{% empty %}
<p>{% trans "You don´t have permission to edit anything." %}</p>
{% endfor %}
</div>
<div class="g-d-6 g-d-l">
<div class="grp-module" id="grp-recent-actions-module">
<h2>{% trans 'Recent Actions' %}</h2>
<div class="grp-module">
<h3>{% trans 'My Actions' %}</h3>
{% get_admin_log 20 as admin_log for_user user %}
{% if not admin_log %}
<p>{% trans 'None available' %}</p>
{% else %}
<ul class="grp-listing-small">
{% for entry in admin_log %}
<li class="grp-row{% if entry.is_addition %} grp-add-link{% endif %}{% if entry.is_change %} grp-change-link{% endif %}{% if entry.is_deletion %} grp-delete-link{% endif %}">
{% if entry.is_deletion %}
<span>{{ entry.object_repr }}</span>
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<span class="grp-font-color-quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -8,13 +8,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# CSS #}
{# CSS #}
<link type="text/css" rel="stylesheet" href="{% static "css/bootstrap.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/cof.css" %}" />
<link href="https://fonts.googleapis.com/css?family=Dosis|Dosis:700|Raleway|Roboto:300,300i,700" rel="stylesheet">
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
{# JS #}
{# JS #}
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
{% block extra_head %}{% endblock %}

View file

@ -7,7 +7,7 @@
{% if success %}
<p class="success">Votre demande a été enregistrée avec succès !</p>
{% else %}
<form id="demandecours" method="post" action="{% url "gestioncof.petits_cours_views.demande_raw" %}">
<form id="demandecours" method="post" action="{% url "petits-cours-demande-raw" %}">
{% csrf_token %}
<table>
{{ form | bootstrap }}

View file

@ -5,7 +5,7 @@
{% if success %}
<p class="success">Votre demande a été enregistrée avec succès !</p>
{% else %}
<form id="demandecours" method="post" action="{% url "gestioncof.petits_cours_views.demande" %}">
<form id="demandecours" method="post" action="{% url "petits-cours-demande" %}">
{% csrf_token %}
<table>
{{ form.as_table }}

View file

@ -11,7 +11,7 @@
{% endif %}
{% include "tristate_js.html" %}
<h3>Filtres</h3>
<form method="post" action="{% url 'gestioncof.views.event_status' event.id %}">
<form method="post" action="{% url 'event.details.status' event.id %}">
{% csrf_token %}
{{ form.as_p }}
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />

View file

@ -0,0 +1,23 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% load i18n %}
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>{% trans "Global configuration" %}</h2>
<form id="profile form-horizontal" method="post" action="">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">
{% for field in form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right"
value={% trans "Save" %} />
</div>
</form>
{% endblock %}

View file

@ -3,7 +3,7 @@
{% block content %}
<header>
<div class="container banner">
<a href="{% url "gestioncof.views.home" %}">
<a href="{% url "home" %}">
<h1>GestioCOF</h1>
{% block homelink %}
<span class="glyphicon glyphicon-home" aria-hidden=true></span>
@ -11,11 +11,19 @@
</a>
<div class="secondary">
<span class="hidden-xxs">&nbsp;&nbsp;|&nbsp; </span>
<span><a href="{% url "gestioncof.views.logout" %}">Se déconnecter&nbsp;<span class="glyphicon glyphicon-log-out"></span></a></span>
<span><a href="{% url "cof-logout" %}">Se déconnecter&nbsp;<span class="glyphicon glyphicon-log-out"></span></a></span>
</div>
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
</div><!-- /.container -->
</header>
{% if config.gestion_banner %}
<div id="banner" class="container">
<span class="glyphicon glyphicon-bullhorn"></span>
<span>{{ config.gestion_banner }}</span>
</div>
{% endif %}
{% if messages %}
{% for message in messages %}
<div class="messages">

View file

@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
{% if token %}
<p>Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à
<a href="{% url 'gestioncof.views.calendar_ics' token %}">cette adresse</a>.</p>
<a href="{% url 'calendar.ics' token %}">cette adresse</a>.</p>
<ul>
<li>Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller

View file

@ -5,7 +5,7 @@
{% if event.details %}
<p>{{ event.details }}</p>
{% endif %}
<form method="post" action="{% url 'gestioncof.views.event' event.id %}">
<form method="post" action="{% url 'event.details' event.id %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" class="btn-submit" value="Enregistrer" />

View file

@ -5,7 +5,7 @@
{% block realcontent %}
<h2>Modifier mon profil</h2>
<form id="profile form-horizontal" method="post" action="{% url 'gestioncof.views.profile' %}">
<form id="profile form-horizontal" method="post" action="{% url 'profile' %}">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">

View file

@ -7,7 +7,7 @@
{% else %}
<h3>Inscription d'un nouveau compte (extérieur ?)</h3>
{% endif %}
<form role="form" id="profile" method="post" action="{% url 'gestioncof.views.registration' %}">
<form role="form" id="profile" method="post" action="{% url 'registration' %}">
{% csrf_token %}
<table>
{{ user_form | bootstrap }}

View file

@ -8,7 +8,7 @@
{% if survey.details %}
<p>{{ survey.details }}</p>
{% endif %}
<form class="form-horizontal" method="post" action="{% url 'gestioncof.views.survey' survey.id %}">
<form class="form-horizontal" method="post" action="{% url 'survey.details' survey.id %}">
{% csrf_token %}
{{ form | bootstrap}}

View file

@ -7,15 +7,15 @@
<h2>Liens utiles du COF</h2>
<h3>COF</h3>
<ul>
<li><a href="{% url 'gestioncof.views.export_members' %}">Export des membres du COF</a></li>
<li><a href="{% url 'gestioncof.views.liste_diffcof' %}">Diffusion COF</a></li>
<li><a href="{% url 'cof.membres_export' %}">Export des membres du COF</a></li>
<li><a href="{% url 'ml_diffcof' %}">Diffusion COF</a></li>
</ul>
<h3>Mega</h3>
<ul>
<li><a href="{% url 'gestioncof.views.export_mega_participants' %}">Export des non-orgas uniquement</a></li>
<li><a href="{% url 'gestioncof.views.export_mega_orgas' %}">Export des orgas uniquement</a></li>
<li><a href="{% url 'gestioncof.views.export_mega' %}">Export de tout le monde</a></li>
<li><a href="{% url 'cof.mega_export_participants' %}">Export des non-orgas uniquement</a></li>
<li><a href="{% url 'cof.mega_export_orgas' %}">Export des orgas uniquement</a></li>
<li><a href="{% url 'cof.mega_export' %}">Export de tout le monde</a></li>
</ul>
<p>Note&nbsp;: pour ouvrir les fichiers .csv avec Excel, il faut

View file

@ -1,4 +1,5 @@
{% extends "gestioncof/base_header.html" %}
{% load wagtailcore_tags %}
{% block homelink %}
{% endblock %}
@ -13,7 +14,7 @@
<div class="hm-block">
<ul>
{% for event in open_events %}
<li><a href="{% url "gestioncof.views.event" event.id %}">{{ event.title }}</a></li>
<li><a href="{% url "event.details" event.id %}">{{ event.title }}</a></li>
{% endfor %}
</ul>
</div>
@ -23,7 +24,7 @@
<div class="hm-block">
<ul>
{% for survey in open_surveys %}
<li><a href="{% url "gestioncof.views.survey" survey.id %}">{{ survey.title }}</a></li>
<li><a href="{% url "survey.details" survey.id %}">{{ survey.title }}</a></li>
{% endfor %}
</ul>
</div>
@ -42,9 +43,10 @@
<li><a href="{% url "bda-etat-places" tirage.id %}">État des demandes</a></li>
{% else %}
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li>
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BdA-Revente</a></li>
<li><a href="{% url "bda-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
<li><a href="{% url "bda-revente-manage" tirage.id %}">Gérer les places que je revends</a></li>
<li><a href="{% url "bda-revente-tirages" tirage.id %}">Voir les reventes en cours</a></li>
<li><a href="{% url "bda-revente-subscribe" tirage.id %}">Indiquer les spectacles qui m'intéressent</a></li>
<li><a href="{% url "bda-revente-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
{% endif %}
</ul>
{% endfor %}
@ -55,7 +57,8 @@
<h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3>
<div class="hm-block">
<ul>
<li><a href="{% url "kfet.home" %}">Page d'accueil</a></li>
{# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
<li><a href="{% slugurl "k-fet" %}">Page d'accueil</a></li>
<li><a href="https://www.cof.ens.fr/k-fet/calendrier">Calendrier</a></li>
{% if perms.kfet.is_team %}
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>
@ -67,11 +70,11 @@
<h3 class="block-title">Divers<span class="pull-right glyphicon glyphicon-question-sign"></span></h3>
<div class="hm-block">
<ul>
<li><a href="{% url "gestioncof.views.calendar" %}">Calendrier dynamique</a></li>
<li><a href="{% url "calendar" %}">Calendrier dynamique</a></li>
{% if user.profile.is_cof %}<li><a href="{% url "petits-cours-inscription" %}">Inscription pour donner des petits cours</a></li>{% endif %}
<li><a href="{% url "gestioncof.views.profile" %}">Éditer mon profil</a></li>
{% if not user.profile.login_clipper %}<li><a href="{% url "django.contrib.auth.views.password_change" %}">Changer mon mot de passe</a></li>{% endif %}
<li><a href="{% url "profile" %}">Éditer mon profil</a></li>
{% if not user.profile.login_clipper %}<li><a href="{% url "password_change" %}">Changer mon mot de passe</a></li>{% endif %}
</ul>
</div>
{% endif %}
@ -84,16 +87,16 @@
<h4>Général</h4>
<li><a href="{% url "admin:index" %}">Administration générale</a></li>
<li><a href="{% url "petits-cours-demandes-list" %}">Demandes de petits cours</a></li>
<li><a href="{% url "gestioncof.views.registration" %}">Inscription d'un nouveau membre</a></li>
<li><a href="{% url "registration" %}">Inscription d'un nouveau membre</a></li>
<li><a href="{% url "liste-clubs" %}">Gestion des clubs</a></li>
</ul>
<ul>
<h4>Évènements & Sondages</h4>
{% for event in events %}
<li><a href="{% url "gestioncof.views.event_status" event.id %}">Événement : {{ event.title }}</a></li>
<li><a href="{% url "event.details.status" event.id %}">Événement : {{ event.title }}</a></li>
{% endfor %}
{% for survey in surveys %}
<li><a href="{% url "gestioncof.views.survey_status" survey.id %}">Sondage : {{ survey.title }}</a></li>
<li><a href="{% url "survey.details.status" survey.id %}">Sondage : {{ survey.title }}</a></li>
{% endfor %}
</ul>
</div>
@ -118,8 +121,8 @@
<h3 class="block-title">Liens utiles<span class="pull-right glyphicon glyphicon-link"></span></h3>
<div class="hm-block">
<ul>
<li><a href="{% url "gestioncof.views.utile_cof" %}">Liens utiles du COF</a></li>
<li><a href="{% url "gestioncof.views.utile_bda" %}">Liens utiles BdA</a></li>
<li><a href="{% url "utile_cof" %}">Liens utiles du COF</a></li>
<li><a href="{% url "utile_bda" %}">Liens utiles BdA</a></li>
</ul>
</div>
</div>

View file

@ -16,7 +16,7 @@
</tr></thead>
<tbody class="bda_formset_content">
{% endif %}
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
{% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}">

View file

@ -15,7 +15,7 @@
<p class="error">Identifiants incorrects.</p>
{% endif %}
<form class="form-horizontal" method="post"
action="{% url 'gestioncof.views.login_ext' %}?next={{ next|urlencode }}">
action="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
{% csrf_token %}
<div class="form-group">
<input class="form-control" id="id_username" maxlength="254" name="username" type="text" placeholder="Nom d'utilisateur">

View file

@ -12,13 +12,13 @@
<div class="container-fluid">
<div class="row" style="margin:0;">
<a aria-label="Compte clipper"
href="{% url 'django_cas_ng.views.login' %}?next={{ next|urlencode }}">
href="{% url 'cas_login_view' %}?next={{ next|urlencode }}">
<div class="col-xs-12 col-sm-6" id="login_clipper">
Compte clipper
</div>
</a>
<a aria-label="Extérieur"
href="{% url 'gestioncof.views.login_ext' %}?next={{ next|urlencode }}">
href="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
<div class="col-xs-12 col-sm-6" id="login_outsider">
Extérieur
</div>

View file

@ -4,7 +4,7 @@
{% block page_size %}col-sm-8{% endblock %}
{% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
<script src="{% static "vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js" %}" type="text/javascript"></script>
{% endblock %}
{% block realcontent %}
@ -18,7 +18,7 @@
// On attend que la page soit prête pour executer le code
$(document).ready(function() {
$('input#search_autocomplete').yourlabsAutocomplete({
url: '{% url 'gestioncof.autocomplete.autocomplete' %}',
url: '{% url 'cof.registration.autocomplete' %}',
minimumCharacters: 3,
id: 'search_autocomplete',
choiceSelector: 'li:has(a)',

View file

@ -5,5 +5,5 @@
{% block realcontent %}
<h2>Mot de passe modifié avec succès !</h2>
<h3><a href="{% url "gestioncof.views.home" %}">Retour au menu principal</a></h3>
<h3><a href="{% url "home" %}">Retour au menu principal</a></h3>
{% endblock %}

View file

@ -5,7 +5,7 @@
{% block realcontent %}
<h2>Changement de mot de passe</h2>
<form class="form-horizontal" method="post" action="{% url 'django.contrib.auth.views.password_change' %}">
<form class="form-horizontal" method="post" action="{% url 'password_change' %}">
{% csrf_token %}
{{ form | bootstrap }}
<input type="submit" class="btn btn-primary pull-right" value="Changer" />

View file

@ -11,7 +11,7 @@
{% endif %}
<h3>Filtres</h3>
{% include "tristate_js.html" %}
<form method="post" action="{% url 'gestioncof.views.survey_status' survey.id %}">
<form method="post" action="{% url 'survey.details.status' survey.id %}">
{% csrf_token %}
{{ form.as_p }}
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />

View file

@ -7,7 +7,7 @@
<h2>Liens utiles du BdA</h2>
<h3>Listes mail</h3>
<ul>
<li><a href="{% url 'gestioncof.views.liste_bdadiff' %}">BdA diffusion</a></li>
<li><a href="{% url 'gestioncof.views.liste_bdarevente' %}">BdA revente</a></li>
<li><a href="{% url 'ml_diffbda' %}">BdA diffusion</a></li>
<li><a href="{% url 'ml_bda_revente' %}">BdA revente</a></li>
</ul>
{% endblock %}

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import template
from django.utils.safestring import mark_safe

View file

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
@ -6,10 +5,6 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.test import TestCase
from gestioncof.models import CofProfile, User

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