Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket

This commit is contained in:
Ludovic Stephan 2018-01-10 19:12:09 +01:00
commit f93cadc12c
218 changed files with 11800 additions and 3955 deletions

5
.gitignore vendored
View file

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

View file

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

118
README.md
View file

@ -66,119 +66,65 @@ car par défaut Django n'écoute que sur l'adresse locale de la machine virtuell
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
entrer dans le navigateur est `localhost:8000`. entrer dans le navigateur est `localhost:8000`.
#### Serveur de développement type production #### Serveur de développement type production
Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de Juste histoire de jouer, pas indispensable pour développer :
façon similaire à la version en production : on utilise
La VM Vagrant héberge en plus un serveur nginx configuré pour servir GestioCOF
comme en production : on utilise
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker` [Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
derrière un reverse-proxy apache. Le tout est monitoré par derrière un reverse-proxy nginx.
[supervisor](http://supervisord.org/).
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code `localhost:8080/gestion/`. Toutefois il ne se recharge pas tout seul lorsque le
change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour code change, il faut relancer le worker avec `sudo systemctl restart
visualiser la dernière version du code. worker.service` pour visualiser la dernière version du code.
### Installation manuelle ### Installation manuelle
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant,
est fortement conseillé d'utiliser un environnement virtuel pour Python. il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python, un Il vous faudra installer pip, les librairies de développement de python ainsi
client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
(Ubuntu, ...) : Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server sudo apt-get install python3-pip python3-dev sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant : (le dossier où se trouve ce README), et créez-le maintenant :
virtualenv env -p $(which python3) python3 -m venv venv
L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir Pour l'activer, il faut faire
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 . venv/bin/activate
dans le même dossier. dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` : `requirements-devel.txt` :
pip install -U pip
pip install -r requirements-devel.txt pip install -r requirements-devel.txt
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`. 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:
#### Installation avec MySQL ln -s secret_example.py cof/settings/secret.py
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 #### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande suivante : 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 :
python manage.py migrate bash provisioning/prepare_django.sh
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 Vous êtes prêts à développer ! Lancer GestioCOF en faisant
@ -188,7 +134,7 @@ Vous êtes prêts à développer ! Lancer GestioCOF en faisant
Pour mettre à jour les paquets Python, utiliser la commande suivante : Pour mettre à jour les paquets Python, utiliser la commande suivante :
pip install --upgrade -r requirements.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 : Pour mettre à jour les modèles après une migration, il faut ensuite faire :
@ -197,6 +143,6 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire :
## Documentation utilisateur ## Documentation utilisateur
Une brève documentation utilisateur pour se familiariser plus vite avec l'outil Une brève documentation utilisateur est accessible sur le
est accessible sur le [wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home) pour avoir une
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home). idée de la façon dont le COF utilise GestioCOF.

View file

@ -0,0 +1 @@

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import autocomplete_light
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail from custommail.shortcuts import send_mass_custom_mail
@ -9,6 +8,9 @@ from django.db.models import Sum, Count
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
from django.utils import timezone from django.utils import timezone
from django import forms from django import forms
from dal.autocomplete import ModelSelect2
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
@ -24,8 +26,17 @@ class ReadOnlyMixin(object):
return readonly_fields + self.readonly_fields_update 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): class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle model = ChoixSpectacle
form = ChoixSpectacleAdminForm
sortable_field_name = "priority" sortable_field_name = "priority"
@ -56,17 +67,17 @@ class AttributionInline(admin.TabularInline):
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
if self.listing is not None: if self.listing is not None:
qs.filter(spectacle__listing=self.listing) qs = qs.filter(spectacle__listing=self.listing)
return qs return qs
class WithListingAttributionInline(AttributionInline): class WithListingAttributionInline(AttributionInline):
exclude = ('given', )
form = WithListingAttributionTabularAdminForm form = WithListingAttributionTabularAdminForm
listing = True listing = True
class WithoutListingAttributionInline(AttributionInline): class WithoutListingAttributionInline(AttributionInline):
exclude = ('given', )
form = WithoutListingAttributionTabularAdminForm form = WithoutListingAttributionTabularAdminForm
listing = False listing = False
@ -180,7 +191,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
class ChoixSpectacleAdmin(admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(ChoixSpectacle, exclude=[]) form = ChoixSpectacleAdminForm
def tirage(self, obj): def tirage(self, obj):
return obj.participant.tirage return obj.participant.tirage

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

@ -59,7 +59,7 @@ class Migration(migrations.Migration):
('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)), ('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)),
('slots', models.IntegerField(verbose_name=b'Places')), ('slots', models.IntegerField(verbose_name=b'Places')),
('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')), ('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={ options={
'ordering': ('priority', 'date', 'title'), 'ordering': ('priority', 'date', 'title'),
@ -79,27 +79,27 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='participant', model_name='participant',
name='user', name='user',
field=models.OneToOneField(to=settings.AUTH_USER_MODEL), field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='choixspectacle', model_name='choixspectacle',
name='participant', name='participant',
field=models.ForeignKey(to='bda.Participant'), field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='choixspectacle', model_name='choixspectacle',
name='spectacle', 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( migrations.AddField(
model_name='attribution', model_name='attribution',
name='participant', name='participant',
field=models.ForeignKey(to='bda.Participant'), field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='attribution', model_name='attribution',
name='spectacle', 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( migrations.AlterUniqueTogether(
name='choixspectacle', name='choixspectacle',

View file

@ -5,17 +5,34 @@ from django.db import migrations, models
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
def forwards_func(apps, schema_editor):
def fill_tirage_fields(apps, schema_editor):
"""
Create a `Tirage` to fill new field `tirage` of `Participant`
and `Spectacle` already existing.
"""
Participant = apps.get_model("bda", "Participant")
Spectacle = apps.get_model("bda", "Spectacle")
Tirage = apps.get_model("bda", "Tirage") Tirage = apps.get_model("bda", "Tirage")
db_alias = schema_editor.connection.alias
Tirage.objects.using(db_alias).bulk_create([ # These querysets only contains instances not linked to any `Tirage`.
Tirage( participants = Participant.objects.filter(tirage=None)
id=1, spectacles = Spectacle.objects.filter(tirage=None)
title="Tirage de test (migration)",
active=False, if not participants.count() and not spectacles.count():
ouverture=timezone.now(), # No need to create a "trash" tirage.
fermeture=timezone.now()), 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): class Migration(migrations.Migration):
@ -35,22 +52,33 @@ class Migration(migrations.Migration):
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')), ('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
], ],
), ),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
migrations.AlterField( migrations.AlterField(
model_name='participant', model_name='participant',
name='user', name='user',
field=models.ForeignKey(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( migrations.AddField(
model_name='participant', model_name='participant',
name='tirage', name='tirage',
field=models.ForeignKey(default=1, to='bda.Tirage'), field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE),
preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='spectacle', model_name='spectacle',
name='tirage', name='tirage',
field=models.ForeignKey(default=1, to='bda.Tirage'), field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE),
preserve_default=False, ),
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', model_name='spectacle',
name='category', name='category',
field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle', field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle',
on_delete=models.CASCADE,
null=True), null=True),
), ),
migrations.AddField( migrations.AddField(
@ -84,6 +85,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='quote', model_name='quote',
name='spectacle', 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', model_name='spectaclerevente',
name='attribution', name='attribution',
field=models.OneToOneField(to='bda.Attribution', field=models.OneToOneField(to='bda.Attribution',
on_delete=models.CASCADE,
related_name='revente'), related_name='revente'),
), ),
migrations.AddField( migrations.AddField(
model_name='spectaclerevente', model_name='spectaclerevente',
name='seller', name='seller',
field=models.ForeignKey(to='bda.Participant', field=models.ForeignKey(to='bda.Participant',
on_delete=models.CASCADE,
verbose_name='Vendeur', verbose_name='Vendeur',
related_name='original_shows'), related_name='original_shows'),
), ),
@ -60,6 +62,7 @@ class Migration(migrations.Migration):
model_name='spectaclerevente', model_name='spectaclerevente',
name='soldTo', name='soldTo',
field=models.ForeignKey(to='bda.Participant', field=models.ForeignKey(to='bda.Participant',
on_delete=models.CASCADE,
verbose_name='Vendue à', null=True, verbose_name='Vendue à', null=True,
blank=True), blank=True),
), ),

View file

@ -6,11 +6,23 @@ from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail from custommail.shortcuts import send_mass_custom_mail
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail
from django.db import models from django.db import models
from django.db.models import Count
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.utils import timezone, formats 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): class Tirage(models.Model):
title = models.CharField("Titre", max_length=300) title = models.CharField("Titre", max_length=300)
@ -50,9 +62,12 @@ class CategorieSpectacle(models.Model):
class Spectacle(models.Model): class Spectacle(models.Model):
title = models.CharField("Titre", max_length=300) title = models.CharField("Titre", max_length=300)
category = models.ForeignKey(CategorieSpectacle, blank=True, null=True) category = models.ForeignKey(
CategorieSpectacle, on_delete=models.CASCADE,
blank=True, null=True,
)
date = models.DateTimeField("Date & heure") date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle) location = models.ForeignKey(Salle, on_delete=models.CASCADE)
vips = models.TextField('Personnalités', blank=True) vips = models.TextField('Personnalités', blank=True)
description = models.TextField("Description", blank=True) description = models.TextField("Description", blank=True)
slots_description = models.TextField("Description des places", blank=True) slots_description = models.TextField("Description des places", blank=True)
@ -62,7 +77,7 @@ class Spectacle(models.Model):
max_length=500) max_length=500)
price = models.FloatField("Prix d'une place") price = models.FloatField("Prix d'une place")
slots = models.IntegerField("Places") slots = models.IntegerField("Places")
tirage = models.ForeignKey(Tirage) tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
listing = models.BooleanField("Les places sont sur listing") listing = models.BooleanField("Les places sont sur listing")
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True,
null=True) null=True)
@ -96,32 +111,29 @@ class Spectacle(models.Model):
Envoie un mail de rappel à toutes les personnes qui ont une place pour Envoie un mail de rappel à toutes les personnes qui ont une place pour
ce spectacle. ce spectacle.
""" """
# On récupère la liste des participants # On récupère la liste des participants + le BdA
members = {} members = list(
for attr in Attribution.objects.filter(spectacle=self).all(): User.objects
member = attr.participant.user .filter(participant__attributions=self)
if member.id in members: .annotate(nb_attr=Count("id")).order_by()
members[member.id][1] = 2 )
else: bda_generic = get_generic_user()
members[member.id] = [member, 1] bda_generic.nb_attr = 1
# FIXME : faire quelque chose de ça, un utilisateur bda_generic ? members.append(bda_generic)
# # Pour le BdA
# members[0] = ['BdA', 1, 'bda@ens.fr']
# members[-1] = ['BdA', 2, 'bda@ens.fr']
# On écrit un mail personnalisé à chaque participant # On écrit un mail personnalisé à chaque participant
datatuple = [( datatuple = [(
'bda-rappel', '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'], settings.MAIL_DATA['rappels']['FROM'],
[member[0].email]) [member.email])
for member in members.values() for member in members
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu # On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now() self.rappel_sent = timezone.now()
self.save() self.save()
# On renvoie la liste des destinataires # On renvoie la liste des destinataires
return members.values() return members
@property @property
def is_past(self): def is_past(self):
@ -129,7 +141,7 @@ class Spectacle(models.Model):
class Quote(models.Model): class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle) spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
text = models.TextField('Citation') text = models.TextField('Citation')
author = models.CharField('Auteur', max_length=200) author = models.CharField('Auteur', max_length=200)
@ -143,7 +155,7 @@ PAYMENT_TYPES = (
class Participant(models.Model): class Participant(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(Spectacle, choices = models.ManyToManyField(Spectacle,
through="ChoixSpectacle", through="ChoixSpectacle",
related_name="chosen_by") related_name="chosen_by")
@ -154,7 +166,7 @@ class Participant(models.Model):
paymenttype = models.CharField("Moyen de paiement", paymenttype = models.CharField("Moyen de paiement",
max_length=6, choices=PAYMENT_TYPES, max_length=6, choices=PAYMENT_TYPES,
blank=True) blank=True)
tirage = models.ForeignKey(Tirage) tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(Spectacle, choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed", related_name="subscribed",
blank=True) blank=True)
@ -170,8 +182,11 @@ DOUBLE_CHOICES = (
class ChoixSpectacle(models.Model): class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant) participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(Spectacle, related_name="participants") spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE,
related_name="participants",
)
priority = models.PositiveIntegerField("Priorité") priority = models.PositiveIntegerField("Priorité")
double_choice = models.CharField("Nombre de places", double_choice = models.CharField("Nombre de places",
default="1", choices=DOUBLE_CHOICES, default="1", choices=DOUBLE_CHOICES,
@ -198,8 +213,11 @@ class ChoixSpectacle(models.Model):
class Attribution(models.Model): class Attribution(models.Model):
participant = models.ForeignKey(Participant) participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(Spectacle, related_name="attribues") spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE,
related_name="attribues",
)
given = models.BooleanField("Donnée", default=False) given = models.BooleanField("Donnée", default=False)
def __str__(self): def __str__(self):
@ -208,18 +226,25 @@ class Attribution(models.Model):
class SpectacleRevente(models.Model): class SpectacleRevente(models.Model):
attribution = models.OneToOneField(Attribution, attribution = models.OneToOneField(
related_name="revente") Attribution, on_delete=models.CASCADE,
related_name="revente",
)
date = models.DateTimeField("Date de mise en vente", date = models.DateTimeField("Date de mise en vente",
default=timezone.now) default=timezone.now)
answered_mail = models.ManyToManyField(Participant, answered_mail = models.ManyToManyField(Participant,
related_name="wanted", related_name="wanted",
blank=True) blank=True)
seller = models.ForeignKey(Participant, seller = models.ForeignKey(
related_name="original_shows", Participant, on_delete=models.CASCADE,
verbose_name="Vendeur") verbose_name="Vendeur",
soldTo = models.ForeignKey(Participant, blank=True, null=True, related_name="original_shows",
verbose_name="Vendue à") )
soldTo = models.ForeignKey(
Participant, on_delete=models.CASCADE,
verbose_name="Vendue à",
blank=True, null=True,
)
notif_sent = models.BooleanField("Notification envoyée", notif_sent = models.BooleanField("Notification envoyée",
default=False) default=False)
@ -306,37 +331,55 @@ class SpectacleRevente(models.Model):
# Envoie un mail au gagnant et au vendeur # Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits) winner = random.choice(inscrits)
self.soldTo = winner self.soldTo = winner
datatuple = []
mails = []
context = { context = {
'acheteur': winner.user, 'acheteur': winner.user,
'vendeur': seller.user, 'vendeur': seller.user,
'show': spectacle, 'show': spectacle,
} }
datatuple.append((
'bda-revente-winner', c_mails_qs = CustomMail.objects.filter(shortname__in=[
context, 'bda-revente-winner', 'bda-revente-loser',
settings.MAIL_DATA['revente']['FROM'],
[winner.user.email],
))
datatuple.append((
'bda-revente-seller', 'bda-revente-seller',
context, ])
settings.MAIL_DATA['revente']['FROM'],
[seller.user.email] 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 # Envoie un mail aux perdants
for inscrit in inscrits: for inscrit in inscrits:
if inscrit != winner: if inscrit != winner:
new_context = dict(context) new_context = dict(context)
new_context['acheteur'] = inscrit.user new_context['acheteur'] = inscrit.user
datatuple.append((
'bda-revente-loser', mails.append(
new_context, c_mails['bda-revente-loser'].get_message(
settings.MAIL_DATA['revente']['FROM'], new_context,
[inscrit.user.email] from_email=settings.MAIL_DATA['revente']['FROM'],
)) to=[inscrit.user.email],
send_mass_custom_mail(datatuple) )
)
mail_conn = mail.get_connection()
mail_conn.send_messages(mails)
# Si personne ne veut de la place, elle part au shotgun # Si personne ne veut de la place, elle part au shotgun
else: else:
self.shotgun = True self.shotgun = True

View file

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

View file

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

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.answered_mail = 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']],
)

View file

@ -1,9 +1,10 @@
import json import json
from django.contrib.auth.models import User
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils import timezone from django.utils import timezone
from .models import Tirage, Spectacle, Salle, CategorieSpectacle from bda.models import Tirage, Spectacle, Salle, CategorieSpectacle
class TestBdAViews(TestCase): 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): def test_catalogue(self):
"""Test the catalogue JSON API""" """Test the catalogue JSON API"""
client = Client() client = Client()
# The `list` hooh # The `list` hook
resp = client.get("/bda/catalogue/list") resp = client.get("/bda/catalogue/list")
self.assertJSONEqual( self.assertJSONEqual(
resp.content.decode("utf-8"), resp.content.decode("utf-8"),

View file

@ -32,6 +32,12 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$', url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
views.unpaid, views.unpaid,
name="bda-unpaid"), name="bda-unpaid"),
url(r'^spectacles/autocomplete$',
views.spectacle_autocomplete,
name="bda-spectacle-autocomplete"),
url(r'^participants/autocomplete$',
views.participant_autocomplete,
name="bda-participant-autocomplete"),
url(r'^liste-revente/(?P<tirage_id>\d+)$', url(r'^liste-revente/(?P<tirage_id>\d+)$',
views.list_revente, views.list_revente,
name="bda-liste-revente"), name="bda-liste-revente"),
@ -44,7 +50,10 @@ urlpatterns = [
url(r'^revente-immediat/(?P<tirage_id>\d+)$', url(r'^revente-immediat/(?P<tirage_id>\d+)$',
views.revente_shotgun, views.revente_shotgun,
name="bda-shotgun"), name="bda-shotgun"),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel), url(r'^mails-rappel/(?P<spectacle_id>\d+)$',
views.send_rappel,
name="bda-rappels"
),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'), name='bda-descriptions'),
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue, url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue,

View file

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from collections import defaultdict from collections import defaultdict
from functools import partial
import random import random
import hashlib import hashlib
import time import time
import json import json
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import ( from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
send_mass_custom_mail, send_custom_mail, render_custom_mail from custommail.models import CustomMail
)
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
@ -27,7 +25,7 @@ from django.views.generic.list import ListView
from gestioncof.decorators import cof_required, buro_required from gestioncof.decorators import cof_required, buro_required
from bda.models import ( from bda.models import (
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage, Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
SpectacleRevente, Salle, Quote, CategorieSpectacle SpectacleRevente, Salle, CategorieSpectacle
) )
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import ( from bda.forms import (
@ -35,6 +33,8 @@ from bda.forms import (
InscriptionInlineFormSet, InscriptionInlineFormSet,
) )
from utils.views.autocomplete import Select2QuerySetView
@cof_required @cof_required
def etat_places(request, tirage_id): def etat_places(request, tirage_id):
@ -305,7 +305,8 @@ def do_tirage(tirage_elt, token):
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues # On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
ChoixRevente = Participant.choicesrevente.through 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 ChoixRevente.objects
.filter(spectacle__tirage=tirage_elt) .filter(spectacle__tirage=tirage_elt)
@ -612,7 +613,7 @@ def spectacle(request, tirage_id, spectacle_id):
participants_info = sorted(participants.values(), participants_info = sorted(participants.values(),
key=lambda part: part['lastname']) key=lambda part: part['lastname'])
return render(request, "bda-participants.html", return render(request, "bda/participants.html",
{"spectacle": spectacle, "participants": participants_info}) {"spectacle": spectacle, "participants": participants_info})
@ -651,20 +652,24 @@ def unpaid(request, tirage_id):
def send_rappel(request, spectacle_id): def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id) show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples # 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, 'member': request.user,
'show': show, 'show': show,
'nb_attr': 1 'nb_attr': 1
}) })
exemple_mail_2places = render_custom_mail('bda-rappel', { exemple_mail_2places = custommail.render({
'member': request.user, 'member': request.user,
'show': show, 'show': show,
'nb_attr': 2 'nb_attr': 2
}) })
# Contexte # Contexte
ctxt = {'show': show, ctxt = {
'exemple_mail_1place': exemple_mail_1place, 'show': show,
'exemple_mail_2places': exemple_mail_2places} 'exemple_mail_1place': exemple_mail_1place,
'exemple_mail_2places': exemple_mail_2places,
'custommail': custommail,
}
# Envoi confirmé # Envoi confirmé
if request.method == 'POST': if request.method == 'POST':
members = show.send_rappel() members = show.send_rappel()
@ -673,6 +678,14 @@ def send_rappel(request, spectacle_id):
# Demande de confirmation # Demande de confirmation
else: else:
ctxt['sent'] = False 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) return render(request, "bda/mails-rappel.html", ctxt)
@ -771,9 +784,9 @@ def catalogue(request, request_type):
.select_related('location') .select_related('location')
.prefetch_related('quote_set') .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) 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) shows_qs = shows_qs.filter(location__id__in=locations_id)
# On convertit les descriptions à envoyer en une liste facilement # On convertit les descriptions à envoyer en une liste facilement
@ -802,3 +815,26 @@ def catalogue(request, request_type):
return JsonResponse(data_return, safe=False) return JsonResponse(data_return, safe=False)
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur # Si la requête n'est pas de la forme attendue, on quitte avec une erreur
return HttpResponseBadRequest() 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,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

@ -8,25 +8,45 @@ the local development server should be here.
import os import os
# Database credentials
try: try:
from .secret import DBNAME, DBUSER, DBPASSWD from . import secret
except ImportError: except ImportError:
# On the local development VM, theses credentials are in the environment raise ImportError(
DBNAME = os.environ["DBNAME"] "The secret.py file is missing.\n"
DBUSER = os.environ["DBUSER"] "For a development environment, simply copy secret_example.py"
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
) )
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")
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
BASE_DIR = os.path.dirname( BASE_DIR = os.path.dirname(
@ -37,39 +57,64 @@ BASE_DIR = os.path.dirname(
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'gestioncof', '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.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'grappelli',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.admindocs', 'django.contrib.admindocs',
'bda', 'bda',
'autocomplete_light',
'captcha', 'captcha',
'django_cas_ng', 'django_cas_ng',
'bootstrapform', 'bootstrapform',
'kfet', 'kfet',
'kfet.open',
'channels', 'channels',
'widget_tweaks', 'widget_tweaks',
'django_js_reverse', 'django_js_reverse',
'custommail', 'custommail',
'djconfig', 'djconfig',
'wagtail.wagtailforms',
'wagtail.wagtailredirects',
'wagtail.wagtailembeds',
'wagtail.wagtailsites',
'wagtail.wagtailusers',
'wagtail.wagtailsnippets',
'wagtail.wagtaildocs',
'wagtail.wagtailimages',
'wagtail.wagtailsearch',
'wagtail.wagtailadmin',
'wagtail.wagtailcore',
'wagtail.contrib.modeladmin',
'wagtailmenus',
'modelcluster',
'taggit',
'kfet.auth',
'kfet.cms',
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'kfet.middleware.KFetAuthenticationMiddleware', 'kfet.auth.middleware.TemporaryAuthMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'djconfig.middleware.DjConfigMiddleware', 'djconfig.middleware.DjConfigMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
] ]
ROOT_URLCONF = 'cof.urls' ROOT_URLCONF = 'cof.urls'
@ -85,12 +130,13 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'django.core.context_processors.i18n', 'django.template.context_processors.i18n',
'django.core.context_processors.media', 'django.template.context_processors.media',
'django.core.context_processors.static', 'django.template.context_processors.static',
'wagtailmenus.context_processors.wagtailmenus',
'djconfig.context_processors.config', 'djconfig.context_processors.config',
'gestioncof.shared.context_processor', 'gestioncof.shared.context_processor',
'kfet.context_processors.auth', 'kfet.auth.context_processors.temporary_auth',
'kfet.context_processors.config', 'kfet.context_processors.config',
], ],
}, },
@ -99,7 +145,7 @@ TEMPLATES = [
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': DBNAME, 'NAME': DBNAME,
'USER': DBUSER, 'USER': DBUSER,
'PASSWORD': DBPASSWD, 'PASSWORD': DBPASSWD,
@ -144,17 +190,32 @@ LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
CAS_VERSION = '3'
CAS_LOGIN_MSG = None
CAS_IGNORE_REFERER = True CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/' CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'gestioncof.shared.COFCASBackend', 'gestioncof.shared.COFCASBackend',
'kfet.backends.GenericTeamBackend', 'kfet.auth.backends.GenericBackend',
) )
RECAPTCHA_USE_SSL = True RECAPTCHA_USE_SSL = True
# Cache settings
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:{passwd}@{host}:{port}/db'
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
port=REDIS_PORT, db=REDIS_DB),
}
}
# Channels settings # Channels settings
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
@ -167,8 +228,14 @@ CHANNEL_LAYERS = {
port=REDIS_PORT, db=REDIS_DB) port=REDIS_PORT, db=REDIS_DB)
)], )],
}, },
"ROUTING": "cof.routing.channel_routing", "ROUTING": "cof.routing.routing",
} }
} }
FORMAT_MODULE_PATH = 'cof.locale' FORMAT_MODULE_PATH = 'cof.locale'
# Wagtail settings
WAGTAIL_SITE_NAME = 'GestioCOF'
WAGTAIL_ENABLE_UPDATE_CHECK = False
TAGGIT_CASE_INSENSITIVE = True

View file

@ -3,9 +3,8 @@ Django development settings for the cof project.
The settings that are not listed here are imported from .common The settings that are not listed here are imported from .common
""" """
import os from .common import * # NOQA
from .common import INSTALLED_APPS, MIDDLEWARE
from .common import *
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@ -18,9 +17,9 @@ DEBUG = True
# --- # ---
STATIC_URL = '/static/' 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/' MEDIA_URL = '/media/'
@ -38,10 +37,11 @@ def show_toolbar(request):
return DEBUG return DEBUG
INSTALLED_APPS += ["debug_toolbar", "debug_panel"] INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE_CLASSES = (
["debug_panel.middleware.DebugPanelMiddleware"] MIDDLEWARE = [
+ MIDDLEWARE_CLASSES "debug_panel.middleware.DebugPanelMiddleware"
) ] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar, '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 import os
from .common import * from .common import * # NOQA
from .common import BASE_DIR
DEBUG = False DEBUG = False
@ -16,11 +17,14 @@ ALLOWED_HOSTS = [
"dev.cof.ens.fr" "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/" STATIC_URL = "/gestion/static/"
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
MEDIA_URL = "/gestion/media/" MEDIA_URL = "/gestion/media/"
LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636"
EMAIL_HOST = "nef.ens.fr"

View file

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

@ -4,8 +4,6 @@
Fichier principal de configuration des urls du projet GestioCOF Fichier principal de configuration des urls du projet GestioCOF
""" """
import autocomplete_light
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
@ -16,13 +14,16 @@ from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views from django_cas_ng import views as django_cas_views
from django_js_reverse.views import urls_js from django_js_reverse.views import urls_js
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 import views as gestioncof_views, csv_views
from gestioncof.urls import export_patterns, petitcours_patterns, \ from gestioncof.urls import export_patterns, petitcours_patterns, \
surveys_patterns, events_patterns, calendar_patterns, \ surveys_patterns, events_patterns, calendar_patterns, \
clubs_patterns clubs_patterns
from gestioncof.autocomplete import autocomplete from gestioncof.autocomplete import autocomplete
autocomplete_light.autodiscover()
admin.autodiscover() admin.autodiscover()
urlpatterns = [ urlpatterns = [
@ -47,18 +48,22 @@ urlpatterns = [
name="cof-denied"), name="cof-denied"),
url(r'^cas/login$', django_cas_views.login, name="cas_login_view"), url(r'^cas/login$', django_cas_views.login, name="cas_login_view"),
url(r'^cas/logout$', django_cas_views.logout), 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'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
url(r'^login$', gestioncof_views.login, name="cof-login"), 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 # Infos persos
url(r'^profile$', gestioncof_views.profile), url(r'^profile$', gestioncof_views.profile,
url(r'^outsider/password-change$', django_views.password_change), name='profile'),
url(r'^outsider/password-change$', django_views.password_change,
name='password_change'),
url(r'^outsider/password-change-done$', url(r'^outsider/password-change-done$',
django_views.password_change_done, django_views.password_change_done,
name='password_change_done'), name='password_change_done'),
# Inscription d'un nouveau membre # 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-]+)/' url(r'^registration/clipper/(?P<login_clipper>[\w-]+)/'
r'(?P<fullname>.*)$', r'(?P<fullname>.*)$',
gestioncof_views.registration_form2, name="clipper-registration"), gestioncof_views.registration_form2, name="clipper-registration"),
@ -68,7 +73,8 @@ urlpatterns = [
name="empty-registration"), name="empty-registration"),
# Autocompletion # Autocompletion
url(r'^autocomplete/registration$', autocomplete), url(r'^autocomplete/registration$', autocomplete),
url(r'^autocomplete/', include('autocomplete_light.urls')), url(r'^user/autocomplete$', gestioncof_views.user_autocomplete,
name='cof-user-autocomplete'),
# Interface admin # Interface admin
url(r'^admin/logout/', gestioncof_views.logout), url(r'^admin/logout/', gestioncof_views.logout),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
@ -76,15 +82,20 @@ urlpatterns = [
csv_views.admin_list_export, csv_views.admin_list_export,
{'fields': ['username', ]}), {'fields': ['username', ]}),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^grappelli/', include('grappelli.urls')),
# Liens utiles du COF et du BdA # Liens utiles du COF et du BdA
url(r'^utile_cof$', gestioncof_views.utile_cof), url(r'^utile_cof$', gestioncof_views.utile_cof,
url(r'^utile_bda$', gestioncof_views.utile_bda), name='utile_cof'),
url(r'^utile_bda$', gestioncof_views.utile_bda,
name='utile_bda'),
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff), url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls')), url(r'^k-fet/', include('kfet.urls')),
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'), url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# djconfig
url(r"^config", gestioncof_views.ConfigUpdate.as_view()),
] ]
if 'debug_toolbar' in settings.INSTALLED_APPS: if 'debug_toolbar' in settings.INSTALLED_APPS:
@ -93,7 +104,13 @@ if 'debug_toolbar' in settings.INSTALLED_APPS:
url(r'^__debug__/', include(debug_toolbar.urls)), url(r'^__debug__/', include(debug_toolbar.urls)),
] ]
if settings.DEBUG:
# Si on est en production, MEDIA_ROOT est servi par Apache. # 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. # Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns += static(settings.MEDIA_URL, urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT) 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 import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _ 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.core.urlresolvers import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models import Q 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, def add_link_field(target_model='', field='', link_text=str,
desc_text=six.text_type): desc_text=str):
def add_link(cls): def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower() 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", User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper") "Login clipper")
User.profile_num = FkeyLookup("profile__num", "Numéro")
User.profile_phone = ProfileInfo("phone", "Téléphone") User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation") User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement") User.profile_departement = ProfileInfo("departement", "Departement")
@ -166,10 +158,12 @@ class UserProfileAdmin(UserAdmin):
is_cof.short_description = 'Membre du COF' is_cof.short_description = 'Membre du COF'
is_cof.boolean = True is_cof.boolean = True
list_display = ('profile_num',) + UserAdmin.list_display \ list_display = (
UserAdmin.list_display
+ ('profile_login_clipper', 'profile_phone', 'profile_occupation', + ('profile_login_clipper', 'profile_phone', 'profile_occupation',
'profile_mailing_cof', 'profile_mailing_bda', 'profile_mailing_cof', 'profile_mailing_bda',
'profile_mailing_bda_revente', 'is_cof', 'is_buro', ) 'profile_mailing_bda_revente', 'is_cof', 'is_buro', )
)
list_display_links = ('username', 'email', 'first_name', 'last_name') list_display_links = ('username', 'email', 'first_name', 'last_name')
list_filter = UserAdmin.list_filter \ list_filter = UserAdmin.list_filter \
+ ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof', + ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof',
@ -215,21 +209,25 @@ class UserProfileAdmin(UserAdmin):
# FIXME: This is absolutely horrible. # FIXME: This is absolutely horrible.
def user_unicode(self): def user_str(self):
if self.first_name and self.last_name: 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: else:
return self.username return self.username
if six.PY2: User.__str__ = user_str
User.__unicode__ = user_unicode
else:
User.__str__ = user_unicode class EventRegistrationAdminForm(forms.ModelForm):
class Meta:
widgets = {
'user': ModelSelect2(url='cof-user-autocomplete'),
}
class EventRegistrationAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[]) form = EventRegistrationAdminForm
list_display = ('__unicode__' if six.PY2 else '__str__', 'event', 'user',
'paid') list_display = ('__str__', 'event', 'user', 'paid')
list_filter = ('paid',) list_filter = ('paid',)
search_fields = ('user__username', 'user__first_name', 'user__last_name', search_fields = ('user__username', 'user__first_name', 'user__last_name',
'user__email', 'event__title') 'user__email', 'event__title')

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

@ -58,7 +58,7 @@ def autocomplete(request):
) )
# Fetching data from the SPI # Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'): if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching # Fetching
ldap_query = '(&{:s})'.format(''.join( ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit) '(|(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,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 import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory 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, \ from gestioncof.models import CofProfile, EventCommentValue, \
CalendarSubscription, Club CalendarSubscription, Club
from gestioncof.widgets import TriStateCheckbox from gestioncof.widgets import TriStateCheckbox
from gestioncof.shared import lock_table, unlock_table
from bda.models import Spectacle from bda.models import Spectacle
@ -203,9 +196,6 @@ class RegistrationUserForm(forms.ModelForm):
super(RegistrationUserForm, self).__init__(*args, **kw) super(RegistrationUserForm, self).__init__(*args, **kw)
self.fields['username'].help_text = "" self.fields['username'].help_text = ""
def force_long_username(self):
self.fields['username'].validators = [MinLengthValidator(9)]
class Meta: class Meta:
model = User model = User
fields = ("username", "first_name", "last_name", "email") fields = ("username", "first_name", "last_name", "email")
@ -243,7 +233,6 @@ class RegistrationProfileForm(forms.ModelForm):
self.fields['mailing_cof'].initial = True self.fields['mailing_cof'].initial = True
self.fields['mailing_bda'].initial = True self.fields['mailing_bda'].initial = True
self.fields['mailing_bda_revente'].initial = True self.fields['mailing_bda_revente'].initial = True
self.fields['num'].widget.attrs['readonly'] = True
self.fields.keyOrder = [ self.fields.keyOrder = [
'login_clipper', 'login_clipper',
@ -251,7 +240,6 @@ class RegistrationProfileForm(forms.ModelForm):
'occupation', 'occupation',
'departement', 'departement',
'is_cof', 'is_cof',
'num',
'type_cotiz', 'type_cotiz',
'mailing_cof', 'mailing_cof',
'mailing_bda', 'mailing_bda',
@ -259,24 +247,9 @@ class RegistrationProfileForm(forms.ModelForm):
'comments' '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: class Meta:
model = CofProfile model = CofProfile
fields = ("login_clipper", "num", "phone", "occupation", fields = ("login_clipper", "phone", "occupation",
"departement", "is_cof", "type_cotiz", "mailing_cof", "departement", "is_cof", "type_cotiz", "mailing_cof",
"mailing_bda", "mailing_bda_revente", "comments") "mailing_bda", "mailing_bda_revente", "comments")
@ -403,3 +376,16 @@ class ClubsForm(forms.Form):
queryset=Club.objects.all(), queryset=Club.objects.all(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) 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,587 +1,600 @@
[ [
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 1,
"fields": { "fields": {
"kind": "model",
"content_type": [ "content_type": [
"auth", "auth",
"user" "user"
], ],
"inner1": null, "inner1": null,
"kind": "model",
"inner2": null "inner2": null
} },
"pk": 1
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 2,
"fields": { "fields": {
"kind": "int",
"content_type": null, "content_type": null,
"inner1": null, "inner1": null,
"kind": "int",
"inner2": null "inner2": null
} },
"pk": 2
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 3,
"fields": { "fields": {
"kind": "model",
"content_type": [ "content_type": [
"bda", "bda",
"spectacle" "spectacle"
], ],
"inner1": null, "inner1": null,
"kind": "model",
"inner2": null "inner2": null
} },
"pk": 3
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 4,
"fields": { "fields": {
"kind": "model",
"content_type": [ "content_type": [
"bda", "bda",
"spectaclerevente" "spectaclerevente"
], ],
"inner1": null, "inner1": null,
"kind": "model",
"inner2": null "inner2": null
} },
"pk": 4
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 5,
"fields": { "fields": {
"kind": "model",
"content_type": [ "content_type": [
"sites", "sites",
"site" "site"
], ],
"inner1": null, "inner1": null,
"kind": "model",
"inner2": null "inner2": null
} },
"pk": 5
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 6,
"fields": { "fields": {
"kind": "model",
"content_type": [ "content_type": [
"gestioncof", "gestioncof",
"petitcoursdemande" "petitcoursdemande"
], ],
"inner1": null, "inner1": null,
"kind": "model",
"inner2": null "inner2": null
} },
"pk": 6
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 7,
"fields": { "fields": {
"content_type": null,
"inner1": null,
"kind": "list", "kind": "list",
"content_type": null,
"inner1": 12,
"inner2": null "inner2": null
} },
"pk": 7
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 8,
"fields": { "fields": {
"kind": "list",
"content_type": null, "content_type": null,
"inner1": 1, "inner1": 1,
"kind": "list",
"inner2": null "inner2": null
} },
"pk": 8
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 9,
"fields": { "fields": {
"content_type": null,
"inner1": null,
"kind": "pair", "kind": "pair",
"content_type": null,
"inner1": 12,
"inner2": 8 "inner2": 8
} },
"pk": 9
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 10,
"fields": { "fields": {
"kind": "list",
"content_type": null, "content_type": null,
"inner1": 9, "inner1": 9,
"kind": "list",
"inner2": null "inner2": null
} },
"pk": 10
}, },
{ {
"model": "custommail.variabletype", "model": "custommail.type",
"pk": 11,
"fields": { "fields": {
"kind": "list",
"content_type": null, "content_type": null,
"inner1": 3, "inner1": 3,
"kind": "list",
"inner2": null "inner2": null
} },
"pk": 11
},
{
"model": "custommail.type",
"fields": {
"kind": "model",
"content_type": [
"gestioncof",
"petitcourssubject"
],
"inner1": null,
"inner2": null
},
"pk": 12
}, },
{ {
"model": "custommail.custommail", "model": "custommail.custommail",
"pk": 1,
"fields": { "fields": {
"shortname": "welcome", "shortname": "welcome",
"subject": "Bienvenue au COF", "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", "model": "custommail.custommail",
"pk": 2,
"fields": { "fields": {
"shortname": "bda-rappel", "shortname": "bda-rappel",
"subject": "{{ show }}", "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", "model": "custommail.custommail",
"pk": 3,
"fields": { "fields": {
"shortname": "bda-revente", "shortname": "bda-revente",
"subject": "{{ show }}", "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",
"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 le signaler qu'une place vient d'\u00eatre mise en vente."
} },
"pk": 3
}, },
{ {
"model": "custommail.custommail", "model": "custommail.custommail",
"pk": 4,
"fields": { "fields": {
"shortname": "bda-shotgun", "shortname": "bda-shotgun",
"subject": "{{ show }}", "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-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es."
} },
"pk": 4
}, },
{ {
"model": "custommail.custommail", "model": "custommail.custommail",
"pk": 5,
"fields": { "fields": {
"shortname": "bda-revente-winner", "shortname": "bda-revente-winner",
"subject": "BdA-Revente : {{ show.title }}", "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", "model": "custommail.custommail",
"pk": 6,
"fields": { "fields": {
"shortname": "bda-revente-loser", "shortname": "bda-revente-loser",
"subject": "BdA-Revente : {{ show.title }}", "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", "model": "custommail.custommail",
"pk": 7,
"fields": { "fields": {
"shortname": "bda-revente-seller", "shortname": "bda-revente-seller",
"subject": "BdA-Revente : {{ show.title }}", "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", "model": "custommail.custommail",
"pk": 8,
"fields": { "fields": {
"shortname": "bda-revente-new", "shortname": "bda-revente-new",
"subject": "BdA-Revente : {{ show.title }}", "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", "model": "custommail.custommail",
"pk": 9,
"fields": { "fields": {
"shortname": "bda-buy-shotgun", "shortname": "bda-buy-shotgun",
"subject": "BdA-Revente : {{ show.title }}", "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", "model": "custommail.custommail",
"pk": 10,
"fields": { "fields": {
"shortname": "petit-cours-mail-eleve", "shortname": "petit-cours-mail-eleve",
"subject": "Petits cours ENS par le COF", "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", "model": "custommail.custommail",
"pk": 11,
"fields": { "fields": {
"shortname": "petits-cours-mail-demandeur", "shortname": "petits-cours-mail-demandeur",
"subject": "Cours particuliers ENS", "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", "model": "custommail.custommail",
"pk": 12,
"fields": { "fields": {
"shortname": "bda-attributions", "shortname": "bda-attributions",
"subject": "R\u00e9sultats du tirage au sort", "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", "model": "custommail.custommail",
"pk": 13,
"fields": { "fields": {
"shortname": "bda-attributions-decus", "shortname": "bda-attributions-decus",
"subject": "R\u00e9sultats du tirage au sort", "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", "model": "custommail.variable",
"pk": 1,
"fields": { "fields": {
"name": "member",
"description": "Utilisateur de GestioCOF",
"custommail": 1, "custommail": 1,
"type": 1 "type": 1,
}
},
{
"model": "custommail.custommailvariable",
"pk": 2,
"fields": {
"name": "member", "name": "member",
"description": "Utilisateur ayant eu une place pour ce spectacle", "description": "Utilisateur de GestioCOF"
"custommail": 2, },
"type": 1 "pk": 1
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 3,
"fields": { "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", "name": "show",
"description": "Spectacle", "description": "Spectacle"
"custommail": 2, },
"type": 3 "pk": 3
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 4,
"fields": { "fields": {
"custommail": 2,
"type": 2,
"name": "nb_attr", "name": "nb_attr",
"description": "Nombre de places obtenues", "description": "Nombre de places obtenues"
"custommail": 2, },
"type": 2 "pk": 4
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 5,
"fields": { "fields": {
"custommail": 3,
"type": 4,
"name": "revente", "name": "revente",
"description": "Revente mentionn\u00e9e dans le mail", "description": "Revente mentionn\u00e9e dans le mail"
"custommail": 3, },
"type": 4 "pk": 5
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 6,
"fields": { "fields": {
"custommail": 3,
"type": 1,
"name": "member", "name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place", "description": "Personne int\u00e9ress\u00e9e par la place"
"custommail": 3, },
"type": 1 "pk": 6
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 7,
"fields": { "fields": {
"custommail": 3,
"type": 3,
"name": "show", "name": "show",
"description": "Spectacle", "description": "Spectacle"
"custommail": 3, },
"type": 3 "pk": 7
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 8,
"fields": { "fields": {
"name": "site",
"description": "Site web (gestioCOF)",
"custommail": 3, "custommail": 3,
"type": 5 "type": 5,
} "name": "site",
"description": "Site web (gestioCOF)"
},
"pk": 8
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 9,
"fields": { "fields": {
"name": "site",
"description": "Site web (gestioCOF)",
"custommail": 4, "custommail": 4,
"type": 5 "type": 5,
} "name": "site",
"description": "Site web (gestioCOF)"
},
"pk": 9
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 10,
"fields": { "fields": {
"custommail": 4,
"type": 3,
"name": "show", "name": "show",
"description": "Spectacle", "description": "Spectacle"
"custommail": 4, },
"type": 3 "pk": 10
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 11,
"fields": { "fields": {
"custommail": 4,
"type": 1,
"name": "member", "name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place", "description": "Personne int\u00e9ress\u00e9e par la place"
"custommail": 4, },
"type": 1 "pk": 11
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 12,
"fields": { "fields": {
"name": "acheteur",
"description": "Gagnant-e du tirage",
"custommail": 5, "custommail": 5,
"type": 1 "type": 1,
}
},
{
"model": "custommail.custommailvariable",
"pk": 13,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 5,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 14,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 5,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 15,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 6,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 16,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 6,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 17,
"fields": {
"name": "acheteur", "name": "acheteur",
"description": "Personne inscrite au tirage qui n'a pas eu la place", "description": "Gagnant-e du tirage"
"custommail": 6, },
"type": 1 "pk": 12
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 18,
"fields": {
"name": "acheteur",
"description": "Gagnant-e du tirage",
"custommail": 7,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 19,
"fields": { "fields": {
"custommail": 5,
"type": 1,
"name": "vendeur", "name": "vendeur",
"description": "Personne qui vend une place", "description": "Personne qui vend une place"
"custommail": 7, },
"type": 1 "pk": 13
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 20,
"fields": { "fields": {
"custommail": 5,
"type": 3,
"name": "show", "name": "show",
"description": "Spectacle", "description": "Spectacle"
"custommail": 7, },
"type": 3 "pk": 14
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 21,
"fields": { "fields": {
"custommail": 6,
"type": 3,
"name": "show", "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, "custommail": 8,
"type": 3 "type": 3,
} "name": "show",
"description": "Spectacle"
},
"pk": 21
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 22,
"fields": { "fields": {
"name": "vendeur",
"description": "Personne qui vend la place",
"custommail": 8, "custommail": 8,
"type": 1 "type": 1,
} "name": "vendeur",
"description": "Personne qui vend la place"
},
"pk": 22
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 23,
"fields": { "fields": {
"custommail": 8,
"type": 4,
"name": "revente", "name": "revente",
"description": "Revente mentionn\u00e9e dans le mail", "description": "Revente mentionn\u00e9e dans le mail"
"custommail": 8, },
"type": 4 "pk": 23
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 24,
"fields": { "fields": {
"custommail": 9,
"type": 1,
"name": "vendeur", "name": "vendeur",
"description": "Personne qui vend la place", "description": "Personne qui vend la place"
"custommail": 9, },
"type": 1 "pk": 24
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 25,
"fields": { "fields": {
"custommail": 9,
"type": 3,
"name": "show", "name": "show",
"description": "Spectacle", "description": "Spectacle"
"custommail": 9, },
"type": 3 "pk": 25
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 26,
"fields": { "fields": {
"custommail": 9,
"type": 1,
"name": "acheteur", "name": "acheteur",
"description": "Personne qui prend la place au shotgun", "description": "Personne qui prend la place au shotgun"
"custommail": 9, },
"type": 1 "pk": 26
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 27,
"fields": { "fields": {
"custommail": 10,
"type": 6,
"name": "demande", "name": "demande",
"description": "Demande de petit cours", "description": "Demande de petit cours"
"custommail": 10, },
"type": 6 "pk": 27
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 28,
"fields": { "fields": {
"custommail": 10,
"type": 7,
"name": "matieres", "name": "matieres",
"description": "Liste des mati\u00e8res concern\u00e9es par la demande", "description": "Liste des mati\u00e8res concern\u00e9es par la demande"
"custommail": 10, },
"type": 7 "pk": 28
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 29,
"fields": { "fields": {
"custommail": 11,
"type": 10,
"name": "proposals", "name": "proposals",
"description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re", "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re"
"custommail": 11, },
"type": 10 "pk": 29
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 30,
"fields": { "fields": {
"custommail": 11,
"type": 7,
"name": "unsatisfied", "name": "unsatisfied",
"description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer", "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer"
"custommail": 11, },
"type": 7 "pk": 30
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 31,
"fields": { "fields": {
"custommail": 12,
"type": 11,
"name": "places", "name": "places",
"description": "Places de spectacle du participant", "description": "Places de spectacle du participant"
"custommail": 12, },
"type": 11 "pk": 31
}
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 32,
"fields": { "fields": {
"name": "member",
"description": "Participant du tirage au sort",
"custommail": 12, "custommail": 12,
"type": 1 "type": 1,
} "name": "member",
"description": "Participant du tirage au sort"
},
"pk": 32
}, },
{ {
"model": "custommail.custommailvariable", "model": "custommail.variable",
"pk": 33,
"fields": { "fields": {
"name": "member",
"description": "Participant du tirage au sort",
"custommail": 13, "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')), ('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_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)), ('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={ options={
'verbose_name': 'Profil COF', 'verbose_name': 'Profil COF',
@ -91,7 +91,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=200, verbose_name=b'Champ')), ('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')])), ('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)), ('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={ options={
'verbose_name': 'Champ', 'verbose_name': 'Champ',
@ -102,7 +102,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('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)), ('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( migrations.CreateModel(
@ -111,7 +111,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=200, verbose_name=b'Option')), ('name', models.CharField(max_length=200, verbose_name=b'Option')),
('multi_choices', models.BooleanField(default=False, verbose_name=b'Choix multiples')), ('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={ options={
'verbose_name': 'Option', 'verbose_name': 'Option',
@ -122,7 +122,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('value', models.CharField(max_length=200, verbose_name=b'Valeur')), ('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={ options={
'verbose_name': 'Choix', 'verbose_name': 'Choix',
@ -133,10 +133,10 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('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')), ('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')), ('filledcomments', models.ManyToManyField(to='gestioncof.EventCommentField', through='gestioncof.EventCommentValue')),
('options', models.ManyToManyField(to='gestioncof.EventOptionChoice')), ('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={ options={
'verbose_name': 'Inscription', '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)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('question', models.CharField(max_length=200, verbose_name=b'Question')), ('question', models.CharField(max_length=200, verbose_name=b'Question')),
('multi_answers', models.BooleanField(default=False, verbose_name=b'Choix multiples')), ('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={ options={
'verbose_name': 'Question', 'verbose_name': 'Question',
@ -251,7 +251,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('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')), ('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={ options={
'verbose_name': 'R\xe9ponse', 'verbose_name': 'R\xe9ponse',
@ -265,12 +265,12 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='surveyanswer', model_name='surveyanswer',
name='survey', name='survey',
field=models.ForeignKey(to='gestioncof.Survey'), field=models.ForeignKey(to='gestioncof.Survey', on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='surveyanswer', model_name='surveyanswer',
name='user', name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL), field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='petitcoursdemande', model_name='petitcoursdemande',
@ -280,47 +280,47 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='petitcoursdemande', model_name='petitcoursdemande',
name='traitee_par', 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( migrations.AddField(
model_name='petitcoursattributioncounter', model_name='petitcoursattributioncounter',
name='matiere', 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( migrations.AddField(
model_name='petitcoursattributioncounter', model_name='petitcoursattributioncounter',
name='user', name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL), field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='petitcoursattribution', model_name='petitcoursattribution',
name='demande', 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( migrations.AddField(
model_name='petitcoursattribution', model_name='petitcoursattribution',
name='matiere', 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( migrations.AddField(
model_name='petitcoursattribution', model_name='petitcoursattribution',
name='user', name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL), field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='petitcoursability', model_name='petitcoursability',
name='matiere', 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( migrations.AddField(
model_name='petitcoursability', model_name='petitcoursability',
name='user', name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL), field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='eventcommentvalue', model_name='eventcommentvalue',
name='registration', 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( migrations.AlterUniqueTogether(
name='surveyanswer', name='surveyanswer',

View file

@ -23,7 +23,8 @@ class Migration(migrations.Migration):
('subscribe_to_events', models.BooleanField(default=True)), ('subscribe_to_events', models.BooleanField(default=True)),
('subscribe_to_my_shows', models.BooleanField(default=True)), ('subscribe_to_my_shows', models.BooleanField(default=True)),
('other_shows', models.ManyToManyField(to='bda.Spectacle')), ('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( 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.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ 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 django.db.models.signals import post_save, post_delete
from gestioncof.petits_cours_models import choices_length from gestioncof.petits_cours_models import choices_length
from bda.models import Spectacle 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 = ( TYPE_COMMENT_FIELD = (
('text', _("Texte long")), ('text', _("Texte long")),
('char', _("Texte court")), ('char', _("Texte court")),
) )
@python_2_unicode_compatible
class CofProfile(models.Model): class CofProfile(models.Model):
user = models.OneToOneField(User, related_name="profile") STATUS_EXTE = "exterieur"
login_clipper = models.CharField("Login clipper", max_length=8, blank=True) 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) 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) phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField(_("Occupation"), occupation = models.CharField(_("Occupation"),
default="1A", default="1A",
@ -72,7 +88,7 @@ class CofProfile(models.Model):
verbose_name_plural = "Profils COF" verbose_name_plural = "Profils COF"
def __str__(self): def __str__(self):
return six.text_type(self.user.username) return self.user.username
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
@ -86,7 +102,6 @@ def post_delete_user(sender, instance, *args, **kwargs):
instance.user.delete() instance.user.delete()
@python_2_unicode_compatible
class Club(models.Model): class Club(models.Model):
name = models.CharField("Nom", max_length=200, unique=True) name = models.CharField("Nom", max_length=200, unique=True)
description = models.TextField("Description", blank=True) description = models.TextField("Description", blank=True)
@ -98,7 +113,6 @@ class Club(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class Event(models.Model): class Event(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
location = models.CharField("Lieu", max_length=200) location = models.CharField("Lieu", max_length=200)
@ -115,12 +129,14 @@ class Event(models.Model):
verbose_name = "Événement" verbose_name = "Événement"
def __str__(self): def __str__(self):
return six.text_type(self.title) return self.title
@python_2_unicode_compatible
class EventCommentField(models.Model): 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) name = models.CharField("Champ", max_length=200)
fieldtype = models.CharField("Type", max_length=10, fieldtype = models.CharField("Type", max_length=10,
choices=TYPE_COMMENT_FIELD, default="text") choices=TYPE_COMMENT_FIELD, default="text")
@ -130,23 +146,29 @@ class EventCommentField(models.Model):
verbose_name = "Champ" verbose_name = "Champ"
def __str__(self): def __str__(self):
return six.text_type(self.name) return self.name
@python_2_unicode_compatible
class EventCommentValue(models.Model): class EventCommentValue(models.Model):
commentfield = models.ForeignKey(EventCommentField, related_name="values") commentfield = models.ForeignKey(
registration = models.ForeignKey("EventRegistration", EventCommentField, on_delete=models.CASCADE,
related_name="comments") related_name="values",
)
registration = models.ForeignKey(
"EventRegistration", on_delete=models.CASCADE,
related_name="comments",
)
content = models.TextField("Contenu", blank=True, null=True) content = models.TextField("Contenu", blank=True, null=True)
def __str__(self): def __str__(self):
return "Commentaire de %s" % self.commentfield return "Commentaire de %s" % self.commentfield
@python_2_unicode_compatible
class EventOption(models.Model): 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) name = models.CharField("Option", max_length=200)
multi_choices = models.BooleanField("Choix multiples", default=False) multi_choices = models.BooleanField("Choix multiples", default=False)
@ -154,12 +176,14 @@ class EventOption(models.Model):
verbose_name = "Option" verbose_name = "Option"
def __str__(self): def __str__(self):
return six.text_type(self.name) return self.name
@python_2_unicode_compatible
class EventOptionChoice(models.Model): 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) value = models.CharField("Valeur", max_length=200)
class Meta: class Meta:
@ -167,13 +191,12 @@ class EventOptionChoice(models.Model):
verbose_name_plural = "Choix" verbose_name_plural = "Choix"
def __str__(self): def __str__(self):
return six.text_type(self.value) return self.value
@python_2_unicode_compatible
class EventRegistration(models.Model): class EventRegistration(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User, on_delete=models.CASCADE)
event = models.ForeignKey(Event) event = models.ForeignKey(Event, on_delete=models.CASCADE)
options = models.ManyToManyField(EventOptionChoice) options = models.ManyToManyField(EventOptionChoice)
filledcomments = models.ManyToManyField(EventCommentField, filledcomments = models.ManyToManyField(EventCommentField,
through=EventCommentValue) through=EventCommentValue)
@ -184,11 +207,9 @@ class EventRegistration(models.Model):
unique_together = ("user", "event") unique_together = ("user", "event")
def __str__(self): def __str__(self):
return "Inscription de %s à %s" % (six.text_type(self.user), return "Inscription de {} à {}".format(self.user, self.event.title)
six.text_type(self.event.title))
@python_2_unicode_compatible
class Survey(models.Model): class Survey(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
details = models.TextField("Détails", blank=True) details = models.TextField("Détails", blank=True)
@ -199,12 +220,14 @@ class Survey(models.Model):
verbose_name = "Sondage" verbose_name = "Sondage"
def __str__(self): def __str__(self):
return six.text_type(self.title) return self.title
@python_2_unicode_compatible
class SurveyQuestion(models.Model): 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) question = models.CharField("Question", max_length=200)
multi_answers = models.BooleanField("Choix multiples", default=False) multi_answers = models.BooleanField("Choix multiples", default=False)
@ -212,25 +235,26 @@ class SurveyQuestion(models.Model):
verbose_name = "Question" verbose_name = "Question"
def __str__(self): def __str__(self):
return six.text_type(self.question) return self.question
@python_2_unicode_compatible
class SurveyQuestionAnswer(models.Model): 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) answer = models.CharField("Réponse", max_length=200)
class Meta: class Meta:
verbose_name = "Réponse" verbose_name = "Réponse"
def __str__(self): def __str__(self):
return six.text_type(self.answer) return self.answer
@python_2_unicode_compatible
class SurveyAnswer(models.Model): class SurveyAnswer(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User, on_delete=models.CASCADE)
survey = models.ForeignKey(Survey) survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
answers = models.ManyToManyField(SurveyQuestionAnswer, answers = models.ManyToManyField(SurveyQuestionAnswer,
related_name="selected_by") related_name="selected_by")
@ -244,10 +268,9 @@ class SurveyAnswer(models.Model):
self.survey.title) self.survey.title)
@python_2_unicode_compatible
class CalendarSubscription(models.Model): class CalendarSubscription(models.Model):
token = models.UUIDField() token = models.UUIDField()
user = models.OneToOneField(User) user = models.OneToOneField(User, on_delete=models.CASCADE)
other_shows = models.ManyToManyField(Spectacle) other_shows = models.ManyToManyField(Spectacle)
subscribe_to_events = models.BooleanField(default=True) subscribe_to_events = models.BooleanField(default=True)
subscribe_to_my_shows = models.BooleanField(default=True) subscribe_to_my_shows = models.BooleanField(default=True)

View file

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

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from datetime import datetime
from custommail.shortcuts import render_custom_mail from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
@ -12,15 +11,16 @@ from django.views.decorators.csrf import csrf_exempt
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.db import transaction
from django.utils import timezone
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import ( from gestioncof.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject PetitCoursAbility
) )
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables
class DemandeListView(ListView): class DemandeListView(ListView):
@ -274,20 +274,20 @@ def _traitement_post(request, demande):
headers={'Reply-To': replyto})) headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=False) connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send) connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User) with transaction.atomic():
for matiere in proposals: for matiere in proposals:
for rank, user in enumerate(proposals[matiere]): for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(user=user, counter = PetitCoursAttributionCounter.objects.get(
matiere=matiere) user=user, matiere=matiere
counter.count += 1 )
counter.save() counter.count += 1
attrib = PetitCoursAttribution(user=user, matiere=matiere, counter.save()
demande=demande, rank=rank + 1) attrib = PetitCoursAttribution(user=user, matiere=matiere,
attrib.save() demande=demande, rank=rank + 1)
unlock_tables() attrib.save()
demande.traitee = True demande.traitee = True
demande.traitee_par = request.user demande.traitee_par = request.user
demande.processed = datetime.now() demande.processed = timezone.now()
demande.save() demande.save()
return render(request, return render(request,
"gestioncof/traitement_demande_petit_cours_success.html", "gestioncof/traitement_demande_petit_cours_success.html",
@ -309,17 +309,15 @@ def inscription(request):
profile.petits_cours_accept = "receive_proposals" in request.POST profile.petits_cours_accept = "receive_proposals" in request.POST
profile.petits_cours_remarques = request.POST["remarques"] profile.petits_cours_remarques = request.POST["remarques"]
profile.save() profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, with transaction.atomic():
PetitCoursSubject) abilities = (
abilities = ( PetitCoursAbility.objects.filter(user=request.user).all()
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
) )
unlock_tables() for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
success = True success = True
formset = MatieresFormSet(instance=request.user) formset = MatieresFormSet(instance=request.user)
else: 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.conf import settings
from django.contrib.sites.models import Site
from django_cas_ng.backends import CASBackend 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 from gestioncof.models import CofProfile
User = get_user_model()
class COFCASBackend(CASBackend): 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 # 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 # et à la fin, ainsi quavec une casse variable. On normalise pour
# éviter les doublons. # éviter les doublons.
username = username.strip().lower() return username.strip().lower()
profiles = CofProfile.objects.filter(login_clipper=username) def configure_user(self, user):
if len(profiles) > 0: clipper = user.username
profile = profiles.order_by('-is_cof')[0] user.profile.login_clipper = clipper
user = profile.user user.profile.save()
return user user.email = settings.CAS_EMAIL_FORMAT % clipper
try: user.save()
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()
return user return user
@ -74,25 +30,3 @@ def context_processor(request):
"site": Site.objects.get_current(), "site": Site.objects.get_current(),
} }
return data 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; background: transparent;
} }
div.empty-form {
padding-bottom: 2em;
}
a:hover { a:hover {
color: #444; color: #444;
@ -341,10 +342,12 @@ fieldset legend {
font-weight: 700; font-weight: 700;
font-size: large; font-size: large;
color:#DE826B; color:#DE826B;
padding-bottom: .5em;
} }
#main-content h4 { #main-content h4 {
color:#DE826B; color:#DE826B;
padding-bottom: .5em;
} }
#main-content h2, #main-content h2,
@ -778,6 +781,17 @@ header .open > .dropdown-toggle.btn-default {
border-color: white; border-color: white;
} }
/* Announcements banner ------------------ */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* FORMS --------------------------------- */ /* FORMS --------------------------------- */

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 http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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/bootstrap.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/cof.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 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" %}"> <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://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> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}

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 %} {% block content %}
<header> <header>
<div class="container banner"> <div class="container banner">
<a href="{% url "gestioncof.views.home" %}"> <a href="{% url "home" %}">
<h1>GestioCOF</h1> <h1>GestioCOF</h1>
{% block homelink %} {% block homelink %}
<span class="glyphicon glyphicon-home" aria-hidden=true></span> <span class="glyphicon glyphicon-home" aria-hidden=true></span>
@ -11,11 +11,19 @@
</a> </a>
<div class="secondary"> <div class="secondary">
<span class="hidden-xxs">&nbsp;&nbsp;|&nbsp; </span> <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> </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> <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 --> </div><!-- /.container -->
</header> </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 %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="messages"> <div class="messages">

View file

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

View file

@ -1,4 +1,5 @@
{% extends "gestioncof/base_header.html" %} {% extends "gestioncof/base_header.html" %}
{% load wagtailcore_tags %}
{% block homelink %} {% block homelink %}
{% endblock %} {% endblock %}
@ -13,7 +14,7 @@
<div class="hm-block"> <div class="hm-block">
<ul> <ul>
{% for event in open_events %} {% 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 %} {% endfor %}
</ul> </ul>
</div> </div>
@ -23,7 +24,7 @@
<div class="hm-block"> <div class="hm-block">
<ul> <ul>
{% for survey in open_surveys %} {% 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 %} {% endfor %}
</ul> </ul>
</div> </div>
@ -55,7 +56,8 @@
<h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3> <h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3>
<div class="hm-block"> <div class="hm-block">
<ul> <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> <li><a href="https://www.cof.ens.fr/k-fet/calendrier">Calendrier</a></li>
{% if perms.kfet.is_team %} {% if perms.kfet.is_team %}
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li> <li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>
@ -67,11 +69,11 @@
<h3 class="block-title">Divers<span class="pull-right glyphicon glyphicon-question-sign"></span></h3> <h3 class="block-title">Divers<span class="pull-right glyphicon glyphicon-question-sign"></span></h3>
<div class="hm-block"> <div class="hm-block">
<ul> <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 %} {% 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> <li><a href="{% url "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 %} {% if not user.profile.login_clipper %}<li><a href="{% url "password_change" %}">Changer mon mot de passe</a></li>{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -84,16 +86,16 @@
<h4>Général</h4> <h4>Général</h4>
<li><a href="{% url "admin:index" %}">Administration générale</a></li> <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 "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> <li><a href="{% url "liste-clubs" %}">Gestion des clubs</a></li>
</ul> </ul>
<ul> <ul>
<h4>Évènements & Sondages</h4> <h4>Évènements & Sondages</h4>
{% for event in events %} {% 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 %} {% endfor %}
{% for survey in surveys %} {% 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 %} {% endfor %}
</ul> </ul>
</div> </div>
@ -118,8 +120,8 @@
<h3 class="block-title">Liens utiles<span class="pull-right glyphicon glyphicon-link"></span></h3> <h3 class="block-title">Liens utiles<span class="pull-right glyphicon glyphicon-link"></span></h3>
<div class="hm-block"> <div class="hm-block">
<ul> <ul>
<li><a href="{% url "gestioncof.views.utile_cof" %}">Liens utiles du COF</a></li> <li><a href="{% url "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_bda" %}">Liens utiles BdA</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@
<p class="error">Identifiants incorrects.</p> <p class="error">Identifiants incorrects.</p>
{% endif %} {% endif %}
<form class="form-horizontal" method="post" <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 %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<input class="form-control" id="id_username" maxlength="254" name="username" type="text" placeholder="Nom d'utilisateur"> <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="container-fluid">
<div class="row" style="margin:0;"> <div class="row" style="margin:0;">
<a aria-label="Compte clipper" <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"> <div class="col-xs-12 col-sm-6" id="login_clipper">
Compte clipper Compte clipper
</div> </div>
</a> </a>
<a aria-label="Extérieur" <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"> <div class="col-xs-12 col-sm-6" id="login_outsider">
Extérieur Extérieur
</div> </div>

View file

@ -5,5 +5,5 @@
{% block realcontent %} {% block realcontent %}
<h2>Mot de passe modifié avec succès !</h2> <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 %} {% endblock %}

View file

@ -5,7 +5,7 @@
{% block realcontent %} {% block realcontent %}
<h2>Changement de mot de passe</h2> <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 %} {% csrf_token %}
{{ form | bootstrap }} {{ form | bootstrap }}
<input type="submit" class="btn btn-primary pull-right" value="Changer" /> <input type="submit" class="btn btn-primary pull-right" value="Changer" />

View file

@ -10,7 +10,7 @@ export_patterns = [
url(r'^mega/avecremarques$', views.export_mega_remarksonly), url(r'^mega/avecremarques$', views.export_mega_remarksonly),
url(r'^mega/participants$', views.export_mega_participants), url(r'^mega/participants$', views.export_mega_participants),
url(r'^mega/orgas$', views.export_mega_orgas), url(r'^mega/orgas$', views.export_mega_orgas),
url(r'^mega/(?P<type>.+)$', views.export_mega_bytype), # url(r'^mega/(?P<type>.+)$', views.export_mega_bytype),
url(r'^mega$', views.export_mega), url(r'^mega$', views.export_mega),
] ]
@ -36,19 +36,23 @@ petitcours_patterns = [
] ]
surveys_patterns = [ surveys_patterns = [
url(r'^(?P<survey_id>\d+)/status$', views.survey_status), url(r'^(?P<survey_id>\d+)/status$', views.survey_status,
url(r'^(?P<survey_id>\d+)$', views.survey), name='survey.details.status'),
url(r'^(?P<survey_id>\d+)$', views.survey,
name='survey.details'),
] ]
events_patterns = [ events_patterns = [
url(r'^(?P<event_id>\d+)$', views.event), url(r'^(?P<event_id>\d+)$', views.event,
url(r'^(?P<event_id>\d+)/status$', views.event_status), name='event.details'),
url(r'^(?P<event_id>\d+)/status$', views.event_status,
name='event.details.status'),
] ]
calendar_patterns = [ calendar_patterns = [
url(r'^subscription$', 'gestioncof.views.calendar'), url(r'^subscription$', views.calendar,
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', name='calendar'),
'gestioncof.views.calendar_ics') url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics)
] ]
clubs_patterns = [ clubs_patterns = [

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import unicodecsv import unicodecsv
import uuid import uuid
from datetime import timedelta from datetime import timedelta
@ -9,12 +7,20 @@ from custommail.shortcuts import send_custom_mail
from django.shortcuts import redirect, get_object_or_404, render from django.shortcuts import redirect, get_object_or_404, render
from django.http import Http404, HttpResponse, HttpResponseForbidden from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import login as django_login_view from django.contrib.auth.views import (
login as django_login_view, logout as django_logout_view,
)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse_lazy
from django.views.generic import FormView
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.contrib import messages from django.contrib import messages
import django.utils.six as six
from django_cas_ng.views import logout as cas_logout_view
from utils.views.autocomplete import Select2QuerySetView
from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \
SurveyQuestionAnswer SurveyQuestionAnswer
@ -24,10 +30,11 @@ from gestioncof.models import EventCommentField, EventCommentValue, \
CalendarSubscription CalendarSubscription
from gestioncof.models import CofProfile, Club from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ from gestioncof.forms import (
SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ UserProfileForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm,
RegistrationProfileForm, EventForm, CalendarForm, EventFormset, \ RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm,
RegistrationPassUserForm, ClubsForm EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm
)
from bda.models import Tirage, Spectacle from bda.models import Tirage, Spectacle
@ -49,8 +56,8 @@ def home(request):
def login(request): def login(request):
if request.user.is_authenticated(): if request.user.is_authenticated:
return redirect("gestioncof.views.home") return redirect("home")
context = {} context = {}
if request.method == "GET" and 'next' in request.GET: if request.method == "GET" and 'next' in request.GET:
context['next'] = request.GET['next'] context['next'] = request.GET['next']
@ -81,15 +88,21 @@ def login_ext(request):
@login_required @login_required
def logout(request): def logout(request, next_page=None):
try: if next_page is None:
profile = request.user.profile next_page = request.GET.get('next', None)
except CofProfile.DoesNotExist:
profile, created = CofProfile.objects.get_or_create(user=request.user) profile = getattr(request.user, 'profile', None)
if profile.login_clipper:
return redirect("django_cas_ng.views.logout") if profile and profile.login_clipper:
msg = _('Déconnexion de GestioCOF et CAS réussie. À bientôt {}.')
logout_view = cas_logout_view
else: else:
return redirect("django.contrib.auth.views.logout") msg = _('Déconnexion de GestioCOF réussie. À bientôt {}.')
logout_view = django_logout_view
messages.success(request, msg.format(request.user.get_short_name()))
return logout_view(request, next_page=next_page)
@login_required @login_required
@ -387,7 +400,6 @@ def registration_form2(request, login_clipper=None, username=None,
elif not login_clipper: elif not login_clipper:
# new user # new user
user_form = RegistrationPassUserForm() user_form = RegistrationPassUserForm()
user_form.force_long_username()
profile_form = RegistrationProfileForm() profile_form = RegistrationProfileForm()
event_formset = EventFormset(events=events, prefix='events') event_formset = EventFormset(events=events, prefix='events')
clubs_form = ClubsForm() clubs_form = ClubsForm()
@ -403,12 +415,8 @@ def registration_form2(request, login_clipper=None, username=None,
def registration(request): def registration(request):
if request.POST: if request.POST:
request_dict = request.POST.copy() request_dict = request.POST.copy()
# num ne peut pas être défini manuellement
if "num" in request_dict:
del request_dict["num"]
member = None member = None
login_clipper = None login_clipper = None
success = False
# ----- # -----
# Remplissage des formulaires # Remplissage des formulaires
@ -430,12 +438,10 @@ def registration(request):
user_form = RegistrationUserForm(request_dict, instance=member) user_form = RegistrationUserForm(request_dict, instance=member)
if member.profile.login_clipper: if member.profile.login_clipper:
login_clipper = member.profile.login_clipper login_clipper = member.profile.login_clipper
else:
user_form.force_long_username()
except User.DoesNotExist: except User.DoesNotExist:
user_form.force_long_username() pass
else: else:
user_form.force_long_username() pass
# ----- # -----
# Validation des formulaires # Validation des formulaires
@ -445,7 +451,6 @@ def registration(request):
member = user_form.save() member = user_form.save()
profile, _ = CofProfile.objects.get_or_create(user=member) profile, _ = CofProfile.objects.get_or_create(user=member)
was_cof = profile.is_cof was_cof = profile.is_cof
request_dict["num"] = profile.num
# Maintenant on remplit le formulaire de profil # Maintenant on remplit le formulaire de profil
profile_form = RegistrationProfileForm(request_dict, profile_form = RegistrationProfileForm(request_dict,
instance=profile) instance=profile)
@ -499,16 +504,18 @@ def registration(request):
for club in clubs_form.cleaned_data['clubs']: for club in clubs_form.cleaned_data['clubs']:
club.membres.add(member) club.membres.add(member)
club.save() club.save()
success = True
# Messages # ---
if success: # Success
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été " # ---
"enregistrée avec succès"
.format(member.get_full_name(), member.email)) msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
if member.profile.is_cof: "enregistrée avec succès."
msg += "Il est désormais membre du COF n°{:d} !".format( .format(member.get_full_name(), member.email))
member.profile.num) if profile.is_cof:
messages.success(request, msg, extra_tags='safe') msg += "\nIl est désormais membre du COF n°{:d} !".format(
member.profile.id)
messages.success(request, msg, extra_tags='safe')
return render(request, "gestioncof/registration_post.html", return render(request, "gestioncof/registration_post.html",
{"user_form": user_form, {"user_form": user_form,
"profile_form": profile_form, "profile_form": profile_form,
@ -572,10 +579,10 @@ def export_members(request):
writer = unicodecsv.writer(response) writer = unicodecsv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all(): for profile in CofProfile.objects.filter(is_cof=True).all():
user = profile.user user = profile.user
bits = [profile.num, user.username, user.first_name, user.last_name, bits = [user.id, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation, user.email, profile.phone, profile.occupation,
profile.departement, profile.type_cotiz] profile.departement, profile.type_cotiz]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@ -591,78 +598,80 @@ def csv_export_mega(filename, qs):
comments = "---".join( comments = "---".join(
[comment.content for comment in reg.comments.all()]) [comment.content for comment in reg.comments.all()])
bits = [user.username, user.first_name, user.last_name, user.email, bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.phone, user.id,
profile.comments if profile.comments else "", comments] profile.comments if profile.comments else "", comments]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@buro_required @buro_required
def export_mega_remarksonly(request): def export_mega_remarksonly(request):
filename = 'remarques_mega_2016.csv' filename = 'remarques_mega_2017.csv'
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=' + filename response['Content-Disposition'] = 'attachment; filename=' + filename
writer = unicodecsv.writer(response) writer = unicodecsv.writer(response)
event = Event.objects.get(title="Mega 2016") event = Event.objects.get(title="MEGA 2017")
commentfield = event.commentfields.get(name="Commentaires") commentfield = event.commentfields.get(name="Commentaire")
for val in commentfield.values.all(): for val in commentfield.values.all():
reg = val.registration reg = val.registration
user = reg.user user = reg.user
profile = user.profile profile = user.profile
bits = [user.username, user.first_name, user.last_name, user.email, bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.comments, val.content] profile.phone, profile.id, profile.comments, val.content]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@buro_required # @buro_required
def export_mega_bytype(request, type): # def export_mega_bytype(request, type):
types = {"orga-actif": "Orga élève", # types = {"orga-actif": "Orga élève",
"orga-branleur": "Orga étudiant", # "orga-branleur": "Orga étudiant",
"conscrit-eleve": "Conscrit élève", # "conscrit-eleve": "Conscrit élève",
"conscrit-etudiant": "Conscrit étudiant"} # "conscrit-etudiant": "Conscrit étudiant"}
#
if type not in types: # if type not in types:
raise Http404 # raise Http404
#
event = Event.objects.get(title="Mega 2016") # event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Type") # type_option = event.options.get(name="Type")
participant_type = type_option.choices.get(value=types[type]).id # participant_type = type_option.choices.get(value=types[type]).id
qs = EventRegistration.objects.filter(event=event).filter( # qs = EventRegistration.objects.filter(event=event).filter(
options__id__exact=participant_type) # options__id__exact=participant_type)
return csv_export_mega(type + '_mega_2016.csv', qs) # return csv_export_mega(type + '_mega_2017.csv', qs)
@buro_required @buro_required
def export_mega_orgas(request): def export_mega_orgas(request):
event = Event.objects.get(title="Mega 2016") event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit ou orga ?") type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Vieux").id participant_type = type_option.choices.get(value="Orga").id
qs = EventRegistration.objects.filter(event=event).exclude( qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type) options__id=participant_type
return csv_export_mega('orgas_mega_2016.csv', qs) )
return csv_export_mega('orgas_mega_2017.csv', qs)
@buro_required @buro_required
def export_mega_participants(request): def export_mega_participants(request):
event = Event.objects.get(title="Mega 2016") event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit ou orga ?") type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Conscrit").id participant_type = type_option.choices.get(value="Conscrit").id
qs = EventRegistration.objects.filter(event=event).filter( qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type) options__id=participant_type
return csv_export_mega('participants_mega_2016.csv', qs) )
return csv_export_mega('participants_mega_2017.csv', qs)
@buro_required @buro_required
def export_mega(request): def export_mega(request):
event = Event.objects.filter(title="Mega 2016") event = Event.objects.filter(title="MEGA 2017")
qs = EventRegistration.objects.filter(event=event) \ qs = EventRegistration.objects.filter(event=event) \
.order_by("user__username") .order_by("user__username")
return csv_export_mega('all_mega_2016.csv', qs) return csv_export_mega('all_mega_2017.csv', qs)
@buro_required @buro_required
@ -764,3 +773,33 @@ def calendar_ics(request, token):
response = HttpResponse(content=vcal.to_ical()) response = HttpResponse(content=vcal.to_ical())
response['Content-Type'] = "text/calendar" response['Content-Type'] = "text/calendar"
return response return response
class ConfigUpdate(FormView):
form_class = GestioncofConfigForm
template_name = "gestioncof/banner_update.html"
success_url = reverse_lazy("home")
def dispatch(self, request, *args, **kwargs):
if request.user is None or not request.user.is_superuser:
raise Http404
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.save()
return super().form_valid(form)
##
# Autocomplete views
#
# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view
##
class UserAutocomplete(Select2QuerySetView):
model = User
search_fields = ('username', 'first_name', 'last_name')
user_autocomplete = buro_required(UserAutocomplete.as_view())

View file

@ -11,7 +11,6 @@ class KFetConfig(AppConfig):
verbose_name = "Application K-Fêt" verbose_name = "Application K-Fêt"
def ready(self): def ready(self):
import kfet.signals
self.register_config() self.register_config()
def register_config(self): def register_config(self):

4
kfet/auth/__init__.py Normal file
View file

@ -0,0 +1,4 @@
default_app_config = 'kfet.auth.apps.KFetAuthConfig'
KFET_GENERIC_USERNAME = 'kfet_genericteam'
KFET_GENERIC_TRIGRAMME = 'GNR'

14
kfet/auth/apps.py Normal file
View file

@ -0,0 +1,14 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _
class KFetAuthConfig(AppConfig):
name = 'kfet.auth'
label = 'kfetauth'
verbose_name = _("K-Fêt - Authentification et Autorisation")
def ready(self):
from . import signals # noqa
from .utils import setup_kfet_generic_user
post_migrate.connect(setup_kfet_generic_user, sender=self)

43
kfet/auth/backends.py Normal file
View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from kfet.models import Account, GenericTeamToken
from .utils import get_kfet_generic_user
User = get_user_model()
class BaseKFetBackend:
def get_user(self, user_id):
"""
Add extra select related up to Account.
"""
try:
return (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_id)
)
except User.DoesNotExist:
return None
class AccountBackend(BaseKFetBackend):
def authenticate(self, request, kfet_password=None):
try:
return Account.objects.get_by_password(kfet_password).user
except Account.DoesNotExist:
return None
class GenericBackend(BaseKFetBackend):
def authenticate(self, request, kfet_token=None):
try:
team_token = GenericTeamToken.objects.get(token=kfet_token)
except GenericTeamToken.DoesNotExist:
return
# No need to keep the token.
team_token.delete()
return get_kfet_generic_user()

View file

@ -0,0 +1,10 @@
from django.contrib.auth.context_processors import PermWrapper
def temporary_auth(request):
if hasattr(request, 'real_user'):
return {
'user': request.real_user,
'perms': PermWrapper(request.real_user),
}
return {}

20
kfet/auth/fields.py Normal file
View file

@ -0,0 +1,20 @@
from django import forms
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.forms import widgets
class KFetPermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, *args, **kwargs):
queryset = Permission.objects.filter(
content_type__in=ContentType.objects.filter(app_label="kfet"),
)
super().__init__(
queryset=queryset,
widget=widgets.CheckboxSelectMultiple,
*args, **kwargs
)
def label_from_instance(self, obj):
return obj.name

48
kfet/auth/forms.py Normal file
View file

@ -0,0 +1,48 @@
from django import forms
from django.contrib.auth.models import Group, User
from .fields import KFetPermissionsField
class GroupForm(forms.ModelForm):
permissions = KFetPermissionsField()
def clean_name(self):
name = self.cleaned_data['name']
return 'K-Fêt %s' % name
def clean_permissions(self):
kfet_perms = self.cleaned_data['permissions']
# TODO: With Django >=1.11, the QuerySet method 'difference' can be
# used.
# other_groups = self.instance.permissions.difference(
# self.fields['permissions'].queryset
# )
if self.instance.pk is None:
return kfet_perms
other_perms = self.instance.permissions.exclude(
pk__in=[p.pk for p in self.fields['permissions'].queryset],
)
return list(kfet_perms) + list(other_perms)
class Meta:
model = Group
fields = ['name', 'permissions']
class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'),
label='Statut équipe',
required=False)
def clean_groups(self):
kfet_groups = self.cleaned_data.get('groups')
if self.instance.pk is None:
return kfet_groups
other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return list(kfet_groups) + list(other_groups)
class Meta:
model = User
fields = ['groups']

43
kfet/auth/middleware.py Normal file
View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from .backends import AccountBackend
User = get_user_model()
class TemporaryAuthMiddleware:
"""Authenticate another user for this request if AccountBackend succeeds.
By the way, if a user is authenticated, we refresh its from db to add
values from CofProfile and Account of this user.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
# avoid multiple db accesses in views and templates
request.user = (
User.objects
.select_related('profile__account_kfet')
.get(pk=request.user.pk)
)
temp_request_user = AccountBackend().authenticate(
request,
kfet_password=self.get_kfet_password(request),
)
if temp_request_user:
request.real_user = request.user
request.user = temp_request_user
return self.get_response(request)
def get_kfet_password(self, request):
return (
request.META.get('HTTP_KFETPASSWORD') or
request.POST.get('KFETPASSWORD')
)

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0006_require_contenttypes_0002'),
# Following dependency allows using Account model to set up the kfet
# generic user in post_migrate receiver.
('kfet', '0058_delete_genericteamtoken'),
]
operations = [
migrations.CreateModel(
name='GenericTeamToken',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('token', models.CharField(unique=True, max_length=50)),
],
),
]

View file

17
kfet/auth/models.py Normal file
View file

@ -0,0 +1,17 @@
from django.db import models
from django.utils.crypto import get_random_string
class GenericTeamTokenManager(models.Manager):
def create_token(self):
token = get_random_string(50)
while self.filter(token=token).exists():
token = get_random_string(50)
return self.create(token=token)
class GenericTeamToken(models.Model):
token = models.CharField(max_length=50, unique=True)
objects = GenericTeamTokenManager()

40
kfet/auth/signals.py Normal file
View file

@ -0,0 +1,40 @@
from django.contrib import messages
from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse
from django.dispatch import receiver
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from .utils import get_kfet_generic_user
@receiver(user_logged_in)
def suggest_auth_generic(sender, request, user, **kwargs):
"""
Suggest logged in user to continue as the kfet generic user.
Message is only added if the following conditions are met:
- the next page (where user is going to be redirected due to successful
authentication) is related to kfet, i.e. 'k-fet' is in its url.
- logged in user is a kfet staff member (except the generic user).
"""
# Filter against the next page.
if not(hasattr(request, 'GET') and 'next' in request.GET):
return
next_page = request.GET['next']
generic_url = reverse('kfet.login.generic')
if not('k-fet' in next_page and not next_page.startswith(generic_url)):
return
# Filter against the logged in user.
if not(user.has_perm('kfet.is_team') and user != get_kfet_generic_user()):
return
# Seems legit to add message.
text = _("K-Fêt — Ouvrir une session partagée ?")
messages.info(request, mark_safe(
'<a href="#" data-url="{}" onclick="submit_url(this)">{}</a>'
.format(generic_url, text)
))

369
kfet/auth/tests.py Normal file
View file

@ -0,0 +1,369 @@
# -*- coding: utf-8 -*-
from unittest import mock
from django.core import signing
from django.core.urlresolvers import reverse
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
from django.test import RequestFactory, TestCase
from kfet.forms import UserGroupForm
from kfet.models import Account
from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME
from .backends import AccountBackend, GenericBackend
from .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken
from .utils import get_kfet_generic_user
from .views import GenericLoginView
##
# Forms
##
class UserGroupFormTests(TestCase):
"""Test suite for UserGroupForm."""
def setUp(self):
# create user
self.user = User.objects.create(username="foo", password="foo")
# create some K-Fêt groups
prefix_name = "K-Fêt "
names = ["Group 1", "Group 2", "Group 3"]
self.kfet_groups = [
Group.objects.create(name=prefix_name+name)
for name in names
]
# create a non-K-Fêt group
self.other_group = Group.objects.create(name="Other group")
def test_choices(self):
"""Only K-Fêt groups are selectable."""
form = UserGroupForm(instance=self.user)
groups_field = form.fields['groups']
self.assertQuerysetEqual(
groups_field.queryset,
[repr(g) for g in self.kfet_groups],
ordered=False,
)
def test_keep_others(self):
"""User stays in its non-K-Fêt groups."""
user = self.user
# add user to a non-K-Fêt group
user.groups.add(self.other_group)
# add user to some K-Fêt groups through UserGroupForm
data = {
'groups': [group.pk for group in self.kfet_groups],
}
form = UserGroupForm(data, instance=user)
form.is_valid()
form.save()
self.assertQuerysetEqual(
user.groups.all(),
[repr(g) for g in [self.other_group] + self.kfet_groups],
ordered=False,
)
class KFetGenericUserTests(TestCase):
def test_exists(self):
"""
The account is set up when app is ready, so it should exist.
"""
generic = Account.objects.get_generic()
self.assertEqual(generic.trigramme, KFET_GENERIC_TRIGRAMME)
self.assertEqual(generic.user.username, KFET_GENERIC_USERNAME)
self.assertEqual(get_kfet_generic_user(), generic.user)
##
# Backends
##
class AccountBackendTests(TestCase):
def setUp(self):
self.request = RequestFactory().get('/')
def test_valid(self):
acc = Account(trigramme='000')
acc.change_pwd('valid')
acc.save({'username': 'user'})
auth = AccountBackend().authenticate(
self.request, kfet_password='valid')
self.assertEqual(auth, acc.user)
def test_invalid(self):
auth = AccountBackend().authenticate(
self.request, kfet_password='invalid')
self.assertIsNone(auth)
class GenericBackendTests(TestCase):
def setUp(self):
self.request = RequestFactory().get('/')
def test_valid(self):
token = GenericTeamToken.objects.create_token()
auth = GenericBackend().authenticate(
self.request, kfet_token=token.token)
self.assertEqual(auth, get_kfet_generic_user())
self.assertEqual(GenericTeamToken.objects.all().count(), 0)
def test_invalid(self):
auth = GenericBackend().authenticate(
self.request, kfet_token='invalid')
self.assertIsNone(auth)
##
# Views
##
class GenericLoginViewTests(TestCase):
def setUp(self):
patcher_messages = mock.patch('gestioncof.signals.messages')
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
user_acc = Account(trigramme='000')
user_acc.save({'username': 'user'})
self.user = user_acc.user
self.user.set_password('user')
self.user.save()
team_acc = Account(trigramme='100')
team_acc.save({'username': 'team'})
self.team = team_acc.user
self.team.set_password('team')
self.team.save()
self.team.user_permissions.add(
Permission.objects.get(
content_type__app_label='kfet', codename='is_team'),
)
self.url = reverse('kfet.login.generic')
self.generic_user = get_kfet_generic_user()
def test_url(self):
self.assertEqual(self.url, '/k-fet/login/generic')
def test_notoken_get(self):
"""
Send confirmation for user to emit POST request, instead of GET.
"""
self.client.login(username='team', password='team')
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
self.assertTemplateUsed(r, 'kfet/confirm_form.html')
def test_notoken_post(self):
"""
POST request without token in COOKIES sets a token and redirects to
logout url.
"""
self.client.login(username='team', password='team')
r = self.client.post(self.url)
self.assertRedirects(
r, '/logout?next={}'.format(self.url),
fetch_redirect_response=False,
)
def test_notoken_not_team(self):
"""
Logged in user must be a team user to initiate login as generic user.
"""
self.client.login(username='user', password='user')
# With GET.
r = self.client.get(self.url)
self.assertRedirects(
r, '/login?next={}'.format(self.url),
fetch_redirect_response=False,
)
# Also with POST.
r = self.client.post(self.url)
self.assertRedirects(
r, '/login?next={}'.format(self.url),
fetch_redirect_response=False,
)
def _set_signed_cookie(self, client, key, value):
signed_value = signing.get_cookie_signer(salt=key).sign(value)
client.cookies.load({key: signed_value})
def _is_cookie_deleted(self, client, key):
try:
self.assertNotIn(key, client.cookies)
except AssertionError:
try:
cookie = client.cookies[key]
# It also can be emptied.
self.assertEqual(cookie.value, '')
self.assertEqual(
cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT')
self.assertEqual(cookie['max-age'], 0)
except AssertionError:
raise AssertionError("The cookie '%s' still exists." % key)
def test_withtoken_valid(self):
"""
The kfet generic user is logged in.
"""
token = GenericTeamToken.objects.create(token='valid')
self._set_signed_cookie(
self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'valid')
r = self.client.get(self.url)
self.assertRedirects(r, reverse('kfet.kpsul'))
self.assertEqual(r.wsgi_request.user, self.generic_user)
self._is_cookie_deleted(
self.client, GenericLoginView.TOKEN_COOKIE_NAME)
with self.assertRaises(GenericTeamToken.DoesNotExist):
token.refresh_from_db()
def test_withtoken_invalid(self):
"""
If token is invalid, delete it and try again.
"""
self._set_signed_cookie(
self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'invalid')
r = self.client.get(self.url)
self.assertRedirects(r, self.url, fetch_redirect_response=False)
self.assertEqual(r.wsgi_request.user, AnonymousUser())
self._is_cookie_deleted(
self.client, GenericLoginView.TOKEN_COOKIE_NAME)
def test_flow_ok(self):
"""
A team user is logged in as the kfet generic user.
"""
self.client.login(username='team', password='team')
next_url = '/k-fet/'
r = self.client.post(
'{}?next={}'.format(self.url, next_url), follow=True)
self.assertEqual(r.wsgi_request.user, self.generic_user)
self.assertEqual(r.wsgi_request.path, '/k-fet/')
##
# Temporary authentication
#
# Includes:
# - TemporaryAuthMiddleware
# - temporary_auth context processor
##
class TemporaryAuthTests(TestCase):
def setUp(self):
patcher_messages = mock.patch('gestioncof.signals.messages')
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
self.factory = RequestFactory()
self.middleware = TemporaryAuthMiddleware(mock.Mock())
user1_acc = Account(trigramme='000')
user1_acc.change_pwd('kfet_user1')
user1_acc.save({'username': 'user1'})
self.user1 = user1_acc.user
self.user1.set_password('user1')
self.user1.save()
user2_acc = Account(trigramme='100')
user2_acc.change_pwd('kfet_user2')
user2_acc.save({'username': 'user2'})
self.user2 = user2_acc.user
self.user2.set_password('user2')
self.user2.save()
self.perm = Permission.objects.get(
content_type__app_label='kfet', codename='is_team')
self.user2.user_permissions.add(self.perm)
def test_middleware_header(self):
"""
A user can be authenticated if ``HTTP_KFETPASSWORD`` header of a
request contains a valid kfet password.
"""
request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2')
request.user = self.user1
self.middleware(request)
self.assertEqual(request.user, self.user2)
self.assertEqual(request.real_user, self.user1)
def test_middleware_post(self):
"""
A user can be authenticated if ``KFETPASSWORD`` of POST data contains
a valid kfet password.
"""
request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'})
request.user = self.user1
self.middleware(request)
self.assertEqual(request.user, self.user2)
self.assertEqual(request.real_user, self.user1)
def test_middleware_invalid(self):
"""
The given password must be a password of an Account.
"""
request = self.factory.post('/', {'KFETPASSWORD': 'invalid'})
request.user = self.user1
self.middleware(request)
self.assertEqual(request.user, self.user1)
self.assertFalse(hasattr(request, 'real_user'))
def test_context_processor(self):
"""
Context variables give the real authenticated user and his permissions.
"""
self.client.login(username='user1', password='user1')
r = self.client.get('/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2')
self.assertEqual(r.context['user'], self.user1)
self.assertNotIn('kfet.is_team', r.context['perms'])
def test_auth_not_persistent(self):
"""
The authentication is temporary, i.e. for one request.
"""
self.client.login(username='user1', password='user1')
r1 = self.client.get(
'/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2')
self.assertEqual(r1.wsgi_request.user, self.user2)
r2 = self.client.get('/k-fet/accounts/')
self.assertEqual(r2.wsgi_request.user, self.user1)

34
kfet/auth/utils.py Normal file
View file

@ -0,0 +1,34 @@
import hashlib
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from kfet.models import Account
User = get_user_model()
def get_kfet_generic_user():
"""
Return the user related to the kfet generic account.
"""
return Account.objects.get_generic().user
def setup_kfet_generic_user(**kwargs):
"""
First steps of setup of the kfet generic user are done in a migration, as
it is more robust against database schema changes.
Following steps cannot be done from migration.
"""
generic = get_kfet_generic_user()
generic.user_permissions.add(
Permission.objects.get(
content_type__app_label='kfet',
codename='is_team',
)
)
def hash_password(password):
return hashlib.sha256(password.encode('utf-8')).hexdigest()

136
kfet/auth/views.py Normal file
View file

@ -0,0 +1,136 @@
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import Group, User
from django.contrib.auth.views import redirect_to_login
from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Prefetch
from django.http import QueryDict
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
from django.views.decorators.http import require_http_methods
from django.views.generic.edit import CreateView, UpdateView
from .forms import GroupForm
from .models import GenericTeamToken
class GenericLoginView(View):
"""
View to authenticate as kfet generic user.
It is a 2-step view. First, issue a token if user is a team member and send
him to the logout view (for proper disconnect) with callback url to here.
Then authenticate the token to log in as the kfet generic user.
Token is stored in COOKIES to avoid share it with the authentication
provider, which can be external. Session is unusable as it will be cleared
on logout.
"""
TOKEN_COOKIE_NAME = 'kfettoken'
@method_decorator(require_http_methods(['GET', 'POST']))
def dispatch(self, request, *args, **kwargs):
token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None)
if not token:
if not request.user.has_perm('kfet.is_team'):
return redirect_to_login(request.get_full_path())
if request.method == 'POST':
# Step 1: set token and logout user.
return self.prepare_auth()
else:
# GET request should not change server/client states. Send a
# confirmation template to emit a POST request.
return render(request, 'kfet/confirm_form.html', {
'title': _("Ouvrir une session partagée"),
'text': _(
"Êtes-vous sûr·e de vouloir ouvrir une session "
"partagée ?"
),
})
else:
# Step 2: validate token.
return self.validate_auth(token)
def prepare_auth(self):
# Issue token.
token = GenericTeamToken.objects.create_token()
# Prepare callback of logout.
here_url = reverse(login_generic)
if 'next' in self.request.GET:
# Keep given next page.
here_qd = QueryDict(mutable=True)
here_qd['next'] = self.request.GET['next']
here_url += '?{}'.format(here_qd.urlencode())
logout_url = reverse('cof-logout')
logout_qd = QueryDict(mutable=True)
logout_qd['next'] = here_url
logout_url += '?{}'.format(logout_qd.urlencode(safe='/'))
resp = redirect(logout_url)
resp.set_signed_cookie(
self.TOKEN_COOKIE_NAME, token.token, httponly=True)
return resp
def validate_auth(self, token):
# Authenticate with GenericBackend.
user = authenticate(request=self.request, kfet_token=token)
if user:
# Log in generic user.
login(self.request, user)
messages.success(self.request, _(
"K-Fêt — Ouverture d'une session partagée."
))
resp = redirect(self.get_next_url())
else:
# Try again.
resp = redirect(self.request.get_full_path())
# Prevents blocking due to an invalid COOKIE.
resp.delete_cookie(self.TOKEN_COOKIE_NAME)
return resp
def get_next_url(self):
return self.request.GET.get('next', reverse('kfet.kpsul'))
login_generic = GenericLoginView.as_view()
@permission_required('kfet.manage_perms')
def account_group(request):
user_pre = Prefetch(
'user_set',
queryset=User.objects.select_related('profile__account_kfet'),
)
groups = (
Group.objects
.filter(name__icontains='K-Fêt')
.prefetch_related('permissions', user_pre)
)
return render(request, 'kfet/account_group.html', {
'groups': groups,
})
class AccountGroupCreate(SuccessMessageMixin, CreateView):
model = Group
template_name = 'kfet/account_group_form.html'
form_class = GroupForm
success_message = 'Nouveau groupe : %(name)s'
success_url = reverse_lazy('kfet.account.group')
class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
queryset = Group.objects.filter(name__icontains='K-Fêt')
template_name = 'kfet/account_group_form.html'
form_class = GroupForm
success_message = 'Groupe modifié : %(name)s'
success_url = reverse_lazy('kfet.account.group')

View file

@ -76,7 +76,7 @@ def account_create(request):
queries['users_notcof'].values_list('username', flat=True)) queries['users_notcof'].values_list('username', flat=True))
# Fetching data from the SPI # Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'): if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching # Fetching
ldap_query = '(&{:s})'.format(''.join( ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=word) '(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=word)
@ -106,6 +106,7 @@ def account_create(request):
return render(request, "kfet/account_create_autocomplete.html", data) return render(request, "kfet/account_create_autocomplete.html", data)
@teamkfet_required
def account_search(request): def account_search(request):
if "q" not in request.GET: if "q" not in request.GET:
raise Http404 raise Http404

View file

@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
import hashlib
from django.contrib.auth.models import User, Permission
from gestioncof.models import CofProfile
from kfet.models import Account, GenericTeamToken
class KFetBackend(object):
def authenticate(self, request):
password = request.POST.get('KFETPASSWORD', '')
password = request.META.get('HTTP_KFETPASSWORD', password)
if not password:
return None
try:
password_sha256 = (
hashlib.sha256(password.encode('utf-8'))
.hexdigest()
)
account = Account.objects.get(password=password_sha256)
return account.cofprofile.user
except Account.DoesNotExist:
return None
class GenericTeamBackend(object):
def authenticate(self, username=None, token=None):
valid_token = GenericTeamToken.objects.get(token=token)
if username == 'kfet_genericteam' and valid_token:
# Création du user s'il n'existe pas déjà
user, _ = User.objects.get_or_create(username='kfet_genericteam')
profile, _ = CofProfile.objects.get_or_create(user=user)
account, _ = Account.objects.get_or_create(
cofprofile=profile,
trigramme='GNR')
# Ajoute la permission kfet.is_team à ce user
perm_is_team = Permission.objects.get(codename='is_team')
user.user_permissions.add(perm_is_team)
return user
return None
def get_user(self, user_id):
try:
return (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_id)
)
except User.DoesNotExist:
return None

1
kfet/cms/__init__.py Normal file
View file

@ -0,0 +1 @@
default_app_config = 'kfet.cms.apps.KFetCMSAppConfig'

10
kfet/cms/apps.py Normal file
View file

@ -0,0 +1,10 @@
from django.apps import AppConfig
class KFetCMSAppConfig(AppConfig):
name = 'kfet.cms'
label = 'kfetcms'
verbose_name = 'CMS K-Fêt'
def ready(self):
from . import hooks

View file

@ -0,0 +1,20 @@
from kfet.models import Article
def get_articles(request=None):
articles = (
Article.objects
.filter(is_sold=True, hidden=False)
.select_related('category')
.order_by('category__name', 'name')
)
pressions, others = [], []
for article in articles:
if article.category.name == 'Pression':
pressions.append(article)
else:
others.append(article)
return {
'pressions': pressions,
'articles': others,
}

File diff suppressed because one or more lines are too long

12
kfet/cms/hooks.py Normal file
View file

@ -0,0 +1,12 @@
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.utils.html import format_html
from wagtail.wagtailcore import hooks
@hooks.register('insert_editor_css')
def editor_css():
return format_html(
'<link rel="stylesheet" href="{}">',
static('kfetcms/css/editor.css'),
)

View file

@ -0,0 +1,35 @@
from django.contrib.auth.models import Group
from django.core.management import call_command
from django.core.management.base import BaseCommand
from wagtail.wagtailcore.models import Page, Site
class Command(BaseCommand):
help = "Importe des données pour Wagtail"
def add_arguments(self, parser):
parser.add_argument('--file', default='kfet_wagtail_17_05')
def handle(self, *args, **options):
self.stdout.write("Import des données wagtail")
# Nettoyage des données initiales posées par Wagtail dans la migration
# wagtailcore/0002
Group.objects.filter(name__in=('Moderators', 'Editors')).delete()
try:
homepage = Page.objects.get(
title="Welcome to your new Wagtail site!"
)
homepage.delete()
Site.objects.filter(root_page=homepage).delete()
except Page.DoesNotExist:
pass
# Import des données
# Par défaut, il s'agit d'une copie du site K-Fêt (17-05)
call_command('loaddata', options['file'])

View file

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.wagtailsnippets.blocks
import wagtail.wagtailcore.blocks
import wagtail.wagtailcore.fields
import django.db.models.deletion
import kfet.cms.models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0033_remove_golive_expiry_help_text'),
('wagtailimages', '0019_delete_filter'),
]
operations = [
migrations.CreateModel(
name='KFetPage',
fields=[
('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page', on_delete=models.CASCADE)),
('no_header', models.BooleanField(verbose_name='Sans en-tête', help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", default=False)),
('content', wagtail.wagtailcore.fields.StreamField((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses'))))), ('group', wagtail.wagtailcore.blocks.StreamBlock((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses')))))), label='Contenu groupé'))), verbose_name='Contenu')),
('layout', models.CharField(max_length=255, choices=[('kfet/base_col_1.html', 'Une colonne : centrée sur la page'), ('kfet/base_col_2.html', 'Deux colonnes : fixe à gauche, contenu à droite'), ('kfet/base_col_mult.html', 'Contenu scindé sur plusieurs colonnes')], help_text='Comment cette page devrait être affichée ?', verbose_name='Template', default='kfet/base_col_mult.html')),
('main_size', models.CharField(max_length=255, blank=True, verbose_name='Taille de la colonne de contenu')),
('col_count', models.CharField(max_length=255, blank=True, verbose_name='Nombre de colonnes', help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes")),
],
options={
'verbose_name': 'page K-Fêt',
'verbose_name_plural': 'pages K-Fêt',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='MemberTeam',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('first_name', models.CharField(blank=True, max_length=255, verbose_name='Prénom', default='')),
('last_name', models.CharField(blank=True, max_length=255, verbose_name='Nom', default='')),
('nick_name', models.CharField(verbose_name='Alias', blank=True, default='', max_length=255)),
('photo', models.ForeignKey(null=True, related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='Photo', blank=True, to='wagtailimages.Image')),
],
options={
'verbose_name': 'K-Fêt-eux-se',
},
),
]

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 = [
('kfetcms', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='kfetpage',
name='col_count',
field=models.CharField(blank=True, max_length=255, verbose_name='Nombre de colonnes', help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes."),
),
]

View file

174
kfet/cms/models.py Normal file
View file

@ -0,0 +1,174 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel, FieldRowPanel, MultiFieldPanel, StreamFieldPanel
)
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.fields import StreamField
from wagtail.wagtailcore.models import Page
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailsnippets.blocks import SnippetChooserBlock
from wagtail.wagtailsnippets.models import register_snippet
from kfet.cms.context_processors import get_articles
@register_snippet
class MemberTeam(models.Model):
first_name = models.CharField(
verbose_name=_('Prénom'),
blank=True, default='', max_length=255,
)
last_name = models.CharField(
verbose_name=_('Nom'),
blank=True, default='', max_length=255,
)
nick_name = models.CharField(
verbose_name=_('Alias'),
blank=True, default='', max_length=255,
)
photo = models.ForeignKey(
'wagtailimages.Image',
verbose_name=_('Photo'),
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='+',
)
class Meta:
verbose_name = _('K-Fêt-eux-se')
panels = [
FieldPanel('first_name'),
FieldPanel('last_name'),
FieldPanel('nick_name'),
ImageChooserPanel('photo'),
]
def __str__(self):
return self.get_full_name()
def get_full_name(self):
return '{} {}'.format(self.first_name, self.last_name).strip()
class MenuBlock(blocks.StaticBlock):
class Meta:
icon = 'list-ul'
label = _('Carte')
template = 'kfetcms/block_menu.html'
def get_context(self, *args, **kwargs):
context = super().get_context(*args, **kwargs)
context.update(get_articles())
return context
class GroupTeamBlock(blocks.StructBlock):
show_only = blocks.IntegerBlock(
label=_('Montrer seulement'),
required=False,
help_text=_(
'Nombre initial de membres affichés. Laisser vide pour tou-te-s '
'les afficher.'
),
)
members = blocks.ListBlock(
SnippetChooserBlock(MemberTeam),
label=_('K-Fêt-eux-ses'),
classname='team-group',
)
class Meta:
icon = 'group'
label = _('Groupe de K-Fêt-eux-ses')
template = 'kfetcms/block_teamgroup.html'
class ChoicesStreamBlock(blocks.StreamBlock):
rich = blocks.RichTextBlock(label=_('Éditeur'))
carte = MenuBlock()
group_team = GroupTeamBlock()
class KFetStreamBlock(ChoicesStreamBlock):
group = ChoicesStreamBlock(label=_('Contenu groupé'))
class KFetPage(Page):
content = StreamField(KFetStreamBlock, verbose_name=_('Contenu'))
# Layout fields
TEMPLATE_COL_1 = 'kfet/base_col_1.html'
TEMPLATE_COL_2 = 'kfet/base_col_2.html'
TEMPLATE_COL_MULT = 'kfet/base_col_mult.html'
no_header = models.BooleanField(
verbose_name=_('Sans en-tête'),
default=False,
help_text=_(
"Coché, l'en-tête (avec le titre) de la page n'est pas affiché."
),
)
layout = models.CharField(
verbose_name=_('Template'),
choices=[
(TEMPLATE_COL_1, _('Une colonne : centrée sur la page')),
(TEMPLATE_COL_2, _('Deux colonnes : fixe à gauche, contenu à droite')),
(TEMPLATE_COL_MULT, _('Contenu scindé sur plusieurs colonnes')),
],
default=TEMPLATE_COL_MULT, max_length=255,
help_text=_(
"Comment cette page devrait être affichée ?"
),
)
main_size = models.CharField(
verbose_name=_('Taille de la colonne de contenu'),
blank=True, max_length=255,
)
col_count = models.CharField(
verbose_name=_('Nombre de colonnes'),
blank=True, max_length=255,
help_text=_(
"S'applique au page dont le contenu est scindé sur plusieurs colonnes."
),
)
# Panels
content_panels = Page.content_panels + [
StreamFieldPanel('content'),
]
layout_panel = [
FieldPanel('no_header'),
FieldPanel('layout'),
FieldRowPanel([
FieldPanel('main_size'),
FieldPanel('col_count'),
]),
]
settings_panels = [
MultiFieldPanel(layout_panel, _('Affichage'))
] + Page.settings_panels
# Base template
template = "kfetcms/base.html"
class Meta:
verbose_name = _('page K-Fêt')
verbose_name_plural = _('pages K-Fêt')
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
page = context['page']
if not page.seo_title:
page.seo_title = page.title
return context

View file

@ -0,0 +1,93 @@
.main.cms {
padding: 20px 15px;
}
@media (min-width: 768px) {
.main.cms {
padding: 35px 30px;
}
}
.cms {
text-align: justify;
font-size: 1.1em;
}
@media (min-width:768px) {
.cms {
font-size: 1.2em;
line-height: 1.6em;
}
}
/* Titles */
.cms h2, .cms h3 {
clear: both;
margin: 0 0 15px;
padding-bottom: 10px;
border-bottom: 1px solid #c8102e;
text-align: left;
font-weight: bold;
}
@media (min-width: 768px) {
.cms h2, .cms h3 {
padding-bottom: 15px;
}
}
/* Paragraphs */
.cms p {
margin-bottom: 20px;
text-indent: 2em;
}
.cms p + :not(h2):not(h3):not(div) {
margin-top: -10px;
}
@media (min-width: 768px) {
.cms p {
padding-bottom: 15px;
}
.cms p + :not(h2):not(h3):not(div) {
margin-top: -30px;
}
}
/* Lists */
.cms ol, .cms ul {
padding: 0 0 0 15px;
margin: 0 0 10px;
}
.cms ul {
list-style-type: square;
}
.cms ol > li, .cms ul > li {
padding-left: 5px;
}
/* Images */
.cms .richtext-image {
max-height: 100%;
margin: 5px 0 15px;
}
.cms .richtext-image.left {
float: left;
margin-right: 30px;
}
.cms .richtext-image.right {
float: right;
margin-left: 30px;
}

View file

@ -0,0 +1,18 @@
.snippets.listing thead, .snippets.listing thead tr {
border: 0;
}
.snippets.listing tbody {
display: block;
column-count: 2;
}
.snippets.listing tbody tr {
display: block;
}
@media (min-width: 992px) {
.snippets.listing tbody {
column-count: 3;
}
}

View file

@ -0,0 +1,3 @@
@import url("base.css");
@import url("menu.css");
@import url("team.css");

View file

@ -0,0 +1,58 @@
.carte {
margin-bottom: 15px;
font-family: "Roboto Slab";
}
.carte .carte-title {
padding-top: 0;
margin-top: 0;
margin-bottom: 0;
}
.carte .carte-list {
width: 100%;
padding: 15px;
list-style-type: none;
}
.carte .carte-item {
position: relative;
text-align: right;
white-space: nowrap;
padding: 0;
}
.carte .carte-item .filler {
position: absolute;
left: 0;
right: 0;
border-bottom: 2px dotted #333;
height: 75%;
}
.carte .carte-item > span {
position: relative;
}
.carte .carte-item .carte-label {
background: white;
float: left;
padding-right: 10px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.carte .carte-item .carte-ukf {
padding: 0 10px;
background: #ffdbc7;
}
.carte-inverted .carte-list,
.carte-inverted .carte-item .carte-label {
background: #ffdbc7;
}
.carte-inverted .carte-item .carte-ukf {
background: white;
}

View file

@ -0,0 +1,47 @@
.team-group {
margin-bottom: 20px;
}
.team-group .col-btn {
margin-bottom: 20px;
}
.team-group .member-more {
display: none;
}
.team-member {
padding: 0;
margin-bottom: 20px;
min-height: 190px;
background-color: inherit;
border: 0;
}
.team-member img {
max-width: 100%;
max-height: 125px;
width: auto;
height: auto;
display: block;
}
.team-member .infos {
height: 50px;
margin-top: 15px;
}
@media (min-width: 768px) {
.team-group {
margin-left: 20px;
margin-right: 20px;
}
.team-member {
min-height: 215px;
}
.team-member img {
max-height: 150px;
}
}

View file

@ -0,0 +1,41 @@
{% extends page.layout %}
{% load static wagtailcore_tags wagtailuserbar %}
{# CSS/JS #}
{% block extra_head %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "kfetcms/css/index.css" %}">
{% endblock %}
{# Titles #}
{% block title %}{{ page.seo_title }}{% endblock %}
{% block header-title %}{{ page.title }}{% endblock %}
{# Layout #}
{% block main-size %}{{ page.main_size|default:block.super }}{% endblock %}
{% block mult-count %}{{ page.col_count|default:block.super }}{% endblock %}
{% block main-class %}cms main-bg{% endblock %}
{# Content #}
{% block main %}
{% for block in page.content %}
<div class="{% if block.block_type == "rich" or block.block_type == "group" %}unbreakable{% endif %}">
{% include_block block %}
</div>
{% endfor %}
{% wagtailuserbar %}
{% endblock %}
{# Footer #}
{% block footer %}
{% include "kfet/base_footer.html" %}
{% endblock %}

View file

@ -0,0 +1,11 @@
{% load static %}
{% if pressions %}
{% include "kfetcms/block_menu_category.html" with title="Pressions du moment" articles=pressions class="carte-inverted" %}
{% endif %}
{% regroup articles by category as categories %}
{% for category in categories %}
{% include "kfetcms/block_menu_category.html" with title=category.grouper.name articles=category.list %}
{% endfor %}

View file

@ -0,0 +1,12 @@
<div class="carte {{ class }} unbreakable">
<h3 class="carte-title">{{ title }}</h3>
<ul class="carte-list">
{% for article in articles %}
<li class="carte-item">
<div class="filler"></div>
<span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price_ukf }} UKF</span>
</li>
{% endfor %}
</ul>
</div>

View file

@ -0,0 +1,66 @@
{% load wagtailcore_tags wagtailimages_tags %}
{% with groupteam=value len=value.members|length %}
<div class="team-group row">
{% if len == 2 %}
<div class="visible-sm col-sm-3"></div>
{% endif %}
{% for member in groupteam.members %}
<div class="
{% if len == 1 %}
col-xs-12
{% else %}
col-xs-6
{% if len == 3 %}
col-sm-4
{% elif len == 2 %}
col-sm-3 col-md-6
{% else %}
col-sm-3 col-md-4 col-lg-3
{% endif %}
{% endif %}
{% if groupteam.show_only != None and forloop.counter0 >= groupteam.show_only %}
member-more
{% endif %}
">
<div class="team-member thumbnail text-center">
{% image member.photo max-200x500 %}
<div class="infos">
<b>{{ member.get_full_name }}</b>
<br>
{% if member.nick_name %}
<i>alias</i> {{ member.nick_name }}
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% if groupteam.show_only != None and len > groupteam.show_only %}
<div class="col-xs-12 col-btn text-center">
<button class="btn btn-primary btn-lg more">
{% if groupteam.show_only %}
Y'en a plus !
{% else %}
Les voir
{% endif %}
</button>
</div>
{% endif %}
</div>
{% endwith %}
<script type="text/javascript">
$( function() {
$('.more').click( function() {
$(this).closest('.col-btn').hide();
$(this).closest('.team-group').children('.member-more').show();
});
});
</script>

View file

@ -1,39 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.core.serializers.json import json, DjangoJSONEncoder from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
from channels.generic.websockets import JsonWebsocketConsumer
class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer):
"""Custom Json Websocket Consumer.
Encode to JSON with DjangoJSONEncoder.
"""
@classmethod
def encode_json(cls, content):
return json.dumps(content, cls=DjangoJSONEncoder)
class PermConsumerMixin(object):
"""Add support to check permissions on Consumers.
Attributes:
perms_connect (list): Required permissions to connect to this
consumer.
"""
http_user = True # Enable message.user
perms_connect = []
def connect(self, message, **kwargs):
"""Check permissions on connection."""
if message.user.has_perms(self.perms_connect):
super().connect(message, **kwargs)
else:
self.close()
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):

View file

@ -1,18 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth.context_processors import PermWrapper
from kfet.config import kfet_config from kfet.config import kfet_config
def auth(request):
if hasattr(request, 'real_user'):
return {
'user': request.real_user,
'perms': PermWrapper(request.real_user),
}
return {}
def config(request): def config(request):
return {'kfet_config': kfet_config} return {'kfet_config': kfet_config}

View file

@ -5,9 +5,7 @@ from decimal import Decimal
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.forms import modelformset_factory from django.forms import modelformset_factory
from django.utils import timezone from django.utils import timezone
@ -19,24 +17,25 @@ from kfet.models import (
TransferGroup, Supplier) TransferGroup, Supplier)
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from .auth.forms import UserGroupForm # noqa
# ----- # -----
# Widgets # Widgets
# ----- # -----
class DateTimeWidget(forms.DateTimeInput): class DateTimeWidget(forms.DateTimeInput):
def __init__(self, attrs = None): def __init__(self, *args, **kwargs):
super(DateTimeWidget, self).__init__(attrs) super().__init__(*args, **kwargs)
self.attrs['format'] = '%Y-%m-%d %H:%M' self.attrs['format'] = '%Y-%m-%d %H:%M'
class Media: class Media:
css = { css = {
'all': ('kfet/css/bootstrap-datetimepicker.min.css',) 'all': ('kfet/css/bootstrap-datetimepicker.min.css',)
} }
js = ( js = ('kfet/js/bootstrap-datetimepicker.min.js',)
'kfet/js/moment.js',
'kfet/js/moment-fr.js',
'kfet/js/bootstrap-datetimepicker.min.js',
)
# ----- # -----
# Account forms # Account forms
# ----- # -----
@ -111,21 +110,16 @@ class CofRestrictForm(CofForm):
class Meta(CofForm.Meta): class Meta(CofForm.Meta):
fields = ['departement'] fields = ['departement']
class UserForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
from_clipper = kwargs.pop('from_clipper', False)
new_user = kwargs.get('instance') is None and not from_clipper
super(UserForm, self).__init__(*args, **kwargs)
if new_user:
self.fields['username'].validators = [MinLengthValidator(9)]
class UserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ['username', 'first_name', 'last_name', 'email'] fields = ['username', 'first_name', 'last_name', 'email']
help_texts = { help_texts = {
'username': '' 'username': ''
} }
class UserRestrictForm(UserForm): class UserRestrictForm(UserForm):
class Meta(UserForm.Meta): class Meta(UserForm.Meta):
fields = ['first_name', 'last_name'] fields = ['first_name', 'last_name']
@ -135,35 +129,6 @@ class UserRestrictTeamForm(UserForm):
fields = ['first_name', 'last_name', 'email'] fields = ['first_name', 'last_name', 'email']
class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'),
label='Statut équipe',
required=False)
def clean_groups(self):
kfet_groups = self.cleaned_data.get('groups')
other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return list(kfet_groups) + list(other_groups)
class Meta:
model = User
fields = ['groups']
class GroupForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField(
queryset= Permission.objects.filter(content_type__in=
ContentType.objects.filter(app_label='kfet')))
def clean_name(self):
name = self.cleaned_data['name']
return 'K-Fêt %s' % name
class Meta:
model = Group
fields = ['name', 'permissions']
class AccountNegativeForm(forms.ModelForm): class AccountNegativeForm(forms.ModelForm):
class Meta: class Meta:
model = AccountNegative model = AccountNegative
@ -445,8 +410,11 @@ class KFetConfigForm(ConfigForm):
class FilterHistoryForm(forms.Form): class FilterHistoryForm(forms.Form):
checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all())
accounts = forms.ModelMultipleChoiceField(queryset = Account.objects.all()) accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all())
from_date = forms.DateTimeField(widget=DateTimeWidget)
to_date = forms.DateTimeField(widget=DateTimeWidget)
# ----- # -----
# Transfer forms # Transfer forms
@ -525,11 +493,7 @@ class OrderArticleForm(forms.Form):
self.category = kwargs['initial']['category'] self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name'] self.category_name = kwargs['initial']['category__name']
self.box_capacity = kwargs['initial']['box_capacity'] self.box_capacity = kwargs['initial']['box_capacity']
self.v_s1 = kwargs['initial']['v_s1'] self.v_all = kwargs['initial']['v_all']
self.v_s2 = kwargs['initial']['v_s2']
self.v_s3 = kwargs['initial']['v_s3']
self.v_s4 = kwargs['initial']['v_s4']
self.v_s5 = kwargs['initial']['v_s5']
self.v_moy = kwargs['initial']['v_moy'] self.v_moy = kwargs['initial']['v_moy']
self.v_et = kwargs['initial']['v_et'] self.v_et = kwargs['initial']['v_et']
self.v_prev = kwargs['initial']['v_prev'] self.v_prev = kwargs['initial']['v_prev']

View file

@ -147,3 +147,9 @@ class Command(MyBaseCommand):
# --- # ---
call_command('createopes', '100', '7', '--transfers=20') call_command('createopes', '100', '7', '--transfers=20')
# ---
# Wagtail CMS
# ---
call_command('kfet_loadwagtail')

View file

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User
from kfet.backends import KFetBackend
class KFetAuthenticationMiddleware(object):
"""Authenticate another user for this request if KFetBackend succeeds.
By the way, if a user is authenticated, we refresh its from db to add
values from CofProfile and Account of this user.
"""
def process_request(self, request):
if request.user.is_authenticated():
# avoid multiple db accesses in views and templates
user_pk = request.user.pk
request.user = (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_pk)
)
kfet_backend = KFetBackend()
temp_request_user = kfet_backend.authenticate(request)
if temp_request_user:
request.real_user = request.user
request.user = temp_request_user

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 = [
('kfet', '0053_created_at'),
]
operations = [
migrations.AlterField(
model_name='account',
name='promo',
field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017)], default=2017, null=True),
),
]

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