Merge branch 'master' into evarin/site-cof

This commit is contained in:
Martin Pépin 2018-11-19 23:30:33 +01:00
commit 712588af7d
264 changed files with 22039 additions and 7802 deletions

4
.gitignore vendored
View file

@ -11,7 +11,11 @@ media/
*.log
.sass-cache/
*.sqlite3
.coverage
# PyCharm
.idea
.cache
# VSCode
.vscode/

View file

@ -1,6 +1,4 @@
services:
- postgres:latest
- redis:latest
image: "python:3.5"
variables:
# GestioCOF settings
@ -10,7 +8,7 @@ variables:
REDIS_PASSWD: "dummy"
# Cached packages
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
# postgres service configuration
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
@ -20,22 +18,44 @@ variables:
# psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD
cache:
paths:
- vendor/python
- vendor/pip
- vendor/apt
before_script:
- mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
test:
stage: test
before_script:
- mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade -r requirements.txt coverage
- python --version
script:
- python manage.py test
- coverage run manage.py test
after_script:
- coverage report
services:
- postgres:9.6
- redis:latest
cache:
key: test
paths:
- vendor/
# For GitLab CI to get coverage from build.
# Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
linters:
image: python:3.6
stage: test
before_script:
- mkdir -p vendor/pip
- pip install --upgrade black isort flake8
script:
- black --check .
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
# Print errors only
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
cache:
key: linters
paths:
- vendor/

106
.pre-commit.sh Executable file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env bash
# pre-commit hook for gestioCOF project.
#
# Run formatters first, then checkers.
# Formatters which changed a file must set the flag 'formatter_updated'.
exit_code=0
formatter_updated=0
checker_dirty=0
# TODO(AD): We should check only staged changes.
# Working? -> Stash unstaged changes, run it, pop stash
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$")
# Formatter: black
printf "> black ... "
if type black &>/dev/null; then
if [ -z "$STAGED_PYTHON_FILES" ]; then
printf "OK\n"
else
BLACK_OUTPUT="/tmp/gc-black-output.log"
touch $BLACK_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black --check &>$BLACK_OUTPUT; then
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black &>$BLACK_OUTPUT
tail -1 $BLACK_OUTPUT
formatter_updated=1
else
printf "OK\n"
fi
fi
else
printf "SKIP: program not found\n"
printf "HINT: Install black with 'pip3 install black' (black requires Python>=3.6)\n"
fi
# Formatter: isort
printf "> isort ... "
if type isort &>/dev/null; then
if [ -z "$STAGED_PYTHON_FILES" ]; then
printf "OK\n"
else
ISORT_OUTPUT="/tmp/gc-isort-output.log"
touch $ISORT_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check-only &>$ISORT_OUTPUT; then
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
printf "Reformatted.\n"
formatter_updated=1
else
printf "OK\n"
fi
fi
else
printf "SKIP: program not found\n"
printf "HINT: Install isort with 'pip install isort'\n"
fi
# Checker: flake8
printf "> flake8 ... "
if type flake8 &>/dev/null; then
if [ -z "$STAGED_PYTHON_FILES" ]; then
printf "OK\n"
else
FLAKE8_OUTPUT="/tmp/gc-flake8-output.log"
touch $FLAKE8_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then
printf "FAIL\n"
cat $FLAKE8_OUTPUT
checker_dirty=1
else
printf "OK\n"
fi
fi
else
printf "SKIP: program not found\n"
printf "HINT: Install flake8 with 'pip install flake8'\n"
fi
# End
if [ $checker_dirty -ne 0 ]
then
printf ">>> Checker(s) detect(s) issue(s)\n"
printf " You can still commit and push :)\n"
printf " Be warned that our CI may cause you more trouble.\n"
fi
if [ $formatter_updated -ne 0 ]
then
printf ">>> Working tree updated by formatter(s)\n"
printf " Add changes to staging area and retry.\n"
exit_code=1
fi
printf "\n"
exit $exit_code

148
README.md
View file

@ -1,17 +1,86 @@
# GestioCOF
[![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
## Installation
Il est possible d'installer GestioCOF sur votre machine de deux façons différentes :
- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère
- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde
### Installation manuelle
Il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python ainsi
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python3-pip python3-dev python3-venv sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
python3 -m venv venv
Pour l'activer, il faut taper
. venv/bin/activate
depuis le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -U pip # parfois nécessaire la première fois
pip install -r requirements-devel.txt
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py cof/settings/secret.py
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
encore l'installation *via* un lien symbolique:
ln -s ../../.pre-commit.sh .git/hooks/pre-commit
Pour plus d'informations à ce sujet, consulter la
[page](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/coding-style)
du wiki gestioCOF liée aux conventions.
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
données bidons bien pratiques pour développer avec la commande suivante :
bash provisioning/prepare_django.sh
Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas
pour plus de détails.
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
### Vagrant
La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser
Une autre façon d'installer GestioCOF sur votre machine est d'utiliser
[Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
le monde à la même configuration de développement (même sous Windows !), et
l'installation se fait en une commande.
Pour utiliser Vagrant, il faut le
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
Si vous êtes sous Linux, votre distribution propose probablement des paquets
Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui
@ -81,55 +150,6 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
code change, il faut relancer le worker avec `sudo systemctl restart
worker.service` pour visualiser la dernière version du code.
### Installation manuelle
Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant,
il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python ainsi
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python3-pip python3-dev sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
python3 -m venv venv
Pour l'activer, il faut faire
. venv/bin/activate
dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -U pip
pip install -r requirements-devel.txt
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py cof/settings/secret.py
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
données bidons bien pratiques pour développer avec la commande suivante :
bash provisioning/prepare_django.sh
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
### Mise à jour
Pour mettre à jour les paquets Python, utiliser la commande suivante :
@ -141,6 +161,32 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire :
python manage.py migrate
## Outils pour développer
### Base de donnée
Quelle que soit la méthode d'installation choisie, la base de donnée locale est
peuplée avec des données artificielles pour faciliter le développement.
- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez
vous sur ce compte pour accéder à tout GestioCOF.
- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques
spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA.
- À chaque compte est associé un trigramme K-Fêt
- Un certain nombre d'articles K-Fêt sont renseignés.
### Tests unitaires
On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab
à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante.
Pour lancer les tests :
```
python manage.py test
```
## Documentation utilisateur
Une brève documentation utilisateur est accessible sur le

1
TODO_PROD.md Normal file
View file

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

View file

@ -1 +0,0 @@

View file

@ -1,18 +1,24 @@
# -*- coding: utf-8 -*-
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from custommail.shortcuts import send_mass_custom_mail
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin
from django.db.models import Sum, Count
from django.db.models import Count, Sum
from django.template.defaultfilters import pluralize
from django.utils import timezone
from django import forms
from dal.autocomplete import ModelSelect2
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
from bda.models import (
Attribution,
CategorieSpectacle,
ChoixSpectacle,
Participant,
Quote,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
class ReadOnlyMixin(object):
@ -29,8 +35,8 @@ class ReadOnlyMixin(object):
class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta:
widgets = {
'participant': ModelSelect2(url='bda-participant-autocomplete'),
'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'),
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
@ -45,10 +51,10 @@ class AttributionTabularAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
spectacles = Spectacle.objects.select_related('location')
spectacles = Spectacle.objects.select_related("location")
if self.listing is not None:
spectacles = spectacles.filter(listing=self.listing)
self.fields['spectacle'].queryset = spectacles
self.fields["spectacle"].queryset = spectacles
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
@ -72,7 +78,7 @@ class AttributionInline(admin.TabularInline):
class WithListingAttributionInline(AttributionInline):
exclude = ('given', )
exclude = ("given",)
form = WithListingAttributionTabularAdminForm
listing = True
@ -83,12 +89,10 @@ class WithoutListingAttributionInline(AttributionInline):
class ParticipantAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['choicesrevente'].queryset = (
Spectacle.objects
.select_related('location')
self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
"location"
)
@ -96,11 +100,13 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
def get_queryset(self, request):
return Participant.objects.annotate(nb_places=Count('attributions'),
total=Sum('attributions__price'))
return Participant.objects.annotate(
nb_places=Count("attributions"), total=Sum("attributions__price")
)
def nb_places(self, obj):
return obj.nb_places
nb_places.admin_order_field = "nb_places"
nb_places.short_description = "Nombre de places"
@ -110,33 +116,32 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
return "%.02f" % tot
else:
return "0 €"
total.admin_order_field = "total"
total.short_description = "Total à payer"
list_display = ("user", "nb_places", "total", "paid", "paymenttype",
"tirage")
list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage")
list_filter = ("paid", "tirage")
search_fields = ('user__username', 'user__first_name', 'user__last_name')
actions = ['send_attribs', ]
search_fields = ("user__username", "user__first_name", "user__last_name")
actions = ["send_attribs"]
actions_on_bottom = True
list_per_page = 400
readonly_fields = ("total",)
readonly_fields_update = ('user', 'tirage')
readonly_fields_update = ("user", "tirage")
form = ParticipantAdminForm
def send_attribs(self, request, queryset):
datatuple = []
for member in queryset.all():
attribs = member.attributions.all()
context = {'member': member.user}
context = {"member": member.user}
shortname = ""
if len(attribs) == 0:
shortname = "bda-attributions-decus"
else:
shortname = "bda-attributions"
context['places'] = attribs
context["places"] = attribs
print(context)
datatuple.append((shortname, context, "bda@ens.fr",
[member.user.email]))
datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
send_mass_custom_mail(datatuple)
count = len(queryset.all())
if count == 1:
@ -145,49 +150,53 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
else:
message_bit = "%d membres ont" % count
plural = "s"
self.message_user(request, "%s été informé%s avec succès."
% (message_bit, plural))
self.message_user(
request, "%s été informé%s avec succès." % (message_bit, plural)
)
send_attribs.short_description = "Envoyer les résultats par mail"
class AttributionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'spectacle' in self.fields:
self.fields['spectacle'].queryset = (
Spectacle.objects
.select_related('location')
if "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')
if "participant" in self.fields:
self.fields["participant"].queryset = Participant.objects.select_related(
"user", "tirage"
)
def clean(self):
cleaned_data = super(AttributionAdminForm, self).clean()
cleaned_data = super().clean()
participant = cleaned_data.get("participant")
spectacle = cleaned_data.get("spectacle")
if participant and spectacle:
if participant.tirage != spectacle.tirage:
raise forms.ValidationError(
"Erreur : le participant et le spectacle n'appartiennent"
"pas au même tirage")
"pas au même tirage"
)
return cleaned_data
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
def paid(self, obj):
return obj.participant.paid
paid.short_description = 'A payé'
paid.short_description = "A payé"
paid.boolean = True
list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = ('spectacle__title', 'participant__user__username',
'participant__user__first_name',
'participant__user__last_name')
search_fields = (
"spectacle__title",
"participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
)
form = AttributionAdminForm
readonly_fields_update = ('spectacle', 'participant')
readonly_fields_update = ("spectacle", "participant")
class ChoixSpectacleAdmin(admin.ModelAdmin):
@ -195,13 +204,15 @@ class ChoixSpectacleAdmin(admin.ModelAdmin):
def tirage(self, obj):
return obj.participant.tirage
list_display = ("participant", "tirage", "spectacle", "priority",
"double_choice")
list_display = ("participant", "tirage", "spectacle", "priority", "double_choice")
list_filter = ("double_choice", "participant__tirage")
search_fields = ('participant__user__username',
'participant__user__first_name',
'participant__user__last_name',
'spectacle__title')
search_fields = (
"participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
"spectacle__title",
)
class QuoteInline(admin.TabularInline):
@ -211,42 +222,36 @@ class QuoteInline(admin.TabularInline):
class SpectacleAdmin(admin.ModelAdmin):
inlines = [QuoteInline]
model = Spectacle
list_display = ("title", "date", "tirage", "location", "slots", "price",
"listing")
list_filter = ("location", "tirage",)
list_display = ("title", "date", "tirage", "location", "slots", "price", "listing")
list_filter = ("location", "tirage")
search_fields = ("title", "location__name")
readonly_fields = ("rappel_sent", )
readonly_fields = ("rappel_sent",)
class TirageAdmin(admin.ModelAdmin):
model = Tirage
list_display = ("title", "ouverture", "fermeture", "active",
"enable_do_tirage")
readonly_fields = ("tokens", )
list_filter = ("active", )
search_fields = ("title", )
list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage")
readonly_fields = ("tokens",)
list_filter = ("active",)
search_fields = ("title",)
class SalleAdmin(admin.ModelAdmin):
model = Salle
search_fields = ('name', 'address')
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["confirmed_entry"].queryset = Participant.objects.select_related(
"user", "tirage"
)
self.fields['seller'].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')
self.fields["soldTo"].queryset = Participant.objects.select_related(
"user", "tirage"
)
@ -254,6 +259,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
"""
Administration des reventes de spectacles
"""
model = SpectacleRevente
def spectacle(self, obj):
@ -265,12 +271,14 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
list_display = ("spectacle", "seller", "date", "soldTo")
raw_id_fields = ("attribution",)
readonly_fields = ("date_tirage",)
search_fields = ['attribution__spectacle__title',
'seller__user__username',
'seller__user__first_name',
'seller__user__last_name']
search_fields = [
"attribution__spectacle__title",
"seller__user__username",
"seller__user__first_name",
"seller__user__last_name",
]
actions = ['transfer', 'reinit']
actions = ["transfer", "reinit"]
actions_on_bottom = True
form = SpectacleReventeAdminForm
@ -286,10 +294,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
attrib.save()
self.message_user(
request,
"%d attribution%s %s été transférée%s avec succès." % (
count, pluralize(count),
pluralize(count, "a,ont"), pluralize(count))
)
"%d attribution%s %s été transférée%s avec succès."
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
)
transfer.short_description = "Transférer les reventes sélectionnées"
def reinit(self, request, queryset):
@ -298,20 +306,15 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
"""
count = queryset.count()
for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()):
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
attribution__spectacle__date__gte=timezone.now()
):
revente.reset(new_date=timezone.now() - timedelta(hours=1))
self.message_user(
request,
"%d attribution%s %s été réinitialisée%s avec succès." % (
count, pluralize(count),
pluralize(count, "a,ont"), pluralize(count))
)
"%d attribution%s %s été réinitialisée%s avec succès."
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
)
reinit.short_description = "Réinitialiser les reventes sélectionnées"

View file

@ -1,11 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db.models import Max
import random
@ -22,7 +14,7 @@ class Algorithm(object):
show.requests
- on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings"""
self.max_group = 2*max(choice.priority for choice in choices)
self.max_group = 2 * max(choice.priority for choice in choices)
self.shows = []
showdict = {}
for show in shows:
@ -60,16 +52,19 @@ class Algorithm(object):
self.ranks[member][show] -= increment
def appendResult(self, l, member, show):
l.append((member,
self.ranks[member][show],
self.origranks[member][show],
self.choices[member][show].double))
l.append(
(
member,
self.ranks[member][show],
self.origranks[member][show],
self.choices[member][show].double,
)
)
def __call__(self, seed):
random.seed(seed)
results = []
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots,
reverse=True)
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True)
for show in shows:
# On regroupe tous les gens ayant le même rang
groups = dict([(i, []) for i in range(1, self.max_group + 1)])
@ -88,8 +83,10 @@ class Algorithm(object):
if len(winners) + 1 < show.slots:
self.appendResult(winners, member, show)
self.appendResult(winners, member, show)
elif not self.choices[member][show].autoquit \
and len(winners) < show.slots:
elif (
not self.choices[member][show].autoquit
and len(winners) < show.slots
):
self.appendResult(winners, member, show)
self.appendResult(losers, member, show)
else:

View file

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-
from django import forms
from django.forms.models import BaseInlineFormSet
from django.utils import timezone
from bda.models import Attribution, Spectacle
from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -18,9 +15,9 @@ class InscriptionInlineFormSet(BaseInlineFormSet):
# set once for all "spectacle" field choices
# - restrict choices to the spectacles of this tirage
# - force_choices avoid many db requests
spectacles = tirage.spectacle_set.select_related('location')
spectacles = tirage.spectacle_set.select_related("location")
choices = [(sp.pk, str(sp)) for sp in spectacles]
self.force_choices('spectacle', choices)
self.force_choices("spectacle", choices)
def force_choices(self, name, choices):
"""Set choices of a field.
@ -32,7 +29,7 @@ class InscriptionInlineFormSet(BaseInlineFormSet):
for form in self.forms:
field = form.fields[name]
if field.empty_label is not None:
field.choices = [('', field.empty_label)] + choices
field.choices = [("", field.empty_label)] + choices
else:
field.choices = choices
@ -43,75 +40,140 @@ class TokenForm(forms.Form):
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return "%s" % str(obj.spectacle)
return str(obj.spectacle)
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, *args, own=True, **kwargs):
super().__init__(*args, **kwargs)
self.own = own
def label_from_instance(self, obj):
label = "{show}{suffix}"
suffix = ""
if self.own:
# C'est notre propre revente : informations sur le statut
if obj.soldTo is not None:
suffix = " -- Vendue à {firstname} {lastname}".format(
firstname=obj.soldTo.user.first_name,
lastname=obj.soldTo.user.last_name,
)
elif obj.shotgun:
suffix = " -- Tirage infructueux"
elif obj.notif_sent:
suffix = " -- Inscriptions au tirage en cours"
else:
# Ce n'est pas à nous : on ne voit jamais l'acheteur
suffix = " -- Vendue par {firstname} {lastname}".format(
firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name
)
return label.format(show=str(obj.attribution.spectacle), suffix=suffix)
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now())
super().__init__(*args, **kwargs)
self.fields["attributions"].queryset = (
participant.attribution_set.filter(spectacle__date__gte=timezone.now())
.exclude(revente__seller=participant)
.select_related('spectacle', 'spectacle__location',
'participant__user')
.select_related("spectacle", "spectacle__location", "participant__user")
)
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
reventes = ReventeModelMultipleChoiceField(
own=True,
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super(AnnulForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__notif_sent=False,
revente__soldTo__isnull=True)
.select_related('spectacle', 'spectacle__location',
'participant__user')
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = (
participant.original_shows.filter(
attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
)
.order_by("-date")
)
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, tirage, *args, **kwargs):
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
self.fields['spectacles'].queryset = (
tirage.spectacle_set
.select_related('location')
.filter(date__gte=timezone.now())
super().__init__(*args, **kwargs)
self.fields["spectacles"].queryset = tirage.spectacle_set.select_related(
"location"
).filter(date__gte=timezone.now())
class ReventeTirageAnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
label="",
queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = participant.entered.filter(
soldTo__isnull=True
).select_related("attribution__spectacle", "seller__user")
class ReventeTirageForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
label="",
queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = (
SpectacleRevente.objects.filter(
notif_sent=True, shotgun=False, tirage_done=False
)
.exclude(confirmed_entry=participant)
.select_related("attribution__spectacle")
)
class SoldForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple)
reventes = ReventeModelMultipleChoiceField(
own=True,
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
)
def __init__(self, participant, *args, **kwargs):
super(SoldForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(revente__isnull=False,
revente__soldTo__isnull=False)
.exclude(revente__soldTo=participant)
.select_related('spectacle', 'spectacle__location',
'participant__user')
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = (
participant.original_shows.filter(soldTo__isnull=False)
.exclude(soldTo=participant)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
)
)

View file

@ -5,17 +5,15 @@ Crée deux tirages de test et y inscrit les utilisateurs
import os
import random
from django.utils import timezone
from django.contrib.auth.models import User
from django.utils import timezone
from gestioncof.management.base import MyBaseCommand
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage
from bda.views import do_tirage
from gestioncof.management.base import MyBaseCommand
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'data')
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
class Command(MyBaseCommand):
@ -27,27 +25,29 @@ class Command(MyBaseCommand):
# ---
Tirage.objects.all().delete()
Tirage.objects.bulk_create([
Tirage(
title="Tirage de test 1",
ouverture=timezone.now()-timezone.timedelta(days=7),
fermeture=timezone.now(),
active=True
),
Tirage(
title="Tirage de test 2",
ouverture=timezone.now(),
fermeture=timezone.now()+timezone.timedelta(days=60),
active=True
)
])
Tirage.objects.bulk_create(
[
Tirage(
title="Tirage de test 1",
ouverture=timezone.now() - timezone.timedelta(days=7),
fermeture=timezone.now(),
active=True,
),
Tirage(
title="Tirage de test 2",
ouverture=timezone.now(),
fermeture=timezone.now() + timezone.timedelta(days=60),
active=True,
),
]
)
tirages = Tirage.objects.all()
# ---
# Salles
# ---
locations = self.from_json('locations.json', DATA_DIR, Salle)
locations = self.from_json("locations.json", DATA_DIR, Salle)
# ---
# Spectacles
@ -60,15 +60,13 @@ class Command(MyBaseCommand):
"""
show.tirage = random.choice(tirages)
show.listing = bool(random.randint(0, 1))
show.date = (
show.tirage.fermeture
+ timezone.timedelta(days=random.randint(60, 90))
show.date = show.tirage.fermeture + timezone.timedelta(
days=random.randint(60, 90)
)
show.location = random.choice(locations)
return show
shows = self.from_json(
'shows.json', DATA_DIR, Spectacle, show_callback
)
shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback)
# ---
# Inscriptions
@ -79,23 +77,19 @@ class Command(MyBaseCommand):
choices = []
for user in User.objects.filter(profile__is_cof=True):
for tirage in tirages:
part, _ = Participant.objects.get_or_create(
user=user,
tirage=tirage
)
part, _ = Participant.objects.get_or_create(user=user, tirage=tirage)
shows = random.sample(
list(tirage.spectacle_set.all()),
tirage.spectacle_set.count() // 2
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
)
for (rank, show) in enumerate(shows):
choices.append(ChoixSpectacle(
participant=part,
spectacle=show,
priority=rank + 1,
double_choice=random.choice(
['1', 'double', 'autoquit']
choices.append(
ChoixSpectacle(
participant=part,
spectacle=show,
priority=rank + 1,
double_choice=random.choice(["1", "double", "autoquit"]),
)
))
)
ChoixSpectacle.objects.bulk_create(choices)
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))

View file

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

View file

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

View file

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

View file

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

View file

@ -6,19 +6,17 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bda', '0002_add_tirage'),
]
dependencies = [("bda", "0002_add_tirage")]
operations = [
migrations.AlterField(
model_name='spectacle',
name='price',
model_name="spectacle",
name="price",
field=models.FloatField(verbose_name=b"Prix d'une place"),
),
migrations.AlterField(
model_name='tirage',
name='active',
field=models.BooleanField(default=False, verbose_name=b'Tirage actif'),
model_name="tirage",
name="active",
field=models.BooleanField(default=False, verbose_name=b"Tirage actif"),
),
]

View file

@ -6,20 +6,22 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bda', '0003_update_tirage_and_spectacle'),
]
dependencies = [("bda", "0003_update_tirage_and_spectacle")]
operations = [
migrations.AddField(
model_name='spectacle',
name='listing',
field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'),
model_name="spectacle",
name="listing",
field=models.BooleanField(
default=False, verbose_name=b"Les places sont sur listing"
),
preserve_default=False,
),
migrations.AddField(
model_name='spectacle',
name='rappel_sent',
field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True),
model_name="spectacle",
name="rappel_sent",
field=models.DateTimeField(
null=True, verbose_name=b"Mail de rappel envoy\xc3\xa9", blank=True
),
),
]

View file

@ -6,24 +6,24 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bda', '0004_mails-rappel'),
]
dependencies = [("bda", "0004_mails-rappel")]
operations = [
migrations.AlterField(
model_name='choixspectacle',
name='priority',
field=models.PositiveIntegerField(verbose_name='Priorit\xe9'),
model_name="choixspectacle",
name="priority",
field=models.PositiveIntegerField(verbose_name="Priorit\xe9"),
),
migrations.AlterField(
model_name='spectacle',
name='priority',
field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'),
model_name="spectacle",
name="priority",
field=models.IntegerField(default=1000, verbose_name="Priorit\xe9"),
),
migrations.AlterField(
model_name='spectacle',
name='rappel_sent',
field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True),
model_name="spectacle",
name="rappel_sent",
field=models.DateTimeField(
null=True, verbose_name="Mail de rappel envoy\xe9", blank=True
),
),
]

View file

@ -10,26 +10,25 @@ def forwards_func(apps, schema_editor):
db_alias = schema_editor.connection.alias
for tirage in Tirage.objects.using(db_alias).all():
if tirage.tokens:
tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
tirage.tokens)
tirage.tokens = 'Before %s\n"""%s"""\n' % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
tirage.tokens,
)
tirage.save()
class Migration(migrations.Migration):
dependencies = [
('bda', '0005_encoding'),
]
dependencies = [("bda", "0005_encoding")]
operations = [
migrations.RenameField('tirage', 'token', 'tokens'),
migrations.RenameField("tirage", "token", "tokens"),
migrations.AddField(
model_name='tirage',
name='enable_do_tirage',
model_name="tirage",
name="enable_do_tirage",
field=models.BooleanField(
default=False,
verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'),
default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9"
),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]

View file

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

View file

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

View file

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

View file

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

View file

@ -6,17 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bda', '0010_spectaclerevente_shotgun'),
]
dependencies = [("bda", "0010_spectaclerevente_shotgun")]
operations = [
migrations.AddField(
model_name='tirage',
name='appear_catalogue',
model_name="tirage",
name="appear_catalogue",
field=models.BooleanField(
default=False,
verbose_name='Tirage à afficher dans le catalogue'
default=False, verbose_name="Tirage à afficher dans le catalogue"
),
),
)
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,14 @@ var django = {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
// Cloning <select> element doesn't properly propagate the default
// selected <option>, so we set it manually.
newElement.find('select').each(function (index, select) {
var defaultValue = $(select).find('option[selected]').val();
if (typeof defaultValue !== 'undefined') {
$(select).val(defaultValue);
}
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
@ -44,6 +52,11 @@ var django = {
} else {
deleteInput.attr("checked", true);
}
} else {
// Reset the default values
var selects = $(form).find("select");
$(selects[0]).val("");
$(selects[1]).val("1");
}
// callback
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
<p>Si personne n'était intéressé, elle est maintenant disponible
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
<a href="{% url "bda-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
{% else %}
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
{% endif %}

View file

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

View file

@ -7,28 +7,33 @@ from django.test import TestCase
from django.utils import timezone
from bda.models import (
Attribution, Participant, Salle, Spectacle, SpectacleRevente, Tirage,
Attribution,
Participant,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
User = get_user_model()
class SpectacleReventeTests(TestCase):
fixtures = ['gestioncof/management/data/custommail.json']
fixtures = ["gestioncof/management/data/custommail.json"]
def setUp(self):
now = timezone.now()
self.t = Tirage.objects.create(
title='Tirage',
title="Tirage",
ouverture=now - timedelta(days=7),
fermeture=now - timedelta(days=3),
active=True,
)
self.s = Spectacle.objects.create(
title='Spectacle',
title="Spectacle",
date=now + timedelta(days=20),
location=Salle.objects.create(name='Salle', address='Address'),
location=Salle.objects.create(name="Salle", address="Address"),
price=10.5,
slots=5,
tirage=self.t,
@ -36,40 +41,37 @@ class SpectacleReventeTests(TestCase):
)
self.seller = Participant.objects.create(
user=User.objects.create(
username='seller', email='seller@mail.net'),
user=User.objects.create(username="seller", email="seller@mail.net"),
tirage=self.t,
)
self.p1 = Participant.objects.create(
user=User.objects.create(username='part1', email='part1@mail.net'),
user=User.objects.create(username="part1", email="part1@mail.net"),
tirage=self.t,
)
self.p2 = Participant.objects.create(
user=User.objects.create(username='part2', email='part2@mail.net'),
user=User.objects.create(username="part2", email="part2@mail.net"),
tirage=self.t,
)
self.p3 = Participant.objects.create(
user=User.objects.create(username='part3', email='part3@mail.net'),
user=User.objects.create(username="part3", email="part3@mail.net"),
tirage=self.t,
)
self.attr = Attribution.objects.create(
participant=self.seller,
spectacle=self.s,
participant=self.seller, spectacle=self.s
)
self.rev = SpectacleRevente.objects.create(
attribution=self.attr,
seller=self.seller,
attribution=self.attr, seller=self.seller
)
def test_tirage(self):
revente = self.rev
wanted_by = [self.p1, self.p2, self.p3]
revente.answered_mail = wanted_by
revente.confirmed_entry = wanted_by
with mock.patch('bda.models.random.choice') as mc:
with mock.patch("bda.models.random.choice") as mc:
# Set winner to self.p1.
mc.return_value = self.p1
@ -87,14 +89,14 @@ class SpectacleReventeTests(TestCase):
self.assertEqual(len(mails), 4)
m_seller = mails['seller@mail.net']
self.assertListEqual(m_seller.to, ['seller@mail.net'])
self.assertListEqual(m_seller.reply_to, ['part1@mail.net'])
m_seller = mails["seller@mail.net"]
self.assertListEqual(m_seller.to, ["seller@mail.net"])
self.assertListEqual(m_seller.reply_to, ["part1@mail.net"])
m_winner = mails['part1@mail.net']
self.assertListEqual(m_winner.to, ['part1@mail.net'])
m_winner = mails["part1@mail.net"]
self.assertListEqual(m_winner.to, ["part1@mail.net"])
self.assertCountEqual(
[mails['part2@mail.net'].to, mails['part3@mail.net'].to],
[['part2@mail.net'], ['part3@mail.net']],
[mails["part2@mail.net"].to, mails["part3@mail.net"].to],
[["part2@mail.net"], ["part3@mail.net"]],
)

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

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
import os
from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ:

View file

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

View file

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Django common settings for cof project.
@ -7,6 +6,7 @@ the local development server should be here.
"""
import os
import sys
try:
from . import secret
@ -42,27 +42,23 @@ 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__)))
)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TESTING = sys.argv[1] == "test"
# Application definition
INSTALLED_APPS = [
'gestioncof',
"shared",
"gestioncof",
# Must be before 'django.contrib.admin'.
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
'dal',
'dal_select2',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
@ -71,7 +67,6 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.admin',
'django.contrib.admindocs',
'bda',
'captcha',
'django_cas_ng',
@ -96,15 +91,17 @@ INSTALLED_APPS = [
'wagtail.contrib.modeladmin',
'wagtail.contrib.wagtailroutablepage',
'wagtailmenus',
'wagtail_modeltranslation',
'modelcluster',
'taggit',
'wagtail_modeltranslation',
'kfet.auth',
'kfet.cms',
'gestioncof.cms',
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -120,39 +117,39 @@ MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware',
]
ROOT_URLCONF = 'cof.urls'
ROOT_URLCONF = "cof.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'wagtailmenus.context_processors.wagtailmenus',
'djconfig.context_processors.config',
'gestioncof.shared.context_processor',
'kfet.auth.context_processors.temporary_auth',
'kfet.context_processors.config',
],
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"wagtailmenus.context_processors.wagtailmenus",
"djconfig.context_processors.config",
"gestioncof.shared.context_processor",
"kfet.auth.context_processors.temporary_auth",
"kfet.context_processors.config",
]
},
},
}
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': DBNAME,
'USER': DBUSER,
'PASSWORD': DBPASSWD,
'HOST': os.environ.get('DBHOST', 'localhost'),
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": DBNAME,
"USER": DBUSER,
"PASSWORD": DBPASSWD,
"HOST": os.environ.get("DBHOST", "localhost"),
}
}
@ -160,9 +157,9 @@ DATABASES = {
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
LANGUAGE_CODE = "fr-fr"
TIME_ZONE = 'Europe/Paris'
TIME_ZONE = "Europe/Paris"
USE_I18N = True
@ -179,47 +176,57 @@ LANGUAGES = (
SITE_ID = 1
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
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>'},
"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_SERVER_URL = "https://cas.eleves.ens.fr/"
CAS_VERSION = "2"
CAS_LOGIN_MSG = None
CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/'
CAS_REDIRECT_URL = "/"
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'gestioncof.shared.COFCASBackend',
'kfet.auth.backends.GenericBackend',
"django.contrib.auth.backends.ModelBackend",
"gestioncof.shared.COFCASBackend",
"kfet.auth.backends.GenericBackend",
)
# reCAPTCHA settings
# https://github.com/praekelt/django-recaptcha
#
# Default settings authorize reCAPTCHA usage for local developement.
# Public and private keys are appended in the 'prod' module settings.
NOCAPTCHA = True
RECAPTCHA_USE_SSL = True
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
# Cache settings
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:{passwd}@{host}:{port}/db'
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
port=REDIS_PORT, db=REDIS_DB),
"default": {
"BACKEND": "redis_cache.RedisCache",
"LOCATION": "redis://:{passwd}@{host}:{port}/db".format(
passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB
),
}
}
@ -230,20 +237,25 @@ 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)
)],
"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'
FORMAT_MODULE_PATH = "cof.locale"
# Wagtail settings
WAGTAIL_SITE_NAME = 'GestioCOF'
WAGTAIL_SITE_NAME = "GestioCOF"
WAGTAIL_ENABLE_UPDATE_CHECK = False
TAGGIT_CASE_INSENSITIVE = True

View file

@ -4,29 +4,32 @@ The settings that are not listed here are imported from .common
"""
from .common import * # NOQA
from .common import INSTALLED_APPS, MIDDLEWARE
from .common import INSTALLED_APPS, MIDDLEWARE, TESTING
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEBUG = True
if TESTING:
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# ---
# Apache static/media config
# ---
STATIC_URL = '/static/'
STATIC_ROOT = '/srv/gestiocof/static/'
STATIC_URL = "/static/"
STATIC_ROOT = "/srv/gestiocof/static/"
MEDIA_ROOT = '/srv/gestiocof/media/'
MEDIA_URL = '/media/'
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
@ -36,12 +39,10 @@ def show_toolbar(request):
"""
return DEBUG
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE = [
"debug_panel.middleware.DebugPanelMiddleware"
] + MIDDLEWARE
if not TESTING:
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
}
MIDDLEWARE = ["debug_panel.middleware.DebugPanelMiddleware"] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}

View file

@ -8,21 +8,16 @@ 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")
"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"
}
}
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
# Use the default in memory asgi backend for local development
CHANNEL_LAYERS = {

View file

@ -6,25 +6,21 @@ The settings that are not listed here are imported from .common
import os
from .common import * # NOQA
from .common import BASE_DIR
from .common import BASE_DIR, import_secret
DEBUG = False
ALLOWED_HOSTS = [
"cof.ens.fr",
"www.cof.ens.fr",
"dev.cof.ens.fr"
]
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",
os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static"
)
STATIC_URL = "/gestion/static/"
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
MEDIA_URL = "/gestion/media/"
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")

View file

@ -1,4 +1,4 @@
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah"
ADMINS = None
SERVER_EMAIL = "root@vagrant"
EMAIL_HOST = "localhost"

View file

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

View file

@ -1 +1 @@
default_app_config = 'gestioncof.apps.GestioncofConfig'
default_app_config = "gestioncof.apps.GestioncofConfig"

View file

@ -1,23 +1,35 @@
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from gestioncof.models import SurveyQuestionAnswer, SurveyQuestion, \
CofProfile, EventOption, EventOptionChoice, Event, Club, \
Survey, EventCommentField, EventRegistration
from gestioncof.petits_cours_models import PetitCoursDemande, \
PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
PetitCoursAttributionCounter
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group, Permission, User
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from dal.autocomplete import ModelSelect2
from gestioncof.models import (
Club,
CofProfile,
Event,
EventCommentField,
EventOption,
EventOptionChoice,
EventRegistration,
Survey,
SurveyQuestion,
SurveyQuestionAnswer,
)
from gestioncof.petits_cours_models import (
PetitCoursAbility,
PetitCoursAttribution,
PetitCoursAttributionCounter,
PetitCoursDemande,
PetitCoursSubject,
)
def add_link_field(target_model='', field='', link_text=str,
desc_text=str):
def add_link_field(target_model="", field="", link_text=str, desc_text=str):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
@ -28,14 +40,14 @@ def add_link_field(target_model='', field='', link_text=str,
if not link_obj.id:
return ""
url = reverse(reverse_path, args=(link_obj.id,))
return mark_safe("<a href='%s'>%s</a>"
% (url, link_text(link_obj)))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = desc_text(reverse_name + ' link')
link.short_description = desc_text(reverse_name + " link")
cls.link = link
cls.readonly_fields =\
list(getattr(cls, 'readonly_fields', [])) + ['link']
cls.readonly_fields = list(getattr(cls, "readonly_fields", [])) + ["link"]
return cls
return add_link
@ -43,32 +55,28 @@ class SurveyQuestionAnswerInline(admin.TabularInline):
model = SurveyQuestionAnswer
@add_link_field(desc_text=lambda x: "Réponses",
link_text=lambda x: "Éditer les réponses")
@add_link_field(
desc_text=lambda x: "Réponses", link_text=lambda x: "Éditer les réponses"
)
class SurveyQuestionInline(admin.TabularInline):
model = SurveyQuestion
class SurveyQuestionAdmin(admin.ModelAdmin):
search_fields = ('survey__title', 'answer')
inlines = [
SurveyQuestionAnswerInline,
]
search_fields = ("survey__title", "answer")
inlines = [SurveyQuestionAnswerInline]
class SurveyAdmin(admin.ModelAdmin):
search_fields = ('title', 'details')
inlines = [
SurveyQuestionInline,
]
search_fields = ("title", "details")
inlines = [SurveyQuestionInline]
class EventOptionChoiceInline(admin.TabularInline):
model = EventOptionChoice
@add_link_field(desc_text=lambda x: "Choix",
link_text=lambda x: "Éditer les choix")
@add_link_field(desc_text=lambda x: "Choix", link_text=lambda x: "Éditer les choix")
class EventOptionInline(admin.TabularInline):
model = EventOption
@ -78,18 +86,13 @@ class EventCommentFieldInline(admin.TabularInline):
class EventOptionAdmin(admin.ModelAdmin):
search_fields = ('event__title', 'name')
inlines = [
EventOptionChoiceInline,
]
search_fields = ("event__title", "name")
inlines = [EventOptionChoiceInline]
class EventAdmin(admin.ModelAdmin):
search_fields = ('title', 'location', 'description')
inlines = [
EventOptionInline,
EventCommentFieldInline,
]
search_fields = ("title", "location", "description")
inlines = [EventOptionInline, EventCommentFieldInline]
class CofProfileInline(admin.StackedInline):
@ -98,10 +101,9 @@ class CofProfileInline(admin.StackedInline):
class FkeyLookup(object):
def __init__(self, fkeydecl, short_description=None,
admin_order_field=None):
self.fk, fkattrs = fkeydecl.split('__', 1)
self.fkattrs = fkattrs.split('__')
def __init__(self, fkeydecl, short_description=None, admin_order_field=None):
self.fk, fkattrs = fkeydecl.split("__", 1)
self.fkattrs = fkattrs.split("__")
self.short_description = short_description or self.fkattrs[-1]
self.admin_order_field = admin_order_field or fkeydecl
@ -126,19 +128,19 @@ def ProfileInfo(field, short_description, boolean=False):
return getattr(self.profile, field)
except CofProfile.DoesNotExist:
return ""
getter.short_description = short_description
getter.boolean = boolean
return getter
User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper")
User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper")
User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement")
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True)
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True)
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente",
"ML BdA-R", True)
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente", "ML BdA-R", True)
class UserProfileAdmin(UserAdmin):
@ -147,7 +149,8 @@ class UserProfileAdmin(UserAdmin):
return obj.profile.is_buro
except CofProfile.DoesNotExist:
return False
is_buro.short_description = 'Membre du Buro'
is_buro.short_description = "Membre du Buro"
is_buro.boolean = True
def is_cof(self, obj):
@ -155,44 +158,50 @@ class UserProfileAdmin(UserAdmin):
return obj.profile.is_cof
except CofProfile.DoesNotExist:
return False
is_cof.short_description = 'Membre du COF'
is_cof.short_description = "Membre du COF"
is_cof.boolean = True
list_display = (
UserAdmin.list_display
+ ('profile_login_clipper', 'profile_phone', 'profile_occupation',
'profile_mailing_cof', 'profile_mailing_bda',
'profile_mailing_bda_revente', 'is_cof', 'is_buro', )
list_display = UserAdmin.list_display + (
"profile_login_clipper",
"profile_phone",
"profile_occupation",
"profile_mailing_cof",
"profile_mailing_bda",
"profile_mailing_bda_revente",
"is_cof",
"is_buro",
)
list_display_links = ('username', 'email', 'first_name', 'last_name')
list_filter = UserAdmin.list_filter \
+ ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof',
'profile__mailing_bda')
search_fields = UserAdmin.search_fields + ('profile__phone',)
inlines = [
CofProfileInline,
]
list_display_links = ("username", "email", "first_name", "last_name")
list_filter = UserAdmin.list_filter + (
"profile__is_cof",
"profile__is_buro",
"profile__mailing_cof",
"profile__mailing_bda",
)
search_fields = UserAdmin.search_fields + ("profile__phone",)
inlines = [CofProfileInline]
staff_fieldsets = [
(None, {'fields': ['username', 'password']}),
(_('Personal info'), {'fields': ['first_name', 'last_name', 'email']}),
(None, {"fields": ["username", "password"]}),
(_("Personal info"), {"fields": ["first_name", "last_name", "email"]}),
]
def get_fieldsets(self, request, user=None):
if not request.user.is_superuser:
return self.staff_fieldsets
return super(UserProfileAdmin, self).get_fieldsets(request, user)
return super().get_fieldsets(request, user)
def save_model(self, request, user, form, change):
cof_group, created = Group.objects.get_or_create(name='COF')
cof_group, created = Group.objects.get_or_create(name="COF")
if created:
# Si le groupe COF n'était pas déjà dans la bdd
# On lui assigne les bonnes permissions
perms = Permission.objects.filter(
Q(content_type__app_label='gestioncof')
| Q(content_type__app_label='bda')
| (Q(content_type__app_label='auth')
& Q(content_type__model='user')))
Q(content_type__app_label="gestioncof")
| Q(content_type__app_label="bda")
| (Q(content_type__app_label="auth") & Q(content_type__model="user"))
)
cof_group.permissions = perms
# On y associe les membres du Burô
cof_group.user_set = User.objects.filter(profile__is_buro=True)
@ -214,72 +223,97 @@ def user_str(self):
return "{} ({})".format(self.get_full_name(), self.username)
else:
return self.username
User.__str__ = user_str
class EventRegistrationAdminForm(forms.ModelForm):
class Meta:
widgets = {
'user': ModelSelect2(url='cof-user-autocomplete'),
}
widgets = {"user": ModelSelect2(url="cof-user-autocomplete")}
class EventRegistrationAdmin(admin.ModelAdmin):
form = EventRegistrationAdminForm
list_display = ('__str__', 'event', 'user', 'paid')
list_filter = ('paid',)
search_fields = ('user__username', 'user__first_name', 'user__last_name',
'user__email', 'event__title')
list_display = ("__str__", "event", "user", "paid")
list_filter = ("paid",)
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"event__title",
)
class PetitCoursAbilityAdmin(admin.ModelAdmin):
list_display = ('user', 'matiere', 'niveau', 'agrege')
search_fields = ('user__username', 'user__first_name', 'user__last_name',
'user__email', 'matiere__name', 'niveau')
list_filter = ('matiere', 'niveau', 'agrege')
list_display = ("user", "matiere", "niveau", "agrege")
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"matiere__name",
"niveau",
)
list_filter = ("matiere", "niveau", "agrege")
class PetitCoursAttributionAdmin(admin.ModelAdmin):
list_display = ('user', 'demande', 'matiere', 'rank', )
search_fields = ('user__username', 'matiere__name')
list_display = ("user", "demande", "matiere", "rank")
search_fields = ("user__username", "matiere__name")
class PetitCoursAttributionCounterAdmin(admin.ModelAdmin):
list_display = ('user', 'matiere', 'count', )
list_filter = ('matiere',)
search_fields = ('user__username', 'user__first_name', 'user__last_name',
'user__email', 'matiere__name')
actions = ['reset', ]
list_display = ("user", "matiere", "count")
list_filter = ("matiere",)
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"matiere__name",
)
actions = ["reset"]
actions_on_bottom = True
def reset(self, request, queryset):
queryset.update(count=0)
reset.short_description = "Remise à zéro du compteur"
class PetitCoursDemandeAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'agrege_requis', 'niveau', 'created',
'traitee', 'processed')
list_filter = ('traitee', 'niveau')
search_fields = ('name', 'email', 'phone', 'lieu', 'remarques')
list_display = (
"name",
"email",
"agrege_requis",
"niveau",
"created",
"traitee",
"processed",
)
list_filter = ("traitee", "niveau")
search_fields = ("name", "email", "phone", "lieu", "remarques")
class ClubAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = super(ClubAdminForm, self).clean()
respos = cleaned_data.get('respos')
members = cleaned_data.get('membres')
cleaned_data = super().clean()
respos = cleaned_data.get("respos")
members = cleaned_data.get("membres")
for respo in respos.all():
if respo not in members:
raise forms.ValidationError(
"Erreur : le respo %s n'est pas membre du club."
% respo.get_full_name())
% respo.get_full_name()
)
return cleaned_data
class ClubAdmin(admin.ModelAdmin):
list_display = ['name']
list_display = ["name"]
form = ClubAdminForm
@ -294,7 +328,6 @@ admin.site.register(Club, ClubAdmin)
admin.site.register(PetitCoursSubject)
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)
admin.site.register(PetitCoursAttributionCounter,
PetitCoursAttributionCounterAdmin)
admin.site.register(PetitCoursAttributionCounter, PetitCoursAttributionCounterAdmin)
admin.site.register(PetitCoursDemande, PetitCoursDemandeAdmin)
admin.site.register(EventRegistration, EventRegistrationAdmin)

View file

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

View file

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

View file

@ -1,20 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import csv
from django.apps import apps
from django.http import HttpResponse, HttpResponseForbidden
from django.template.defaultfilters import slugify
from django.apps import apps
def export(qs, fields=None):
model = qs.model
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=%s.csv' \
% slugify(model.__name__)
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = "attachment; filename=%s.csv" % slugify(
model.__name__
)
writer = csv.writer(response)
# Write headers to CSV file
if fields:
@ -38,8 +34,9 @@ def export(qs, fields=None):
return response
def admin_list_export(request, model_name, app_label, queryset=None,
fields=None, list_display=True):
def admin_list_export(
request, model_name, app_label, queryset=None, fields=None, list_display=True
):
"""
Put the following line in your urls.py BEFORE your admin include
(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import user_passes_test
@ -7,9 +5,10 @@ def is_cof(user):
try:
profile = user.profile
return profile.is_cof
except:
except Exception:
return False
cof_required = user_passes_test(is_cof)
@ -17,7 +16,8 @@ def is_buro(user):
try:
profile = user.profile
return profile.is_buro
except:
except Exception:
return False
buro_required = user_passes_test(is_buro)

View file

@ -1,16 +1,13 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory
from django.forms.widgets import CheckboxSelectMultiple, RadioSelect
from django.utils.translation import ugettext_lazy as _
from djconfig.forms import ConfigForm
from gestioncof.models import CofProfile, EventCommentValue, \
CalendarSubscription, Club
from gestioncof.widgets import TriStateCheckbox
from bda.models import Spectacle
from gestioncof.models import CalendarSubscription, Club, CofProfile, EventCommentValue
from gestioncof.widgets import TriStateCheckbox
class EventForm(forms.Form):
@ -18,7 +15,7 @@ class EventForm(forms.Form):
event = kwargs.pop("event")
self.event = event
current_choices = kwargs.pop("current_choices", None)
super(EventForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
choices = {}
if current_choices:
for choice in current_choices.all():
@ -28,31 +25,33 @@ class EventForm(forms.Form):
choices[choice.event_option.id].append(choice.id)
all_choices = choices
for option in event.options.all():
choices = [(choice.id, choice.value)
for choice in option.choices.all()]
choices = [(choice.id, choice.value) for choice in option.choices.all()]
if option.multi_choices:
initial = [] if option.id not in all_choices \
else all_choices[option.id]
initial = [] if option.id not in all_choices else all_choices[option.id]
field = forms.MultipleChoiceField(
label=option.name,
choices=choices,
widget=CheckboxSelectMultiple,
required=False,
initial=initial)
initial=initial,
)
else:
initial = None if option.id not in all_choices \
else all_choices[option.id][0]
field = forms.ChoiceField(label=option.name,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial)
initial = (
None if option.id not in all_choices else all_choices[option.id][0]
)
field = forms.ChoiceField(
label=option.name,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial,
)
field.option_id = option.id
self.fields["option_%d" % option.id] = field
def choices(self):
for name, value in self.cleaned_data.items():
if name.startswith('option_'):
if name.startswith("option_"):
yield (self.fields[name].option_id, value)
@ -60,7 +59,7 @@ class SurveyForm(forms.Form):
def __init__(self, *args, **kwargs):
survey = kwargs.pop("survey")
current_answers = kwargs.pop("current_answers", None)
super(SurveyForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
answers = {}
if current_answers:
for answer in current_answers.all():
@ -69,43 +68,44 @@ class SurveyForm(forms.Form):
else:
answers[answer.survey_question.id].append(answer.id)
for question in survey.questions.all():
choices = [(answer.id, answer.answer)
for answer in question.answers.all()]
choices = [(answer.id, answer.answer) for answer in question.answers.all()]
if question.multi_answers:
initial = [] if question.id not in answers\
else answers[question.id]
initial = [] if question.id not in answers else answers[question.id]
field = forms.MultipleChoiceField(
label=question.question,
choices=choices,
widget=CheckboxSelectMultiple,
required=False,
initial=initial)
initial=initial,
)
else:
initial = None if question.id not in answers\
else answers[question.id][0]
field = forms.ChoiceField(label=question.question,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial)
initial = (
None if question.id not in answers else answers[question.id][0]
)
field = forms.ChoiceField(
label=question.question,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial,
)
field.question_id = question.id
self.fields["question_%d" % question.id] = field
def answers(self):
for name, value in self.cleaned_data.items():
if name.startswith('question_'):
if name.startswith("question_"):
yield (self.fields[name].question_id, value)
class SurveyStatusFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
survey = kwargs.pop("survey")
super(SurveyStatusFilterForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
for question in survey.questions.all():
for answer in question.answers.all():
name = "question_%d_answer_%d" % (question.id, answer.id)
if self.is_bound \
and self.data.get(self.add_prefix(name), None):
if self.is_bound and self.data.get(self.add_prefix(name), None):
initial = self.data.get(self.add_prefix(name), None)
else:
initial = "none"
@ -114,27 +114,30 @@ class SurveyStatusFilterForm(forms.Form):
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial)
initial=initial,
)
field.question_id = question.id
field.answer_id = answer.id
self.fields[name] = field
def filters(self):
for name, value in self.cleaned_data.items():
if name.startswith('question_'):
yield (self.fields[name].question_id,
self.fields[name].answer_id, value)
if name.startswith("question_"):
yield (
self.fields[name].question_id,
self.fields[name].answer_id,
value,
)
class EventStatusFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
event = kwargs.pop("event")
super(EventStatusFilterForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
for option in event.options.all():
for choice in option.choices.all():
name = "option_%d_choice_%d" % (option.id, choice.id)
if self.is_bound \
and self.data.get(self.add_prefix(name), None):
if self.is_bound and self.data.get(self.add_prefix(name), None):
initial = self.data.get(self.add_prefix(name), None)
else:
initial = "none"
@ -143,7 +146,8 @@ class EventStatusFilterForm(forms.Form):
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial)
initial=initial,
)
field.option_id = option.id
field.choice_id = choice.id
self.fields[name] = field
@ -153,48 +157,45 @@ class EventStatusFilterForm(forms.Form):
initial = self.data.get(self.add_prefix(name), None)
else:
initial = "none"
field = forms.ChoiceField(label="Événement payé",
choices=[("yes", "yes"), ("no", "no"),
("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial)
field = forms.ChoiceField(
label="Événement payé",
choices=[("yes", "yes"), ("no", "no"), ("none", "none")],
widget=TriStateCheckbox,
required=False,
initial=initial,
)
self.fields[name] = field
def filters(self):
for name, value in self.cleaned_data.items():
if name.startswith('option_'):
yield (self.fields[name].option_id,
self.fields[name].choice_id, value)
if name.startswith("option_"):
yield (self.fields[name].option_id, self.fields[name].choice_id, value)
elif name == "event_has_paid":
yield ("has_paid", None, value)
class UserProfileForm(forms.ModelForm):
first_name = forms.CharField(label=_('Prénom'), max_length=30)
last_name = forms.CharField(label=_('Nom'), max_length=30)
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ["first_name", "last_name", "email"]
def __init__(self, *args, **kw):
super(UserProfileForm, self).__init__(*args, **kw)
self.fields['first_name'].initial = self.instance.user.first_name
self.fields['last_name'].initial = self.instance.user.last_name
def save(self, *args, **kw):
super(UserProfileForm, self).save(*args, **kw)
self.instance.user.first_name = self.cleaned_data.get('first_name')
self.instance.user.last_name = self.cleaned_data.get('last_name')
self.instance.user.save()
class ProfileForm(forms.ModelForm):
class Meta:
model = CofProfile
fields = ["first_name", "last_name", "phone", "mailing_cof",
"mailing_bda", "mailing_bda_revente"]
fields = [
"phone",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
]
class RegistrationUserForm(forms.ModelForm):
def __init__(self, *args, **kw):
super(RegistrationUserForm, self).__init__(*args, **kw)
self.fields['username'].help_text = ""
super().__init__(*args, **kw)
self.fields["username"].help_text = ""
class Meta:
model = User
@ -205,23 +206,23 @@ class RegistrationPassUserForm(RegistrationUserForm):
"""
Formulaire pour changer le mot de passe d'un utilisateur.
"""
password1 = forms.CharField(label=_('Mot de passe'),
widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Confirmation du mot de passe'),
widget=forms.PasswordInput)
password1 = forms.CharField(label=_("Mot de passe"), widget=forms.PasswordInput)
password2 = forms.CharField(
label=_("Confirmation du mot de passe"), widget=forms.PasswordInput
)
def clean_password2(self):
pass1 = self.cleaned_data['password1']
pass2 = self.cleaned_data['password2']
pass1 = self.cleaned_data["password1"]
pass2 = self.cleaned_data["password2"]
if pass1 and pass2:
if pass1 != pass2:
raise forms.ValidationError(_('Mots de passe non identiques.'))
raise forms.ValidationError(_("Mots de passe non identiques."))
return pass2
def save(self, commit=True, *args, **kwargs):
user = super(RegistrationPassUserForm, self).save(commit, *args,
**kwargs)
user.set_password(self.cleaned_data['password2'])
user = super().save(commit, *args, **kwargs)
user.set_password(self.cleaned_data["password2"])
if commit:
user.save()
return user
@ -229,52 +230,70 @@ class RegistrationPassUserForm(RegistrationUserForm):
class RegistrationProfileForm(forms.ModelForm):
def __init__(self, *args, **kw):
super(RegistrationProfileForm, self).__init__(*args, **kw)
self.fields['mailing_cof'].initial = True
self.fields['mailing_bda'].initial = True
self.fields['mailing_bda_revente'].initial = True
super().__init__(*args, **kw)
self.fields["mailing_cof"].initial = True
self.fields["mailing_bda"].initial = True
self.fields["mailing_bda_revente"].initial = True
self.fields["mailing_unernestaparis"].initial = True
self.fields.keyOrder = [
'login_clipper',
'phone',
'occupation',
'departement',
'is_cof',
'type_cotiz',
'mailing_cof',
'mailing_bda',
'mailing_bda_revente',
'comments'
]
"login_clipper",
"phone",
"occupation",
"departement",
"is_cof",
"type_cotiz",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
"comments",
]
class Meta:
model = CofProfile
fields = ("login_clipper", "phone", "occupation",
"departement", "is_cof", "type_cotiz", "mailing_cof",
"mailing_bda", "mailing_bda_revente", "comments")
fields = (
"login_clipper",
"phone",
"occupation",
"departement",
"is_cof",
"type_cotiz",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
"comments",
)
STATUS_CHOICES = (('no', 'Non'),
('wait', 'Oui mais attente paiement'),
('paid', 'Oui payé'),)
STATUS_CHOICES = (
("no", "Non"),
("wait", "Oui mais attente paiement"),
("paid", "Oui payé"),
)
class AdminEventForm(forms.Form):
status = forms.ChoiceField(label="Inscription", initial="no",
choices=STATUS_CHOICES, widget=RadioSelect)
status = forms.ChoiceField(
label="Inscription", initial="no", choices=STATUS_CHOICES, widget=RadioSelect
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event")
registration = kwargs.pop("current_registration", None)
current_choices, paid = \
(registration.options.all(), registration.paid) \
if registration is not None else ([], None)
current_choices, paid = (
(registration.options.all(), registration.paid)
if registration is not None
else ([], None)
)
if paid is True:
kwargs["initial"] = {"status": "paid"}
elif paid is False:
kwargs["initial"] = {"status": "wait"}
else:
kwargs["initial"] = {"status": "no"}
super(AdminEventForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
choices = {}
for choice in current_choices:
if choice.event_option.id not in choices:
@ -283,99 +302,107 @@ class AdminEventForm(forms.Form):
choices[choice.event_option.id].append(choice.id)
all_choices = choices
for option in self.event.options.all():
choices = [(choice.id, choice.value)
for choice in option.choices.all()]
choices = [(choice.id, choice.value) for choice in option.choices.all()]
if option.multi_choices:
initial = [] if option.id not in all_choices\
else all_choices[option.id]
initial = [] if option.id not in all_choices else all_choices[option.id]
field = forms.MultipleChoiceField(
label=option.name,
choices=choices,
widget=CheckboxSelectMultiple,
required=False,
initial=initial)
initial=initial,
)
else:
initial = None if option.id not in all_choices\
else all_choices[option.id][0]
field = forms.ChoiceField(label=option.name,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial)
initial = (
None if option.id not in all_choices else all_choices[option.id][0]
)
field = forms.ChoiceField(
label=option.name,
choices=choices,
widget=RadioSelect,
required=False,
initial=initial,
)
field.option_id = option.id
self.fields["option_%d" % option.id] = field
for commentfield in self.event.commentfields.all():
initial = commentfield.default
if registration is not None:
try:
initial = registration.comments \
.get(commentfield=commentfield).content
initial = registration.comments.get(
commentfield=commentfield
).content
except EventCommentValue.DoesNotExist:
pass
widget = forms.Textarea if commentfield.fieldtype == "text" \
else forms.TextInput
field = forms.CharField(label=commentfield.name,
widget=widget,
required=False,
initial=initial)
widget = (
forms.Textarea if commentfield.fieldtype == "text" else forms.TextInput
)
field = forms.CharField(
label=commentfield.name, widget=widget, required=False, initial=initial
)
field.comment_id = commentfield.id
self.fields["comment_%d" % commentfield.id] = field
def choices(self):
for name, value in self.cleaned_data.items():
if name.startswith('option_'):
if name.startswith("option_"):
yield (self.fields[name].option_id, value)
def comments(self):
for name, value in self.cleaned_data.items():
if name.startswith('comment_'):
if name.startswith("comment_"):
yield (self.fields[name].comment_id, value)
class BaseEventRegistrationFormset(BaseFormSet):
def __init__(self, *args, **kwargs):
self.events = kwargs.pop('events')
self.current_registrations = kwargs.pop('current_registrations', None)
self.events = kwargs.pop("events")
self.current_registrations = kwargs.pop("current_registrations", None)
self.extra = len(self.events)
super(BaseEventRegistrationFormset, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
def _construct_form(self, index, **kwargs):
kwargs['event'] = self.events[index]
kwargs["event"] = self.events[index]
if self.current_registrations is not None:
kwargs['current_registration'] = self.current_registrations[index]
return super(BaseEventRegistrationFormset, self)._construct_form(
index, **kwargs)
kwargs["current_registration"] = self.current_registrations[index]
return super()._construct_form(index, **kwargs)
EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
class CalendarForm(forms.ModelForm):
subscribe_to_events = forms.BooleanField(
initial=True,
label="Événements du COF")
initial=True, label="Événements du COF", required=False
)
subscribe_to_my_shows = forms.BooleanField(
initial=True,
label="Les spectacles pour lesquels j'ai obtenu une place")
initial=True,
label="Les spectacles pour lesquels j'ai obtenu une place",
required=False,
)
other_shows = forms.ModelMultipleChoiceField(
label="Spectacles supplémentaires",
queryset=Spectacle.objects.filter(tirage__active=True),
widget=forms.CheckboxSelectMultiple,
required=False)
label="Spectacles supplémentaires",
queryset=Spectacle.objects.filter(tirage__active=True),
widget=forms.CheckboxSelectMultiple,
required=False,
)
class Meta:
model = CalendarSubscription
fields = ['subscribe_to_events', 'subscribe_to_my_shows',
'other_shows']
fields = ["subscribe_to_events", "subscribe_to_my_shows", "other_shows"]
class ClubsForm(forms.Form):
"""
Formulaire d'inscription d'un membre à plusieurs clubs du COF.
"""
clubs = forms.ModelMultipleChoiceField(
label="Inscriptions aux clubs du COF",
queryset=Club.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False)
label="Inscriptions aux clubs du COF",
queryset=Club.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
# ---
@ -383,9 +410,10 @@ class ClubsForm(forms.Form):
# 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
max_length=2048,
)

View file

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

View file

@ -15,13 +15,14 @@ from django.core.management import call_command
from gestioncof.management.base import MyBaseCommand
from gestioncof.petits_cours_models import (
PetitCoursAbility, PetitCoursSubject, LEVELS_CHOICES,
PetitCoursAttributionCounter
LEVELS_CHOICES,
PetitCoursAbility,
PetitCoursAttributionCounter,
PetitCoursSubject,
)
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'data')
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
class Command(MyBaseCommand):
@ -32,11 +33,11 @@ class Command(MyBaseCommand):
Permet de ne pas créer l'utilisateur "root".
"""
parser.add_argument(
'--no-root',
action='store_true',
dest='no-root',
"--no-root",
action="store_true",
dest="no-root",
default=False,
help='Ne crée pas l\'utilisateur "root"'
help='Ne crée pas l\'utilisateur "root"',
)
def handle(self, *args, **options):
@ -45,24 +46,25 @@ class Command(MyBaseCommand):
# ---
# Gaulois
gaulois = self.from_json('gaulois.json', DATA_DIR, User)
gaulois = self.from_json("gaulois.json", DATA_DIR, User)
for user in gaulois:
user.profile.is_cof = True
user.profile.save()
# Romains
self.from_json('romains.json', DATA_DIR, User)
self.from_json("romains.json", DATA_DIR, User)
# Root
no_root = options.get('no-root', False)
no_root = options.get("no-root", False)
if not no_root:
self.stdout.write("Création de l'utilisateur root")
root, _ = User.objects.get_or_create(
username='root',
first_name='super',
last_name='user',
email='root@localhost')
root.set_password('root')
username="root",
first_name="super",
last_name="user",
email="root@localhost",
)
root.set_password("root")
root.is_staff = True
root.is_superuser = True
root.profile.is_cof = True
@ -87,18 +89,17 @@ class Command(MyBaseCommand):
# L'utilisateur est compétent dans une matière
subject = random.choice(subjects)
if not PetitCoursAbility.objects.filter(
user=user,
matiere=subject).exists():
user=user, matiere=subject
).exists():
PetitCoursAbility.objects.create(
user=user,
matiere=subject,
niveau=random.choice(levels),
agrege=bool(random.randint(0, 1))
agrege=bool(random.randint(0, 1)),
)
# On initialise son compteur d'attributions
PetitCoursAttributionCounter.objects.get_or_create(
user=user,
matiere=subject
user=user, matiere=subject
)
self.stdout.write("- {:d} inscriptions".format(nb_of_teachers))
@ -106,10 +107,10 @@ class Command(MyBaseCommand):
# Le BdA
# ---
call_command('loadbdadevdata')
call_command("loadbdadevdata")
# ---
# La K-Fêt
# ---
call_command('loadkfetdevdata')
call_command("loadkfetdevdata")

View file

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

View file

@ -159,23 +159,23 @@
},
{
"model": "custommail.custommail",
"pk": 3,
"fields": {
"shortname": "bda-revente",
"subject": "{{ show }}",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA",
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente."
},
"pk": 3
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour leur signaler qu'une place vient d'\u00eatre mise en vente.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 4,
"fields": {
"shortname": "bda-shotgun",
"subject": "{{ show }}",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA",
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es."
},
"pk": 4
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",

File diff suppressed because it is too large Load diff

View file

@ -6,14 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0001_initial'),
]
dependencies = [("gestioncof", "0001_initial")]
operations = [
migrations.AlterField(
model_name='petitcoursdemande',
name='processed',
field=models.DateTimeField(null=True, verbose_name='Date de traitement', blank=True),
),
model_name="petitcoursdemande",
name="processed",
field=models.DateTimeField(
null=True, verbose_name="Date de traitement", blank=True
),
)
]

View file

@ -6,14 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0002_enable_unprocessed_demandes'),
]
dependencies = [("gestioncof", "0002_enable_unprocessed_demandes")]
operations = [
migrations.AddField(
model_name='event',
name='image',
field=models.ImageField(upload_to=b'imgs/events/', null=True, verbose_name=b'Image', blank=True),
),
model_name="event",
name="image",
field=models.ImageField(
upload_to=b"imgs/events/", null=True, verbose_name=b"Image", blank=True
),
)
]

View file

@ -8,27 +8,28 @@ def create_mail(apps, schema_editor):
CustomMail = apps.get_model("gestioncof", "CustomMail")
db_alias = schema_editor.connection.alias
if CustomMail.objects.filter(shortname="bienvenue").count() == 0:
CustomMail.objects.using(db_alias).bulk_create([
CustomMail(
shortname="bienvenue",
title="Bienvenue au COF",
content="Mail de bienvenue au COF, envoyé automatiquement à " \
+ "l'inscription.\n\n" \
+ "Les balises {{ ... }} sont interprétées comme expliqué " \
CustomMail.objects.using(db_alias).bulk_create(
[
CustomMail(
shortname="bienvenue",
title="Bienvenue au COF",
content="Mail de bienvenue au COF, envoyé automatiquement à "
+ "l'inscription.\n\n"
+ "Les balises {{ ... }} sont interprétées comme expliqué "
+ "ci-dessous à l'envoi.",
comments="{{ nom }} \t fullname de la personne.\n"\
+ "{{ prenom }} \t prénom de la personne.")
])
comments="{{ nom }} \t fullname de la personne.\n"
+ "{{ prenom }} \t prénom de la personne.",
)
]
)
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0003_event_image'),
]
dependencies = [("gestioncof", "0003_event_image")]
operations = [
# Pas besoin de supprimer le mail lors de la migration dans l'autre
# sens.
migrations.RunPython(create_mail, migrations.RunPython.noop),
migrations.RunPython(create_mail, migrations.RunPython.noop)
]

View file

@ -6,62 +6,71 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0004_registration_mail'),
]
dependencies = [("gestioncof", "0004_registration_mail")]
operations = [
migrations.AlterModelOptions(
name='custommail',
options={'verbose_name': 'Mail personnalisable', 'verbose_name_plural': 'Mails personnalisables'},
name="custommail",
options={
"verbose_name": "Mail personnalisable",
"verbose_name_plural": "Mails personnalisables",
},
),
migrations.AlterModelOptions(
name='eventoptionchoice',
options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'},
name="eventoptionchoice",
options={"verbose_name": "Choix", "verbose_name_plural": "Choix"},
),
migrations.AlterField(
model_name='cofprofile',
name='is_buro',
field=models.BooleanField(default=False, verbose_name='Membre du Bur\xf4'),
model_name="cofprofile",
name="is_buro",
field=models.BooleanField(default=False, verbose_name="Membre du Bur\xf4"),
),
migrations.AlterField(
model_name='cofprofile',
name='num',
field=models.IntegerField(default=0, verbose_name="Num\xe9ro d'adh\xe9rent", blank=True),
model_name="cofprofile",
name="num",
field=models.IntegerField(
default=0, verbose_name="Num\xe9ro d'adh\xe9rent", blank=True
),
),
migrations.AlterField(
model_name='cofprofile',
name='phone',
field=models.CharField(max_length=20, verbose_name='T\xe9l\xe9phone', blank=True),
model_name="cofprofile",
name="phone",
field=models.CharField(
max_length=20, verbose_name="T\xe9l\xe9phone", blank=True
),
),
migrations.AlterField(
model_name='event',
name='old',
field=models.BooleanField(default=False, verbose_name='Archiver (\xe9v\xe9nement fini)'),
model_name="event",
name="old",
field=models.BooleanField(
default=False, verbose_name="Archiver (\xe9v\xe9nement fini)"
),
),
migrations.AlterField(
model_name='event',
name='start_date',
field=models.DateField(null=True, verbose_name='Date de d\xe9but', blank=True),
model_name="event",
name="start_date",
field=models.DateField(
null=True, verbose_name="Date de d\xe9but", blank=True
),
),
migrations.AlterField(
model_name='eventcommentfield',
name='default',
field=models.TextField(verbose_name='Valeur par d\xe9faut', blank=True),
model_name="eventcommentfield",
name="default",
field=models.TextField(verbose_name="Valeur par d\xe9faut", blank=True),
),
migrations.AlterField(
model_name='eventregistration',
name='paid',
field=models.BooleanField(default=False, verbose_name='A pay\xe9'),
model_name="eventregistration",
name="paid",
field=models.BooleanField(default=False, verbose_name="A pay\xe9"),
),
migrations.AlterField(
model_name='survey',
name='details',
field=models.TextField(verbose_name='D\xe9tails', blank=True),
model_name="survey",
name="details",
field=models.TextField(verbose_name="D\xe9tails", blank=True),
),
migrations.AlterField(
model_name='surveyquestionanswer',
name='answer',
field=models.CharField(max_length=200, verbose_name='R\xe9ponse'),
model_name="surveyquestionanswer",
name="answer",
field=models.CharField(max_length=200, verbose_name="R\xe9ponse"),
),
]

View file

@ -1,51 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bda', '0004_mails-rappel'),
("bda", "0004_mails-rappel"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gestioncof', '0005_encoding'),
("gestioncof", "0005_encoding"),
]
operations = [
migrations.CreateModel(
name='CalendarSubscription',
name="CalendarSubscription",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False,
auto_created=True, primary_key=True)),
('token', models.UUIDField()),
('subscribe_to_events', models.BooleanField(default=True)),
('subscribe_to_my_shows', models.BooleanField(default=True)),
('other_shows', models.ManyToManyField(to='bda.Spectacle')),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("token", models.UUIDField()),
("subscribe_to_events", models.BooleanField(default=True)),
("subscribe_to_my_shows", models.BooleanField(default=True)),
("other_shows", models.ManyToManyField(to="bda.Spectacle")),
(
"user",
models.OneToOneField(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
],
),
migrations.AlterModelOptions(
name='custommail',
options={'verbose_name': 'Mail personnalisable',
'verbose_name_plural': 'Mails personnalisables'},
name="custommail",
options={
"verbose_name": "Mail personnalisable",
"verbose_name_plural": "Mails personnalisables",
},
),
migrations.AlterModelOptions(
name='eventoptionchoice',
options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'},
name="eventoptionchoice",
options={"verbose_name": "Choix", "verbose_name_plural": "Choix"},
),
migrations.AlterField(
model_name='event',
name='end_date',
field=models.DateTimeField(null=True, verbose_name=b'Date de fin',
blank=True),
),
migrations.AlterField(
model_name='event',
name='start_date',
model_name="event",
name="end_date",
field=models.DateTimeField(
null=True, verbose_name=b'Date de d\xc3\xa9but', blank=True),
null=True, verbose_name=b"Date de fin", blank=True
),
),
migrations.AlterField(
model_name="event",
name="start_date",
field=models.DateTimeField(
null=True, verbose_name=b"Date de d\xc3\xa9but", blank=True
),
),
]

View file

@ -1,47 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0006_add_calendar'),
]
dependencies = [("gestioncof", "0006_add_calendar")]
operations = [
migrations.AlterField(
model_name='club',
name='name',
field=models.CharField(unique=True, max_length=200,
verbose_name='Nom')
model_name="club",
name="name",
field=models.CharField(unique=True, max_length=200, verbose_name="Nom"),
),
migrations.AlterField(
model_name='club',
name='description',
field=models.TextField(verbose_name='Description', blank=True)
model_name="club",
name="description",
field=models.TextField(verbose_name="Description", blank=True),
),
migrations.AlterField(
model_name='club',
name='membres',
field=models.ManyToManyField(related_name='clubs',
to=settings.AUTH_USER_MODEL,
blank=True),
model_name="club",
name="membres",
field=models.ManyToManyField(
related_name="clubs", to=settings.AUTH_USER_MODEL, blank=True
),
),
migrations.AlterField(
model_name='club',
name='respos',
field=models.ManyToManyField(related_name='clubs_geres',
to=settings.AUTH_USER_MODEL,
blank=True),
model_name="club",
name="respos",
field=models.ManyToManyField(
related_name="clubs_geres", to=settings.AUTH_USER_MODEL, blank=True
),
),
migrations.AlterField(
model_name='event',
name='start_date',
field=models.DateTimeField(null=True,
verbose_name='Date de d\xe9but',
blank=True),
model_name="event",
name="start_date",
field=models.DateTimeField(
null=True, verbose_name="Date de d\xe9but", blank=True
),
),
]

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
def forwards(apps, schema_editor):
@ -11,243 +11,266 @@ def forwards(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0007_alter_club'),
]
dependencies = [("gestioncof", "0007_alter_club")]
operations = [
migrations.AlterField(
model_name='clipper',
name='fullname',
field=models.CharField(verbose_name='Nom complet', max_length=200),
model_name="clipper",
name="fullname",
field=models.CharField(verbose_name="Nom complet", max_length=200),
),
migrations.AlterField(
model_name='clipper',
name='username',
field=models.CharField(verbose_name='Identifiant', max_length=20),
model_name="clipper",
name="username",
field=models.CharField(verbose_name="Identifiant", max_length=20),
),
migrations.AlterField(
model_name='cofprofile',
name='comments',
model_name="cofprofile",
name="comments",
field=models.TextField(
verbose_name="Commentaires visibles par l'utilisateur",
blank=True),
verbose_name="Commentaires visibles par l'utilisateur", blank=True
),
),
migrations.AlterField(
model_name='cofprofile',
name='is_cof',
field=models.BooleanField(verbose_name='Membre du COF',
default=False),
model_name="cofprofile",
name="is_cof",
field=models.BooleanField(verbose_name="Membre du COF", default=False),
),
migrations.AlterField(
model_name='cofprofile',
name='login_clipper',
field=models.CharField(verbose_name='Login clipper', max_length=8,
blank=True),
model_name="cofprofile",
name="login_clipper",
field=models.CharField(
verbose_name="Login clipper", max_length=8, blank=True
),
),
migrations.AlterField(
model_name='cofprofile',
name='mailing_bda',
field=models.BooleanField(verbose_name='Recevoir les mails BdA',
default=False),
),
migrations.AlterField(
model_name='cofprofile',
name='mailing_bda_revente',
model_name="cofprofile",
name="mailing_bda",
field=models.BooleanField(
verbose_name='Recevoir les mails de revente de places BdA',
default=False),
verbose_name="Recevoir les mails BdA", default=False
),
),
migrations.AlterField(
model_name='cofprofile',
name='mailing_cof',
field=models.BooleanField(verbose_name='Recevoir les mails COF',
default=False),
model_name="cofprofile",
name="mailing_bda_revente",
field=models.BooleanField(
verbose_name="Recevoir les mails de revente de places BdA",
default=False,
),
),
migrations.AlterField(
model_name='cofprofile',
name='occupation',
field=models.CharField(verbose_name='Occupation',
choices=[('exterieur', 'Extérieur'),
('1A', '1A'),
('2A', '2A'),
('3A', '3A'),
('4A', '4A'),
('archicube', 'Archicube'),
('doctorant', 'Doctorant'),
('CST', 'CST')],
max_length=9, default='1A'),
model_name="cofprofile",
name="mailing_cof",
field=models.BooleanField(
verbose_name="Recevoir les mails COF", default=False
),
),
migrations.AlterField(
model_name='cofprofile',
name='petits_cours_accept',
field=models.BooleanField(verbose_name='Recevoir des petits cours',
default=False),
model_name="cofprofile",
name="occupation",
field=models.CharField(
verbose_name="Occupation",
choices=[
("exterieur", "Extérieur"),
("1A", "1A"),
("2A", "2A"),
("3A", "3A"),
("4A", "4A"),
("archicube", "Archicube"),
("doctorant", "Doctorant"),
("CST", "CST"),
],
max_length=9,
default="1A",
),
),
migrations.AlterField(
model_name='cofprofile',
name='petits_cours_remarques',
model_name="cofprofile",
name="petits_cours_accept",
field=models.BooleanField(
verbose_name="Recevoir des petits cours", default=False
),
),
migrations.AlterField(
model_name="cofprofile",
name="petits_cours_remarques",
field=models.TextField(
blank=True,
verbose_name='Remarques et précisions pour les petits cours',
default=''),
verbose_name="Remarques et précisions pour les petits cours",
default="",
),
),
migrations.AlterField(
model_name='cofprofile',
name='type_cotiz',
model_name="cofprofile",
name="type_cotiz",
field=models.CharField(
verbose_name='Type de cotisation',
choices=[('etudiant', 'Normalien étudiant'),
('normalien', 'Normalien élève'),
('exterieur', 'Extérieur')],
max_length=9, default='normalien'),
verbose_name="Type de cotisation",
choices=[
("etudiant", "Normalien étudiant"),
("normalien", "Normalien élève"),
("exterieur", "Extérieur"),
],
max_length=9,
default="normalien",
),
),
migrations.AlterField(
model_name='custommail',
name='comments',
model_name="custommail",
name="comments",
field=models.TextField(
verbose_name='Informations contextuelles sur le mail',
blank=True),
verbose_name="Informations contextuelles sur le mail", blank=True
),
),
migrations.AlterField(
model_name='custommail',
name='content',
field=models.TextField(verbose_name='Contenu'),
model_name="custommail",
name="content",
field=models.TextField(verbose_name="Contenu"),
),
migrations.AlterField(
model_name='custommail',
name='title',
field=models.CharField(verbose_name='Titre', max_length=200),
model_name="custommail",
name="title",
field=models.CharField(verbose_name="Titre", max_length=200),
),
migrations.AlterField(
model_name='event',
name='description',
field=models.TextField(verbose_name='Description', blank=True),
model_name="event",
name="description",
field=models.TextField(verbose_name="Description", blank=True),
),
migrations.AlterField(
model_name='event',
name='end_date',
field=models.DateTimeField(null=True, verbose_name='Date de fin',
blank=True),
model_name="event",
name="end_date",
field=models.DateTimeField(
null=True, verbose_name="Date de fin", blank=True
),
),
migrations.AlterField(
model_name='event',
name='image',
field=models.ImageField(upload_to='imgs/events/', null=True,
verbose_name='Image', blank=True),
model_name="event",
name="image",
field=models.ImageField(
upload_to="imgs/events/", null=True, verbose_name="Image", blank=True
),
),
migrations.AlterField(
model_name='event',
name='location',
field=models.CharField(verbose_name='Lieu', max_length=200),
model_name="event",
name="location",
field=models.CharField(verbose_name="Lieu", max_length=200),
),
migrations.AlterField(
model_name='event',
name='registration_open',
field=models.BooleanField(verbose_name='Inscriptions ouvertes',
default=True),
model_name="event",
name="registration_open",
field=models.BooleanField(
verbose_name="Inscriptions ouvertes", default=True
),
),
migrations.AlterField(
model_name='event',
name='title',
field=models.CharField(verbose_name='Titre', max_length=200),
model_name="event",
name="title",
field=models.CharField(verbose_name="Titre", max_length=200),
),
migrations.AlterField(
model_name='eventcommentfield',
name='fieldtype',
field=models.CharField(verbose_name='Type',
choices=[('text', 'Texte long'),
('char', 'Texte court')],
max_length=10, default='text'),
),
migrations.AlterField(
model_name='eventcommentfield',
name='name',
field=models.CharField(verbose_name='Champ', max_length=200),
),
migrations.AlterField(
model_name='eventcommentvalue',
name='content',
field=models.TextField(null=True, verbose_name='Contenu',
blank=True),
),
migrations.AlterField(
model_name='eventoption',
name='multi_choices',
field=models.BooleanField(verbose_name='Choix multiples',
default=False),
),
migrations.AlterField(
model_name='eventoption',
name='name',
field=models.CharField(verbose_name='Option', max_length=200),
),
migrations.AlterField(
model_name='eventoptionchoice',
name='value',
field=models.CharField(verbose_name='Valeur', max_length=200),
),
migrations.AlterField(
model_name='petitcoursability',
name='niveau',
model_name="eventcommentfield",
name="fieldtype",
field=models.CharField(
choices=[('college', 'Collège'), ('lycee', 'Lycée'),
('prepa1styear', 'Prépa 1ère année / L1'),
('prepa2ndyear', 'Prépa 2ème année / L2'),
('licence3', 'Licence 3'),
('other', 'Autre (préciser dans les commentaires)')],
max_length=12, verbose_name='Niveau'),
verbose_name="Type",
choices=[("text", "Texte long"), ("char", "Texte court")],
max_length=10,
default="text",
),
),
migrations.AlterField(
model_name='petitcoursattribution',
name='rank',
model_name="eventcommentfield",
name="name",
field=models.CharField(verbose_name="Champ", max_length=200),
),
migrations.AlterField(
model_name="eventcommentvalue",
name="content",
field=models.TextField(null=True, verbose_name="Contenu", blank=True),
),
migrations.AlterField(
model_name="eventoption",
name="multi_choices",
field=models.BooleanField(verbose_name="Choix multiples", default=False),
),
migrations.AlterField(
model_name="eventoption",
name="name",
field=models.CharField(verbose_name="Option", max_length=200),
),
migrations.AlterField(
model_name="eventoptionchoice",
name="value",
field=models.CharField(verbose_name="Valeur", max_length=200),
),
migrations.AlterField(
model_name="petitcoursability",
name="niveau",
field=models.CharField(
choices=[
("college", "Collège"),
("lycee", "Lycée"),
("prepa1styear", "Prépa 1ère année / L1"),
("prepa2ndyear", "Prépa 2ème année / L2"),
("licence3", "Licence 3"),
("other", "Autre (préciser dans les commentaires)"),
],
max_length=12,
verbose_name="Niveau",
),
),
migrations.AlterField(
model_name="petitcoursattribution",
name="rank",
field=models.IntegerField(verbose_name="Rang dans l'email"),
),
migrations.AlterField(
model_name='petitcoursattributioncounter',
name='count',
field=models.IntegerField(verbose_name="Nombre d'envois",
default=0),
model_name="petitcoursattributioncounter",
name="count",
field=models.IntegerField(verbose_name="Nombre d'envois", default=0),
),
migrations.AlterField(
model_name='petitcoursdemande',
name='niveau',
model_name="petitcoursdemande",
name="niveau",
field=models.CharField(
verbose_name='Niveau',
choices=[('college', 'Collège'), ('lycee', 'Lycée'),
('prepa1styear', 'Prépa 1ère année / L1'),
('prepa2ndyear', 'Prépa 2ème année / L2'),
('licence3', 'Licence 3'),
('other', 'Autre (préciser dans les commentaires)')],
max_length=12, default=''),
verbose_name="Niveau",
choices=[
("college", "Collège"),
("lycee", "Lycée"),
("prepa1styear", "Prépa 1ère année / L1"),
("prepa2ndyear", "Prépa 2ème année / L2"),
("licence3", "Licence 3"),
("other", "Autre (préciser dans les commentaires)"),
],
max_length=12,
default="",
),
),
migrations.AlterField(
model_name='survey',
name='old',
field=models.BooleanField(verbose_name='Archiver (sondage fini)',
default=False),
model_name="survey",
name="old",
field=models.BooleanField(
verbose_name="Archiver (sondage fini)", default=False
),
),
migrations.AlterField(
model_name='survey',
name='survey_open',
field=models.BooleanField(verbose_name='Sondage ouvert',
default=True),
model_name="survey",
name="survey_open",
field=models.BooleanField(verbose_name="Sondage ouvert", default=True),
),
migrations.AlterField(
model_name='survey',
name='title',
field=models.CharField(verbose_name='Titre', max_length=200),
model_name="survey",
name="title",
field=models.CharField(verbose_name="Titre", max_length=200),
),
migrations.AlterField(
model_name='surveyquestion',
name='multi_answers',
field=models.BooleanField(verbose_name='Choix multiples',
default=False),
model_name="surveyquestion",
name="multi_answers",
field=models.BooleanField(verbose_name="Choix multiples", default=False),
),
migrations.AlterField(
model_name='surveyquestion',
name='question',
field=models.CharField(verbose_name='Question', max_length=200),
model_name="surveyquestion",
name="question",
field=models.CharField(verbose_name="Question", max_length=200),
),
migrations.RunPython(forwards, migrations.RunPython.noop),
]

View file

@ -6,12 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0008_py3'),
]
dependencies = [("gestioncof", "0008_py3")]
operations = [
migrations.DeleteModel(
name='Clipper',
),
]
operations = [migrations.DeleteModel(name="Clipper")]

View file

@ -5,12 +5,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0009_delete_clipper'),
]
dependencies = [("gestioncof", "0009_delete_clipper")]
operations = [
migrations.DeleteModel(
name='CustomMail',
),
]
operations = [migrations.DeleteModel(name="CustomMail")]

View file

@ -6,14 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0010_delete_custommail'),
]
dependencies = [("gestioncof", "0010_delete_custommail")]
operations = [
migrations.AlterField(
model_name='cofprofile',
name='login_clipper',
field=models.CharField(verbose_name='Login clipper', blank=True, max_length=32),
),
model_name="cofprofile",
name="login_clipper",
field=models.CharField(
verbose_name="Login clipper", blank=True, max_length=32
),
)
]

View file

@ -6,13 +6,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0010_delete_custommail'),
]
dependencies = [("gestioncof", "0010_delete_custommail")]
operations = [
migrations.RemoveField(
model_name='cofprofile',
name='num',
),
]
operations = [migrations.RemoveField(model_name="cofprofile", name="num")]

View file

@ -7,9 +7,8 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0011_remove_cofprofile_num'),
('gestioncof', '0011_longer_clippers'),
("gestioncof", "0011_remove_cofprofile_num"),
("gestioncof", "0011_longer_clippers"),
]
operations = [
]
operations = []

View file

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

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-02 21:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("gestioncof", "0013_pei")]
operations = [
migrations.AddField(
model_name="cofprofile",
name="mailing_unernestaparis",
field=models.BooleanField(
default=False, verbose_name="Recevoir les mails unErnestAParis"
),
)
]

View file

@ -1,17 +1,13 @@
from django.db import models
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.db.models.signals import post_save, post_delete
from gestioncof.petits_cours_models import choices_length
from bda.models import Spectacle
from gestioncof.petits_cours_models import choices_length
TYPE_COMMENT_FIELD = (
('text', _("Texte long")),
('char', _("Texte court")),
)
TYPE_COMMENT_FIELD = (("text", _("Texte long")), ("char", _("Texte court")))
class CofProfile(models.Model):
@ -49,39 +45,39 @@ class CofProfile(models.Model):
(COTIZ_GRATIS, _("Gratuit")),
)
user = models.OneToOneField(
User, on_delete=models.CASCADE,
related_name="profile",
)
login_clipper = models.CharField(
"Login clipper", max_length=32, blank=True
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
login_clipper = models.CharField("Login clipper", max_length=32, blank=True)
is_cof = models.BooleanField("Membre du COF", default=False)
phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField(_("Occupation"),
default="1A",
choices=OCCUPATION_CHOICES,
max_length=choices_length(
OCCUPATION_CHOICES))
departement = models.CharField(_("Département"), max_length=50,
blank=True)
type_cotiz = models.CharField(_("Type de cotisation"),
default="normalien",
choices=TYPE_COTIZ_CHOICES,
max_length=choices_length(
TYPE_COTIZ_CHOICES))
occupation = models.CharField(
_("Occupation"),
default="1A",
choices=OCCUPATION_CHOICES,
max_length=choices_length(OCCUPATION_CHOICES),
)
departement = models.CharField(_("Département"), max_length=50, blank=True)
type_cotiz = models.CharField(
_("Type de cotisation"),
default="normalien",
choices=TYPE_COTIZ_CHOICES,
max_length=choices_length(TYPE_COTIZ_CHOICES),
)
mailing_cof = models.BooleanField("Recevoir les mails COF", default=False)
mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False)
mailing_unernestaparis = models.BooleanField(
"Recevoir les mails unErnestAParis", default=False
)
mailing_bda_revente = models.BooleanField(
"Recevoir les mails de revente de places BdA", default=False)
comments = models.TextField(
"Commentaires visibles par l'utilisateur", blank=True)
"Recevoir les mails de revente de places BdA", default=False
)
comments = models.TextField("Commentaires visibles par l'utilisateur", blank=True)
is_buro = models.BooleanField("Membre du Burô", default=False)
petits_cours_accept = models.BooleanField(
"Recevoir des petits cours", default=False)
"Recevoir des petits cours", default=False
)
petits_cours_remarques = models.TextField(
_("Remarques et précisions pour les petits cours"),
blank=True, default="")
_("Remarques et précisions pour les petits cours"), blank=True, default=""
)
class Meta:
verbose_name = "Profil COF"
@ -105,8 +101,7 @@ def post_delete_user(sender, instance, *args, **kwargs):
class Club(models.Model):
name = models.CharField("Nom", max_length=200, unique=True)
description = models.TextField("Description", blank=True)
respos = models.ManyToManyField(User, related_name="clubs_geres",
blank=True)
respos = models.ManyToManyField(User, related_name="clubs_geres", blank=True)
membres = models.ManyToManyField(User, related_name="clubs", blank=True)
def __str__(self):
@ -119,10 +114,8 @@ class Event(models.Model):
start_date = models.DateTimeField("Date de début", blank=True, null=True)
end_date = models.DateTimeField("Date de fin", blank=True, null=True)
description = models.TextField("Description", blank=True)
image = models.ImageField("Image", blank=True, null=True,
upload_to="imgs/events/")
registration_open = models.BooleanField("Inscriptions ouvertes",
default=True)
image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/events/")
registration_open = models.BooleanField("Inscriptions ouvertes", default=True)
old = models.BooleanField("Archiver (événement fini)", default=False)
class Meta:
@ -134,12 +127,12 @@ class Event(models.Model):
class EventCommentField(models.Model):
event = models.ForeignKey(
Event, on_delete=models.CASCADE,
related_name="commentfields",
Event, on_delete=models.CASCADE, related_name="commentfields"
)
name = models.CharField("Champ", max_length=200)
fieldtype = models.CharField("Type", max_length=10,
choices=TYPE_COMMENT_FIELD, default="text")
fieldtype = models.CharField(
"Type", max_length=10, choices=TYPE_COMMENT_FIELD, default="text"
)
default = models.TextField("Valeur par défaut", blank=True)
class Meta:
@ -151,12 +144,10 @@ class EventCommentField(models.Model):
class EventCommentValue(models.Model):
commentfield = models.ForeignKey(
EventCommentField, on_delete=models.CASCADE,
related_name="values",
EventCommentField, on_delete=models.CASCADE, related_name="values"
)
registration = models.ForeignKey(
"EventRegistration", on_delete=models.CASCADE,
related_name="comments",
"EventRegistration", on_delete=models.CASCADE, related_name="comments"
)
content = models.TextField("Contenu", blank=True, null=True)
@ -165,10 +156,7 @@ class EventCommentValue(models.Model):
class EventOption(models.Model):
event = models.ForeignKey(
Event, on_delete=models.CASCADE,
related_name="options",
)
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="options")
name = models.CharField("Option", max_length=200)
multi_choices = models.BooleanField("Choix multiples", default=False)
@ -181,8 +169,7 @@ class EventOption(models.Model):
class EventOptionChoice(models.Model):
event_option = models.ForeignKey(
EventOption, on_delete=models.CASCADE,
related_name="choices",
EventOption, on_delete=models.CASCADE, related_name="choices"
)
value = models.CharField("Valeur", max_length=200)
@ -198,8 +185,9 @@ class EventRegistration(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
options = models.ManyToManyField(EventOptionChoice)
filledcomments = models.ManyToManyField(EventCommentField,
through=EventCommentValue)
filledcomments = models.ManyToManyField(
EventCommentField, through=EventCommentValue
)
paid = models.BooleanField("A payé", default=False)
class Meta:
@ -225,8 +213,7 @@ class Survey(models.Model):
class SurveyQuestion(models.Model):
survey = models.ForeignKey(
Survey, on_delete=models.CASCADE,
related_name="questions",
Survey, on_delete=models.CASCADE, related_name="questions"
)
question = models.CharField("Question", max_length=200)
multi_answers = models.BooleanField("Choix multiples", default=False)
@ -240,8 +227,7 @@ class SurveyQuestion(models.Model):
class SurveyQuestionAnswer(models.Model):
survey_question = models.ForeignKey(
SurveyQuestion, on_delete=models.CASCADE,
related_name="answers",
SurveyQuestion, on_delete=models.CASCADE, related_name="answers"
)
answer = models.CharField("Réponse", max_length=200)
@ -255,8 +241,7 @@ class SurveyQuestionAnswer(models.Model):
class SurveyAnswer(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
answers = models.ManyToManyField(SurveyQuestionAnswer,
related_name="selected_by")
answers = models.ManyToManyField(SurveyQuestionAnswer, related_name="selected_by")
class Meta:
verbose_name = "Réponses"
@ -264,8 +249,9 @@ class SurveyAnswer(models.Model):
def __str__(self):
return "Réponse de %s sondage %s" % (
self.user.get_full_name(),
self.survey.title)
self.user.get_full_name(),
self.survey.title,
)
class CalendarSubscription(models.Model):

View file

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

View file

@ -1,30 +1,30 @@
# -*- coding: utf-8 -*-
from functools import reduce
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Min
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
LEVELS_CHOICES = (
('college', _("Collège")),
('lycee', _("Lycée")),
('prepa1styear', _("Prépa 1ère année / L1")),
('prepa2ndyear', _("Prépa 2ème année / L2")),
('licence3', _("Licence 3")),
('other', _("Autre (préciser dans les commentaires)")),
("college", _("Collège")),
("lycee", _("Lycée")),
("prepa1styear", _("Prépa 1ère année / L1")),
("prepa2ndyear", _("Prépa 2ème année / L2")),
("licence3", _("Licence 3")),
("other", _("Autre (préciser dans les commentaires)")),
)
class PetitCoursSubject(models.Model):
name = models.CharField(_("Matière"), max_length=30)
users = models.ManyToManyField(User, related_name="petits_cours_matieres",
through="PetitCoursAbility")
users = models.ManyToManyField(
User, related_name="petits_cours_matieres", through="PetitCoursAbility"
)
class Meta:
verbose_name = "Matière de petits cours"
@ -37,12 +37,11 @@ class PetitCoursSubject(models.Model):
class PetitCoursAbility(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE,
verbose_name=_("Matière"),
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière")
)
niveau = models.CharField(
_("Niveau"), choices=LEVELS_CHOICES, max_length=choices_length(LEVELS_CHOICES)
)
niveau = models.CharField(_("Niveau"),
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES))
agrege = models.BooleanField(_("Agrégé"), default=False)
class Meta:
@ -58,41 +57,50 @@ class PetitCoursAbility(models.Model):
class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300)
phone = models.CharField(_("Téléphone (facultatif)"),
max_length=20, blank=True)
phone = models.CharField(_("Téléphone (facultatif)"), max_length=20, blank=True)
quand = models.CharField(
_("Quand ?"),
help_text=_("Indiquez ici la période désirée pour les petits"
" cours (vacances scolaires, semaine, week-end)."),
max_length=300, blank=True)
help_text=_(
"Indiquez ici la période désirée pour les petits"
" cours (vacances scolaires, semaine, week-end)."
),
max_length=300,
blank=True,
)
freq = models.CharField(
_("Fréquence"),
help_text=_("Indiquez ici la fréquence envisagée "
"(hebdomadaire, 2 fois par semaine, ...)"),
max_length=300, blank=True)
help_text=_(
"Indiquez ici la fréquence envisagée "
"(hebdomadaire, 2 fois par semaine, ...)"
),
max_length=300,
blank=True,
)
lieu = models.CharField(
_("Lieu (si préférence)"),
help_text=_("Si vous avez avez une préférence sur le lieu."),
max_length=300, blank=True)
max_length=300,
blank=True,
)
matieres = models.ManyToManyField(
PetitCoursSubject, verbose_name=_("Matières"),
related_name="demandes")
PetitCoursSubject, verbose_name=_("Matières"), related_name="demandes"
)
agrege_requis = models.BooleanField(_("Agrégé requis"), default=False)
niveau = models.CharField(_("Niveau"),
default="",
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES))
niveau = models.CharField(
_("Niveau"),
default="",
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES),
)
remarques = models.TextField(_("Remarques et précisions"), blank=True)
traitee = models.BooleanField(_("Traitée"), default=False)
traitee_par = models.ForeignKey(
User, on_delete=models.CASCADE,
blank=True, null=True,
User, on_delete=models.CASCADE, blank=True, null=True
)
processed = models.DateTimeField(_("Date de traitement"),
blank=True, null=True)
processed = models.DateTimeField(_("Date de traitement"), blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=True)
def get_candidates(self, redo=False):
@ -107,18 +115,15 @@ class PetitCoursDemande(models.Model):
matiere=matiere,
niveau=self.niveau,
user__profile__is_cof=True,
user__profile__petits_cours_accept=True
user__profile__petits_cours_accept=True,
)
if self.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attrs = self.petitcoursattribution_set.filter(matiere=matiere)
already_proposed = [
attr.user
for attr in attrs
]
already_proposed = [attr.user for attr in attrs]
candidates = candidates.exclude(user__in=already_proposed)
candidates = candidates.order_by('?').select_related().all()
candidates = candidates.order_by("?").select_related().all()
yield (matiere, candidates)
class Meta:
@ -126,25 +131,20 @@ class PetitCoursDemande(models.Model):
verbose_name_plural = "Demandes de petits cours"
def __str__(self):
return "Demande {:d} du {:s}".format(
self.id, self.created.strftime("%d %b %Y")
)
return "Demande {:d} du {:s}".format(self.id, self.created.strftime("%d %b %Y"))
class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
demande = models.ForeignKey(
PetitCoursDemande, on_delete=models.CASCADE,
verbose_name=_("Demande"),
PetitCoursDemande, on_delete=models.CASCADE, verbose_name=_("Demande")
)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE,
verbose_name=_("Matière"),
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière")
)
date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True)
rank = models.IntegerField("Rang dans l'email")
selected = models.BooleanField(_("Sélectionné par le demandeur"),
default=False)
selected = models.BooleanField(_("Sélectionné par le demandeur"), default=False)
class Meta:
verbose_name = "Attribution de petits cours"
@ -159,8 +159,7 @@ class PetitCoursAttribution(models.Model):
class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE,
verbose_name=_("Matiere"),
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matiere")
)
count = models.IntegerField("Nombre d'envois", default=0)
@ -171,15 +170,12 @@ class PetitCoursAttributionCounter(models.Model):
n'existe pas encore, il est initialisé avec le minimum des valeurs des
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(
user=user,
matiere=matiere,
)
counter, created = cls.objects.get_or_create(user=user, matiere=matiere)
if created:
mincount = (
cls.objects.filter(matiere=matiere).exclude(user=user)
.aggregate(Min('count'))
['count__min']
cls.objects.filter(matiere=matiere)
.exclude(user=user)
.aggregate(Min("count"))["count__min"]
)
counter.count = mincount or 0
counter.save()

View file

@ -1,33 +1,31 @@
# -*- coding: utf-8 -*-
import json
from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail
from django.contrib.auth.models import User
from django.views.generic import ListView, DetailView
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import mail
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import DetailView, ListView
from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility
)
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required
from gestioncof.models import CofProfile
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.petits_cours_models import (
PetitCoursAbility,
PetitCoursAttribution,
PetitCoursAttributionCounter,
PetitCoursDemande,
)
class DemandeListView(ListView):
queryset = (
PetitCoursDemande.objects
.prefetch_related('matieres')
.order_by('traitee', '-id')
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
"traitee", "-id"
)
template_name = "petits_cours_demandes_list.html"
paginate_by = 20
@ -35,18 +33,16 @@ class DemandeListView(ListView):
class DemandeDetailView(DetailView):
model = PetitCoursDemande
queryset = (
PetitCoursDemande.objects
.prefetch_related('petitcoursattribution_set',
'matieres')
queryset = PetitCoursDemande.objects.prefetch_related(
"petitcoursattribution_set", "matieres"
)
template_name = "gestioncof/details_demande_petit_cours.html"
context_object_name = "demande"
def get_context_data(self, **kwargs):
context = super(DemandeDetailView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
obj = self.object
context['attributions'] = obj.petitcoursattribution_set.all()
context["attributions"] = obj.petitcoursattribution_set.all()
return context
@ -66,13 +62,15 @@ def traitement(request, demande_id, redo=False):
tuples = []
for candidate in candidates:
user = candidate.user
tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples.append(
(
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere),
)
)
tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))]
candidates = candidates[0 : min(3, len(candidates))]
attribdata[matiere.id] = []
proposals[matiere] = []
for candidate in candidates:
@ -85,8 +83,9 @@ def traitement(request, demande_id, redo=False):
proposed_for[user].append(matiere)
else:
unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals,
proposed_for, unsatisfied, attribdata, redo)
return _finalize_traitement(
request, demande, proposals, proposed_for, unsatisfied, attribdata, redo
)
@buro_required
@ -94,43 +93,56 @@ def retraitement(request, demande_id):
return traitement(request, demande_id, redo=True)
def _finalize_traitement(request, demande, proposals, proposed_for,
unsatisfied, attribdata, redo=False, errors=None):
def _finalize_traitement(
request,
demande,
proposals,
proposed_for,
unsatisfied,
attribdata,
redo=False,
errors=None,
):
proposals = proposals.items()
proposed_for = proposed_for.items()
attribdata = list(attribdata.items())
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals,
"unsatisfied": unsatisfied,
"extra":
'<textarea name="extra" '
mainmail = render_custom_mail(
"petits-cours-mail-demandeur",
{
"proposals": proposals,
"unsatisfied": unsatisfied,
"extra": '<textarea name="extra" '
'style="width:99%; height: 90px;">'
'</textarea>'
})
"</textarea>",
},
)
if errors is not None:
for error in errors:
messages.error(request, error)
return render(request, "gestioncof/traitement_demande_petit_cours.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
"proposed_mails": proposed_mails,
"mainmail": mainmail,
"attribdata": json.dumps(attribdata),
"redo": redo,
})
return render(
request,
"gestioncof/traitement_demande_petit_cours.html",
{
"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
"proposed_mails": proposed_mails,
"mainmail": mainmail,
"attribdata": json.dumps(attribdata),
"redo": redo,
},
)
def _generate_eleve_email(demande, proposed_for):
return [
(
user,
render_custom_mail('petit-cours-mail-eleve', {
"demande": demande,
"matieres": matieres
})
render_custom_mail(
"petit-cours-mail-eleve", {"demande": demande, "matieres": matieres}
),
)
for user, matieres in proposed_for
]
@ -145,25 +157,30 @@ def _traitement_other_preparing(request, demande):
errors = []
for matiere, candidates in demande.get_candidates(redo):
if candidates:
candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates])
candidates = dict(
[(candidate.user.id, candidate.user) for candidate in candidates]
)
attribdata[matiere.id] = []
proposals[matiere] = []
for choice_id in range(min(3, len(candidates))):
choice = int(
request.POST["proposal-{:d}-{:d}"
.format(matiere.id, choice_id)]
request.POST["proposal-{:d}-{:d}".format(matiere.id, choice_id)]
)
if choice == -1:
continue
if choice not in candidates:
errors.append("Choix invalide pour la proposition {:d}"
"en {!s}".format(choice_id + 1, matiere))
errors.append(
"Choix invalide pour la proposition {:d}"
"en {!s}".format(choice_id + 1, matiere)
)
continue
user = candidates[choice]
if user in proposals[matiere]:
errors.append("La proposition {:d} en {!s} est un doublon"
.format(choice_id + 1, matiere))
errors.append(
"La proposition {:d} en {!s} est un doublon".format(
choice_id + 1, matiere
)
)
continue
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
@ -174,15 +191,24 @@ def _traitement_other_preparing(request, demande):
if not proposals[matiere]:
errors.append("Aucune proposition pour {!s}".format(matiere))
elif len(proposals[matiere]) < 3:
errors.append("Seulement {:d} proposition{:s} pour {!s}"
.format(
len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "",
matiere))
errors.append(
"Seulement {:d} proposition{:s} pour {!s}".format(
len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "",
matiere,
)
)
else:
unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals, proposed_for,
unsatisfied, attribdata, errors=errors)
return _finalize_traitement(
request,
demande,
proposals,
proposed_for,
unsatisfied,
attribdata,
errors=errors,
)
def _traitement_other(request, demande, redo):
@ -200,10 +226,12 @@ def _traitement_other(request, demande, redo):
tuples = []
for candidate in candidates:
user = candidate.user
tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples.append(
(
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere),
)
)
tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples)
attribdata[matiere.id] = []
@ -220,13 +248,16 @@ def _traitement_other(request, demande, redo):
unsatisfied.append(matiere)
proposals = proposals.items()
proposed_for = proposed_for.items()
return render(request,
"gestioncof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
})
return render(
request,
"gestioncof/traitement_demande_petit_cours_autre_niveau.html",
{
"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
},
)
def _traitement_post(request, demande):
@ -254,24 +285,32 @@ def _traitement_post(request, demande):
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail_object, mainmail_body = render_custom_mail(
"petits-cours-mail-demandeur",
{
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra
}
{"proposals": proposals_list, "unsatisfied": unsatisfied, "extra": extra},
)
frommail = settings.MAIL_DATA['petits_cours']['FROM']
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
frommail = settings.MAIL_DATA["petits_cours"]["FROM"]
bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
replyto = settings.MAIL_DATA["petits_cours"]["REPLYTO"]
mails_to_send = []
for (user, (mail_object, body)) in proposed_mails:
msg = mail.EmailMessage(mail_object, body, frommail, [user.email],
[bccaddress], headers={'Reply-To': replyto})
msg = mail.EmailMessage(
mail_object,
body,
frommail,
[user.email],
[bccaddress],
headers={"Reply-To": replyto},
)
mails_to_send.append(msg)
mails_to_send.append(mail.EmailMessage(mainmail_object, mainmail_body,
frommail, [demande.email],
[bccaddress],
headers={'Reply-To': replyto}))
mails_to_send.append(
mail.EmailMessage(
mainmail_object,
mainmail_body,
frommail,
[demande.email],
[bccaddress],
headers={"Reply-To": replyto},
)
)
connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send)
with transaction.atomic():
@ -282,18 +321,19 @@ def _traitement_post(request, demande):
)
counter.count += 1
counter.save()
attrib = PetitCoursAttribution(user=user, matiere=matiere,
demande=demande, rank=rank + 1)
attrib = PetitCoursAttribution(
user=user, matiere=matiere, demande=demande, rank=rank + 1
)
attrib.save()
demande.traitee = True
demande.traitee_par = request.user
demande.processed = timezone.now()
demande.save()
return render(request,
"gestioncof/traitement_demande_petit_cours_success.html",
{"demande": demande,
"redo": redo,
})
return render(
request,
"gestioncof/traitement_demande_petit_cours_success.html",
{"demande": demande, "redo": redo},
)
@login_required
@ -310,22 +350,25 @@ def inscription(request):
profile.petits_cours_remarques = request.POST["remarques"]
profile.save()
with transaction.atomic():
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
abilities = PetitCoursAbility.objects.filter(user=request.user).all()
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
ability.user, ability.matiere
)
success = True
formset = MatieresFormSet(instance=request.user)
else:
formset = MatieresFormSet(instance=request.user)
return render(request, "inscription-petit-cours.html",
{"formset": formset, "success": success,
"receive_proposals": profile.petits_cours_accept,
"remarques": profile.petits_cours_remarques})
return render(
request,
"inscription-petit-cours.html",
{
"formset": formset,
"success": success,
"receive_proposals": profile.petits_cours_accept,
"remarques": profile.petits_cours_remarques,
},
)
@csrf_exempt
@ -338,8 +381,9 @@ def demande(request):
success = True
else:
form = DemandeForm()
return render(request, "demande-petit-cours.html", {"form": form,
"success": success})
return render(
request, "demande-petit-cours.html", {"form": form, "success": success}
)
@csrf_exempt
@ -352,5 +396,6 @@ def demande_raw(request):
success = True
else:
form = DemandeForm()
return render(request, "demande-petit-cours-raw.html",
{"form": form, "success": success})
return render(
request, "demande-petit-cours-raw.html", {"form": form, "success": success}
)

View file

@ -1,13 +1,9 @@
from django.conf import settings
from django.contrib.sites.models import Site
from django_cas_ng.backends import CASBackend
from gestioncof.models import CofProfile
class COFCASBackend(CASBackend):
def clean_username(self, username):
# Le CAS de l'ENS accepte les logins avec des espaces au début
# et à la fin, ainsi quavec une casse variable. On normalise pour
@ -24,9 +20,6 @@ class COFCASBackend(CASBackend):
def context_processor(request):
'''Append extra data to the context of the given request'''
data = {
"user": request.user,
"site": Site.objects.get_current(),
}
"""Append extra data to the context of the given request"""
data = {"user": request.user, "site": Site.objects.get_current()}
return data

View file

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

View file

@ -1140,3 +1140,14 @@ p.help-block {
margin: 5px auto;
width: 90%;
}
div.bg-info {
border-radius: 3px;
padding: 0.3em 1em;
margin-left: 1em;
margin-right: 1em;
}
.bootstrap-form-reduce > .form-group {
margin-top: -16px;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,26 +4,23 @@
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>Modifier mon profil</h2>
<form id="profile form-horizontal" method="post" action="{% url 'gestioncof.views.profile' %}">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">
{% for field in form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
</div>
{% if user.profile.comments %}
<div class="row" style="margin: 0 15%;">
<h4>Commentaires</h4>
<p>
{{ user.profile.comments }}
</p>
</div>
{% endif %}
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer" />
</div>
</form>
<h2>Modifier mon profil</h2>
<form id="profile form-horizontal" method="post" action="">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
{{ user_form | bootstrap }}
{{ profile_form | bootstrap }}
</div>
{% if user.profile.comments %}
<div class="row" style="margin: 0 15%;">
<h4>Commentaires</h4>
<p>{{ user.profile.comments }}</p>
</div>
{% endif %}
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer" />
</div>
</form>
{% endblock %}

View file

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

View file

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

View file

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

View file

@ -43,9 +43,10 @@
<li><a href="{% url "bda-etat-places" tirage.id %}">État des demandes</a></li>
{% else %}
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li>
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BdA-Revente</a></li>
<li><a href="{% url "bda-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
<li><a href="{% url "bda-revente-manage" tirage.id %}">Gérer les places que je revends</a></li>
<li><a href="{% url "bda-revente-tirages" tirage.id %}">Voir les reventes en cours</a></li>
<li><a href="{% url "bda-revente-subscribe" tirage.id %}">Indiquer les spectacles qui m'intéressent</a></li>
<li><a href="{% url "bda-revente-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
{% endif %}
</ul>
{% endfor %}

View file

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

View file

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

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