forked from DGNum/gestioCOF
Merge branch 'master' into supportBDS
This commit is contained in:
commit
2aa2dafa13
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -9,3 +9,8 @@ venv/
|
||||||
/src
|
/src
|
||||||
media/
|
media/
|
||||||
*.log
|
*.log
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea
|
||||||
|
.cache
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
services:
|
services:
|
||||||
- mysql:latest
|
- postgres:latest
|
||||||
- redis:latest
|
- redis:latest
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# GestioCOF settings
|
# GestioCOF settings
|
||||||
DJANGO_SETTINGS_MODULE: "gestioCOF.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,10 +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
|
||||||
- pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt
|
# Remove the old test database if it has not been done yet
|
||||||
|
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
||||||
|
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
|
|
118
README.md
118
README.md
|
@ -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.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
104
bda/admin.py
104
bda/admin.py
|
@ -12,32 +12,76 @@ from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
|
||||||
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
|
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyMixin(object):
|
||||||
|
readonly_fields_update = ()
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
readonly_fields = super().get_readonly_fields(request, obj)
|
||||||
|
if obj is None:
|
||||||
|
return readonly_fields
|
||||||
|
else:
|
||||||
|
return readonly_fields + self.readonly_fields_update
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacleInline(admin.TabularInline):
|
class ChoixSpectacleInline(admin.TabularInline):
|
||||||
model = ChoixSpectacle
|
model = ChoixSpectacle
|
||||||
sortable_field_name = "priority"
|
sortable_field_name = "priority"
|
||||||
|
|
||||||
|
|
||||||
|
class AttributionTabularAdminForm(forms.ModelForm):
|
||||||
|
listing = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
spectacles = Spectacle.objects.select_related('location')
|
||||||
|
if self.listing is not None:
|
||||||
|
spectacles = spectacles.filter(listing=self.listing)
|
||||||
|
self.fields['spectacle'].queryset = spectacles
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
|
||||||
|
listing = False
|
||||||
|
|
||||||
|
|
||||||
|
class WithListingAttributionTabularAdminForm(AttributionTabularAdminForm):
|
||||||
|
listing = True
|
||||||
|
|
||||||
|
|
||||||
class AttributionInline(admin.TabularInline):
|
class AttributionInline(admin.TabularInline):
|
||||||
model = Attribution
|
model = Attribution
|
||||||
extra = 0
|
extra = 0
|
||||||
|
listing = None
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(AttributionInline, self).get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.filter(spectacle__listing=False)
|
if self.listing is not None:
|
||||||
|
qs = qs.filter(spectacle__listing=self.listing)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class AttributionInlineListing(admin.TabularInline):
|
class WithListingAttributionInline(AttributionInline):
|
||||||
model = Attribution
|
|
||||||
exclude = ('given', )
|
exclude = ('given', )
|
||||||
extra = 0
|
form = WithListingAttributionTabularAdminForm
|
||||||
|
listing = True
|
||||||
def get_queryset(self, request):
|
|
||||||
qs = super(AttributionInlineListing, self).get_queryset(request)
|
|
||||||
return qs.filter(spectacle__listing=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdmin(admin.ModelAdmin):
|
class WithoutListingAttributionInline(AttributionInline):
|
||||||
inlines = [AttributionInline, AttributionInlineListing]
|
form = WithoutListingAttributionTabularAdminForm
|
||||||
|
listing = False
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['choicesrevente'].queryset = (
|
||||||
|
Spectacle.objects
|
||||||
|
.select_related('location')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
|
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return Participant.objects.annotate(nb_places=Count('attributions'),
|
return Participant.objects.annotate(nb_places=Count('attributions'),
|
||||||
|
@ -64,6 +108,8 @@ class ParticipantAdmin(admin.ModelAdmin):
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
list_per_page = 400
|
list_per_page = 400
|
||||||
readonly_fields = ("total",)
|
readonly_fields = ("total",)
|
||||||
|
readonly_fields_update = ('user', 'tirage')
|
||||||
|
form = ParticipantAdminForm
|
||||||
|
|
||||||
def send_attribs(self, request, queryset):
|
def send_attribs(self, request, queryset):
|
||||||
datatuple = []
|
datatuple = []
|
||||||
|
@ -93,6 +139,20 @@ class ParticipantAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class AttributionAdminForm(forms.ModelForm):
|
class AttributionAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if 'spectacle' in self.fields:
|
||||||
|
self.fields['spectacle'].queryset = (
|
||||||
|
Spectacle.objects
|
||||||
|
.select_related('location')
|
||||||
|
)
|
||||||
|
if 'participant' in self.fields:
|
||||||
|
self.fields['participant'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(AttributionAdminForm, self).clean()
|
cleaned_data = super(AttributionAdminForm, self).clean()
|
||||||
participant = cleaned_data.get("participant")
|
participant = cleaned_data.get("participant")
|
||||||
|
@ -105,7 +165,7 @@ class AttributionAdminForm(forms.ModelForm):
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class AttributionAdmin(admin.ModelAdmin):
|
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
def paid(self, obj):
|
def paid(self, obj):
|
||||||
return obj.participant.paid
|
return obj.participant.paid
|
||||||
paid.short_description = 'A payé'
|
paid.short_description = 'A payé'
|
||||||
|
@ -115,6 +175,7 @@ class AttributionAdmin(admin.ModelAdmin):
|
||||||
'participant__user__first_name',
|
'participant__user__first_name',
|
||||||
'participant__user__last_name')
|
'participant__user__last_name')
|
||||||
form = AttributionAdminForm
|
form = AttributionAdminForm
|
||||||
|
readonly_fields_update = ('spectacle', 'participant')
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
||||||
|
@ -157,6 +218,24 @@ class SalleAdmin(admin.ModelAdmin):
|
||||||
search_fields = ('name', 'address')
|
search_fields = ('name', 'address')
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleReventeAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['answered_mail'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
self.fields['seller'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
self.fields['soldTo'].queryset = (
|
||||||
|
Participant.objects
|
||||||
|
.select_related('user', 'tirage')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SpectacleReventeAdmin(admin.ModelAdmin):
|
class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Administration des reventes de spectacles
|
Administration des reventes de spectacles
|
||||||
|
@ -179,6 +258,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
actions = ['transfer', 'reinit']
|
actions = ['transfer', 'reinit']
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
|
form = SpectacleReventeAdminForm
|
||||||
|
|
||||||
def transfer(self, request, queryset):
|
def transfer(self, request, queryset):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,8 +22,7 @@ class Algorithm(object):
|
||||||
show.requests
|
show.requests
|
||||||
- on crée des tables de demandes pour chaque personne, afin de
|
- on crée des tables de demandes pour chaque personne, afin de
|
||||||
pouvoir modifier les rankings"""
|
pouvoir modifier les rankings"""
|
||||||
self.max_group = \
|
self.max_group = 2*max(choice.priority for choice in choices)
|
||||||
2 * choices.aggregate(Max('priority'))['priority__max']
|
|
||||||
self.shows = []
|
self.shows = []
|
||||||
showdict = {}
|
showdict = {}
|
||||||
for show in shows:
|
for show in shows:
|
||||||
|
|
100
bda/forms.py
100
bda/forms.py
|
@ -1,37 +1,40 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import division
|
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.models import BaseInlineFormSet
|
from django.forms.models import BaseInlineFormSet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bda.models import Attribution, Spectacle
|
from bda.models import Attribution, Spectacle
|
||||||
|
|
||||||
|
|
||||||
class BaseBdaFormSet(BaseInlineFormSet):
|
class InscriptionInlineFormSet(BaseInlineFormSet):
|
||||||
def clean(self):
|
|
||||||
"""Checks that no two articles have the same title."""
|
def __init__(self, *args, **kwargs):
|
||||||
super(BaseBdaFormSet, self).clean()
|
super().__init__(*args, **kwargs)
|
||||||
if any(self.errors):
|
|
||||||
# Don't bother validating the formset unless each form is valid on
|
# self.instance is a Participant object
|
||||||
# its own
|
tirage = self.instance.tirage
|
||||||
return
|
|
||||||
spectacles = []
|
# set once for all "spectacle" field choices
|
||||||
for i in range(0, self.total_form_count()):
|
# - restrict choices to the spectacles of this tirage
|
||||||
form = self.forms[i]
|
# - force_choices avoid many db requests
|
||||||
if not form.cleaned_data:
|
spectacles = tirage.spectacle_set.select_related('location')
|
||||||
continue
|
choices = [(sp.pk, str(sp)) for sp in spectacles]
|
||||||
spectacle = form.cleaned_data['spectacle']
|
self.force_choices('spectacle', choices)
|
||||||
delete = form.cleaned_data['DELETE']
|
|
||||||
if not delete and spectacle in spectacles:
|
def force_choices(self, name, choices):
|
||||||
raise forms.ValidationError(
|
"""Set choices of a field.
|
||||||
"Vous ne pouvez pas vous inscrire deux fois pour le "
|
|
||||||
"même spectacle.")
|
As ModelChoiceIterator (default use to get choices of a
|
||||||
spectacles.append(spectacle)
|
ModelChoiceField), it appends an empty selection if requested.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for form in self.forms:
|
||||||
|
field = form.fields[name]
|
||||||
|
if field.empty_label is not None:
|
||||||
|
field.choices = [('', field.empty_label)] + choices
|
||||||
|
else:
|
||||||
|
field.choices = choices
|
||||||
|
|
||||||
|
|
||||||
class TokenForm(forms.Form):
|
class TokenForm(forms.Form):
|
||||||
|
@ -40,35 +43,45 @@ class TokenForm(forms.Form):
|
||||||
|
|
||||||
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj):
|
||||||
return "%s" % obj.spectacle
|
return "%s" % str(obj.spectacle)
|
||||||
|
|
||||||
|
|
||||||
class ResellForm(forms.Form):
|
class ResellForm(forms.Form):
|
||||||
attributions = AttributionModelMultipleChoiceField(
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label='',
|
||||||
queryset=Attribution.objects.none(),
|
queryset=Attribution.objects.none(),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=False)
|
required=False)
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
def __init__(self, participant, *args, **kwargs):
|
||||||
super(ResellForm, self).__init__(*args, **kwargs)
|
super(ResellForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['attributions'].queryset = participant.attribution_set\
|
self.fields['attributions'].queryset = (
|
||||||
.filter(spectacle__date__gte=timezone.now())\
|
participant.attribution_set
|
||||||
|
.filter(spectacle__date__gte=timezone.now())
|
||||||
.exclude(revente__seller=participant)
|
.exclude(revente__seller=participant)
|
||||||
|
.select_related('spectacle', 'spectacle__location',
|
||||||
|
'participant__user')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AnnulForm(forms.Form):
|
class AnnulForm(forms.Form):
|
||||||
attributions = AttributionModelMultipleChoiceField(
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label='',
|
||||||
queryset=Attribution.objects.none(),
|
queryset=Attribution.objects.none(),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=False)
|
required=False)
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
def __init__(self, participant, *args, **kwargs):
|
||||||
super(AnnulForm, self).__init__(*args, **kwargs)
|
super(AnnulForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['attributions'].queryset = participant.attribution_set\
|
self.fields['attributions'].queryset = (
|
||||||
|
participant.attribution_set
|
||||||
.filter(spectacle__date__gte=timezone.now(),
|
.filter(spectacle__date__gte=timezone.now(),
|
||||||
revente__isnull=False,
|
revente__isnull=False,
|
||||||
revente__date__gt=timezone.now()-timedelta(hours=1),
|
revente__notif_sent=False,
|
||||||
revente__soldTo__isnull=True)
|
revente__soldTo__isnull=True)
|
||||||
|
.select_related('spectacle', 'spectacle__location',
|
||||||
|
'participant__user')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InscriptionReventeForm(forms.Form):
|
class InscriptionReventeForm(forms.Form):
|
||||||
|
@ -79,5 +92,26 @@ class InscriptionReventeForm(forms.Form):
|
||||||
|
|
||||||
def __init__(self, tirage, *args, **kwargs):
|
def __init__(self, tirage, *args, **kwargs):
|
||||||
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
|
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['spectacles'].queryset = tirage.spectacle_set.filter(
|
self.fields['spectacles'].queryset = (
|
||||||
date__gte=timezone.now())
|
tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
.filter(date__gte=timezone.now())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoldForm(forms.Form):
|
||||||
|
attributions = AttributionModelMultipleChoiceField(
|
||||||
|
label='',
|
||||||
|
queryset=Attribution.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple)
|
||||||
|
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super(SoldForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['attributions'].queryset = (
|
||||||
|
participant.attribution_set
|
||||||
|
.filter(revente__isnull=False,
|
||||||
|
revente__soldTo__isnull=False)
|
||||||
|
.exclude(revente__soldTo=participant)
|
||||||
|
.select_related('spectacle', 'spectacle__location',
|
||||||
|
'participant__user')
|
||||||
|
)
|
||||||
|
|
|
@ -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,7 +52,6 @@ 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',
|
||||||
|
@ -43,22 +59,36 @@ class Migration(migrations.Migration):
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
to=settings.AUTH_USER_MODEL),
|
to=settings.AUTH_USER_MODEL),
|
||||||
),
|
),
|
||||||
|
# Create fields `spectacle` for `Participant` and `Spectacle` models.
|
||||||
|
# These fields are not nullable, but we first create them as nullable
|
||||||
|
# to give a default value for existing instances of these models.
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='participant',
|
model_name='participant',
|
||||||
name='tirage',
|
name='tirage',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
default=1,
|
to='bda.Tirage',
|
||||||
to='bda.Tirage'),
|
null=True
|
||||||
preserve_default=False,
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='spectacle',
|
model_name='spectacle',
|
||||||
name='tirage',
|
name='tirage',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
default=1,
|
to='bda.Tirage',
|
||||||
to='bda.Tirage'),
|
null=True
|
||||||
preserve_default=False,
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='participant',
|
||||||
|
name='tirage',
|
||||||
|
field=models.ForeignKey(to='bda.Tirage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='spectacle',
|
||||||
|
name='tirage',
|
||||||
|
field=models.ForeignKey(to='bda.Tirage'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
22
bda/migrations/0011_tirage_appear_catalogue.py
Normal file
22
bda/migrations/0011_tirage_appear_catalogue.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0010_spectaclerevente_shotgun'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tirage',
|
||||||
|
name='appear_catalogue',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name='Tirage à afficher dans le catalogue'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,17 +7,30 @@ from custommail.shortcuts import send_mass_custom_mail
|
||||||
|
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
|
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
|
||||||
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
||||||
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
||||||
active = models.BooleanField("Tirage actif", default=False)
|
active = models.BooleanField("Tirage actif", default=False)
|
||||||
|
appear_catalogue = models.BooleanField(
|
||||||
|
"Tirage à afficher dans le catalogue",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
|
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
|
||||||
default=False)
|
default=False)
|
||||||
|
|
||||||
|
@ -83,37 +96,43 @@ class Spectacle(models.Model):
|
||||||
self.price
|
self.price
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def getImgUrl(self):
|
||||||
|
"""
|
||||||
|
Cette fonction permet d'obtenir l'URL de l'image, si elle existe
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.image.url
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def send_rappel(self):
|
def send_rappel(self):
|
||||||
"""
|
"""
|
||||||
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):
|
||||||
|
@ -344,10 +363,11 @@ class SpectacleRevente(models.Model):
|
||||||
# Envoie un mail aux perdants
|
# Envoie un mail aux perdants
|
||||||
for inscrit in inscrits:
|
for inscrit in inscrits:
|
||||||
if inscrit != winner:
|
if inscrit != winner:
|
||||||
context['acheteur'] = inscrit.user
|
new_context = dict(context)
|
||||||
|
new_context['acheteur'] = inscrit.user
|
||||||
datatuple.append((
|
datatuple.append((
|
||||||
'bda-revente-loser',
|
'bda-revente-loser',
|
||||||
context,
|
new_context,
|
||||||
settings.MAIL_DATA['revente']['FROM'],
|
settings.MAIL_DATA['revente']['FROM'],
|
||||||
[inscrit.user.email]
|
[inscrit.user.email]
|
||||||
))
|
))
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
{% extends "base_title.html" %}
|
|
||||||
{% load bootstrap %}
|
|
||||||
|
|
||||||
{% block realcontent %}
|
|
||||||
|
|
||||||
<h2>Revente de place</h2>
|
|
||||||
<h3>Places non revendues</h3>
|
|
||||||
<form class="form-horizontal" action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="multiple-checkbox">
|
|
||||||
<ul>
|
|
||||||
{% for box in resellform.attributions %}
|
|
||||||
<li>
|
|
||||||
{{box.tag}}
|
|
||||||
{{box.choice_label}}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br>
|
|
||||||
{% if annulform.attributions or overdue %}
|
|
||||||
<h3>Places en cours de revente</h3>
|
|
||||||
<form action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="multiple-checkbox">
|
|
||||||
<ul>
|
|
||||||
{% for box in annulform.attributions %}
|
|
||||||
<li>
|
|
||||||
{{box.tag}}
|
|
||||||
{{box.choice_label}}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% for attrib in overdue %}
|
|
||||||
<li>
|
|
||||||
<input type="checkbox" style="visibility:hidden">
|
|
||||||
{{attrib.spectacle}}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if annulform.attributions %}
|
|
||||||
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<br>
|
|
||||||
{% if sold %}
|
|
||||||
<h3>Places revendues</h3>
|
|
||||||
<table class="table">
|
|
||||||
{% for attrib in sold %}
|
|
||||||
<tr>
|
|
||||||
<form action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<td>{{attrib.spectacle}}</td>
|
|
||||||
<td>{{attrib.revente.soldTo.user.get_full_name}}</td>
|
|
||||||
<td><button type="submit" class="btn btn-primary" name="transfer"
|
|
||||||
value="{{attrib.revente.id}}">Transférer</button></td>
|
|
||||||
<td><button type="submit" class="btn btn-primary" name="reinit"
|
|
||||||
value="{{attrib.revente.id}}">Réinitialiser</button></td>
|
|
||||||
</form>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
|
@ -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 }} ?</h3>
|
||||||
{{ show.title }} ?</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 %}
|
||||||
|
|
|
@ -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">
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
|
||||||
|
<pre id="export-salle" style="display:none">{% spaceless %}
|
||||||
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
|
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
|
||||||
{% 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>
|
56
bda/templates/bda/reventes.html
Normal file
56
bda/templates/bda/reventes.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
|
||||||
|
<h2>Revente de place</h2>
|
||||||
|
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
|
||||||
|
|
||||||
|
{% if resellform.attributions %}
|
||||||
|
<h3>Places non revendues</h3>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{resellform|bootstrap}}
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
{% if annul_attributions or overdue %}
|
||||||
|
<h3>Places en cours de revente</h3>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class='form-group'>
|
||||||
|
<div class='multiple-checkbox'>
|
||||||
|
<ul>
|
||||||
|
{% for attrib in annul_attributions %}
|
||||||
|
<li>{{attrib.tag}} {{attrib.choice_label}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for attrib in overdue %}
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" style="visibility:hidden">
|
||||||
|
{{attrib.spectacle}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if annul_attributions %}
|
||||||
|
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
{% if sold_attributions %}
|
||||||
|
<h3>Places revendues</h3>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{soldform|bootstrap}}
|
||||||
|
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
|
||||||
|
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}
|
||||||
|
<p>Plus de reventes possibles !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
115
bda/tests.py
115
bda/tests.py
|
@ -1,22 +1,105 @@
|
||||||
# -*- coding: utf-8 -*-
|
import json
|
||||||
"""
|
|
||||||
This file demonstrates writing tests using the unittest module. These will pass
|
|
||||||
when you run "manage.py test".
|
|
||||||
|
|
||||||
Replace this with more appropriate tests for your application.
|
from django.contrib.auth.models import User
|
||||||
"""
|
from django.test import TestCase, Client
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from __future__ import division
|
from .models import Tirage, Spectacle, Salle, CategorieSpectacle
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
from django.test import TestCase
|
class TestBdAViews(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tirage = Tirage.objects.create(
|
||||||
|
title="Test tirage",
|
||||||
|
appear_catalogue=True,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
self.category = CategorieSpectacle.objects.create(name="Category")
|
||||||
|
self.location = Salle.objects.create(name="here")
|
||||||
|
Spectacle.objects.bulk_create([
|
||||||
|
Spectacle(
|
||||||
|
title="foo", date=timezone.now(), location=self.location,
|
||||||
|
price=0, slots=42, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="bar", date=timezone.now(), location=self.location,
|
||||||
|
price=1, slots=142, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="baz", date=timezone.now(), location=self.location,
|
||||||
|
price=2, slots=242, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.bda_user = User.objects.create_user(
|
||||||
|
username="bda_user", password="bda4ever"
|
||||||
|
)
|
||||||
|
self.bda_user.profile.is_cof = True
|
||||||
|
self.bda_user.profile.is_buro = True
|
||||||
|
self.bda_user.profile.save()
|
||||||
|
|
||||||
class SimpleTest(TestCase):
|
def bda_participants(self):
|
||||||
def test_basic_addition(self):
|
"""The BdA participants views can be queried"""
|
||||||
"""
|
client = Client()
|
||||||
Tests that 1 + 1 always equals 2.
|
show = self.tirage.spectacle_set.first()
|
||||||
"""
|
|
||||||
self.assertEqual(1 + 1, 2)
|
client.login(self.bda_user.username, "bda4ever")
|
||||||
|
tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id))
|
||||||
|
show_resp = client.get(
|
||||||
|
"/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
|
||||||
|
)
|
||||||
|
reminder_url = "/bda/mails-rappel/{}".format(show.id)
|
||||||
|
reminder_get_resp = client.get(reminder_url)
|
||||||
|
reminder_post_resp = client.post(reminder_url)
|
||||||
|
self.assertEqual(200, tirage_resp.status_code)
|
||||||
|
self.assertEqual(200, show_resp.status_code)
|
||||||
|
self.assertEqual(200, reminder_get_resp.status_code)
|
||||||
|
self.assertEqual(200, reminder_post_resp.status_code)
|
||||||
|
|
||||||
|
def test_catalogue(self):
|
||||||
|
"""Test the catalogue JSON API"""
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
# The `list` hook
|
||||||
|
resp = client.get("/bda/catalogue/list")
|
||||||
|
self.assertJSONEqual(
|
||||||
|
resp.content.decode("utf-8"),
|
||||||
|
[{"id": self.tirage.id, "title": self.tirage.title}]
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `details` hook
|
||||||
|
resp = client.get(
|
||||||
|
"/bda/catalogue/details?id={}".format(self.tirage.id)
|
||||||
|
)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
resp.content.decode("utf-8"),
|
||||||
|
{
|
||||||
|
"categories": [{
|
||||||
|
"id": self.category.id,
|
||||||
|
"name": self.category.name
|
||||||
|
}],
|
||||||
|
"locations": [{
|
||||||
|
"id": self.location.id,
|
||||||
|
"name": self.location.name
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `descriptions` hook
|
||||||
|
resp = client.get(
|
||||||
|
"/bda/catalogue/descriptions?id={}".format(self.tirage.id)
|
||||||
|
)
|
||||||
|
raw = resp.content.decode("utf-8")
|
||||||
|
try:
|
||||||
|
results = json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Not valid JSON: {}".format(raw))
|
||||||
|
self.assertEqual(len(results), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
{(s["title"], s["price"], s["slots"]) for s in results},
|
||||||
|
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)}
|
||||||
|
)
|
||||||
|
|
|
@ -44,7 +44,12 @@ 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,
|
||||||
|
name='bda-catalogue'),
|
||||||
]
|
]
|
||||||
|
|
460
bda/views.py
460
bda/views.py
|
@ -1,32 +1,39 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
import json
|
||||||
from custommail.shortcuts import (
|
|
||||||
send_mass_custom_mail, send_custom_mail, render_custom_mail
|
from collections import defaultdict
|
||||||
)
|
from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
|
||||||
|
from custommail.models import CustomMail
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.db import models, transaction
|
|
||||||
from django.core import serializers
|
|
||||||
from django.db.models import Count, Q, Sum
|
|
||||||
from django.forms.models import inlineformset_factory
|
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core import serializers
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Count, Q, Prefetch
|
||||||
|
from django.forms.models import inlineformset_factory
|
||||||
|
from django.http import (
|
||||||
|
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
||||||
|
)
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils import timezone, formats
|
from django.utils import timezone, formats
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
|
||||||
from cof.decorators import cof_required, buro_required
|
from cof.decorators import cof_required, buro_required
|
||||||
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
|
|
||||||
Tirage, SpectacleRevente
|
from .models import (
|
||||||
from bda.algorithm import Algorithm
|
Attribution, CategorieSpectacle, ChoixSpectacle, Participant, Salle,
|
||||||
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
|
Spectacle, SpectacleRevente, Tirage
|
||||||
InscriptionReventeForm
|
)
|
||||||
|
from .algorithm import Algorithm
|
||||||
|
from .forms import (
|
||||||
|
TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
|
||||||
|
InscriptionInlineFormSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@cof_required
|
@cof_required
|
||||||
|
@ -39,39 +46,44 @@ def etat_places(request, tirage_id):
|
||||||
Et le total de toutes les demandes
|
Et le total de toutes les demandes
|
||||||
"""
|
"""
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
spectacles1 = ChoixSpectacle.objects \
|
|
||||||
.filter(spectacle__tirage=tirage) \
|
spectacles = tirage.spectacle_set.select_related('location')
|
||||||
.filter(double_choice="1") \
|
spectacles_dict = {} # index of spectacle by id
|
||||||
.all() \
|
|
||||||
.values('spectacle', 'spectacle__title') \
|
|
||||||
.annotate(total=models.Count('spectacle'))
|
|
||||||
spectacles2 = ChoixSpectacle.objects \
|
|
||||||
.filter(spectacle__tirage=tirage) \
|
|
||||||
.exclude(double_choice="1") \
|
|
||||||
.all() \
|
|
||||||
.values('spectacle', 'spectacle__title') \
|
|
||||||
.annotate(total=models.Count('spectacle'))
|
|
||||||
spectacles = tirage.spectacle_set.all()
|
|
||||||
spectacles_dict = {}
|
|
||||||
total = 0
|
|
||||||
for spectacle in spectacles:
|
for spectacle in spectacles:
|
||||||
spectacle.total = 0
|
spectacle.total = 0 # init total requests
|
||||||
spectacle.ratio = 0.0
|
|
||||||
spectacles_dict[spectacle.id] = spectacle
|
spectacles_dict[spectacle.id] = spectacle
|
||||||
for spectacle in spectacles1:
|
|
||||||
spectacles_dict[spectacle["spectacle"]].total += spectacle["total"]
|
choices = (
|
||||||
spectacles_dict[spectacle["spectacle"]].ratio = \
|
ChoixSpectacle.objects
|
||||||
spectacles_dict[spectacle["spectacle"]].total / \
|
.filter(spectacle__in=spectacles)
|
||||||
spectacles_dict[spectacle["spectacle"]].slots
|
.values('spectacle')
|
||||||
total += spectacle["total"]
|
.annotate(total=Count('spectacle'))
|
||||||
for spectacle in spectacles2:
|
)
|
||||||
spectacles_dict[spectacle["spectacle"]].total += 2*spectacle["total"]
|
|
||||||
spectacles_dict[spectacle["spectacle"]].ratio = \
|
# choices *by spectacles* whose only 1 place is requested
|
||||||
spectacles_dict[spectacle["spectacle"]].total / \
|
choices1 = choices.filter(double_choice="1")
|
||||||
spectacles_dict[spectacle["spectacle"]].slots
|
# choices *by spectacles* whose 2 places is requested
|
||||||
total += 2*spectacle["total"]
|
choices2 = choices.exclude(double_choice="1")
|
||||||
|
|
||||||
|
for spectacle in choices1:
|
||||||
|
pk = spectacle['spectacle']
|
||||||
|
spectacles_dict[pk].total += spectacle['total']
|
||||||
|
for spectacle in choices2:
|
||||||
|
pk = spectacle['spectacle']
|
||||||
|
spectacles_dict[pk].total += 2*spectacle['total']
|
||||||
|
|
||||||
|
# here, each spectacle.total contains the number of requests
|
||||||
|
|
||||||
|
slots = 0 # proposed slots
|
||||||
|
total = 0 # requests
|
||||||
|
for spectacle in spectacles:
|
||||||
|
slots += spectacle.slots
|
||||||
|
total += spectacle.total
|
||||||
|
spectacle.ratio = spectacle.total / spectacle.slots
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"proposed": tirage.spectacle_set.aggregate(Sum('slots'))['slots__sum'],
|
"proposed": slots,
|
||||||
"spectacles": spectacles,
|
"spectacles": spectacles,
|
||||||
"total": total,
|
"total": total,
|
||||||
'tirage': tirage
|
'tirage': tirage
|
||||||
|
@ -89,11 +101,16 @@ def _hash_queryset(queryset):
|
||||||
@cof_required
|
@cof_required
|
||||||
def places(request, tirage_id):
|
def places(request, tirage_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
participant, created = Participant.objects.get_or_create(
|
participant, _ = (
|
||||||
user=request.user, tirage=tirage)
|
Participant.objects
|
||||||
places = participant.attribution_set.order_by(
|
.get_or_create(user=request.user, tirage=tirage)
|
||||||
"spectacle__date", "spectacle").all()
|
)
|
||||||
total = sum([place.spectacle.price for place in places])
|
places = (
|
||||||
|
participant.attribution_set
|
||||||
|
.order_by("spectacle__date", "spectacle")
|
||||||
|
.select_related("spectacle", "spectacle__location")
|
||||||
|
)
|
||||||
|
total = sum(place.spectacle.price for place in places)
|
||||||
filtered_places = []
|
filtered_places = []
|
||||||
places_dict = {}
|
places_dict = {}
|
||||||
spectacles = []
|
spectacles = []
|
||||||
|
@ -141,35 +158,31 @@ def inscription(request, tirage_id):
|
||||||
messages.error(request, "Le tirage n'est pas encore ouvert : "
|
messages.error(request, "Le tirage n'est pas encore ouvert : "
|
||||||
"ouverture le {:s}".format(opening))
|
"ouverture le {:s}".format(opening))
|
||||||
return render(request, 'bda/resume-inscription-tirage.html', {})
|
return render(request, 'bda/resume-inscription-tirage.html', {})
|
||||||
|
|
||||||
|
participant, _ = (
|
||||||
|
Participant.objects.select_related('tirage')
|
||||||
|
.get_or_create(user=request.user, tirage=tirage)
|
||||||
|
)
|
||||||
|
|
||||||
if timezone.now() > tirage.fermeture:
|
if timezone.now() > tirage.fermeture:
|
||||||
# Le tirage est fermé.
|
# Le tirage est fermé.
|
||||||
participant, created = Participant.objects.get_or_create(
|
choices = participant.choixspectacle_set.order_by("priority")
|
||||||
user=request.user, tirage=tirage)
|
|
||||||
choices = participant.choixspectacle_set.order_by("priority").all()
|
|
||||||
messages.error(request,
|
messages.error(request,
|
||||||
" C'est fini : tirage au sort dans la journée !")
|
" C'est fini : tirage au sort dans la journée !")
|
||||||
return render(request, "bda/resume-inscription-tirage.html",
|
return render(request, "bda/resume-inscription-tirage.html",
|
||||||
{"choices": choices})
|
{"choices": choices})
|
||||||
|
|
||||||
def formfield_callback(f, **kwargs):
|
|
||||||
"""
|
|
||||||
Fonction utilisée par inlineformset_factory ci dessous.
|
|
||||||
Restreint les spectacles proposés aux spectacles du bo tirage.
|
|
||||||
"""
|
|
||||||
if f.name == "spectacle":
|
|
||||||
kwargs['queryset'] = tirage.spectacle_set
|
|
||||||
return f.formfield(**kwargs)
|
|
||||||
BdaFormSet = inlineformset_factory(
|
BdaFormSet = inlineformset_factory(
|
||||||
Participant,
|
Participant,
|
||||||
ChoixSpectacle,
|
ChoixSpectacle,
|
||||||
fields=("spectacle", "double_choice", "priority"),
|
fields=("spectacle", "double_choice", "priority"),
|
||||||
formset=BaseBdaFormSet,
|
formset=InscriptionInlineFormSet,
|
||||||
formfield_callback=formfield_callback)
|
)
|
||||||
participant, created = Participant.objects.get_or_create(
|
|
||||||
user=request.user, tirage=tirage)
|
|
||||||
success = False
|
success = False
|
||||||
stateerror = False
|
stateerror = False
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
# use *this* queryset
|
||||||
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
||||||
stateerror = True
|
stateerror = True
|
||||||
|
@ -182,9 +195,14 @@ def inscription(request, tirage_id):
|
||||||
formset = BdaFormSet(instance=participant)
|
formset = BdaFormSet(instance=participant)
|
||||||
else:
|
else:
|
||||||
formset = BdaFormSet(instance=participant)
|
formset = BdaFormSet(instance=participant)
|
||||||
|
# use *this* queryset
|
||||||
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
total_price = 0
|
total_price = 0
|
||||||
for choice in participant.choixspectacle_set.all():
|
choices = (
|
||||||
|
participant.choixspectacle_set
|
||||||
|
.select_related('spectacle')
|
||||||
|
)
|
||||||
|
for choice in choices:
|
||||||
total_price += choice.spectacle.price
|
total_price += choice.spectacle.price
|
||||||
if choice.double:
|
if choice.double:
|
||||||
total_price += choice.spectacle.price
|
total_price += choice.spectacle.price
|
||||||
|
@ -213,9 +231,9 @@ def do_tirage(tirage_elt, token):
|
||||||
# Initialisation du dictionnaire data qui va contenir les résultats
|
# Initialisation du dictionnaire data qui va contenir les résultats
|
||||||
start = time.time()
|
start = time.time()
|
||||||
data = {
|
data = {
|
||||||
'shows': tirage_elt.spectacle_set.select_related().all(),
|
'shows': tirage_elt.spectacle_set.select_related('location'),
|
||||||
'token': token,
|
'token': token,
|
||||||
'members': tirage_elt.participant_set.all(),
|
'members': tirage_elt.participant_set.select_related('user'),
|
||||||
'total_slots': 0,
|
'total_slots': 0,
|
||||||
'total_losers': 0,
|
'total_losers': 0,
|
||||||
'total_sold': 0,
|
'total_sold': 0,
|
||||||
|
@ -228,7 +246,7 @@ def do_tirage(tirage_elt, token):
|
||||||
ChoixSpectacle.objects
|
ChoixSpectacle.objects
|
||||||
.filter(spectacle__tirage=tirage_elt)
|
.filter(spectacle__tirage=tirage_elt)
|
||||||
.order_by('participant', 'priority')
|
.order_by('participant', 'priority')
|
||||||
.select_related().all()
|
.select_related('participant', 'participant__user', 'spectacle')
|
||||||
)
|
)
|
||||||
results = Algorithm(data['shows'], data['members'], choices)(token)
|
results = Algorithm(data['shows'], data['members'], choices)(token)
|
||||||
|
|
||||||
|
@ -285,10 +303,31 @@ 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
|
||||||
for (show, _, losers) in results:
|
ChoixRevente = Participant.choicesrevente.through
|
||||||
for (loser, _, _, _) in losers:
|
|
||||||
loser.choicesrevente.add(show)
|
# Suppression des reventes demandées/enregistrées
|
||||||
loser.save()
|
# (si le tirage est relancé)
|
||||||
|
(
|
||||||
|
ChoixRevente.objects
|
||||||
|
.filter(spectacle__tirage=tirage_elt)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
(
|
||||||
|
SpectacleRevente.objects
|
||||||
|
.filter(attribution__spectacle__tirage=tirage_elt)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
|
||||||
|
lost_by = defaultdict(set)
|
||||||
|
for show, _, losers in results:
|
||||||
|
for loser, _, _, _ in losers:
|
||||||
|
lost_by[loser].add(show)
|
||||||
|
|
||||||
|
ChoixRevente.objects.bulk_create(
|
||||||
|
ChoixRevente(participant=member, spectacle=show)
|
||||||
|
for member, shows in lost_by.items()
|
||||||
|
for show in shows
|
||||||
|
)
|
||||||
|
|
||||||
data["duration"] = time.time() - start
|
data["duration"] = time.time() - start
|
||||||
data["results"] = results
|
data["results"] = results
|
||||||
|
@ -316,13 +355,18 @@ def revente(request, tirage_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
participant, created = Participant.objects.get_or_create(
|
participant, created = Participant.objects.get_or_create(
|
||||||
user=request.user, tirage=tirage)
|
user=request.user, tirage=tirage)
|
||||||
|
|
||||||
if not participant.paid:
|
if not participant.paid:
|
||||||
return render(request, "bda-notpaid.html", {})
|
return render(request, "bda-notpaid.html", {})
|
||||||
|
|
||||||
|
resellform = ResellForm(participant, prefix='resell')
|
||||||
|
annulform = AnnulForm(participant, prefix='annul')
|
||||||
|
soldform = SoldForm(participant, prefix='sold')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# On met en vente une place
|
# On met en vente une place
|
||||||
if 'resell' in request.POST:
|
if 'resell' in request.POST:
|
||||||
resellform = ResellForm(participant, request.POST, prefix='resell')
|
resellform = ResellForm(participant, request.POST, prefix='resell')
|
||||||
annulform = AnnulForm(participant, prefix='annul')
|
|
||||||
if resellform.is_valid():
|
if resellform.is_valid():
|
||||||
datatuple = []
|
datatuple = []
|
||||||
attributions = resellform.cleaned_data["attributions"]
|
attributions = resellform.cleaned_data["attributions"]
|
||||||
|
@ -354,7 +398,6 @@ def revente(request, tirage_id):
|
||||||
# On annule une revente
|
# On annule une revente
|
||||||
elif 'annul' in request.POST:
|
elif 'annul' in request.POST:
|
||||||
annulform = AnnulForm(participant, request.POST, prefix='annul')
|
annulform = AnnulForm(participant, request.POST, prefix='annul')
|
||||||
resellform = ResellForm(participant, prefix='resell')
|
|
||||||
if annulform.is_valid():
|
if annulform.is_valid():
|
||||||
attributions = annulform.cleaned_data["attributions"]
|
attributions = annulform.cleaned_data["attributions"]
|
||||||
for attribution in attributions:
|
for attribution in attributions:
|
||||||
|
@ -362,58 +405,42 @@ def revente(request, tirage_id):
|
||||||
# On confirme une vente en transférant la place à la personne qui a
|
# On confirme une vente en transférant la place à la personne qui a
|
||||||
# gagné le tirage
|
# gagné le tirage
|
||||||
elif 'transfer' in request.POST:
|
elif 'transfer' in request.POST:
|
||||||
resellform = ResellForm(participant, prefix='resell')
|
soldform = SoldForm(participant, request.POST, prefix='sold')
|
||||||
annulform = AnnulForm(participant, prefix='annul')
|
if soldform.is_valid():
|
||||||
|
attributions = soldform.cleaned_data['attributions']
|
||||||
|
for attribution in attributions:
|
||||||
|
attribution.participant = attribution.revente.soldTo
|
||||||
|
attribution.save()
|
||||||
|
|
||||||
revente_id = request.POST['transfer'][0]
|
|
||||||
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
|
|
||||||
id=revente_id)
|
|
||||||
if rev.exists():
|
|
||||||
revente = rev.get()
|
|
||||||
attrib = revente.attribution
|
|
||||||
attrib.participant = revente.soldTo
|
|
||||||
attrib.save()
|
|
||||||
# On annule la revente après le tirage au sort (par exemple si
|
# On annule la revente après le tirage au sort (par exemple si
|
||||||
# la personne qui a gagné le tirage ne se manifeste pas). La place est
|
# la personne qui a gagné le tirage ne se manifeste pas). La place est
|
||||||
# alors remise en vente
|
# alors remise en vente
|
||||||
elif 'reinit' in request.POST:
|
elif 'reinit' in request.POST:
|
||||||
resellform = ResellForm(participant, prefix='resell')
|
soldform = SoldForm(participant, request.POST, prefix='sold')
|
||||||
annulform = AnnulForm(participant, prefix='annul')
|
if soldform.is_valid():
|
||||||
revente_id = request.POST['reinit'][0]
|
attributions = soldform.cleaned_data['attributions']
|
||||||
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
|
for attribution in attributions:
|
||||||
id=revente_id)
|
if attribution.spectacle.date > timezone.now():
|
||||||
if rev.exists():
|
revente = attribution.revente
|
||||||
revente = rev.get()
|
revente.date = timezone.now() - timedelta(minutes=65)
|
||||||
if revente.attribution.spectacle.date > timezone.now():
|
revente.soldTo = None
|
||||||
revente.date = timezone.now() - timedelta(hours=1)
|
revente.notif_sent = False
|
||||||
revente.soldTo = None
|
revente.tirage_done = False
|
||||||
revente.notif_sent = False
|
revente.shotgun = False
|
||||||
revente.tirage_done = False
|
if revente.answered_mail:
|
||||||
revente.shotgun = False
|
revente.answered_mail.clear()
|
||||||
if revente.answered_mail:
|
revente.save()
|
||||||
revente.answered_mail.clear()
|
|
||||||
revente.save()
|
|
||||||
|
|
||||||
else:
|
|
||||||
resellform = ResellForm(participant, prefix='resell')
|
|
||||||
annulform = AnnulForm(participant, prefix='annul')
|
|
||||||
else:
|
|
||||||
resellform = ResellForm(participant, prefix='resell')
|
|
||||||
annulform = AnnulForm(participant, prefix='annul')
|
|
||||||
|
|
||||||
overdue = participant.attribution_set.filter(
|
overdue = participant.attribution_set.filter(
|
||||||
spectacle__date__gte=timezone.now(),
|
spectacle__date__gte=timezone.now(),
|
||||||
revente__isnull=False,
|
revente__isnull=False,
|
||||||
revente__seller=participant,
|
revente__seller=participant,
|
||||||
revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
|
revente__notif_sent=True)\
|
||||||
|
.filter(
|
||||||
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
|
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
|
||||||
sold = participant.attribution_set.filter(
|
|
||||||
spectacle__date__gte=timezone.now(),
|
|
||||||
revente__isnull=False,
|
|
||||||
revente__soldTo__isnull=False)
|
|
||||||
|
|
||||||
return render(request, "bda-revente.html",
|
return render(request, "bda/reventes.html",
|
||||||
{'tirage': tirage, 'overdue': overdue, "sold": sold,
|
{'tirage': tirage, 'overdue': overdue, "soldform": soldform,
|
||||||
"annulform": annulform, "resellform": resellform})
|
"annulform": annulform, "resellform": resellform})
|
||||||
|
|
||||||
|
|
||||||
|
@ -465,7 +492,6 @@ def list_revente(request, tirage_id):
|
||||||
)
|
)
|
||||||
if min_resell is not None:
|
if min_resell is not None:
|
||||||
min_resell.answered_mail.add(participant)
|
min_resell.answered_mail.add(participant)
|
||||||
min_resell.save()
|
|
||||||
inscrit_revente.append(spectacle)
|
inscrit_revente.append(spectacle)
|
||||||
success = True
|
success = True
|
||||||
else:
|
else:
|
||||||
|
@ -503,13 +529,13 @@ def buy_revente(request, spectacle_id):
|
||||||
|
|
||||||
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
|
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
|
||||||
# on supprime la revente en question.
|
# on supprime la revente en question.
|
||||||
if reventes.filter(seller=participant).exists():
|
own_reventes = reventes.filter(seller=participant)
|
||||||
revente = reventes.filter(seller=participant)[0]
|
if len(own_reventes) > 0:
|
||||||
revente.delete()
|
own_reventes[0].delete()
|
||||||
return HttpResponseRedirect(reverse("bda-shotgun",
|
return HttpResponseRedirect(reverse("bda-shotgun",
|
||||||
args=[tirage.id]))
|
args=[tirage.id]))
|
||||||
|
|
||||||
reventes_shotgun = list(reventes.filter(shotgun=True).all())
|
reventes_shotgun = reventes.filter(shotgun=True)
|
||||||
|
|
||||||
if not reventes_shotgun:
|
if not reventes_shotgun:
|
||||||
return render(request, "bda-no-revente.html", {})
|
return render(request, "bda-no-revente.html", {})
|
||||||
|
@ -541,16 +567,21 @@ def buy_revente(request, spectacle_id):
|
||||||
@login_required
|
@login_required
|
||||||
def revente_shotgun(request, tirage_id):
|
def revente_shotgun(request, tirage_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
spectacles = tirage.spectacle_set.filter(
|
spectacles = (
|
||||||
date__gte=timezone.now())
|
tirage.spectacle_set
|
||||||
shotgun = []
|
.filter(date__gte=timezone.now())
|
||||||
for spectacle in spectacles:
|
.select_related('location')
|
||||||
reventes = SpectacleRevente.objects.filter(
|
.prefetch_related(Prefetch(
|
||||||
attribution__spectacle=spectacle,
|
'attribues',
|
||||||
shotgun=True,
|
queryset=(
|
||||||
soldTo__isnull=True)
|
Attribution.objects
|
||||||
if reventes.exists():
|
.filter(revente__shotgun=True,
|
||||||
shotgun.append(spectacle)
|
revente__soldTo__isnull=True)
|
||||||
|
),
|
||||||
|
to_attr='shotguns',
|
||||||
|
))
|
||||||
|
)
|
||||||
|
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
||||||
|
|
||||||
return render(request, "bda-shotgun.html",
|
return render(request, "bda-shotgun.html",
|
||||||
{"shotgun": shotgun})
|
{"shotgun": shotgun})
|
||||||
|
@ -560,7 +591,10 @@ def revente_shotgun(request, tirage_id):
|
||||||
def spectacle(request, tirage_id, spectacle_id):
|
def spectacle(request, tirage_id, spectacle_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
|
||||||
attributions = spectacle.attribues.all()
|
attributions = (
|
||||||
|
spectacle.attribues
|
||||||
|
.select_related('participant', 'participant__user')
|
||||||
|
)
|
||||||
participants = {}
|
participants = {}
|
||||||
for attrib in attributions:
|
for attrib in attributions:
|
||||||
participant = attrib.participant
|
participant = attrib.participant
|
||||||
|
@ -579,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})
|
||||||
|
|
||||||
|
|
||||||
|
@ -589,7 +623,10 @@ class SpectacleListView(ListView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id'])
|
self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id'])
|
||||||
categories = self.tirage.spectacle_set.all()
|
categories = (
|
||||||
|
self.tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
)
|
||||||
return categories
|
return categories
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -602,9 +639,12 @@ class SpectacleListView(ListView):
|
||||||
@buro_required
|
@buro_required
|
||||||
def unpaid(request, tirage_id):
|
def unpaid(request, tirage_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
unpaid = tirage.participant_set \
|
unpaid = (
|
||||||
.annotate(nb_attributions=Count('attribution')) \
|
tirage.participant_set
|
||||||
.filter(paid=False, nb_attributions__gt=0).all()
|
.annotate(nb_attributions=Count('attribution'))
|
||||||
|
.filter(paid=False, nb_attributions__gt=0)
|
||||||
|
.select_related('user')
|
||||||
|
)
|
||||||
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
|
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
|
||||||
|
|
||||||
|
|
||||||
|
@ -612,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()
|
||||||
|
@ -634,12 +678,24 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
def descriptions_spectacles(request, tirage_id):
|
def descriptions_spectacles(request, tirage_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
shows_qs = tirage.spectacle_set
|
shows_qs = (
|
||||||
|
tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
.prefetch_related('quote_set')
|
||||||
|
)
|
||||||
category_name = request.GET.get('category', '')
|
category_name = request.GET.get('category', '')
|
||||||
location_id = request.GET.get('location', '')
|
location_id = request.GET.get('location', '')
|
||||||
if category_name:
|
if category_name:
|
||||||
|
@ -650,4 +706,112 @@ def descriptions_spectacles(request, tirage_id):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
"La variable GET 'location' doit contenir un entier")
|
"La variable GET 'location' doit contenir un entier")
|
||||||
return render(request, 'descriptions.html', {'shows': shows_qs.all()})
|
return render(request, 'descriptions.html', {'shows': shows_qs})
|
||||||
|
|
||||||
|
|
||||||
|
def catalogue(request, request_type):
|
||||||
|
"""
|
||||||
|
Vue destinée à communiquer avec un client AJAX, fournissant soit :
|
||||||
|
- la liste des tirages
|
||||||
|
- les catégories et salles d'un tirage
|
||||||
|
- les descriptions d'un tirage (filtrées selon la catégorie et la salle)
|
||||||
|
"""
|
||||||
|
if request_type == "list":
|
||||||
|
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
|
||||||
|
data_return = list(
|
||||||
|
Tirage.objects.filter(appear_catalogue=True).values('id', 'title')
|
||||||
|
)
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
if request_type == "details":
|
||||||
|
# Dans ce cas on retourne une liste des catégories et des salles
|
||||||
|
tirage_id = request.GET.get('id', None)
|
||||||
|
if tirage_id is None:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Missing GET parameter: id <int>"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tirage = get_object_or_404(Tirage, id=int(tirage_id))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Bad format: int expected for `id`"
|
||||||
|
)
|
||||||
|
shows = tirage.spectacle_set.values_list("id", flat=True)
|
||||||
|
categories = list(
|
||||||
|
CategorieSpectacle.objects
|
||||||
|
.filter(spectacle__in=shows)
|
||||||
|
.distinct()
|
||||||
|
.values('id', 'name')
|
||||||
|
)
|
||||||
|
locations = list(
|
||||||
|
Salle.objects
|
||||||
|
.filter(spectacle__in=shows)
|
||||||
|
.distinct()
|
||||||
|
.values('id', 'name')
|
||||||
|
)
|
||||||
|
data_return = {'categories': categories, 'locations': locations}
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
if request_type == "descriptions":
|
||||||
|
# Ici on retourne les descriptions correspondant à la catégorie et
|
||||||
|
# à la salle spécifiées
|
||||||
|
|
||||||
|
tirage_id = request.GET.get('id', '')
|
||||||
|
categories = request.GET.get('category', '[]')
|
||||||
|
locations = request.GET.get('location', '[]')
|
||||||
|
try:
|
||||||
|
tirage_id = int(tirage_id)
|
||||||
|
categories_id = json.loads(categories)
|
||||||
|
locations_id = json.loads(locations)
|
||||||
|
# Integers expected
|
||||||
|
if not all(isinstance(id, int) for id in categories_id):
|
||||||
|
raise ValueError
|
||||||
|
if not all(isinstance(id, int) for id in locations_id):
|
||||||
|
raise ValueError
|
||||||
|
except ValueError: # Contient JSONDecodeError
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Parse error, please ensure the GET parameters have the "
|
||||||
|
"following types:\n"
|
||||||
|
"id: int, category: [int], location: [int]\n"
|
||||||
|
"Data received:\n"
|
||||||
|
"id = {}, category = {}, locations = {}"
|
||||||
|
.format(request.GET.get('id', ''),
|
||||||
|
request.GET.get('category', '[]'),
|
||||||
|
request.GET.get('location', '[]'))
|
||||||
|
)
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
|
||||||
|
shows_qs = (
|
||||||
|
tirage.spectacle_set
|
||||||
|
.select_related('location')
|
||||||
|
.prefetch_related('quote_set')
|
||||||
|
)
|
||||||
|
if categories_id and 0 not in categories_id:
|
||||||
|
shows_qs = shows_qs.filter(category__id__in=categories_id)
|
||||||
|
if locations_id and 0 not in locations_id:
|
||||||
|
shows_qs = shows_qs.filter(location__id__in=locations_id)
|
||||||
|
|
||||||
|
# On convertit les descriptions à envoyer en une liste facilement
|
||||||
|
# JSONifiable (il devrait y avoir un moyen plus efficace en
|
||||||
|
# redéfinissant le serializer de JSON)
|
||||||
|
data_return = [{
|
||||||
|
'title': spectacle.title,
|
||||||
|
'category': str(spectacle.category),
|
||||||
|
'date': str(formats.date_format(
|
||||||
|
timezone.localtime(spectacle.date),
|
||||||
|
"SHORT_DATETIME_FORMAT")),
|
||||||
|
'location': str(spectacle.location),
|
||||||
|
'vips': spectacle.vips,
|
||||||
|
'description': spectacle.description,
|
||||||
|
'slots_description': spectacle.slots_description,
|
||||||
|
'quotes': [dict(author=quote.author,
|
||||||
|
text=quote.text)
|
||||||
|
for quote in spectacle.quote_set.all()],
|
||||||
|
'image': spectacle.getImgUrl(),
|
||||||
|
'ext_link': spectacle.ext_link,
|
||||||
|
'price': spectacle.price,
|
||||||
|
'slots': spectacle.slots
|
||||||
|
}
|
||||||
|
for spectacle in shows_qs
|
||||||
|
]
|
||||||
|
return JsonResponse(data_return, safe=False)
|
||||||
|
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from ldap3 import Connection
|
from ldap3 import Connection
|
||||||
|
|
||||||
from django import shortcuts
|
from django import shortcuts
|
||||||
|
@ -12,6 +10,10 @@ from django.conf import settings
|
||||||
|
|
||||||
class Clipper(object):
|
class Clipper(object):
|
||||||
def __init__(self, clipper, fullname):
|
def __init__(self, clipper, fullname):
|
||||||
|
if fullname is None:
|
||||||
|
fullname = ""
|
||||||
|
assert isinstance(clipper, str)
|
||||||
|
assert isinstance(fullname, str)
|
||||||
self.clipper = clipper
|
self.clipper = clipper
|
||||||
self.fullname = fullname
|
self.fullname = fullname
|
||||||
|
|
||||||
|
@ -52,24 +54,28 @@ 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)
|
||||||
for bit in bits]
|
for bit in bits if bit.isalnum()
|
||||||
))
|
))
|
||||||
with Connection(settings.LDAP_SERVER_URL) as conn:
|
if ldap_query != "(&)":
|
||||||
conn.search(
|
# If none of the bits were legal, we do not perform the query
|
||||||
'dc=spi,dc=ens,dc=fr', ldap_query,
|
entries = None
|
||||||
attributes=['uid', 'cn']
|
with Connection(settings.LDAP_SERVER_URL) as conn:
|
||||||
)
|
conn.search(
|
||||||
queries['clippers'] = conn.entries
|
'dc=spi,dc=ens,dc=fr', ldap_query,
|
||||||
# Clearing redundancies
|
attributes=['uid', 'cn']
|
||||||
queries['clippers'] = [
|
)
|
||||||
Clipper(clipper.uid, clipper.cn)
|
entries = conn.entries
|
||||||
for clipper in queries['clippers']
|
# Clearing redundancies
|
||||||
if str(clipper.uid) not in usernames
|
queries['clippers'] = [
|
||||||
]
|
Clipper(entry.uid.value, entry.cn.value)
|
||||||
|
for entry in entries
|
||||||
|
if entry.uid.value
|
||||||
|
and entry.uid.value not in usernames
|
||||||
|
]
|
||||||
|
|
||||||
# Resulting data
|
# Resulting data
|
||||||
data.update(queries)
|
data.update(queries)
|
||||||
|
|
54
cof/forms.py
54
cof/forms.py
|
@ -1,25 +1,18 @@
|
||||||
# -*- coding: utf-8 -*-
|
from djconfig.forms import ConfigForm
|
||||||
|
|
||||||
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.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
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.forms.widgets import RadioSelect, CheckboxSelectMultiple
|
||||||
from django.core.validators import MinLengthValidator
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from bda.models import Spectacle
|
||||||
|
|
||||||
|
from gestion.models import Profile, EventCommentValue
|
||||||
|
|
||||||
from .models import CofProfile, CalendarSubscription
|
from .models import CofProfile, CalendarSubscription
|
||||||
from .widgets import TriStateCheckbox
|
from .widgets import TriStateCheckbox
|
||||||
|
|
||||||
from gestion.models import Profile, EventCommentValue
|
|
||||||
from gestion.shared import lock_table, unlock_table
|
|
||||||
|
|
||||||
from bda.models import Spectacle
|
|
||||||
|
|
||||||
|
|
||||||
class SurveyForm(forms.Form):
|
class SurveyForm(forms.Form):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -136,8 +129,9 @@ class EventStatusFilterForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class RegistrationUserForm(forms.ModelForm):
|
class RegistrationUserForm(forms.ModelForm):
|
||||||
def force_long_username(self):
|
def __init__(self, *args, **kw):
|
||||||
self.fields['username'].validators = [MinLengthValidator(9)]
|
super(RegistrationUserForm, self).__init__(*args, **kw)
|
||||||
|
self.fields['username'].help_text = ""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
@ -182,21 +176,6 @@ class RegistrationCofProfileForm(forms.ModelForm):
|
||||||
"mailing", "mailing_bda", "mailing_bda_revente",
|
"mailing", "mailing_bda", "mailing_bda_revente",
|
||||||
]
|
]
|
||||||
|
|
||||||
def save(self, *args, **kw):
|
|
||||||
instance = super(RegistrationCofProfileForm, 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 RegistrationProfileForm(forms.ModelForm):
|
class RegistrationProfileForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -318,3 +297,16 @@ class CalendarForm(forms.ModelForm):
|
||||||
model = CalendarSubscription
|
model = CalendarSubscription
|
||||||
fields = ['subscribe_to_events', 'subscribe_to_my_shows',
|
fields = ['subscribe_to_events', 'subscribe_to_my_shows',
|
||||||
'other_shows']
|
'other_shows']
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
|
|
@ -1,41 +1,45 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
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.conf import settings
|
||||||
from django.core import mail
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core import mail
|
||||||
|
from django.db import transaction
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.utils import timezone
|
||||||
from django.views.generic import ListView, DetailView
|
from django.views.generic import ListView, DetailView
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
|
from .decorators import buro_required
|
||||||
from .models import CofProfile
|
from .models import CofProfile
|
||||||
from .petits_cours_models import (
|
from .petits_cours_models import (
|
||||||
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
|
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
|
||||||
PetitCoursAbility, PetitCoursSubject
|
PetitCoursAbility
|
||||||
)
|
)
|
||||||
from .decorators import buro_required
|
|
||||||
from .petits_cours_forms import DemandeForm, MatieresFormSet
|
from .petits_cours_forms import DemandeForm, MatieresFormSet
|
||||||
|
|
||||||
from gestion.shared import lock_table, unlock_tables
|
|
||||||
|
|
||||||
|
|
||||||
class DemandeListView(ListView):
|
class DemandeListView(ListView):
|
||||||
model = PetitCoursDemande
|
queryset = (
|
||||||
|
PetitCoursDemande.objects
|
||||||
|
.prefetch_related('matieres')
|
||||||
|
.order_by('traitee', '-id')
|
||||||
|
)
|
||||||
template_name = "petits_cours_demandes_list.html"
|
template_name = "petits_cours_demandes_list.html"
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return PetitCoursDemande.objects.order_by('traitee', '-id').all()
|
|
||||||
|
|
||||||
|
|
||||||
class DemandeDetailView(DetailView):
|
class DemandeDetailView(DetailView):
|
||||||
model = PetitCoursDemande
|
model = PetitCoursDemande
|
||||||
template_name = "cof/details_demande_petit_cours.html"
|
template_name = "cof/details_demande_petit_cours.html"
|
||||||
|
queryset = (
|
||||||
|
PetitCoursDemande.objects
|
||||||
|
.prefetch_related('petitcoursattribution_set',
|
||||||
|
'matieres')
|
||||||
|
)
|
||||||
context_object_name = "demande"
|
context_object_name = "demande"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -103,8 +107,9 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
|
||||||
'style="width:99%; height: 90px;">'
|
'style="width:99%; height: 90px;">'
|
||||||
'</textarea>'
|
'</textarea>'
|
||||||
})
|
})
|
||||||
for error in errors:
|
if errors is not None:
|
||||||
messages.error(request, error)
|
for error in errors:
|
||||||
|
messages.error(request, error)
|
||||||
return render(request, "cof/traitement_demande_petit_cours.html",
|
return render(request, "cof/traitement_demande_petit_cours.html",
|
||||||
{"demande": demande,
|
{"demande": demande,
|
||||||
"unsatisfied": unsatisfied,
|
"unsatisfied": unsatisfied,
|
||||||
|
@ -215,7 +220,7 @@ def _traitement_other(request, demande, redo):
|
||||||
proposals = proposals.items()
|
proposals = proposals.items()
|
||||||
proposed_for = proposed_for.items()
|
proposed_for = proposed_for.items()
|
||||||
return render(request,
|
return render(request,
|
||||||
"gestiocof/traitement_demande_petit_cours_autre_niveau.html",
|
"cof/traitement_demande_petit_cours_autre_niveau.html",
|
||||||
{"demande": demande,
|
{"demande": demande,
|
||||||
"unsatisfied": unsatisfied,
|
"unsatisfied": unsatisfied,
|
||||||
"proposals": proposals,
|
"proposals": proposals,
|
||||||
|
@ -246,37 +251,39 @@ def _traitement_post(request, demande):
|
||||||
proposals_list = proposals.items()
|
proposals_list = proposals.items()
|
||||||
proposed_for = proposed_for.items()
|
proposed_for = proposed_for.items()
|
||||||
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
||||||
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
|
mainmail_object, mainmail_body = render_custom_mail(
|
||||||
"proposals": proposals_list,
|
"petits-cours-mail-demandeur",
|
||||||
"unsatisfied": unsatisfied,
|
{
|
||||||
"extra": extra,
|
"proposals": proposals_list,
|
||||||
})
|
"unsatisfied": unsatisfied,
|
||||||
|
"extra": extra
|
||||||
|
}
|
||||||
|
)
|
||||||
frommail = settings.MAIL_DATA['petits_cours']['FROM']
|
frommail = settings.MAIL_DATA['petits_cours']['FROM']
|
||||||
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
|
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
|
||||||
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
|
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
|
||||||
mails_to_send = []
|
mails_to_send = []
|
||||||
for (user, msg) in proposed_mails:
|
for (user, (mail_object, body)) in proposed_mails:
|
||||||
msg = mail.EmailMessage("Petits cours ENS par le COF", msg,
|
msg = mail.EmailMessage(mail_object, body, frommail, [user.email],
|
||||||
frommail, [user.email],
|
|
||||||
[bccaddress], headers={'Reply-To': replyto})
|
[bccaddress], headers={'Reply-To': replyto})
|
||||||
mails_to_send.append(msg)
|
mails_to_send.append(msg)
|
||||||
mails_to_send.append(mail.EmailMessage("Cours particuliers ENS", mainmail,
|
mails_to_send.append(mail.EmailMessage(mainmail_object, mainmail_body,
|
||||||
frommail, [demande.email],
|
frommail, [demande.email],
|
||||||
[bccaddress],
|
[bccaddress],
|
||||||
headers={'Reply-To': replyto}))
|
headers={'Reply-To': replyto}))
|
||||||
connection = mail.get_connection(fail_silently=True)
|
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 = timezone.now()
|
demande.processed = timezone.now()
|
||||||
|
@ -301,17 +308,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:
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -814,6 +817,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 --------------------------------- */
|
||||||
|
|
||||||
|
@ -836,7 +850,7 @@ input#search_autocomplete {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 20px;
|
margin-top: 0px;
|
||||||
display: block;
|
display: block;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
@ -1155,3 +1169,10 @@ div.messages div.alert-success div.container {
|
||||||
div.messages div.alert div.container a {
|
div.messages div.alert div.container a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Help text */
|
||||||
|
|
||||||
|
p.help-block {
|
||||||
|
margin: 5px auto;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
|
@ -8,12 +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" %}">
|
||||||
|
|
||||||
{# 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 %}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends "cof/base_header.html" %}
|
{% extends "cof/base_header.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load wagtailcore_tags %}
|
||||||
|
|
||||||
{% block homelink %}
|
{% block homelink %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -41,7 +42,21 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3>
|
||||||
|
<div class="hm-block">
|
||||||
|
<ul>
|
||||||
|
{# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
|
||||||
|
<li><a href="{% slugurl "k-fet" %}">Page d'accueil</a></li>
|
||||||
|
<li><a href="https://www.cof.ens.fr/k-fet/calendrier">Calendrier</a></li>
|
||||||
|
{% if perms.kfet.is_team %}
|
||||||
|
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user.profile.is_cof %}
|
||||||
<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>
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Inscription d'un nouveau membre</h2>
|
<h2>Inscription d'un nouveau membre</h2>
|
||||||
<input type="text" name="q" id="search_autocomplete" spellcheck="false" />
|
<p class="help-block">Les mots contenant des caractères non alphanumériques seront ignorés</p>
|
||||||
|
<input type="text" name="q" id="search_autocomplete" spellcheck="false"
|
||||||
|
placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" />
|
||||||
<div id="form-placeholder"></div>
|
<div id="form-placeholder"></div>
|
||||||
<div class="yourlabs-autocomplete"></div>
|
<div class="yourlabs-autocomplete"></div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
@ -20,7 +22,6 @@
|
||||||
minimumCharacters: 3,
|
minimumCharacters: 3,
|
||||||
id: 'search_autocomplete',
|
id: 'search_autocomplete',
|
||||||
choiceSelector: 'li:has(a)',
|
choiceSelector: 'li:has(a)',
|
||||||
placeholder: "Chercher un utilisateur par nom, prénom ou identifiant clipper",
|
|
||||||
box: $(".yourlabs-autocomplete"),
|
box: $(".yourlabs-autocomplete"),
|
||||||
});
|
});
|
||||||
$('input#search_autocomplete').bind(
|
$('input#search_autocomplete').bind(
|
||||||
|
|
|
@ -23,7 +23,7 @@ def key(d, key_name):
|
||||||
|
|
||||||
|
|
||||||
def highlight_text(text, q):
|
def highlight_text(text, q):
|
||||||
q2 = "|".join(q.split())
|
q2 = "|".join(re.escape(word) for word in q.split())
|
||||||
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
|
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
|
||||||
return mark_safe(re.sub(pattern,
|
return mark_safe(re.sub(pattern,
|
||||||
r"<span class='highlight'>\g<filter></span>",
|
r"<span class='highlight'>\g<filter></span>",
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from .petits_cours_views import DemandeListView, DemandeDetailView
|
from .petits_cours_views import DemandeListView, DemandeDetailView
|
||||||
from .decorators import buro_required
|
from .decorators import buro_required
|
||||||
|
|
167
cof/views.py
167
cof/views.py
|
@ -1,38 +1,38 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import unicodecsv
|
import unicodecsv
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from custommail.shortcuts import send_custom_mail
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from icalendar import Calendar, Event as Vevent
|
from icalendar import Calendar, Event as Vevent
|
||||||
from custommail.shortcuts import send_custom_mail
|
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.contrib import messages
|
||||||
from django.http import Http404, HttpResponse
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
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.http import Http404, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib import messages
|
from django.views.generic import FormView
|
||||||
import django.utils.six as six
|
|
||||||
|
from bda.models import Tirage, Spectacle
|
||||||
|
|
||||||
from gestion.models import (
|
from gestion.models import (
|
||||||
Event, EventRegistration, EventOption, EventOptionChoice,
|
Event, EventRegistration, EventOption, EventOptionChoice,
|
||||||
EventCommentField, EventCommentValue
|
EventCommentField, EventCommentValue, Profile
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Survey, SurveyAnswer, SurveyQuestion, \
|
|
||||||
SurveyQuestionAnswer, CalendarSubscription
|
|
||||||
from .models import CofProfile
|
|
||||||
from .decorators import buro_required, cof_required
|
from .decorators import buro_required, cof_required
|
||||||
from .forms import (
|
from .forms import (
|
||||||
SurveyForm, SurveyStatusFilterForm,
|
SurveyForm, SurveyStatusFilterForm,
|
||||||
RegistrationUserForm, RegistrationProfileForm, RegistrationCofProfileForm,
|
RegistrationUserForm, RegistrationProfileForm, RegistrationCofProfileForm,
|
||||||
CalendarForm, EventFormset, RegistrationPassUserForm
|
CalendarForm, EventFormset, RegistrationPassUserForm,
|
||||||
|
GestioncofConfigForm
|
||||||
|
)
|
||||||
|
from .models import (
|
||||||
|
Survey, SurveyAnswer, SurveyQuestion, SurveyQuestionAnswer,
|
||||||
|
CalendarSubscription, CofProfile
|
||||||
)
|
)
|
||||||
|
|
||||||
from bda.models import Tirage, Spectacle
|
|
||||||
|
|
||||||
from gestion.models import Profile
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -52,7 +52,10 @@ def home(request):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def survey(request, survey_id):
|
def survey(request, survey_id):
|
||||||
survey = get_object_or_404(Survey, id=survey_id)
|
survey = get_object_or_404(
|
||||||
|
Survey.objects.prefetch_related('questions', 'questions__answers'),
|
||||||
|
id=survey_id,
|
||||||
|
)
|
||||||
if not survey.survey_open or survey.old:
|
if not survey.survey_open or survey.old:
|
||||||
raise Http404
|
raise Http404
|
||||||
success = False
|
success = False
|
||||||
|
@ -224,7 +227,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()
|
||||||
cofprofile_form = RegistrationCofProfileForm()
|
cofprofile_form = RegistrationCofProfileForm()
|
||||||
event_formset = EventFormset(events=events, prefix='events')
|
event_formset = EventFormset(events=events, prefix='events')
|
||||||
|
@ -273,12 +275,8 @@ def update_event_form_comments(event, form, registration):
|
||||||
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
|
||||||
|
@ -300,12 +298,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
|
||||||
|
@ -313,10 +309,11 @@ def registration(request):
|
||||||
|
|
||||||
if user_form.is_valid():
|
if user_form.is_valid():
|
||||||
member = user_form.save()
|
member = user_form.save()
|
||||||
cofprofile, _ = (CofProfile.objects
|
cofprofile, _ = (
|
||||||
.get_or_create(profile=member.profile))
|
CofProfile.objects
|
||||||
|
.get_or_create(profile=member.profile)
|
||||||
|
)
|
||||||
was_cof = cofprofile.is_cof
|
was_cof = cofprofile.is_cof
|
||||||
request_dict["num"] = cofprofile.num
|
|
||||||
# Maintenant on remplit le formulaire de profil
|
# Maintenant on remplit le formulaire de profil
|
||||||
cofprofile_form = RegistrationCofProfileForm(
|
cofprofile_form = RegistrationCofProfileForm(
|
||||||
request_dict,
|
request_dict,
|
||||||
|
@ -375,16 +372,17 @@ def registration(request):
|
||||||
# l'inscription aux événements et/ou donner la
|
# l'inscription aux événements et/ou donner la
|
||||||
# possibilité d'associer un mail aux événements
|
# possibilité d'associer un mail aux événements
|
||||||
# send_custom_mail(...)
|
# send_custom_mail(...)
|
||||||
success = True
|
# ---
|
||||||
# Messages
|
# Success
|
||||||
if success:
|
# ---
|
||||||
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
|
|
||||||
"enregistrée avec succès"
|
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
|
||||||
.format(member.get_full_name(), member.email))
|
"enregistrée avec succès."
|
||||||
if member.profile.is_cof:
|
.format(member.get_full_name(), member.email))
|
||||||
msg += "Il est désormais membre du COF n°{:d} !".format(
|
if profile.is_cof:
|
||||||
member.profile.num)
|
msg += "\nIl est désormais membre du COF n°{:d} !".format(
|
||||||
messages.success(request, msg, extra_tags='safe')
|
member.profile.id)
|
||||||
|
messages.success(request, msg, extra_tags='safe')
|
||||||
return render(request, "cof/registration_post.html",
|
return render(request, "cof/registration_post.html",
|
||||||
{"user_form": user_form,
|
{"user_form": user_form,
|
||||||
"profile_form": profile_form,
|
"profile_form": profile_form,
|
||||||
|
@ -405,10 +403,10 @@ def export_members(request):
|
||||||
for profile in CofProfile.objects.filter(
|
for profile in CofProfile.objects.filter(
|
||||||
profile__user__groups__name='cof_members').all():
|
profile__user__groups__name='cof_members').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
|
||||||
|
|
||||||
|
@ -424,78 +422,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
|
||||||
|
@ -600,3 +600,18 @@ 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)
|
||||||
|
|
|
@ -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'),
|
||||||
|
]
|
||||||
|
|
1
gestioCOF/settings/.gitignore
vendored
Normal file
1
gestioCOF/settings/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
secret.py
|
234
gestioCOF/settings/common.py
Normal file
234
gestioCOF/settings/common.py
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Django common settings for cof project.
|
||||||
|
|
||||||
|
Everything which is supposed to be identical between the production server and
|
||||||
|
the local development server should be here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import secret
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"The secret.py file is missing.\n"
|
||||||
|
"For a development environment, simply copy secret_example.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def import_secret(name):
|
||||||
|
"""
|
||||||
|
Shorthand for importing a value from the secret module and raising an
|
||||||
|
informative exception if a secret is missing.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return getattr(secret, name)
|
||||||
|
except AttributeError:
|
||||||
|
raise RuntimeError("Secret missing: {}".format(name))
|
||||||
|
|
||||||
|
|
||||||
|
SECRET_KEY = import_secret("SECRET_KEY")
|
||||||
|
ADMINS = import_secret("ADMINS")
|
||||||
|
SERVER_EMAIL = import_secret("SERVER_EMAIL")
|
||||||
|
|
||||||
|
DBNAME = import_secret("DBNAME")
|
||||||
|
DBUSER = import_secret("DBUSER")
|
||||||
|
DBPASSWD = import_secret("DBPASSWD")
|
||||||
|
|
||||||
|
REDIS_PASSWD = import_secret("REDIS_PASSWD")
|
||||||
|
REDIS_DB = import_secret("REDIS_DB")
|
||||||
|
REDIS_HOST = import_secret("REDIS_HOST")
|
||||||
|
REDIS_PORT = import_secret("REDIS_PORT")
|
||||||
|
|
||||||
|
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
|
||||||
|
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
|
||||||
|
|
||||||
|
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
|
||||||
|
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'gestioncof',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'grappelli',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.admindocs',
|
||||||
|
'bda',
|
||||||
|
'autocomplete_light',
|
||||||
|
'captcha',
|
||||||
|
'django_cas_ng',
|
||||||
|
'bootstrapform',
|
||||||
|
'kfet',
|
||||||
|
'kfet.open',
|
||||||
|
'channels',
|
||||||
|
'widget_tweaks',
|
||||||
|
'custommail',
|
||||||
|
'djconfig',
|
||||||
|
'wagtail.wagtailforms',
|
||||||
|
'wagtail.wagtailredirects',
|
||||||
|
'wagtail.wagtailembeds',
|
||||||
|
'wagtail.wagtailsites',
|
||||||
|
'wagtail.wagtailusers',
|
||||||
|
'wagtail.wagtailsnippets',
|
||||||
|
'wagtail.wagtaildocs',
|
||||||
|
'wagtail.wagtailimages',
|
||||||
|
'wagtail.wagtailsearch',
|
||||||
|
'wagtail.wagtailadmin',
|
||||||
|
'wagtail.wagtailcore',
|
||||||
|
'wagtail.contrib.modeladmin',
|
||||||
|
'wagtailmenus',
|
||||||
|
'modelcluster',
|
||||||
|
'taggit',
|
||||||
|
'kfet.auth',
|
||||||
|
'kfet.cms',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = [
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
|
'kfet.auth.middleware.TemporaryAuthMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'djconfig.middleware.DjConfigMiddleware',
|
||||||
|
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||||
|
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'cof.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'django.core.context_processors.i18n',
|
||||||
|
'django.core.context_processors.media',
|
||||||
|
'django.core.context_processors.static',
|
||||||
|
'wagtailmenus.context_processors.wagtailmenus',
|
||||||
|
'djconfig.context_processors.config',
|
||||||
|
'gestioncof.shared.context_processor',
|
||||||
|
'kfet.auth.context_processors.temporary_auth',
|
||||||
|
'kfet.context_processors.config',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': DBNAME,
|
||||||
|
'USER': DBUSER,
|
||||||
|
'PASSWORD': DBPASSWD,
|
||||||
|
'HOST': os.environ.get('DBHOST', 'localhost'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'fr-fr'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Paris'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Various additional settings
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
|
||||||
|
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
|
||||||
|
|
||||||
|
MAIL_DATA = {
|
||||||
|
'petits_cours': {
|
||||||
|
'FROM': "Le COF <cof@ens.fr>",
|
||||||
|
'BCC': "archivescof@gmail.com",
|
||||||
|
'REPLYTO': "cof@ens.fr"},
|
||||||
|
'rappels': {
|
||||||
|
'FROM': 'Le BdA <bda@ens.fr>',
|
||||||
|
'REPLYTO': 'Le BdA <bda@ens.fr>'},
|
||||||
|
'revente': {
|
||||||
|
'FROM': 'BdA-Revente <bda-revente@ens.fr>',
|
||||||
|
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGIN_URL = "cof-login"
|
||||||
|
LOGIN_REDIRECT_URL = "home"
|
||||||
|
|
||||||
|
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
|
||||||
|
CAS_VERSION = '3'
|
||||||
|
CAS_LOGIN_MSG = None
|
||||||
|
CAS_IGNORE_REFERER = True
|
||||||
|
CAS_REDIRECT_URL = '/'
|
||||||
|
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
'gestioncof.shared.COFCASBackend',
|
||||||
|
'kfet.auth.backends.GenericBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
RECAPTCHA_USE_SSL = True
|
||||||
|
|
||||||
|
# Cache settings
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'redis_cache.RedisCache',
|
||||||
|
'LOCATION': 'redis://:{passwd}@{host}:{port}/db'
|
||||||
|
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
|
||||||
|
port=REDIS_PORT, db=REDIS_DB),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Channels settings
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [(
|
||||||
|
"redis://:{passwd}@{host}:{port}/{db}"
|
||||||
|
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
|
||||||
|
port=REDIS_PORT, db=REDIS_DB)
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
"ROUTING": "cof.routing.routing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FORMAT_MODULE_PATH = 'cof.locale'
|
||||||
|
|
||||||
|
# Wagtail settings
|
||||||
|
|
||||||
|
WAGTAIL_SITE_NAME = 'GestioCOF'
|
||||||
|
WAGTAIL_ENABLE_UPDATE_CHECK = False
|
||||||
|
TAGGIT_CASE_INSENSITIVE = True
|
46
gestioCOF/settings/dev.py
Normal file
46
gestioCOF/settings/dev.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""
|
||||||
|
Django development settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Apache static/media config
|
||||||
|
# ---
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = '/srv/gestiocof/static/'
|
||||||
|
|
||||||
|
MEDIA_ROOT = '/srv/gestiocof/media/'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Debug tool bar
|
||||||
|
# ---
|
||||||
|
|
||||||
|
def show_toolbar(request):
|
||||||
|
"""
|
||||||
|
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
|
||||||
|
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
|
||||||
|
machine physique n'est pas forcément connue, et peut difficilement être
|
||||||
|
mise dans les INTERNAL_IPS.
|
||||||
|
"""
|
||||||
|
return DEBUG
|
||||||
|
|
||||||
|
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||||
|
MIDDLEWARE_CLASSES = (
|
||||||
|
["debug_panel.middleware.DebugPanelMiddleware"]
|
||||||
|
+ MIDDLEWARE_CLASSES
|
||||||
|
)
|
||||||
|
DEBUG_TOOLBAR_CONFIG = {
|
||||||
|
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||||
|
}
|
36
gestioCOF/settings/local.py
Normal file
36
gestioCOF/settings/local.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
Django local settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .dev import * # NOQA
|
||||||
|
from .dev import BASE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
# Use sqlite for local development
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the default cache backend for local development
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the default in memory asgi backend for local development
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "asgiref.inmemory.ChannelLayer",
|
||||||
|
"ROUTING": "cof.routing.routing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# No need to run collectstatic -> unset STATIC_ROOT
|
||||||
|
STATIC_ROOT = None
|
30
gestioCOF/settings/prod.py
Normal file
30
gestioCOF/settings/prod.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""
|
||||||
|
Django development settings for the cof project.
|
||||||
|
The settings that are not listed here are imported from .common
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
from .common import BASE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
"cof.ens.fr",
|
||||||
|
"www.cof.ens.fr",
|
||||||
|
"dev.cof.ens.fr"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(BASE_DIR)),
|
||||||
|
"public",
|
||||||
|
"gestion",
|
||||||
|
"static",
|
||||||
|
)
|
||||||
|
|
||||||
|
STATIC_URL = "/gestion/static/"
|
||||||
|
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
|
||||||
|
MEDIA_URL = "/gestion/media/"
|
20
gestioCOF/settings/secret_example.py
Normal file
20
gestioCOF/settings/secret_example.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
|
||||||
|
ADMINS = None
|
||||||
|
SERVER_EMAIL = "root@vagrant"
|
||||||
|
|
||||||
|
DBUSER = "cof_gestion"
|
||||||
|
DBNAME = "cof_gestion"
|
||||||
|
DBPASSWD = "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
||||||
|
|
||||||
|
REDIS_PASSWD = "dummy"
|
||||||
|
REDIS_PORT = 6379
|
||||||
|
REDIS_DB = 0
|
||||||
|
REDIS_HOST = "127.0.0.1"
|
||||||
|
|
||||||
|
RECAPTCHA_PUBLIC_KEY = "DUMMY"
|
||||||
|
RECAPTCHA_PRIVATE_KEY = "DUMMY"
|
||||||
|
|
||||||
|
EMAIL_HOST = None
|
||||||
|
|
||||||
|
KFETOPEN_TOKEN = "plop"
|
||||||
|
LDAP_SERVER_URL = None
|
|
@ -1,194 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Django settings for cof project.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.8/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/1.8/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
|
||||||
import os
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
INSTALLED_APPS = (
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.sites',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.admindocs',
|
|
||||||
'autocomplete_light',
|
|
||||||
'captcha',
|
|
||||||
'django_cas_ng',
|
|
||||||
'debug_toolbar',
|
|
||||||
'bootstrapform',
|
|
||||||
'channels',
|
|
||||||
'widget_tweaks',
|
|
||||||
'custommail',
|
|
||||||
'nested_admin',
|
|
||||||
'bda.apps.BdAConfig',
|
|
||||||
'bds.apps.BDSConfig',
|
|
||||||
'cof.apps.COFConfig',
|
|
||||||
'gestion.apps.GestionConfig',
|
|
||||||
'kfet.apps.KFetConfig',
|
|
||||||
)
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
'kfet.middleware.kfet_auth_middleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'gestioCOF.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
'gestion.context_processors.context_processor',
|
|
||||||
'kfet.context_processors.auth',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
|
||||||
'NAME': os.environ['DBNAME'],
|
|
||||||
'USER': os.environ['DBUSER'],
|
|
||||||
'PASSWORD': os.environ['DBPASSWD'],
|
|
||||||
'HOST': os.environ.get('DBHOST', 'localhost'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'fr-fr'
|
|
||||||
|
|
||||||
TIME_ZONE = 'Europe/Paris'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
STATIC_ROOT = '/var/www/static/'
|
|
||||||
|
|
||||||
# Media upload (through ImageField, SiteField)
|
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/models/fields/
|
|
||||||
|
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
|
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
|
|
||||||
# Various additional settings
|
|
||||||
SITE_ID = 1
|
|
||||||
|
|
||||||
MAIL_DATA = {
|
|
||||||
'petits_cours': {
|
|
||||||
'FROM': "Le COF <cof@ens.fr>",
|
|
||||||
'BCC': "archivescof@gmail.com",
|
|
||||||
'REPLYTO': "cof@ens.fr"},
|
|
||||||
'rappels': {
|
|
||||||
'FROM': 'Le BdA <bda@ens.fr>',
|
|
||||||
'REPLYTO': 'Le BdA <bda@ens.fr>'},
|
|
||||||
'revente': {
|
|
||||||
'FROM': 'BdA-Revente <bda-revente@ens.fr>',
|
|
||||||
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'},
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGIN_URL = "gestion:login"
|
|
||||||
LOGIN_REDIRECT_URL = "home"
|
|
||||||
|
|
||||||
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
|
|
||||||
CAS_IGNORE_REFERER = True
|
|
||||||
CAS_REDIRECT_URL = '/'
|
|
||||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
|
||||||
'gestion.backends.COFCASBackend',
|
|
||||||
'kfet.backends.GenericTeamBackend',
|
|
||||||
)
|
|
||||||
|
|
||||||
# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636'
|
|
||||||
|
|
||||||
# EMAIL_HOST="nef.ens.fr"
|
|
||||||
|
|
||||||
RECAPTCHA_PUBLIC_KEY = "DUMMY"
|
|
||||||
RECAPTCHA_PRIVATE_KEY = "DUMMY"
|
|
||||||
RECAPTCHA_USE_SSL = True
|
|
||||||
|
|
||||||
# Channels settings
|
|
||||||
|
|
||||||
CHANNEL_LAYERS = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "asgi_redis.RedisChannelLayer",
|
|
||||||
"CONFIG": {
|
|
||||||
"hosts": [(os.environ.get("REDIS_HOST", "localhost"), 6379)],
|
|
||||||
},
|
|
||||||
"ROUTING": "gestioCOF.routing.channel_routing",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def show_toolbar(request):
|
|
||||||
"""
|
|
||||||
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
|
|
||||||
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
|
|
||||||
machine physique n'est pas forcément connue, et peut difficilement être
|
|
||||||
mise dans les INTERNAL_IPS.
|
|
||||||
"""
|
|
||||||
if not DEBUG:
|
|
||||||
return False
|
|
||||||
if request.is_ajax():
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
DEBUG_TOOLBAR_CONFIG = {
|
|
||||||
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
|
||||||
}
|
|
||||||
|
|
||||||
FORMAT_MODULE_PATH = 'gestioCOF.locale'
|
|
|
@ -1,13 +1,12 @@
|
||||||
# -*- coding: utf-8 -*-
|
import autocomplete_light
|
||||||
|
|
||||||
"""
|
|
||||||
Fichier principal de configuration des urls du projet GestioCOF
|
|
||||||
"""
|
|
||||||
|
|
||||||
import gestion.urls
|
import gestion.urls
|
||||||
import kfet.urls
|
import kfet.urls
|
||||||
import bda.urls
|
import bda.urls
|
||||||
|
|
||||||
|
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 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
|
||||||
|
@ -21,6 +20,7 @@ from cof.autocomplete import autocomplete
|
||||||
|
|
||||||
from gestion import views as gestion_views
|
from gestion import views as gestion_views
|
||||||
|
|
||||||
|
autocomplete_light.autodiscover()
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -82,6 +82,11 @@ urlpatterns = [
|
||||||
name="liste_bdarevente"),
|
name="liste_bdarevente"),
|
||||||
url(r'^k-fet/', include(kfet.urls)),
|
url(r'^k-fet/', include(kfet.urls)),
|
||||||
url(r"^_nested_admin/", include("nested_admin.urls")),
|
url(r"^_nested_admin/", include("nested_admin.urls")),
|
||||||
|
# wagtail
|
||||||
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
# djconfig
|
||||||
|
url(r"^config", cof_views.ConfigUpdate.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||||
|
@ -90,7 +95,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)),
|
||||||
|
]
|
||||||
|
|
|
@ -1,57 +1,30 @@
|
||||||
# -*- coding: utf-8 -*-
|
from django_cas_ng.backends import CASBackend
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django_cas_ng.backends import CASBackend
|
from django.contrib.sites.models import Site
|
||||||
from django_cas_ng.utils import get_cas_client
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from gestion.models import Profile
|
|
||||||
|
|
||||||
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 qu’avec une casse variable. On normalise pour
|
# et à la fin, ainsi qu’avec une casse variable. On normalise pour
|
||||||
# éviter les doublons.
|
# éviter les doublons.
|
||||||
username = username.strip().lower()
|
return username.strip().lower()
|
||||||
|
|
||||||
profiles = Profile.objects.filter(login_clipper=username)
|
def configure_user(self, user):
|
||||||
if len(profiles) > 0:
|
clipper = user.username
|
||||||
# XXX. We have to deal with multiple profiles, this should not
|
user.profile.login_clipper = clipper
|
||||||
# happen
|
user.profile.save()
|
||||||
# profile = profiles.order_by('-is_cof')[0]
|
user.email = settings.CAS_EMAIL_FORMAT % clipper
|
||||||
profile = profiles.first()
|
user.save()
|
||||||
user = profile.user
|
|
||||||
return user
|
|
||||||
try:
|
|
||||||
user = User.objects.get(username=username)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
# user will have an "unusable" password
|
|
||||||
user = User.objects.create_user(username, '')
|
|
||||||
user.save()
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def authenticate(self, ticket, service, request):
|
|
||||||
"""Authenticates CAS ticket and retrieves user data"""
|
def context_processor(request):
|
||||||
user = self.authenticate_cas(ticket, service, request)
|
'''Append extra data to the context of the given request'''
|
||||||
if user is None:
|
data = {
|
||||||
return user
|
"user": request.user,
|
||||||
profile = user.profile
|
"site": Site.objects.get_current(),
|
||||||
if not profile.login_clipper:
|
}
|
||||||
profile.login_clipper = user.username
|
return data
|
||||||
profile.save()
|
|
||||||
if not user.email:
|
|
||||||
user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper
|
|
||||||
user.save()
|
|
||||||
return user
|
|
||||||
|
|
|
@ -6,4 +6,9 @@ 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()
|
||||||
|
|
||||||
|
def register_config(self):
|
||||||
|
import djconfig
|
||||||
|
from kfet.forms import KFetConfigForm
|
||||||
|
djconfig.register(KFetConfigForm)
|
||||||
|
|
4
kfet/auth/__init__.py
Normal file
4
kfet/auth/__init__.py
Normal 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
14
kfet/auth/apps.py
Normal 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
43
kfet/auth/backends.py
Normal 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()
|
10
kfet/auth/context_processors.py
Normal file
10
kfet/auth/context_processors.py
Normal 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
20
kfet/auth/fields.py
Normal 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
48
kfet/auth/forms.py
Normal 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']
|
38
kfet/auth/middleware.py
Normal file
38
kfet/auth/middleware.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- 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 process_request(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
|
||||||
|
|
||||||
|
def get_kfet_password(self, request):
|
||||||
|
return (
|
||||||
|
request.META.get('HTTP_KFETPASSWORD') or
|
||||||
|
request.POST.get('KFETPASSWORD')
|
||||||
|
)
|
24
kfet/auth/migrations/0001_initial.py
Normal file
24
kfet/auth/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
kfet/auth/migrations/__init__.py
Normal file
0
kfet/auth/migrations/__init__.py
Normal file
17
kfet/auth/models.py
Normal file
17
kfet/auth/models.py
Normal 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
40
kfet/auth/signals.py
Normal 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)
|
||||||
|
))
|
367
kfet/auth/tests.py
Normal file
367
kfet/auth/tests.py
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
# -*- 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()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
TemporaryAuthMiddleware().process_request(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
|
||||||
|
|
||||||
|
TemporaryAuthMiddleware().process_request(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
|
||||||
|
|
||||||
|
TemporaryAuthMiddleware().process_request(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
34
kfet/auth/utils.py
Normal 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
136
kfet/auth/views.py
Normal 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')
|
|
@ -1,19 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from ldap3 import Connection
|
from ldap3 import Connection
|
||||||
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.http import Http404
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
from kfet.decorators import teamkfet_required
|
from .decorators import teamkfet_required
|
||||||
from kfet.models import Account
|
from .models import Account
|
||||||
|
|
||||||
|
|
||||||
class Clipper(object):
|
class Clipper(object):
|
||||||
def __init__(self, clipper, fullname):
|
def __init__(self, clipper, fullname):
|
||||||
|
if fullname is None:
|
||||||
|
fullname = ""
|
||||||
|
assert isinstance(clipper, str)
|
||||||
|
assert isinstance(fullname, str)
|
||||||
self.clipper = clipper
|
self.clipper = clipper
|
||||||
self.fullname = fullname
|
self.fullname = fullname
|
||||||
|
|
||||||
|
@ -37,7 +39,6 @@ def account_create(request):
|
||||||
queries['kfet'] = Account.objects
|
queries['kfet'] = Account.objects
|
||||||
queries['users_cof'] = User.objects.filter(groups=cof_members)
|
queries['users_cof'] = User.objects.filter(groups=cof_members)
|
||||||
queries['users_notcof'] = User.objects.exclude(groups=cof_members)
|
queries['users_notcof'] = User.objects.exclude(groups=cof_members)
|
||||||
|
|
||||||
for word in search_words:
|
for word in search_words:
|
||||||
queries['kfet'] = queries['kfet'].filter(
|
queries['kfet'] = queries['kfet'].filter(
|
||||||
Q(profile__user__username__icontains=word)
|
Q(profile__user__username__icontains=word)
|
||||||
|
@ -79,27 +80,54 @@ def account_create(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=word)
|
||||||
for bit in search_words]
|
for word in search_words if word.isalnum()
|
||||||
))
|
))
|
||||||
with Connection(settings.LDAP_SERVER_URL) as conn:
|
if ldap_query != "(&)":
|
||||||
conn.search(
|
# If none of the bits were legal, we do not perform the query
|
||||||
'dc=spi,dc=ens,dc=fr', ldap_query,
|
entries = None
|
||||||
attributes=['uid', 'cn']
|
with Connection(settings.LDAP_SERVER_URL) as conn:
|
||||||
)
|
conn.search(
|
||||||
queries['clippers'] = conn.entries
|
'dc=spi,dc=ens,dc=fr', ldap_query,
|
||||||
# Clearing redundancies
|
attributes=['uid', 'cn']
|
||||||
queries['clippers'] = [
|
)
|
||||||
Clipper(clipper.uid, clipper.cn)
|
entries = conn.entries
|
||||||
for clipper in queries['clippers']
|
# Clearing redundancies
|
||||||
if str(clipper.uid) not in usernames
|
queries['clippers'] = [
|
||||||
]
|
Clipper(entry.uid.value, entry.cn.value)
|
||||||
|
for entry in entries
|
||||||
|
if entry.uid.value
|
||||||
|
and entry.uid.value not in usernames
|
||||||
|
]
|
||||||
|
|
||||||
# Resulting data
|
# Resulting data
|
||||||
data.update(queries)
|
data.update(queries)
|
||||||
data['options'] = any(queries.values())
|
data['options'] = any(queries.values())
|
||||||
|
|
||||||
return render(request, "kfet/account_create_autocomplete.html", data)
|
return render(request, "kfet/account_create_autocomplete.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@teamkfet_required
|
||||||
|
def account_search(request):
|
||||||
|
if "q" not in request.GET:
|
||||||
|
raise Http404
|
||||||
|
q = request.GET.get("q")
|
||||||
|
words = q.split()
|
||||||
|
|
||||||
|
data = {'q': q}
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
query = Account.objects.filter(
|
||||||
|
Q(cofprofile__user__username__icontains=word) |
|
||||||
|
Q(cofprofile__user__first_name__icontains=word) |
|
||||||
|
Q(cofprofile__user__last_name__icontains=word)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
query = [(account.trigramme, account.cofprofile.user.get_full_name())
|
||||||
|
for account in query]
|
||||||
|
|
||||||
|
data['accounts'] = query
|
||||||
|
return render(request, 'kfet/account_search_autocomplete.html', data)
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User, Permission
|
|
||||||
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
|
|
||||||
|
|
||||||
password_sha256 = hashlib.sha256(password.encode('utf-8')).hexdigest()
|
|
||||||
try:
|
|
||||||
account = Account.objects.get(password=password_sha256)
|
|
||||||
user = account.profile.user
|
|
||||||
except Account.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
class GenericTeamBackend(object):
|
|
||||||
"""
|
|
||||||
Authenticate using the generic_team user.
|
|
||||||
"""
|
|
||||||
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')
|
|
||||||
account, _ = Account.objects.get_or_create(
|
|
||||||
profile=user.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.get(pk=user_id)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
return None
|
|
1
kfet/cms/__init__.py
Normal file
1
kfet/cms/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'kfet.cms.apps.KFetCMSAppConfig'
|
10
kfet/cms/apps.py
Normal file
10
kfet/cms/apps.py
Normal 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
|
20
kfet/cms/context_processors.py
Normal file
20
kfet/cms/context_processors.py
Normal 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,
|
||||||
|
}
|
1456
kfet/cms/fixtures/kfet_wagtail_17_05.json
Normal file
1456
kfet/cms/fixtures/kfet_wagtail_17_05.json
Normal file
File diff suppressed because one or more lines are too long
12
kfet/cms/hooks.py
Normal file
12
kfet/cms/hooks.py
Normal 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'),
|
||||||
|
)
|
35
kfet/cms/management/commands/kfet_loadwagtail.py
Normal file
35
kfet/cms/management/commands/kfet_loadwagtail.py
Normal 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'])
|
49
kfet/cms/migrations/0001_initial.py
Normal file
49
kfet/cms/migrations/0001_initial.py
Normal 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')),
|
||||||
|
('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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
19
kfet/cms/migrations/0002_alter_kfetpage_colcount.py
Normal file
19
kfet/cms/migrations/0002_alter_kfetpage_colcount.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('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."),
|
||||||
|
),
|
||||||
|
]
|
0
kfet/cms/migrations/__init__.py
Normal file
0
kfet/cms/migrations/__init__.py
Normal file
174
kfet/cms/models.py
Normal file
174
kfet/cms/models.py
Normal 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
|
93
kfet/cms/static/kfetcms/css/base.css
Normal file
93
kfet/cms/static/kfetcms/css/base.css
Normal 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;
|
||||||
|
}
|
18
kfet/cms/static/kfetcms/css/editor.css
Normal file
18
kfet/cms/static/kfetcms/css/editor.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
3
kfet/cms/static/kfetcms/css/index.css
Normal file
3
kfet/cms/static/kfetcms/css/index.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@import url("base.css");
|
||||||
|
@import url("menu.css");
|
||||||
|
@import url("team.css");
|
58
kfet/cms/static/kfetcms/css/menu.css
Normal file
58
kfet/cms/static/kfetcms/css/menu.css
Normal 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;
|
||||||
|
}
|
47
kfet/cms/static/kfetcms/css/team.css
Normal file
47
kfet/cms/static/kfetcms/css/team.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
41
kfet/cms/templates/kfetcms/base.html
Normal file
41
kfet/cms/templates/kfetcms/base.html
Normal 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 %}
|
11
kfet/cms/templates/kfetcms/block_menu.html
Normal file
11
kfet/cms/templates/kfetcms/block_menu.html
Normal 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 %}
|
12
kfet/cms/templates/kfetcms/block_menu_category.html
Normal file
12
kfet/cms/templates/kfetcms/block_menu_category.html
Normal 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>
|
66
kfet/cms/templates/kfetcms/block_teamgroup.html
Normal file
66
kfet/cms/templates/kfetcms/block_teamgroup.html
Normal 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>
|
71
kfet/config.py
Normal file
71
kfet/config.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from djconfig import config
|
||||||
|
|
||||||
|
|
||||||
|
class KFetConfig(object):
|
||||||
|
"""kfet app configuration.
|
||||||
|
|
||||||
|
Enhance isolation with backend used to store config.
|
||||||
|
Usable after DjConfig middleware was called.
|
||||||
|
|
||||||
|
"""
|
||||||
|
prefix = 'kfet_'
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
if key == 'subvention_cof':
|
||||||
|
# Allows accessing to the reduction as a subvention
|
||||||
|
# Other reason: backward compatibility
|
||||||
|
reduction_mult = 1 - self.reduction_cof/100
|
||||||
|
return (1/reduction_mult - 1) * 100
|
||||||
|
return getattr(config, self._get_dj_key(key))
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
"""Get list of kfet app configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(key, value) for each configuration entry as list.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# prevent circular imports
|
||||||
|
from kfet.forms import KFetConfigForm
|
||||||
|
return [(field.label, getattr(config, name), )
|
||||||
|
for name, field in KFetConfigForm.base_fields.items()]
|
||||||
|
|
||||||
|
def _get_dj_key(self, key):
|
||||||
|
return '{}{}'.format(self.prefix, key)
|
||||||
|
|
||||||
|
def set(self, **kwargs):
|
||||||
|
"""Update configuration value(s).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Keyword arguments. Keys must be in kfet config.
|
||||||
|
Config entries are updated to given values.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# prevent circular imports
|
||||||
|
from kfet.forms import KFetConfigForm
|
||||||
|
|
||||||
|
# get old config
|
||||||
|
new_cfg = KFetConfigForm().initial
|
||||||
|
# update to new config
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
dj_key = self._get_dj_key(key)
|
||||||
|
if isinstance(value, models.Model):
|
||||||
|
new_cfg[dj_key] = value.pk
|
||||||
|
else:
|
||||||
|
new_cfg[dj_key] = value
|
||||||
|
# save new config
|
||||||
|
cfg_form = KFetConfigForm(new_cfg)
|
||||||
|
if cfg_form.is_valid():
|
||||||
|
cfg_form.save()
|
||||||
|
else:
|
||||||
|
raise ValidationError(
|
||||||
|
'Invalid values in kfet_config.set: %(fields)s',
|
||||||
|
params={'fields': list(cfg_form.errors)})
|
||||||
|
|
||||||
|
|
||||||
|
kfet_config = KFetConfig()
|
|
@ -1,26 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import (absolute_import, division,
|
from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
|
||||||
print_function, unicode_literals)
|
|
||||||
from builtins import *
|
|
||||||
|
|
||||||
from channels import Group
|
|
||||||
from channels.generic.websockets import JsonWebsocketConsumer
|
|
||||||
|
|
||||||
class KPsul(JsonWebsocketConsumer):
|
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):
|
||||||
|
groups = ['kfet.kpsul']
|
||||||
# Set to True if you want them, else leave out
|
perms_connect = ['kfet.is_team']
|
||||||
strict_ordering = False
|
|
||||||
slight_ordering = False
|
|
||||||
|
|
||||||
def connection_groups(self, **kwargs):
|
|
||||||
return ['kfet.kpsul']
|
|
||||||
|
|
||||||
def connect(self, message, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def receive(self, content, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def disconnect(self, message, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import (absolute_import, division,
|
from kfet.config import kfet_config
|
||||||
print_function, unicode_literals)
|
|
||||||
from builtins import *
|
|
||||||
|
|
||||||
from django.contrib.auth.context_processors import PermWrapper
|
|
||||||
|
|
||||||
def auth(request):
|
def config(request):
|
||||||
if hasattr(request, 'real_user'):
|
return {'kfet_config': kfet_config}
|
||||||
return {
|
|
||||||
'user': request.real_user,
|
|
||||||
'perms': PermWrapper(request.real_user),
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import (absolute_import, division,
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
print_function, unicode_literals)
|
|
||||||
from builtins import *
|
|
||||||
|
|
||||||
from django_cas_ng.decorators import user_passes_test
|
|
||||||
|
|
||||||
def kfet_is_team(user):
|
def kfet_is_team(user):
|
||||||
return user.has_perm('kfet.is_team')
|
return user.has_perm('kfet.is_team')
|
||||||
|
|
||||||
teamkfet_required = user_passes_test(lambda u: kfet_is_team(u))
|
teamkfet_required = user_passes_test(kfet_is_team)
|
||||||
|
|
200
kfet/forms.py
200
kfet/forms.py
|
@ -1,40 +1,39 @@
|
||||||
# -*- coding: utf-8 -*-
|
from datetime import timedelta
|
||||||
|
|
||||||
from __future__ import (absolute_import, division,
|
|
||||||
print_function, unicode_literals)
|
|
||||||
from builtins import *
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from djconfig.forms import ConfigForm
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinLengthValidator
|
from django.forms import modelformset_factory
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.forms import modelformset_factory, inlineformset_factory
|
|
||||||
from django.forms.models import BaseInlineFormSet
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from kfet.models import (Account, Checkout, Article, OperationGroup, Operation,
|
|
||||||
CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer,
|
|
||||||
TransferGroup, Supplier, Inventory, InventoryArticle)
|
|
||||||
from gestion.models import Profile
|
from gestion.models import Profile
|
||||||
|
|
||||||
|
from .auth.forms import UserGroupForm # noqa
|
||||||
|
from .models import (
|
||||||
|
Account, Checkout, Article, OperationGroup, Operation,
|
||||||
|
CheckoutStatement, ArticleCategory, AccountNegative, Transfer,
|
||||||
|
Supplier, TransferGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# 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
|
||||||
# -----
|
# -----
|
||||||
|
@ -78,8 +77,11 @@ class AccountRestrictForm(AccountForm):
|
||||||
|
|
||||||
class AccountPwdForm(forms.Form):
|
class AccountPwdForm(forms.Form):
|
||||||
pwd1 = forms.CharField(
|
pwd1 = forms.CharField(
|
||||||
|
label="Mot de passe K-Fêt",
|
||||||
|
help_text="Le mot de passe doit contenir au moins huit caractères",
|
||||||
widget=forms.PasswordInput)
|
widget=forms.PasswordInput)
|
||||||
pwd2 = forms.CharField(
|
pwd2 = forms.CharField(
|
||||||
|
label="Confirmer le mot de passe",
|
||||||
widget=forms.PasswordInput)
|
widget=forms.PasswordInput)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -108,21 +110,16 @@ class ProfileRestrictForm(ProfileForm):
|
||||||
class Meta(ProfileForm.Meta):
|
class Meta(ProfileForm.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']
|
||||||
|
@ -131,25 +128,6 @@ class UserRestrictTeamForm(UserForm):
|
||||||
class Meta(UserForm.Meta):
|
class Meta(UserForm.Meta):
|
||||||
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'))
|
|
||||||
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:
|
||||||
|
@ -226,22 +204,36 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
|
||||||
model = CheckoutStatement
|
model = CheckoutStatement
|
||||||
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
|
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
|
||||||
|
|
||||||
|
|
||||||
|
# -----
|
||||||
|
# Category
|
||||||
|
# -----
|
||||||
|
|
||||||
|
class CategoryForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ArticleCategory
|
||||||
|
fields = ['name', 'has_addcost']
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Article forms
|
# Article forms
|
||||||
# -----
|
# -----
|
||||||
|
|
||||||
class ArticleForm(forms.ModelForm):
|
class ArticleForm(forms.ModelForm):
|
||||||
category_new = forms.CharField(
|
category_new = forms.CharField(
|
||||||
|
label="Créer une catégorie",
|
||||||
max_length=45,
|
max_length=45,
|
||||||
required = False)
|
required = False)
|
||||||
category = forms.ModelChoiceField(
|
category = forms.ModelChoiceField(
|
||||||
|
label="Catégorie",
|
||||||
queryset = ArticleCategory.objects.all(),
|
queryset = ArticleCategory.objects.all(),
|
||||||
required = False)
|
required = False)
|
||||||
|
|
||||||
suppliers = forms.ModelMultipleChoiceField(
|
suppliers = forms.ModelMultipleChoiceField(
|
||||||
|
label="Fournisseurs",
|
||||||
queryset = Supplier.objects.all(),
|
queryset = Supplier.objects.all(),
|
||||||
required = False)
|
required = False)
|
||||||
supplier_new = forms.CharField(
|
supplier_new = forms.CharField(
|
||||||
|
label="Créer un fournisseur",
|
||||||
max_length = 45,
|
max_length = 45,
|
||||||
required = False)
|
required = False)
|
||||||
|
|
||||||
|
@ -264,12 +256,12 @@ class ArticleForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Article
|
model = Article
|
||||||
fields = ['name', 'is_sold', 'price', 'stock', 'category', 'box_type',
|
fields = ['name', 'is_sold', 'hidden', 'price', 'stock', 'category', 'box_type',
|
||||||
'box_capacity']
|
'box_capacity']
|
||||||
|
|
||||||
class ArticleRestrictForm(ArticleForm):
|
class ArticleRestrictForm(ArticleForm):
|
||||||
class Meta(ArticleForm.Meta):
|
class Meta(ArticleForm.Meta):
|
||||||
fields = ['name', 'is_sold', 'price', 'category', 'box_type',
|
fields = ['name', 'is_sold', 'hidden', 'price', 'category', 'box_type',
|
||||||
'box_capacity']
|
'box_capacity']
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
|
@ -301,12 +293,20 @@ class KPsulAccountForm(forms.ModelForm):
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class KPsulCheckoutForm(forms.Form):
|
class KPsulCheckoutForm(forms.Form):
|
||||||
checkout = forms.ModelChoiceField(
|
checkout = forms.ModelChoiceField(
|
||||||
queryset=Checkout.objects.filter(
|
queryset=(
|
||||||
is_protected=False, valid_from__lte=timezone.now(),
|
Checkout.objects
|
||||||
valid_to__gte=timezone.now()),
|
.filter(
|
||||||
widget=forms.Select(attrs={'id':'id_checkout_select'}))
|
is_protected=False,
|
||||||
|
valid_from__lte=timezone.now(),
|
||||||
|
valid_to__gte=timezone.now(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
widget=forms.Select(attrs={'id': 'id_checkout_select'}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class KPsulOperationForm(forms.ModelForm):
|
class KPsulOperationForm(forms.ModelForm):
|
||||||
article = forms.ModelChoiceField(
|
article = forms.ModelChoiceField(
|
||||||
|
@ -315,11 +315,10 @@ class KPsulOperationForm(forms.ModelForm):
|
||||||
widget = forms.HiddenInput())
|
widget = forms.HiddenInput())
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Operation
|
model = Operation
|
||||||
fields = ['type', 'amount', 'is_checkout', 'article', 'article_nb']
|
fields = ['type', 'amount', 'article', 'article_nb']
|
||||||
widgets = {
|
widgets = {
|
||||||
'type': forms.HiddenInput(),
|
'type': forms.HiddenInput(),
|
||||||
'amount': forms.HiddenInput(),
|
'amount': forms.HiddenInput(),
|
||||||
'is_checkout': forms.HiddenInput(),
|
|
||||||
'article_nb': forms.HiddenInput(),
|
'article_nb': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,7 +334,6 @@ class KPsulOperationForm(forms.ModelForm):
|
||||||
"Un achat nécessite un article et une quantité")
|
"Un achat nécessite un article et une quantité")
|
||||||
if article_nb < 1:
|
if article_nb < 1:
|
||||||
raise ValidationError("Impossible d'acheter moins de 1 article")
|
raise ValidationError("Impossible d'acheter moins de 1 article")
|
||||||
self.cleaned_data['is_checkout'] = True
|
|
||||||
elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]:
|
elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]:
|
||||||
if not amount or article or article_nb:
|
if not amount or article or article_nb:
|
||||||
raise ValidationError("Bad request")
|
raise ValidationError("Bad request")
|
||||||
|
@ -370,44 +368,53 @@ class AddcostForm(forms.Form):
|
||||||
self.cleaned_data['amount'] = 0
|
self.cleaned_data['amount'] = 0
|
||||||
super(AddcostForm, self).clean()
|
super(AddcostForm, self).clean()
|
||||||
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Settings forms
|
# Settings forms
|
||||||
# -----
|
# -----
|
||||||
|
|
||||||
class SettingsForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Settings
|
|
||||||
fields = ['value_decimal', 'value_account', 'value_duration']
|
|
||||||
|
|
||||||
def clean(self):
|
class KFetConfigForm(ConfigForm):
|
||||||
name = self.instance.name
|
|
||||||
value_decimal = self.cleaned_data.get('value_decimal')
|
|
||||||
value_account = self.cleaned_data.get('value_account')
|
|
||||||
value_duration = self.cleaned_data.get('value_duration')
|
|
||||||
|
|
||||||
type_decimal = ['SUBVENTION_COF', 'ADDCOST_AMOUNT', 'OVERDRAFT_AMOUNT']
|
kfet_reduction_cof = forms.DecimalField(
|
||||||
type_account = ['ADDCOST_FOR']
|
label='Réduction COF', initial=Decimal('20'),
|
||||||
type_duration = ['OVERDRAFT_DURATION', 'CANCEL_DURATION']
|
max_digits=6, decimal_places=2,
|
||||||
|
help_text="Réduction, à donner en pourcentage, appliquée lors d'un "
|
||||||
|
"achat par un-e membre du COF sur le montant en euros.",
|
||||||
|
)
|
||||||
|
kfet_addcost_amount = forms.DecimalField(
|
||||||
|
label='Montant de la majoration (en €)', initial=Decimal('0'),
|
||||||
|
required=False,
|
||||||
|
max_digits=6, decimal_places=2,
|
||||||
|
)
|
||||||
|
kfet_addcost_for = forms.ModelChoiceField(
|
||||||
|
label='Destinataire de la majoration', initial=None, required=False,
|
||||||
|
help_text='Laissez vide pour désactiver la majoration.',
|
||||||
|
queryset=(Account.objects
|
||||||
|
.select_related('cofprofile', 'cofprofile__user')
|
||||||
|
.all()),
|
||||||
|
)
|
||||||
|
kfet_overdraft_duration = forms.DurationField(
|
||||||
|
label='Durée du découvert autorisé par défaut',
|
||||||
|
initial=timedelta(days=1),
|
||||||
|
)
|
||||||
|
kfet_overdraft_amount = forms.DecimalField(
|
||||||
|
label='Montant du découvert autorisé par défaut (en €)',
|
||||||
|
initial=Decimal('20'),
|
||||||
|
max_digits=6, decimal_places=2,
|
||||||
|
)
|
||||||
|
kfet_cancel_duration = forms.DurationField(
|
||||||
|
label='Durée pour annuler une commande sans mot de passe',
|
||||||
|
initial=timedelta(minutes=5),
|
||||||
|
)
|
||||||
|
|
||||||
self.cleaned_data['name'] = name
|
|
||||||
if name in type_decimal:
|
|
||||||
if not value_decimal:
|
|
||||||
raise ValidationError('Renseignez une valeur décimale')
|
|
||||||
self.cleaned_data['value_account'] = None
|
|
||||||
self.cleaned_data['value_duration'] = None
|
|
||||||
elif name in type_account:
|
|
||||||
self.cleaned_data['value_decimal'] = None
|
|
||||||
self.cleaned_data['value_duration'] = None
|
|
||||||
elif name in type_duration:
|
|
||||||
if not value_duration:
|
|
||||||
raise ValidationError('Renseignez une durée')
|
|
||||||
self.cleaned_data['value_decimal'] = None
|
|
||||||
self.cleaned_data['value_account'] = None
|
|
||||||
super(SettingsForm, self).clean()
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -454,7 +461,7 @@ class InventoryArticleForm(forms.Form):
|
||||||
queryset = Article.objects.all(),
|
queryset = Article.objects.all(),
|
||||||
widget = forms.HiddenInput(),
|
widget = forms.HiddenInput(),
|
||||||
)
|
)
|
||||||
stock_new = forms.IntegerField(required = False)
|
stock_new = forms.IntegerField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(InventoryArticleForm, self).__init__(*args, **kwargs)
|
super(InventoryArticleForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -463,17 +470,20 @@ class InventoryArticleForm(forms.Form):
|
||||||
self.stock_old = kwargs['initial']['stock_old']
|
self.stock_old = kwargs['initial']['stock_old']
|
||||||
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']
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Order forms
|
# Order forms
|
||||||
# -----
|
# -----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OrderArticleForm(forms.Form):
|
class OrderArticleForm(forms.Form):
|
||||||
article = forms.ModelChoiceField(
|
article = forms.ModelChoiceField(
|
||||||
queryset = Article.objects.all(),
|
queryset=Article.objects.all(),
|
||||||
widget = forms.HiddenInput(),
|
widget=forms.HiddenInput(),
|
||||||
)
|
)
|
||||||
quantity_ordered = forms.IntegerField(required = False)
|
quantity_ordered = forms.IntegerField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(OrderArticleForm, self).__init__(*args, **kwargs)
|
super(OrderArticleForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -483,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']
|
||||||
|
|
219
kfet/management/commands/createopes.py
Normal file
219
kfet/management/commands/createopes.py
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
|
||||||
|
"""
|
||||||
|
Crée des opérations aléatoires réparties sur une période de temps spécifiée
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from kfet.models import (Account, Article, OperationGroup, Operation,
|
||||||
|
Checkout, Transfer, TransferGroup)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = ("Crée des opérations réparties uniformément "
|
||||||
|
"sur une période de temps")
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
# Nombre d'opérations à créer
|
||||||
|
parser.add_argument('opes', type=int,
|
||||||
|
help='Number of opegroups to create')
|
||||||
|
|
||||||
|
# Période sur laquelle créer (depuis num_days avant maintenant)
|
||||||
|
parser.add_argument('days', type=int,
|
||||||
|
help='Period in which to create opegroups')
|
||||||
|
|
||||||
|
# Optionnel : nombre de transfert à créer (défaut 0)
|
||||||
|
parser.add_argument('--transfers', type=int, default=0,
|
||||||
|
help='Number of transfers to create (default 0)')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
self.stdout.write("Génération d'opérations")
|
||||||
|
|
||||||
|
# Output log vars
|
||||||
|
opes_created = 0
|
||||||
|
purchases = 0
|
||||||
|
transfers = 0
|
||||||
|
|
||||||
|
num_ops = options['opes']
|
||||||
|
num_transfers = options['transfers']
|
||||||
|
# Convert to seconds
|
||||||
|
time = options['days'] * 24 * 3600
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
checkout = Checkout.objects.first()
|
||||||
|
articles = Article.objects.all()
|
||||||
|
accounts = Account.objects.exclude(trigramme='LIQ')
|
||||||
|
liq_account = Account.objects.get(trigramme='LIQ')
|
||||||
|
try:
|
||||||
|
con_account = Account.objects.get(
|
||||||
|
cofprofile__user__first_name='Assurancetourix'
|
||||||
|
)
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
con_account = random.choice(accounts)
|
||||||
|
|
||||||
|
# use to fetch OperationGroup pk created by bulk_create
|
||||||
|
at_list = []
|
||||||
|
# use to lazy set OperationGroup pk on Operation objects
|
||||||
|
ope_by_grp = []
|
||||||
|
# OperationGroup objects to bulk_create
|
||||||
|
opegroup_list = []
|
||||||
|
|
||||||
|
for i in range(num_ops):
|
||||||
|
|
||||||
|
# Randomly pick account
|
||||||
|
if random.random() > 0.25:
|
||||||
|
account = random.choice(accounts)
|
||||||
|
else:
|
||||||
|
account = liq_account
|
||||||
|
|
||||||
|
# Randomly pick time
|
||||||
|
at = now - timedelta(seconds=random.randint(0, time))
|
||||||
|
|
||||||
|
# Majoration sur compte 'concert'
|
||||||
|
if random.random() < 0.2:
|
||||||
|
addcost = True
|
||||||
|
addcost_for = con_account
|
||||||
|
addcost_amount = Decimal('0.5')
|
||||||
|
else:
|
||||||
|
addcost = False
|
||||||
|
|
||||||
|
# Initialize opegroup amount
|
||||||
|
amount = Decimal('0')
|
||||||
|
|
||||||
|
# Generating operations
|
||||||
|
ope_list = []
|
||||||
|
for j in range(random.randint(1, 4)):
|
||||||
|
# Operation type
|
||||||
|
typevar = random.random()
|
||||||
|
|
||||||
|
# 0.1 probability to have a charge
|
||||||
|
if typevar > 0.9 and account != liq_account:
|
||||||
|
ope = Operation(
|
||||||
|
type=Operation.DEPOSIT,
|
||||||
|
amount=Decimal(random.randint(1, 99)/10)
|
||||||
|
)
|
||||||
|
# 0.05 probability to have a withdrawal
|
||||||
|
elif typevar > 0.85 and account != liq_account:
|
||||||
|
ope = Operation(
|
||||||
|
type=Operation.WITHDRAW,
|
||||||
|
amount=-Decimal(random.randint(1, 99)/10)
|
||||||
|
)
|
||||||
|
# 0.05 probability to have an edition
|
||||||
|
elif typevar > 0.8 and account != liq_account:
|
||||||
|
ope = Operation(
|
||||||
|
type=Operation.EDIT,
|
||||||
|
amount=Decimal(random.randint(1, 99)/10)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
article = random.choice(articles)
|
||||||
|
nb = random.randint(1, 5)
|
||||||
|
|
||||||
|
ope = Operation(
|
||||||
|
type=Operation.PURCHASE,
|
||||||
|
amount=-article.price*nb,
|
||||||
|
article=article,
|
||||||
|
article_nb=nb
|
||||||
|
)
|
||||||
|
|
||||||
|
purchases += 1
|
||||||
|
|
||||||
|
if addcost:
|
||||||
|
ope.addcost_for = addcost_for
|
||||||
|
ope.addcost_amount = addcost_amount * nb
|
||||||
|
ope.amount -= ope.addcost_amount
|
||||||
|
|
||||||
|
ope_list.append(ope)
|
||||||
|
amount += ope.amount
|
||||||
|
|
||||||
|
opegroup_list.append(OperationGroup(
|
||||||
|
on_acc=account,
|
||||||
|
checkout=checkout,
|
||||||
|
at=at,
|
||||||
|
is_cof=account.cofprofile.is_cof,
|
||||||
|
amount=amount,
|
||||||
|
))
|
||||||
|
at_list.append(at)
|
||||||
|
ope_by_grp.append((at, ope_list, ))
|
||||||
|
|
||||||
|
OperationGroup.objects.bulk_create(opegroup_list)
|
||||||
|
|
||||||
|
# Fetch created OperationGroup objects pk by at
|
||||||
|
opegroups = (OperationGroup.objects
|
||||||
|
.filter(at__in=at_list)
|
||||||
|
.values('id', 'at'))
|
||||||
|
opegroups_by = {grp['at']: grp['id'] for grp in opegroups}
|
||||||
|
|
||||||
|
all_ope = []
|
||||||
|
for _ in range(num_ops):
|
||||||
|
at, ope_list = ope_by_grp.pop()
|
||||||
|
for ope in ope_list:
|
||||||
|
ope.group_id = opegroups_by[at]
|
||||||
|
all_ope.append(ope)
|
||||||
|
|
||||||
|
Operation.objects.bulk_create(all_ope)
|
||||||
|
opes_created = len(all_ope)
|
||||||
|
|
||||||
|
# Transfer generation
|
||||||
|
|
||||||
|
transfer_by_grp = []
|
||||||
|
transfergroup_list = []
|
||||||
|
at_list = []
|
||||||
|
|
||||||
|
for i in range(num_transfers):
|
||||||
|
|
||||||
|
# Randomly pick time
|
||||||
|
at = now - timedelta(seconds=random.randint(0, time))
|
||||||
|
|
||||||
|
# Choose whether to have a comment
|
||||||
|
if random.random() > 0.5:
|
||||||
|
comment = "placeholder comment"
|
||||||
|
else:
|
||||||
|
comment = ""
|
||||||
|
|
||||||
|
transfergroup_list.append(TransferGroup(
|
||||||
|
at=at,
|
||||||
|
comment=comment,
|
||||||
|
valid_by=random.choice(accounts),
|
||||||
|
))
|
||||||
|
at_list.append(at)
|
||||||
|
|
||||||
|
# Randomly generate transfer
|
||||||
|
transfer_list = []
|
||||||
|
for i in range(random.randint(1, 4)):
|
||||||
|
transfer_list.append(Transfer(
|
||||||
|
from_acc=random.choice(accounts),
|
||||||
|
to_acc=random.choice(accounts),
|
||||||
|
amount=Decimal(random.randint(1, 99)/10)
|
||||||
|
))
|
||||||
|
|
||||||
|
transfer_by_grp.append((at, transfer_list, ))
|
||||||
|
|
||||||
|
TransferGroup.objects.bulk_create(transfergroup_list)
|
||||||
|
|
||||||
|
transfergroups = (TransferGroup.objects
|
||||||
|
.filter(at__in=at_list)
|
||||||
|
.values('id', 'at'))
|
||||||
|
transfergroups_by = {grp['at']: grp['id'] for grp in transfergroups}
|
||||||
|
|
||||||
|
all_transfer = []
|
||||||
|
for _ in range(num_transfers):
|
||||||
|
at, transfer_list = transfer_by_grp.pop()
|
||||||
|
for transfer in transfer_list:
|
||||||
|
transfer.group_id = transfergroups_by[at]
|
||||||
|
all_transfer.append(transfer)
|
||||||
|
|
||||||
|
Transfer.objects.bulk_create(all_transfer)
|
||||||
|
transfers += len(all_transfer)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
"- {:d} opérations créées dont {:d} commandes d'articles"
|
||||||
|
.format(opes_created, purchases))
|
||||||
|
|
||||||
|
if transfers:
|
||||||
|
self.stdout.write("- {:d} transferts créés"
|
||||||
|
.format(transfers))
|
|
@ -5,14 +5,18 @@ Crée des utilisateurs, des articles et des opérations aléatoires
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.auth.models import User, Group, Permission, ContentType
|
from django.contrib.auth.models import User, Group, Permission, ContentType
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
from cof.management.base import MyBaseCommand
|
from cof.management.base import MyBaseCommand
|
||||||
from gestion.models import Profile
|
from gestion.models import Profile
|
||||||
from kfet.models import Account, Article, OperationGroup, Operation, Checkout
|
from kfet.models import Account, Article, OperationGroup, Operation, Checkout
|
||||||
|
from gestioncof.management.base import MyBaseCommand
|
||||||
|
from gestioncof.models import CofProfile
|
||||||
|
from kfet.models import (Account, Checkout, CheckoutStatement, Supplier,
|
||||||
|
SupplierArticle, Article)
|
||||||
|
|
||||||
# Où sont stockés les fichiers json
|
# Où sont stockés les fichiers json
|
||||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
@ -87,84 +91,68 @@ class Command(MyBaseCommand):
|
||||||
|
|
||||||
# Compte liquide
|
# Compte liquide
|
||||||
|
|
||||||
|
self.stdout.write("Création du compte liquide")
|
||||||
liq_user, _ = User.objects.get_or_create(username='liquide')
|
liq_user, _ = User.objects.get_or_create(username='liquide')
|
||||||
liq_profile, _ = Profile.objects.get_or_create(user=liq_user)
|
liq_profile, _ = Profile.objects.get_or_create(user=liq_user)
|
||||||
liq_account, _ = Account.objects.get_or_create(profile=liq_profile,
|
liq_account, _ = Account.objects.get_or_create(profile=liq_profile,
|
||||||
trigramme='LIQ')
|
trigramme='LIQ')
|
||||||
|
|
||||||
|
# Root account if existing
|
||||||
|
|
||||||
|
root_profile = CofProfile.objects.filter(user__username='root')
|
||||||
|
if root_profile.exists():
|
||||||
|
self.stdout.write("Création du compte K-Fêt root")
|
||||||
|
root_profile = root_profile.get()
|
||||||
|
Account.objects.get_or_create(cofprofile=root_profile,
|
||||||
|
trigramme='AAA')
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Caisse
|
# Caisse
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
checkout, _ = Checkout.objects.get_or_create(
|
checkout, created = Checkout.objects.get_or_create(
|
||||||
created_by=Account.objects.get(trigramme='000'),
|
created_by=Account.objects.get(trigramme='000'),
|
||||||
name='Chaudron',
|
name='Chaudron',
|
||||||
valid_from=timezone.now(),
|
defaults={
|
||||||
valid_to=timezone.now() + timedelta(days=365)
|
'valid_from': timezone.now(),
|
||||||
|
'valid_to': timezone.now() + timedelta(days=730)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
CheckoutStatement.objects.create(
|
||||||
|
by=Account.objects.get(trigramme='000'),
|
||||||
|
checkout=checkout,
|
||||||
|
balance_old=0,
|
||||||
|
balance_new=0,
|
||||||
|
amount_taken=0,
|
||||||
|
amount_error=0
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Fournisseur
|
||||||
|
# ---
|
||||||
|
|
||||||
|
supplier, created = Supplier.objects.get_or_create(name="Panoramix")
|
||||||
|
if created:
|
||||||
|
articles = random.sample(list(Article.objects.all()), 40)
|
||||||
|
to_create = []
|
||||||
|
for article in articles:
|
||||||
|
to_create.append(SupplierArticle(
|
||||||
|
supplier=supplier,
|
||||||
|
article=article
|
||||||
|
))
|
||||||
|
|
||||||
|
SupplierArticle.objects.bulk_create(to_create)
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Opérations
|
# Opérations
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
self.stdout.write("Génération d'opérations")
|
call_command('createopes', '100', '7', '--transfers=20')
|
||||||
|
|
||||||
articles = Article.objects.all()
|
# ---
|
||||||
accounts = Account.objects.exclude(trigramme='LIQ')
|
# Wagtail CMS
|
||||||
|
# ---
|
||||||
|
|
||||||
num_op = 100
|
call_command('kfet_loadwagtail')
|
||||||
# Operations are put uniformly over the span of a week
|
|
||||||
past_date = 3600*24*7
|
|
||||||
|
|
||||||
for i in range(num_op):
|
|
||||||
if random.random() > 0.25:
|
|
||||||
account = random.choice(accounts)
|
|
||||||
else:
|
|
||||||
account = liq_account
|
|
||||||
|
|
||||||
amount = Decimal('0')
|
|
||||||
at = timezone.now() - timedelta(
|
|
||||||
seconds=random.randint(0, past_date))
|
|
||||||
|
|
||||||
opegroup = OperationGroup(
|
|
||||||
on_acc=account,
|
|
||||||
checkout=checkout,
|
|
||||||
at=at,
|
|
||||||
is_cof=False
|
|
||||||
)
|
|
||||||
if hasattr(account.profile, "cof"):
|
|
||||||
opegroup.is_cof = account.profile.cof.is_cof
|
|
||||||
opegroup.save()
|
|
||||||
opegroup.save()
|
|
||||||
|
|
||||||
for j in range(random.randint(1, 4)):
|
|
||||||
typevar = random.random()
|
|
||||||
if typevar > 0.9 and account != liq_account:
|
|
||||||
ope = Operation(
|
|
||||||
group=opegroup,
|
|
||||||
type=Operation.DEPOSIT,
|
|
||||||
amount=Decimal(random.randint(1, 99)/10,)
|
|
||||||
)
|
|
||||||
elif typevar > 0.8 and account != liq_account:
|
|
||||||
ope = Operation(
|
|
||||||
group=opegroup,
|
|
||||||
type=Operation.WITHDRAW,
|
|
||||||
amount=-Decimal(random.randint(1, 99)/10,)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
article = random.choice(articles)
|
|
||||||
nb = random.randint(1, 5)
|
|
||||||
|
|
||||||
ope = Operation(
|
|
||||||
group=opegroup,
|
|
||||||
type=Operation.PURCHASE,
|
|
||||||
amount=-article.price*nb,
|
|
||||||
article=article,
|
|
||||||
article_nb=nb
|
|
||||||
)
|
|
||||||
|
|
||||||
ope.save()
|
|
||||||
amount += ope.amount
|
|
||||||
|
|
||||||
opegroup.amount = amount
|
|
||||||
opegroup.save()
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from kfet.backends import KFetBackend
|
|
||||||
|
|
||||||
|
|
||||||
def kfet_auth_middleware(get_response):
|
|
||||||
kfet_backend = KFetBackend()
|
|
||||||
|
|
||||||
def middleware(request):
|
|
||||||
temp_request_user = kfet_backend.authenticate(request)
|
|
||||||
if temp_request_user:
|
|
||||||
request.real_user = request.user
|
|
||||||
request.user = temp_request_user
|
|
||||||
return get_response(request)
|
|
||||||
|
|
||||||
return middleware
|
|
19
kfet/migrations/0048_article_hidden.py
Normal file
19
kfet/migrations/0048_article_hidden.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0047_auto_20170104_1528'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='article',
|
||||||
|
name='hidden',
|
||||||
|
field=models.BooleanField(help_text='Si oui, ne sera pas affiché au public ; par exemple sur la carte.', default=False),
|
||||||
|
),
|
||||||
|
]
|
25
kfet/migrations/0048_default_datetime.py
Normal file
25
kfet/migrations/0048_default_datetime.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0047_auto_20170104_1528'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='operationgroup',
|
||||||
|
name='at',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transfergroup',
|
||||||
|
name='at',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
]
|
15
kfet/migrations/0049_merge.py
Normal file
15
kfet/migrations/0049_merge.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0048_article_hidden'),
|
||||||
|
('kfet', '0048_default_datetime'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
38
kfet/migrations/0050_remove_checkout.py
Normal file
38
kfet/migrations/0050_remove_checkout.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def adapt_operation_types(apps, schema_editor):
|
||||||
|
Operation = apps.get_model("kfet", "Operation")
|
||||||
|
Operation.objects.filter(
|
||||||
|
is_checkout=False,
|
||||||
|
type__in=['withdraw', 'deposit']).update(type='edit')
|
||||||
|
|
||||||
|
|
||||||
|
def revert_operation_types(apps, schema_editor):
|
||||||
|
Operation = apps.get_model("kfet", "Operation")
|
||||||
|
edits = Operation.objects.filter(type='edit')
|
||||||
|
edits.filter(amount__gt=0).update(type='deposit')
|
||||||
|
edits.filter(amount__lte=0).update(type='withdraw')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0049_merge'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='operation',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('purchase', 'Achat'), ('deposit', 'Charge'), ('withdraw', 'Retrait'), ('initial', 'Initial'), ('edit', 'Édition')], max_length=8),
|
||||||
|
),
|
||||||
|
migrations.RunPython(adapt_operation_types, revert_operation_types),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='operation',
|
||||||
|
name='is_checkout',
|
||||||
|
),
|
||||||
|
]
|
210
kfet/migrations/0051_verbose_names.py
Normal file
210
kfet/migrations/0051_verbose_names.py
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0050_remove_checkout'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='is_frozen',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='est gelé'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(default='', max_length=255, verbose_name='surnom(s)', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountnegative',
|
||||||
|
name='authz_overdraft_amount',
|
||||||
|
field=models.DecimalField(max_digits=6, blank=True, default=None, null=True, verbose_name='négatif autorisé', decimal_places=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountnegative',
|
||||||
|
name='authz_overdraft_until',
|
||||||
|
field=models.DateTimeField(default=None, null=True, verbose_name='expiration du négatif', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountnegative',
|
||||||
|
name='balance_offset',
|
||||||
|
field=models.DecimalField(blank=True, max_digits=6, help_text="Montant non compris dans l'autorisation de négatif", default=None, null=True, verbose_name='décalage de balance', decimal_places=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountnegative',
|
||||||
|
name='comment',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='commentaire'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='box_capacity',
|
||||||
|
field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='capacité du contenant', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='box_type',
|
||||||
|
field=models.CharField(blank=True, max_length=7, choices=[('caisse', 'caisse'), ('carton', 'carton'), ('palette', 'palette'), ('fût', 'fût')], default=None, null=True, verbose_name='type de contenant'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(related_name='articles', to='kfet.ArticleCategory', on_delete=django.db.models.deletion.PROTECT, verbose_name='catégorie'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='hidden',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='caché', help_text='Si oui, ne sera pas affiché au public ; par exemple sur la carte.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='is_sold',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='en vente'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=45, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='price',
|
||||||
|
field=models.DecimalField(default=0, verbose_name='prix', decimal_places=2, max_digits=6),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='amount_error',
|
||||||
|
field=models.DecimalField(max_digits=6, verbose_name="montant de l'erreur", decimal_places=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='amount_taken',
|
||||||
|
field=models.DecimalField(max_digits=6, verbose_name='montant pris', decimal_places=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='balance_new',
|
||||||
|
field=models.DecimalField(max_digits=6, verbose_name='nouvelle balance', decimal_places=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='balance_old',
|
||||||
|
field=models.DecimalField(max_digits=6, verbose_name='ancienne balance', decimal_places=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='not_count',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='caisse non comptée'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_001',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 1¢'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_002',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 2¢'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_005',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 5¢'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_01',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 10¢'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_02',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 20¢'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_05',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 50¢'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_1',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 1€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_10',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 10€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_100',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 100€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_2',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 2€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_20',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 20€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_200',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 200€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_5',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 5€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_50',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 50€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_500',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 500€'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checkoutstatement',
|
||||||
|
name='taken_cheque',
|
||||||
|
field=models.DecimalField(default=0, verbose_name='montant des chèques', decimal_places=2, max_digits=6),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='address',
|
||||||
|
field=models.TextField(verbose_name='adresse'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='comment',
|
||||||
|
field=models.TextField(verbose_name='commentaire'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(max_length=254, verbose_name='adresse mail'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=45, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='phone',
|
||||||
|
field=models.CharField(max_length=10, verbose_name='téléphone'),
|
||||||
|
),
|
||||||
|
]
|
24
kfet/migrations/0052_category_addcost.py
Normal file
24
kfet/migrations/0052_category_addcost.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0051_verbose_names'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='articlecategory',
|
||||||
|
name='has_addcost',
|
||||||
|
field=models.BooleanField(default=True, help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", verbose_name='majorée'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='articlecategory',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=45, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
]
|
20
kfet/migrations/0053_created_at.py
Normal file
20
kfet/migrations/0053_created_at.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0052_category_addcost'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
]
|
58
kfet/migrations/0054_delete_settings.py
Normal file
58
kfet/migrations/0054_delete_settings.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
from kfet.forms import KFetConfigForm
|
||||||
|
|
||||||
|
|
||||||
|
def adapt_settings(apps, schema_editor):
|
||||||
|
Settings = apps.get_model('kfet', 'Settings')
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
obj = Settings.objects.using(db_alias)
|
||||||
|
|
||||||
|
cfg = {}
|
||||||
|
|
||||||
|
def try_get(new, old, type_field):
|
||||||
|
try:
|
||||||
|
value = getattr(obj.get(name=old), type_field)
|
||||||
|
cfg[new] = value
|
||||||
|
except Settings.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
subvention = obj.get(name='SUBVENTION_COF').value_decimal
|
||||||
|
subvention_mult = 1 + subvention/100
|
||||||
|
reduction = (1 - 1/subvention_mult) * 100
|
||||||
|
cfg['kfet_reduction_cof'] = reduction
|
||||||
|
except Settings.DoesNotExist:
|
||||||
|
pass
|
||||||
|
try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal')
|
||||||
|
try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account')
|
||||||
|
try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration')
|
||||||
|
try_get('kfet_overdraft_amount', 'OVERDRAFT_AMOUNT', 'value_decimal')
|
||||||
|
try_get('kfet_cancel_duration', 'CANCEL_DURATION', 'value_duration')
|
||||||
|
|
||||||
|
cfg_form = KFetConfigForm(initial=cfg)
|
||||||
|
if cfg_form.is_valid():
|
||||||
|
cfg_form.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0053_created_at'),
|
||||||
|
('djconfig', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(adapt_settings),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='settings',
|
||||||
|
name='value_account',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Settings',
|
||||||
|
),
|
||||||
|
]
|
19
kfet/migrations/0054_update_promos.py
Normal file
19
kfet/migrations/0054_update_promos.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('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),
|
||||||
|
),
|
||||||
|
]
|
81
kfet/migrations/0055_move_permissions.py
Normal file
81
kfet/migrations/0055_move_permissions.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_perms(apps, schema_editor):
|
||||||
|
"""Safely delete content type for old kfet.GlobalPermissions model.
|
||||||
|
|
||||||
|
Any permissions (except defaults) linked to this content type are updated
|
||||||
|
to link at its new content type.
|
||||||
|
Then, delete the content type. This will delete the three defaults
|
||||||
|
permissions which are assumed unused.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'contenttype')
|
||||||
|
try:
|
||||||
|
ctype_global = ContentType.objects.get(
|
||||||
|
app_label="kfet", model="globalpermissions",
|
||||||
|
)
|
||||||
|
except ContentType.DoesNotExist:
|
||||||
|
# We are not migrating from existing data, nothing to do.
|
||||||
|
return
|
||||||
|
|
||||||
|
perms = {
|
||||||
|
'account': (
|
||||||
|
'is_team', 'manage_perms', 'manage_addcosts',
|
||||||
|
'edit_balance_account', 'change_account_password',
|
||||||
|
'special_add_account',
|
||||||
|
),
|
||||||
|
'accountnegative': ('view_negs',),
|
||||||
|
'inventory': ('order_to_inventory',),
|
||||||
|
'operation': (
|
||||||
|
'perform_deposit', 'perform_negative_operations',
|
||||||
|
'override_frozen_protection', 'cancel_old_operations',
|
||||||
|
'perform_commented_operations',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
Permission = apps.get_model('auth', 'permission')
|
||||||
|
global_perms = Permission.objects.filter(content_type=ctype_global)
|
||||||
|
|
||||||
|
for modelname, codenames in perms.items():
|
||||||
|
model = apps.get_model('kfet', modelname)
|
||||||
|
ctype = ContentType.objects.get_for_model(model)
|
||||||
|
(
|
||||||
|
global_perms
|
||||||
|
.filter(codename__in=codenames)
|
||||||
|
.update(content_type=ctype)
|
||||||
|
)
|
||||||
|
|
||||||
|
ctype_global.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0054_delete_settings'),
|
||||||
|
('contenttypes', '__latest__'),
|
||||||
|
('auth', '__latest__'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='account',
|
||||||
|
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'))},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='accountnegative',
|
||||||
|
options={'permissions': (('view_negs', 'Voir la liste des négatifs'),)},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='inventory',
|
||||||
|
options={'ordering': ['-at'], 'permissions': (('order_to_inventory', "Générer un inventaire à partir d'une commande"),)},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='operation',
|
||||||
|
options={'permissions': (('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'))},
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards_perms),
|
||||||
|
]
|
18
kfet/migrations/0056_change_account_meta.py
Normal file
18
kfet/migrations/0056_change_account_meta.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0055_move_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='account',
|
||||||
|
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'))},
|
||||||
|
),
|
||||||
|
]
|
15
kfet/migrations/0057_merge.py
Normal file
15
kfet/migrations/0057_merge.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0056_change_account_meta'),
|
||||||
|
('kfet', '0054_update_promos'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
17
kfet/migrations/0058_delete_genericteamtoken.py
Normal file
17
kfet/migrations/0058_delete_genericteamtoken.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0057_merge'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='GenericTeamToken',
|
||||||
|
),
|
||||||
|
]
|
45
kfet/migrations/0059_create_generic.py
Normal file
45
kfet/migrations/0059_create_generic.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from kfet.auth import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME
|
||||||
|
|
||||||
|
|
||||||
|
def setup_kfet_generic_user(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Setup models instances for the kfet generic account.
|
||||||
|
|
||||||
|
Username and trigramme are retrieved from kfet.auth.__init__ module.
|
||||||
|
Other data are registered here.
|
||||||
|
|
||||||
|
See also setup_kfet_generic_user from kfet.auth.utils module.
|
||||||
|
"""
|
||||||
|
User = apps.get_model('auth', 'User')
|
||||||
|
CofProfile = apps.get_model('gestioncof', 'CofProfile')
|
||||||
|
Account = apps.get_model('kfet', 'Account')
|
||||||
|
|
||||||
|
user, _ = User.objects.update_or_create(
|
||||||
|
username=KFET_GENERIC_USERNAME,
|
||||||
|
defaults={
|
||||||
|
'first_name': 'Compte générique K-Fêt',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
profile, _ = CofProfile.objects.update_or_create(user=user)
|
||||||
|
account, _ = Account.objects.update_or_create(
|
||||||
|
cofprofile=profile,
|
||||||
|
defaults={
|
||||||
|
'trigramme': KFET_GENERIC_TRIGRAMME,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0058_delete_genericteamtoken'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(setup_kfet_generic_user),
|
||||||
|
]
|
39
kfet/migrations/0060_amend_supplier.py
Normal file
39
kfet/migrations/0060_amend_supplier.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0059_create_generic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='address',
|
||||||
|
field=models.TextField(verbose_name='adresse', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='articles',
|
||||||
|
field=models.ManyToManyField(verbose_name='articles vendus', through='kfet.SupplierArticle', related_name='suppliers', to='kfet.Article'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='comment',
|
||||||
|
field=models.TextField(verbose_name='commentaire', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(max_length=254, verbose_name='adresse mail', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplier',
|
||||||
|
name='phone',
|
||||||
|
field=models.CharField(max_length=20, verbose_name='téléphone', blank=True),
|
||||||
|
),
|
||||||
|
]
|
18
kfet/migrations/0061_add_perms_config.py
Normal file
18
kfet/migrations/0061_add_perms_config.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0060_amend_supplier'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='account',
|
||||||
|
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'), ('see_config', 'Voir la configuration K-Fêt'), ('change_config', 'Modifier la configuration K-Fêt'))},
|
||||||
|
),
|
||||||
|
]
|
520
kfet/models.py
520
kfet/models.py
|
@ -1,39 +1,69 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division,
|
|
||||||
print_function, unicode_literals)
|
|
||||||
from builtins import *
|
|
||||||
from datetime import date, timedelta
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from datetime import date
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.six.moves import reduce
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.core.cache import cache
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from django.utils.six.moves import reduce
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from gestion.models import Profile
|
from gestion.models import Profile
|
||||||
|
|
||||||
|
from .auth import KFET_GENERIC_TRIGRAMME
|
||||||
|
from .auth.models import GenericTeamToken # noqa
|
||||||
|
from .config import kfet_config
|
||||||
|
from .utils import to_ukf
|
||||||
|
|
||||||
|
|
||||||
def choices_length(choices):
|
def choices_length(choices):
|
||||||
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
|
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
|
||||||
|
|
||||||
|
|
||||||
def default_promo():
|
def default_promo():
|
||||||
now = date.today()
|
now = date.today()
|
||||||
return now.month <= 8 and now.year-1 or now.year
|
return now.month <= 8 and now.year-1 or now.year
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
|
class AccountManager(models.Manager):
|
||||||
|
"""Manager for Account Model."""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Always append related data to this Account."""
|
||||||
|
return super().get_queryset().select_related('cofprofile__user',
|
||||||
|
'negative')
|
||||||
|
|
||||||
|
def get_generic(self):
|
||||||
|
"""
|
||||||
|
Get the kfet generic account instance.
|
||||||
|
"""
|
||||||
|
return self.get(trigramme=KFET_GENERIC_TRIGRAMME)
|
||||||
|
|
||||||
|
def get_by_password(self, password):
|
||||||
|
"""
|
||||||
|
Get a kfet generic account by clear password.
|
||||||
|
|
||||||
|
Raises Account.DoesNotExist if no Account has this password.
|
||||||
|
"""
|
||||||
|
from .auth.utils import hash_password
|
||||||
|
if password is None:
|
||||||
|
raise self.model.DoesNotExist
|
||||||
|
return self.get(password=hash_password(password))
|
||||||
|
|
||||||
|
|
||||||
class Account(models.Model):
|
class Account(models.Model):
|
||||||
# XXX. change this to "profile"
|
objects = AccountManager()
|
||||||
profile = models.OneToOneField(Profile,
|
|
||||||
related_name="account_kfet",
|
profile = models.OneToOneField(
|
||||||
on_delete=models.CASCADE)
|
Profile,
|
||||||
|
related_name="account_kfet",
|
||||||
|
on_delete=models.PROTECT
|
||||||
|
)
|
||||||
trigramme = models.CharField(
|
trigramme = models.CharField(
|
||||||
unique = True,
|
unique = True,
|
||||||
max_length = 3,
|
max_length = 3,
|
||||||
|
@ -42,14 +72,15 @@ class Account(models.Model):
|
||||||
balance = models.DecimalField(
|
balance = models.DecimalField(
|
||||||
max_digits = 6, decimal_places = 2,
|
max_digits = 6, decimal_places = 2,
|
||||||
default = 0)
|
default = 0)
|
||||||
is_frozen = models.BooleanField(default = False)
|
is_frozen = models.BooleanField("est gelé", default = False)
|
||||||
created_at = models.DateTimeField(auto_now_add = True, null = True)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
# Optional
|
# Optional
|
||||||
PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
|
PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
|
||||||
promo = models.IntegerField(
|
promo = models.IntegerField(
|
||||||
choices = PROMO_CHOICES,
|
choices = PROMO_CHOICES,
|
||||||
blank = True, null = True, default = default_promo())
|
blank = True, null = True, default = default_promo())
|
||||||
nickname = models.CharField(
|
nickname = models.CharField(
|
||||||
|
"surnom(s)",
|
||||||
max_length = 255,
|
max_length = 255,
|
||||||
blank = True, default = "")
|
blank = True, default = "")
|
||||||
password = models.CharField(
|
password = models.CharField(
|
||||||
|
@ -57,36 +88,62 @@ class Account(models.Model):
|
||||||
unique = True,
|
unique = True,
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('is_team', 'Is part of the team'),
|
||||||
|
('manage_perms', 'Gérer les permissions K-Fêt'),
|
||||||
|
('manage_addcosts', 'Gérer les majorations'),
|
||||||
|
('edit_balance_account', "Modifier la balance d'un compte"),
|
||||||
|
('change_account_password',
|
||||||
|
"Modifier le mot de passe d'une personne de l'équipe"),
|
||||||
|
('special_add_account',
|
||||||
|
"Créer un compte avec une balance initiale"),
|
||||||
|
('can_force_close', "Fermer manuellement la K-Fêt"),
|
||||||
|
('see_config', "Voir la configuration K-Fêt"),
|
||||||
|
('change_config', "Modifier la configuration K-Fêt"),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%s)' % (self.trigramme, self.name)
|
return '%s (%s)' % (self.trigramme, self.name)
|
||||||
|
|
||||||
# Propriétés pour accéder aux attributs de user et profile et user
|
# Propriétés pour accéder aux attributs de user et profile
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
return self.profile.user
|
return self.profile.user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def username(self):
|
def username(self):
|
||||||
return self.profile.user.username
|
return self.profile.user.username
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def first_name(self):
|
def first_name(self):
|
||||||
return self.profile.user.first_name
|
return self.profile.user.first_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_name(self):
|
def last_name(self):
|
||||||
return self.profile.user.last_name
|
return self.profile.user.last_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
return self.profile.user.email
|
return self.profile.user.email
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def departement(self):
|
def departement(self):
|
||||||
return self.profile.departement
|
return self.profile.departement
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cof(self):
|
def is_cof(self):
|
||||||
return self.profile.cof.is_cof
|
return self.profile.cof.is_cof
|
||||||
|
|
||||||
# Propriétés supplémentaires
|
# Propriétés supplémentaires
|
||||||
|
@property
|
||||||
|
def balance_ukf(self):
|
||||||
|
return to_ukf(self.balance, is_cof=self.is_cof)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def real_balance(self):
|
def real_balance(self):
|
||||||
if (hasattr(self, 'negative')):
|
if hasattr(self, 'negative') and self.negative.balance_offset:
|
||||||
return self.balance - self.negative.balance_offset
|
return self.balance - self.negative.balance_offset
|
||||||
return self.balance
|
return self.balance
|
||||||
|
|
||||||
|
@ -102,6 +159,14 @@ class Account(models.Model):
|
||||||
def need_comment(self):
|
def need_comment(self):
|
||||||
return self.trigramme == '#13'
|
return self.trigramme == '#13'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def readable(self):
|
||||||
|
return self.trigramme != 'GNR'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_team(self):
|
||||||
|
return self.has_perm('kfet.is_team')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_validandfree(trigramme):
|
def is_validandfree(trigramme):
|
||||||
data = { 'is_valid' : False, 'is_free' : False }
|
data = { 'is_valid' : False, 'is_free' : False }
|
||||||
|
@ -114,8 +179,8 @@ class Account(models.Model):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def perms_to_perform_operation(self, amount):
|
def perms_to_perform_operation(self, amount):
|
||||||
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
|
overdraft_duration_max = kfet_config.overdraft_duration
|
||||||
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
|
overdraft_amount_max = kfet_config.overdraft_amount
|
||||||
perms = set()
|
perms = set()
|
||||||
stop_ope = False
|
stop_ope = False
|
||||||
# Checking is cash account
|
# Checking is cash account
|
||||||
|
@ -157,6 +222,7 @@ class Account(models.Model):
|
||||||
# - Enregistre User, Profile à partir de "data"
|
# - Enregistre User, Profile à partir de "data"
|
||||||
# - Enregistre Account
|
# - Enregistre Account
|
||||||
def save(self, data = {}, *args, **kwargs):
|
def save(self, data = {}, *args, **kwargs):
|
||||||
|
|
||||||
if self.pk and data:
|
if self.pk and data:
|
||||||
# Account update
|
# Account update
|
||||||
|
|
||||||
|
@ -203,33 +269,89 @@ class Account(models.Model):
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
super(Account, self).save(*args, **kwargs)
|
super(Account, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def change_pwd(self, clear_password):
|
||||||
|
from .auth.utils import hash_password
|
||||||
|
self.password = hash_password(clear_password)
|
||||||
|
|
||||||
# Surcharge de delete
|
# Surcharge de delete
|
||||||
# Pas de suppression possible
|
# Pas de suppression possible
|
||||||
# Cas à régler plus tard
|
# Cas à régler plus tard
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_negative(self):
|
||||||
|
if self.real_balance < 0:
|
||||||
|
if hasattr(self, 'negative') and not self.negative.start:
|
||||||
|
self.negative.start = timezone.now()
|
||||||
|
self.negative.save()
|
||||||
|
elif not hasattr(self, 'negative'):
|
||||||
|
self.negative = (
|
||||||
|
AccountNegative.objects.create(
|
||||||
|
account=self, start=timezone.now(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif hasattr(self, 'negative'):
|
||||||
|
# self.real_balance >= 0
|
||||||
|
balance_offset = self.negative.balance_offset
|
||||||
|
if balance_offset:
|
||||||
|
(
|
||||||
|
Account.objects
|
||||||
|
.filter(pk=self.pk)
|
||||||
|
.update(balance=F('balance')-balance_offset)
|
||||||
|
)
|
||||||
|
self.refresh_from_db()
|
||||||
|
self.negative.delete()
|
||||||
|
|
||||||
class UserHasAccount(Exception):
|
class UserHasAccount(Exception):
|
||||||
def __init__(self, trigramme):
|
def __init__(self, trigramme):
|
||||||
self.trigramme = trigramme
|
self.trigramme = trigramme
|
||||||
|
|
||||||
class AccountNegative(models.Model):
|
|
||||||
account = models.OneToOneField(
|
|
||||||
Account, on_delete = models.PROTECT,
|
|
||||||
related_name = "negative")
|
|
||||||
start = models.DateTimeField(
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
balance_offset = models.DecimalField(
|
|
||||||
max_digits = 6, decimal_places = 2,
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
authz_overdraft_amount = models.DecimalField(
|
|
||||||
max_digits = 6, decimal_places = 2,
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
authz_overdraft_until = models.DateTimeField(
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
comment = models.CharField(max_length = 255, blank = True)
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
class AccountNegativeManager(models.Manager):
|
||||||
|
"""Manager for AccountNegative model."""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super().get_queryset()
|
||||||
|
.select_related('account__cofprofile__user')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountNegative(models.Model):
|
||||||
|
objects = AccountNegativeManager()
|
||||||
|
|
||||||
|
account = models.OneToOneField(
|
||||||
|
Account, on_delete=models.PROTECT,
|
||||||
|
related_name="negative",
|
||||||
|
)
|
||||||
|
start = models.DateTimeField(blank=True, null=True, default=None)
|
||||||
|
balance_offset = models.DecimalField(
|
||||||
|
"décalage de balance",
|
||||||
|
help_text="Montant non compris dans l'autorisation de négatif",
|
||||||
|
max_digits=6, decimal_places=2,
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
)
|
||||||
|
authz_overdraft_amount = models.DecimalField(
|
||||||
|
"négatif autorisé",
|
||||||
|
max_digits=6, decimal_places=2,
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
)
|
||||||
|
authz_overdraft_until = models.DateTimeField(
|
||||||
|
"expiration du négatif",
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
)
|
||||||
|
comment = models.CharField("commentaire", max_length=255, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_negs', 'Voir la liste des négatifs'),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def until_default(self):
|
||||||
|
return self.start + kfet_config.overdraft_duration
|
||||||
|
|
||||||
|
|
||||||
class Checkout(models.Model):
|
class Checkout(models.Model):
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete = models.PROTECT,
|
||||||
|
@ -269,29 +391,35 @@ class CheckoutStatement(models.Model):
|
||||||
checkout = models.ForeignKey(
|
checkout = models.ForeignKey(
|
||||||
Checkout, on_delete = models.PROTECT,
|
Checkout, on_delete = models.PROTECT,
|
||||||
related_name = "statements")
|
related_name = "statements")
|
||||||
balance_old = models.DecimalField(max_digits = 6, decimal_places = 2)
|
balance_old = models.DecimalField("ancienne balance",
|
||||||
balance_new = models.DecimalField(max_digits = 6, decimal_places = 2)
|
max_digits = 6, decimal_places = 2)
|
||||||
amount_taken = models.DecimalField(max_digits = 6, decimal_places = 2)
|
balance_new = models.DecimalField("nouvelle balance",
|
||||||
amount_error = models.DecimalField(max_digits = 6, decimal_places = 2)
|
max_digits = 6, decimal_places = 2)
|
||||||
|
amount_taken = models.DecimalField("montant pris",
|
||||||
|
max_digits = 6, decimal_places = 2)
|
||||||
|
amount_error = models.DecimalField("montant de l'erreur",
|
||||||
|
max_digits = 6, decimal_places = 2)
|
||||||
at = models.DateTimeField(auto_now_add = True)
|
at = models.DateTimeField(auto_now_add = True)
|
||||||
not_count = models.BooleanField(default=False)
|
not_count = models.BooleanField("caisse non comptée", default=False)
|
||||||
|
|
||||||
taken_001 = models.PositiveSmallIntegerField(default=0)
|
taken_001 = models.PositiveSmallIntegerField("pièces de 1¢", default=0)
|
||||||
taken_002 = models.PositiveSmallIntegerField(default=0)
|
taken_002 = models.PositiveSmallIntegerField("pièces de 2¢", default=0)
|
||||||
taken_005 = models.PositiveSmallIntegerField(default=0)
|
taken_005 = models.PositiveSmallIntegerField("pièces de 5¢", default=0)
|
||||||
taken_01 = models.PositiveSmallIntegerField(default=0)
|
taken_01 = models.PositiveSmallIntegerField("pièces de 10¢", default=0)
|
||||||
taken_02 = models.PositiveSmallIntegerField(default=0)
|
taken_02 = models.PositiveSmallIntegerField("pièces de 20¢", default=0)
|
||||||
taken_05 = models.PositiveSmallIntegerField(default=0)
|
taken_05 = models.PositiveSmallIntegerField("pièces de 50¢", default=0)
|
||||||
taken_1 = models.PositiveSmallIntegerField(default=0)
|
taken_1 = models.PositiveSmallIntegerField("pièces de 1€", default=0)
|
||||||
taken_2 = models.PositiveSmallIntegerField(default=0)
|
taken_2 = models.PositiveSmallIntegerField("pièces de 2€", default=0)
|
||||||
taken_5 = models.PositiveSmallIntegerField(default=0)
|
taken_5 = models.PositiveSmallIntegerField("billets de 5€", default=0)
|
||||||
taken_10 = models.PositiveSmallIntegerField(default=0)
|
taken_10 = models.PositiveSmallIntegerField("billets de 10€", default=0)
|
||||||
taken_20 = models.PositiveSmallIntegerField(default=0)
|
taken_20 = models.PositiveSmallIntegerField("billets de 20€", default=0)
|
||||||
taken_50 = models.PositiveSmallIntegerField(default=0)
|
taken_50 = models.PositiveSmallIntegerField("billets de 50€", default=0)
|
||||||
taken_100 = models.PositiveSmallIntegerField(default=0)
|
taken_100 = models.PositiveSmallIntegerField("billets de 100€", default=0)
|
||||||
taken_200 = models.PositiveSmallIntegerField(default=0)
|
taken_200 = models.PositiveSmallIntegerField("billets de 200€", default=0)
|
||||||
taken_500 = models.PositiveSmallIntegerField(default=0)
|
taken_500 = models.PositiveSmallIntegerField("billets de 500€", default=0)
|
||||||
taken_cheque = models.DecimalField(default=0, max_digits=6, decimal_places=2)
|
taken_cheque = models.DecimalField(
|
||||||
|
"montant des chèques",
|
||||||
|
default=0, max_digits=6, decimal_places=2)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s %s' % (self.checkout, self.at)
|
return '%s %s' % (self.checkout, self.at)
|
||||||
|
@ -323,24 +451,37 @@ class CheckoutStatement(models.Model):
|
||||||
balance=F('balance') - last_statement.balance_new + self.balance_new)
|
balance=F('balance') - last_statement.balance_new + self.balance_new)
|
||||||
super(CheckoutStatement, self).save(*args, **kwargs)
|
super(CheckoutStatement, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ArticleCategory(models.Model):
|
class ArticleCategory(models.Model):
|
||||||
name = models.CharField(max_length = 45)
|
name = models.CharField("nom", max_length=45)
|
||||||
|
has_addcost = models.BooleanField("majorée", default=True,
|
||||||
|
help_text="Si oui et qu'une majoration "
|
||||||
|
"est active, celle-ci sera "
|
||||||
|
"appliquée aux articles de "
|
||||||
|
"cette catégorie.")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
name = models.CharField(max_length = 45)
|
name = models.CharField("nom", max_length = 45)
|
||||||
is_sold = models.BooleanField(default = True)
|
is_sold = models.BooleanField("en vente", default = True)
|
||||||
|
hidden = models.BooleanField("caché",
|
||||||
|
default=False,
|
||||||
|
help_text="Si oui, ne sera pas affiché "
|
||||||
|
"au public ; par exemple "
|
||||||
|
"sur la carte.")
|
||||||
price = models.DecimalField(
|
price = models.DecimalField(
|
||||||
|
"prix",
|
||||||
max_digits = 6, decimal_places = 2,
|
max_digits = 6, decimal_places = 2,
|
||||||
default = 0)
|
default = 0)
|
||||||
stock = models.IntegerField(default = 0)
|
stock = models.IntegerField(default = 0)
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
ArticleCategory, on_delete = models.PROTECT,
|
ArticleCategory, on_delete = models.PROTECT,
|
||||||
related_name = "articles")
|
related_name = "articles", verbose_name='catégorie')
|
||||||
BOX_TYPE_CHOICES = (
|
BOX_TYPE_CHOICES = (
|
||||||
("caisse", "caisse"),
|
("caisse", "caisse"),
|
||||||
("carton", "carton"),
|
("carton", "carton"),
|
||||||
|
@ -348,10 +489,12 @@ class Article(models.Model):
|
||||||
("fût", "fût"),
|
("fût", "fût"),
|
||||||
)
|
)
|
||||||
box_type = models.CharField(
|
box_type = models.CharField(
|
||||||
|
"type de contenant",
|
||||||
choices = BOX_TYPE_CHOICES,
|
choices = BOX_TYPE_CHOICES,
|
||||||
max_length = choices_length(BOX_TYPE_CHOICES),
|
max_length = choices_length(BOX_TYPE_CHOICES),
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
box_capacity = models.PositiveSmallIntegerField(
|
box_capacity = models.PositiveSmallIntegerField(
|
||||||
|
"capacité du contenant",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -360,6 +503,10 @@ class Article(models.Model):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('kfet.article.read', kwargs={'pk': self.pk})
|
return reverse('kfet.article.read', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def price_ukf(self):
|
||||||
|
return to_ukf(self.price)
|
||||||
|
|
||||||
|
|
||||||
class ArticleRule(models.Model):
|
class ArticleRule(models.Model):
|
||||||
article_on = models.OneToOneField(
|
article_on = models.OneToOneField(
|
||||||
Article, on_delete = models.PROTECT,
|
Article, on_delete = models.PROTECT,
|
||||||
|
@ -386,6 +533,10 @@ class Inventory(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-at']
|
ordering = ['-at']
|
||||||
|
permissions = (
|
||||||
|
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InventoryArticle(models.Model):
|
class InventoryArticle(models.Model):
|
||||||
inventory = models.ForeignKey(
|
inventory = models.ForeignKey(
|
||||||
|
@ -403,21 +554,24 @@ class InventoryArticle(models.Model):
|
||||||
self.stock_error = self.stock_new - self.stock_old
|
self.stock_error = self.stock_new - self.stock_old
|
||||||
super(InventoryArticle, self).save(*args, **kwargs)
|
super(InventoryArticle, self).save(*args, **kwargs)
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Supplier(models.Model):
|
class Supplier(models.Model):
|
||||||
articles = models.ManyToManyField(
|
articles = models.ManyToManyField(
|
||||||
Article,
|
Article,
|
||||||
through = 'SupplierArticle',
|
verbose_name=_("articles vendus"),
|
||||||
related_name = "suppliers")
|
through='SupplierArticle',
|
||||||
name = models.CharField(max_length = 45)
|
related_name='suppliers',
|
||||||
address = models.TextField()
|
)
|
||||||
email = models.EmailField()
|
name = models.CharField(_("nom"), max_length=45)
|
||||||
phone = models.CharField(max_length = 10)
|
address = models.TextField(_("adresse"), blank=True)
|
||||||
comment = models.TextField()
|
email = models.EmailField(_("adresse mail"), blank=True)
|
||||||
|
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
|
||||||
|
comment = models.TextField(_("commentaire"), blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class SupplierArticle(models.Model):
|
class SupplierArticle(models.Model):
|
||||||
supplier = models.ForeignKey(
|
supplier = models.ForeignKey(
|
||||||
Supplier, on_delete = models.PROTECT)
|
Supplier, on_delete = models.PROTECT)
|
||||||
|
@ -458,7 +612,7 @@ class OrderArticle(models.Model):
|
||||||
quantity_received = models.IntegerField(default = 0)
|
quantity_received = models.IntegerField(default = 0)
|
||||||
|
|
||||||
class TransferGroup(models.Model):
|
class TransferGroup(models.Model):
|
||||||
at = models.DateTimeField(auto_now_add = True)
|
at = models.DateTimeField(default=timezone.now)
|
||||||
# Optional
|
# Optional
|
||||||
comment = models.CharField(
|
comment = models.CharField(
|
||||||
max_length = 255,
|
max_length = 255,
|
||||||
|
@ -468,24 +622,29 @@ class TransferGroup(models.Model):
|
||||||
related_name = "+",
|
related_name = "+",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
|
||||||
class Transfer(models.Model):
|
class Transfer(models.Model):
|
||||||
group = models.ForeignKey(
|
group = models.ForeignKey(
|
||||||
TransferGroup, on_delete = models.PROTECT,
|
TransferGroup, on_delete=models.PROTECT,
|
||||||
related_name = "transfers")
|
related_name="transfers")
|
||||||
from_acc = models.ForeignKey(
|
from_acc = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
related_name = "transfers_from")
|
related_name="transfers_from")
|
||||||
to_acc = models.ForeignKey(
|
to_acc = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
related_name = "transfers_to")
|
related_name="transfers_to")
|
||||||
amount = models.DecimalField(max_digits = 6, decimal_places = 2)
|
amount = models.DecimalField(max_digits=6, decimal_places=2)
|
||||||
# Optional
|
# Optional
|
||||||
canceled_by = models.ForeignKey(
|
canceled_by = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
null = True, blank = True, default = None,
|
null=True, blank=True, default=None,
|
||||||
related_name = "+")
|
related_name="+")
|
||||||
canceled_at = models.DateTimeField(
|
canceled_at = models.DateTimeField(
|
||||||
null = True, blank = True, default = None)
|
null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount)
|
||||||
|
|
||||||
|
|
||||||
class OperationGroup(models.Model):
|
class OperationGroup(models.Model):
|
||||||
on_acc = models.ForeignKey(
|
on_acc = models.ForeignKey(
|
||||||
|
@ -494,7 +653,7 @@ class OperationGroup(models.Model):
|
||||||
checkout = models.ForeignKey(
|
checkout = models.ForeignKey(
|
||||||
Checkout, on_delete = models.PROTECT,
|
Checkout, on_delete = models.PROTECT,
|
||||||
related_name = "opesgroup")
|
related_name = "opesgroup")
|
||||||
at = models.DateTimeField(auto_now_add = True)
|
at = models.DateTimeField(default=timezone.now)
|
||||||
amount = models.DecimalField(
|
amount = models.DecimalField(
|
||||||
max_digits = 6, decimal_places = 2,
|
max_digits = 6, decimal_places = 2,
|
||||||
default = 0)
|
default = 0)
|
||||||
|
@ -508,180 +667,81 @@ class OperationGroup(models.Model):
|
||||||
related_name = "+",
|
related_name = "+",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ', '.join(map(str, self.opes.all()))
|
||||||
|
|
||||||
|
|
||||||
class Operation(models.Model):
|
class Operation(models.Model):
|
||||||
PURCHASE = 'purchase'
|
PURCHASE = 'purchase'
|
||||||
DEPOSIT = 'deposit'
|
DEPOSIT = 'deposit'
|
||||||
WITHDRAW = 'withdraw'
|
WITHDRAW = 'withdraw'
|
||||||
INITIAL = 'initial'
|
INITIAL = 'initial'
|
||||||
|
EDIT = 'edit'
|
||||||
|
|
||||||
TYPE_ORDER_CHOICES = (
|
TYPE_ORDER_CHOICES = (
|
||||||
(PURCHASE, 'Achat'),
|
(PURCHASE, 'Achat'),
|
||||||
(DEPOSIT, 'Charge'),
|
(DEPOSIT, 'Charge'),
|
||||||
(WITHDRAW, 'Retrait'),
|
(WITHDRAW, 'Retrait'),
|
||||||
(INITIAL, 'Initial'),
|
(INITIAL, 'Initial'),
|
||||||
|
(EDIT, 'Édition'),
|
||||||
)
|
)
|
||||||
|
|
||||||
group = models.ForeignKey(
|
group = models.ForeignKey(
|
||||||
OperationGroup, on_delete = models.PROTECT,
|
OperationGroup, on_delete=models.PROTECT,
|
||||||
related_name = "opes")
|
related_name="opes")
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
choices = TYPE_ORDER_CHOICES,
|
choices=TYPE_ORDER_CHOICES,
|
||||||
max_length = choices_length(TYPE_ORDER_CHOICES))
|
max_length=choices_length(TYPE_ORDER_CHOICES))
|
||||||
amount = models.DecimalField(
|
amount = models.DecimalField(
|
||||||
max_digits = 6, decimal_places = 2,
|
max_digits=6, decimal_places=2,
|
||||||
blank = True, default = 0)
|
blank=True, default=0)
|
||||||
is_checkout = models.BooleanField(default = True)
|
|
||||||
# Optional
|
# Optional
|
||||||
article = models.ForeignKey(
|
article = models.ForeignKey(
|
||||||
Article, on_delete = models.PROTECT,
|
Article, on_delete=models.PROTECT,
|
||||||
related_name = "operations",
|
related_name="operations",
|
||||||
blank = True, null = True, default = None)
|
blank=True, null=True, default=None)
|
||||||
article_nb = models.PositiveSmallIntegerField(
|
article_nb = models.PositiveSmallIntegerField(
|
||||||
blank = True, null = True, default = None)
|
blank=True, null=True, default=None)
|
||||||
canceled_by = models.ForeignKey(
|
canceled_by = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
related_name = "+",
|
related_name="+",
|
||||||
blank = True, null = True, default = None)
|
blank=True, null=True, default=None)
|
||||||
canceled_at = models.DateTimeField(
|
canceled_at = models.DateTimeField(
|
||||||
blank = True, null = True, default = None)
|
blank=True, null=True, default=None)
|
||||||
addcost_for = models.ForeignKey(
|
addcost_for = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
related_name = "addcosts",
|
related_name="addcosts",
|
||||||
blank = True, null = True, default = None)
|
blank=True, null=True, default=None)
|
||||||
addcost_amount = models.DecimalField(
|
addcost_amount = models.DecimalField(
|
||||||
max_digits = 6, decimal_places = 2,
|
max_digits=6, decimal_places=2,
|
||||||
blank = True, null = True, default = None)
|
blank=True, null=True, default=None)
|
||||||
|
|
||||||
class GlobalPermissions(models.Model):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
managed = False
|
|
||||||
permissions = (
|
permissions = (
|
||||||
('is_team', 'Is part of the team'),
|
|
||||||
('perform_deposit', 'Effectuer une charge'),
|
('perform_deposit', 'Effectuer une charge'),
|
||||||
('perform_negative_operations',
|
('perform_negative_operations',
|
||||||
'Enregistrer des commandes en négatif'),
|
'Enregistrer des commandes en négatif'),
|
||||||
('override_frozen_protection', "Forcer le gel d'un compte"),
|
('override_frozen_protection', "Forcer le gel d'un compte"),
|
||||||
('cancel_old_operations', 'Annuler des commandes non récentes'),
|
('cancel_old_operations', 'Annuler des commandes non récentes'),
|
||||||
('manage_perms', 'Gérer les permissions K-Fêt'),
|
('perform_commented_operations',
|
||||||
('manage_addcosts', 'Gérer les majorations'),
|
'Enregistrer des commandes avec commentaires'),
|
||||||
('perform_commented_operations', 'Enregistrer des commandes avec commentaires'),
|
|
||||||
('view_negs', 'Voir la liste des négatifs'),
|
|
||||||
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
|
|
||||||
('edit_balance_account', "Modifier la balance d'un compte"),
|
|
||||||
('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"),
|
|
||||||
('special_add_account', "Créer un compte avec une balance initiale")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Settings(models.Model):
|
@property
|
||||||
name = models.CharField(
|
def is_checkout(self):
|
||||||
max_length = 45,
|
return (self.type == Operation.DEPOSIT or
|
||||||
unique = True,
|
self.type == Operation.WITHDRAW or
|
||||||
db_index = True)
|
(self.type == Operation.PURCHASE and self.group.on_acc.is_cash)
|
||||||
value_decimal = models.DecimalField(
|
)
|
||||||
max_digits = 6, decimal_places = 2,
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
value_account = models.ForeignKey(
|
|
||||||
Account, on_delete = models.PROTECT,
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
value_duration = models.DurationField(
|
|
||||||
blank = True, null = True, default = None)
|
|
||||||
|
|
||||||
@staticmethod
|
def __str__(self):
|
||||||
def setting_inst(name):
|
templates = {
|
||||||
return Settings.objects.get(name=name)
|
self.PURCHASE: "{nb} {article.name} ({amount}€)",
|
||||||
|
self.DEPOSIT: "charge ({amount}€)",
|
||||||
@staticmethod
|
self.WITHDRAW: "retrait ({amount}€)",
|
||||||
def SUBVENTION_COF():
|
self.INITIAL: "initial ({amount}€)",
|
||||||
subvention_cof = cache.get('SUBVENTION_COF')
|
self.EDIT: "édition ({amount}€)",
|
||||||
if subvention_cof:
|
}
|
||||||
return subvention_cof
|
return templates[self.type].format(nb=self.article_nb,
|
||||||
try:
|
article=self.article,
|
||||||
subvention_cof = Settings.setting_inst("SUBVENTION_COF").value_decimal
|
amount=self.amount)
|
||||||
except Settings.DoesNotExist:
|
|
||||||
subvention_cof = 0
|
|
||||||
cache.set('SUBVENTION_COF', subvention_cof)
|
|
||||||
return subvention_cof
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def ADDCOST_AMOUNT():
|
|
||||||
try:
|
|
||||||
return Settings.setting_inst("ADDCOST_AMOUNT").value_decimal
|
|
||||||
except Settings.DoesNotExist:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def ADDCOST_FOR():
|
|
||||||
try:
|
|
||||||
return Settings.setting_inst("ADDCOST_FOR").value_account
|
|
||||||
except Settings.DoesNotExist:
|
|
||||||
return None;
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def OVERDRAFT_DURATION():
|
|
||||||
overdraft_duration = cache.get('OVERDRAFT_DURATION')
|
|
||||||
if overdraft_duration:
|
|
||||||
return overdraft_duration
|
|
||||||
try:
|
|
||||||
overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration
|
|
||||||
except Settings.DoesNotExist:
|
|
||||||
overdraft_duration = timedelta()
|
|
||||||
cache.set('OVERDRAFT_DURATION', overdraft_duration)
|
|
||||||
return overdraft_duration
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def OVERDRAFT_AMOUNT():
|
|
||||||
overdraft_amount = cache.get('OVERDRAFT_AMOUNT')
|
|
||||||
if overdraft_amount:
|
|
||||||
return overdraft_amount
|
|
||||||
try:
|
|
||||||
overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
|
|
||||||
except Settings.DoesNotExist:
|
|
||||||
overdraft_amount = 0
|
|
||||||
cache.set('OVERDRAFT_AMOUNT', overdraft_amount)
|
|
||||||
return overdraft_amount
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def CANCEL_DURATION():
|
|
||||||
cancel_duration = cache.get('CANCEL_DURATION')
|
|
||||||
if cancel_duration:
|
|
||||||
return cancel_duration
|
|
||||||
try:
|
|
||||||
cancel_duration = Settings.setting_inst("CANCEL_DURATION").value_duration
|
|
||||||
except Settings.DoesNotExist:
|
|
||||||
cancel_duration = timedelta()
|
|
||||||
cache.set('CANCEL_DURATION', cancel_duration)
|
|
||||||
return cancel_duration
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_missing():
|
|
||||||
s, created = Settings.objects.get_or_create(name='SUBVENTION_COF')
|
|
||||||
if created:
|
|
||||||
s.value_decimal = 25
|
|
||||||
s.save()
|
|
||||||
s, created = Settings.objects.get_or_create(name='ADDCOST_AMOUNT')
|
|
||||||
if created:
|
|
||||||
s.value_decimal = 0.5
|
|
||||||
s.save()
|
|
||||||
s, created = Settings.objects.get_or_create(name='ADDCOST_FOR')
|
|
||||||
s, created = Settings.objects.get_or_create(name='OVERDRAFT_DURATION')
|
|
||||||
if created:
|
|
||||||
s.value_duration = timedelta(days=1) # 24h
|
|
||||||
s.save()
|
|
||||||
s, created = Settings.objects.get_or_create(name='OVERDRAFT_AMOUNT')
|
|
||||||
if created:
|
|
||||||
s.value_decimal = 20
|
|
||||||
s.save()
|
|
||||||
s, created = Settings.objects.get_or_create(name='CANCEL_DURATION')
|
|
||||||
if created:
|
|
||||||
s.value_duration = timedelta(minutes=5) # 5min
|
|
||||||
s.save()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def empty_cache():
|
|
||||||
cache.delete_many([
|
|
||||||
'SUBVENTION_COF', 'OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT',
|
|
||||||
'CANCEL_DURATION', 'ADDCOST_AMOUNT', 'ADDCOST_FOR',
|
|
||||||
])
|
|
||||||
|
|
||||||
class GenericTeamToken(models.Model):
|
|
||||||
token = models.CharField(max_length = 50, unique = True)
|
|
||||||
|
|
1
kfet/open/__init__.py
Normal file
1
kfet/open/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .open import OpenKfet, kfet_open # noqa
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue