Merge branch 'master' into aureplop/kfet-auth

This commit is contained in:
Aurélien Delobelle 2019-01-14 22:41:38 +01:00
commit fcf4a25745
350 changed files with 29746 additions and 8840 deletions

5
.gitignore vendored
View file

@ -9,8 +9,13 @@ venv/
/src /src
media/ media/
*.log *.log
.sass-cache/
*.sqlite3 *.sqlite3
.coverage
# PyCharm # PyCharm
.idea .idea
.cache .cache
# VSCode
.vscode/

View file

@ -1,6 +1,4 @@
services: image: "python:3.5"
- postgres:latest
- redis:latest
variables: variables:
# GestioCOF settings # GestioCOF settings
@ -10,7 +8,7 @@ variables:
REDIS_PASSWD: "dummy" REDIS_PASSWD: "dummy"
# Cached packages # Cached packages
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
# postgres service configuration # postgres service configuration
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
@ -20,22 +18,44 @@ variables:
# psql password authentication # psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD 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: test:
stage: 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 tblib
- python --version
script: script:
- python manage.py test - coverage run manage.py test --parallel
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 petitscours provisioning shared utils
# Print errors only
- flake8 --exit-zero bda cof gestioncof kfet petitscours 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

14
CHANGELOG Normal file
View file

@ -0,0 +1,14 @@
- Nouveau site du COF en wagtail
- Meilleurs affichage des longues listes de spectacles à cocher dans BdA-Revente
- Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du
COF
* Version 0.2 - 07/11/2018
- Corrections de bugs d'interface dans l'inscription aux tirages BdA
- On peut annuler une revente à tout moment
- Pleiiiiin de tests
* Version 0.1 - 09/09/2018
Début de la numérotation des versions

146
README.md
View file

@ -1,10 +1,79 @@
# GestioCOF # 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 ## 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 ### 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 [Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout 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 le monde à la même configuration de développement (même sous Windows !), et
@ -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 code change, il faut relancer le worker avec `sudo systemctl restart
worker.service` pour visualiser la dernière version du code. 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 ### Mise à jour
Pour mettre à jour les paquets Python, utiliser la commande suivante : 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 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 ## Documentation utilisateur
Une brève documentation utilisateur est accessible sur le 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,16 +1,24 @@
# -*- coding: utf-8 -*-
import autocomplete_light
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from custommail.shortcuts import send_mass_custom_mail
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin 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.template.defaultfilters import pluralize
from django.utils import timezone from django.utils import timezone
from django import forms
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ from bda.models import (
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente Attribution,
CategorieSpectacle,
ChoixSpectacle,
Participant,
Quote,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
class ReadOnlyMixin(object): class ReadOnlyMixin(object):
@ -24,8 +32,17 @@ class ReadOnlyMixin(object):
return readonly_fields + self.readonly_fields_update return readonly_fields + self.readonly_fields_update
class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class ChoixSpectacleInline(admin.TabularInline): class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle model = ChoixSpectacle
form = ChoixSpectacleAdminForm
sortable_field_name = "priority" sortable_field_name = "priority"
@ -34,10 +51,10 @@ class AttributionTabularAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
spectacles = Spectacle.objects.select_related('location') spectacles = Spectacle.objects.select_related("location")
if self.listing is not None: if self.listing is not None:
spectacles = spectacles.filter(listing=self.listing) spectacles = spectacles.filter(listing=self.listing)
self.fields['spectacle'].queryset = spectacles self.fields["spectacle"].queryset = spectacles
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm): class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
@ -61,7 +78,7 @@ class AttributionInline(admin.TabularInline):
class WithListingAttributionInline(AttributionInline): class WithListingAttributionInline(AttributionInline):
exclude = ('given', ) exclude = ("given",)
form = WithListingAttributionTabularAdminForm form = WithListingAttributionTabularAdminForm
listing = True listing = True
@ -72,12 +89,10 @@ class WithoutListingAttributionInline(AttributionInline):
class ParticipantAdminForm(forms.ModelForm): class ParticipantAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['choicesrevente'].queryset = ( self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
Spectacle.objects "location"
.select_related('location')
) )
@ -85,11 +100,13 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
inlines = [WithListingAttributionInline, WithoutListingAttributionInline] inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
def get_queryset(self, request): def get_queryset(self, request):
return Participant.objects.annotate(nb_places=Count('attributions'), return Participant.objects.annotate(
total=Sum('attributions__price')) nb_places=Count("attributions"), total=Sum("attributions__price")
)
def nb_places(self, obj): def nb_places(self, obj):
return obj.nb_places return obj.nb_places
nb_places.admin_order_field = "nb_places" nb_places.admin_order_field = "nb_places"
nb_places.short_description = "Nombre de places" nb_places.short_description = "Nombre de places"
@ -99,33 +116,32 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
return "%.02f" % tot return "%.02f" % tot
else: else:
return "0 €" return "0 €"
total.admin_order_field = "total" total.admin_order_field = "total"
total.short_description = "Total à payer" total.short_description = "Total à payer"
list_display = ("user", "nb_places", "total", "paid", "paymenttype", list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage")
"tirage")
list_filter = ("paid", "tirage") list_filter = ("paid", "tirage")
search_fields = ('user__username', 'user__first_name', 'user__last_name') search_fields = ("user__username", "user__first_name", "user__last_name")
actions = ['send_attribs', ] actions = ["send_attribs"]
actions_on_bottom = True actions_on_bottom = True
list_per_page = 400 list_per_page = 400
readonly_fields = ("total",) readonly_fields = ("total",)
readonly_fields_update = ('user', 'tirage') readonly_fields_update = ("user", "tirage")
form = ParticipantAdminForm form = ParticipantAdminForm
def send_attribs(self, request, queryset): def send_attribs(self, request, queryset):
datatuple = [] datatuple = []
for member in queryset.all(): for member in queryset.all():
attribs = member.attributions.all() attribs = member.attributions.all()
context = {'member': member.user} context = {"member": member.user}
shortname = "" shortname = ""
if len(attribs) == 0: if len(attribs) == 0:
shortname = "bda-attributions-decus" shortname = "bda-attributions-decus"
else: else:
shortname = "bda-attributions" shortname = "bda-attributions"
context['places'] = attribs context["places"] = attribs
print(context) print(context)
datatuple.append((shortname, context, "bda@ens.fr", datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
[member.user.email]))
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
count = len(queryset.all()) count = len(queryset.all())
if count == 1: if count == 1:
@ -134,63 +150,69 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
else: else:
message_bit = "%d membres ont" % count message_bit = "%d membres ont" % count
plural = "s" plural = "s"
self.message_user(request, "%s été informé%s avec succès." self.message_user(
% (message_bit, plural)) request, "%s été informé%s avec succès." % (message_bit, plural)
)
send_attribs.short_description = "Envoyer les résultats par mail" send_attribs.short_description = "Envoyer les résultats par mail"
class AttributionAdminForm(forms.ModelForm): class AttributionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'spectacle' in self.fields: if "spectacle" in self.fields:
self.fields['spectacle'].queryset = ( self.fields["spectacle"].queryset = Spectacle.objects.select_related(
Spectacle.objects "location"
.select_related('location')
) )
if 'participant' in self.fields: if "participant" in self.fields:
self.fields['participant'].queryset = ( self.fields["participant"].queryset = Participant.objects.select_related(
Participant.objects "user", "tirage"
.select_related('user', 'tirage')
) )
def clean(self): def clean(self):
cleaned_data = super(AttributionAdminForm, self).clean() cleaned_data = super().clean()
participant = cleaned_data.get("participant") participant = cleaned_data.get("participant")
spectacle = cleaned_data.get("spectacle") spectacle = cleaned_data.get("spectacle")
if participant and spectacle: if participant and spectacle:
if participant.tirage != spectacle.tirage: if participant.tirage != spectacle.tirage:
raise forms.ValidationError( raise forms.ValidationError(
"Erreur : le participant et le spectacle n'appartiennent" "Erreur : le participant et le spectacle n'appartiennent"
"pas au même tirage") "pas au même tirage"
)
return cleaned_data return cleaned_data
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
def paid(self, obj): def paid(self, obj):
return obj.participant.paid return obj.participant.paid
paid.short_description = 'A payé'
paid.short_description = "A payé"
paid.boolean = True paid.boolean = True
list_display = ("id", "spectacle", "participant", "given", "paid") list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = ('spectacle__title', 'participant__user__username', search_fields = (
'participant__user__first_name', "spectacle__title",
'participant__user__last_name') "participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
)
form = AttributionAdminForm form = AttributionAdminForm
readonly_fields_update = ('spectacle', 'participant') readonly_fields_update = ("spectacle", "participant")
class ChoixSpectacleAdmin(admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(ChoixSpectacle, exclude=[]) form = ChoixSpectacleAdminForm
def tirage(self, obj): def tirage(self, obj):
return obj.participant.tirage return obj.participant.tirage
list_display = ("participant", "tirage", "spectacle", "priority",
"double_choice") list_display = ("participant", "tirage", "spectacle", "priority", "double_choice")
list_filter = ("double_choice", "participant__tirage") list_filter = ("double_choice", "participant__tirage")
search_fields = ('participant__user__username', search_fields = (
'participant__user__first_name', "participant__user__username",
'participant__user__last_name', "participant__user__first_name",
'spectacle__title') "participant__user__last_name",
"spectacle__title",
)
class QuoteInline(admin.TabularInline): class QuoteInline(admin.TabularInline):
@ -200,42 +222,36 @@ class QuoteInline(admin.TabularInline):
class SpectacleAdmin(admin.ModelAdmin): class SpectacleAdmin(admin.ModelAdmin):
inlines = [QuoteInline] inlines = [QuoteInline]
model = Spectacle model = Spectacle
list_display = ("title", "date", "tirage", "location", "slots", "price", list_display = ("title", "date", "tirage", "location", "slots", "price", "listing")
"listing") list_filter = ("location", "tirage")
list_filter = ("location", "tirage",)
search_fields = ("title", "location__name") search_fields = ("title", "location__name")
readonly_fields = ("rappel_sent", ) readonly_fields = ("rappel_sent",)
class TirageAdmin(admin.ModelAdmin): class TirageAdmin(admin.ModelAdmin):
model = Tirage model = Tirage
list_display = ("title", "ouverture", "fermeture", "active", list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage")
"enable_do_tirage") readonly_fields = ("tokens",)
readonly_fields = ("tokens", ) list_filter = ("active",)
list_filter = ("active", ) search_fields = ("title",)
search_fields = ("title", )
class SalleAdmin(admin.ModelAdmin): class SalleAdmin(admin.ModelAdmin):
model = Salle model = Salle
search_fields = ('name', 'address') search_fields = ("name", "address")
class SpectacleReventeAdminForm(forms.ModelForm): class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['answered_mail'].queryset = ( self.fields["confirmed_entry"].queryset = Participant.objects.select_related(
Participant.objects "user", "tirage"
.select_related('user', 'tirage')
) )
self.fields['seller'].queryset = ( self.fields["seller"].queryset = Participant.objects.select_related(
Participant.objects "user", "tirage"
.select_related('user', 'tirage')
) )
self.fields['soldTo'].queryset = ( self.fields["soldTo"].queryset = Participant.objects.select_related(
Participant.objects "user", "tirage"
.select_related('user', 'tirage')
) )
@ -243,6 +259,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
""" """
Administration des reventes de spectacles Administration des reventes de spectacles
""" """
model = SpectacleRevente model = SpectacleRevente
def spectacle(self, obj): def spectacle(self, obj):
@ -254,12 +271,14 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
list_display = ("spectacle", "seller", "date", "soldTo") list_display = ("spectacle", "seller", "date", "soldTo")
raw_id_fields = ("attribution",) raw_id_fields = ("attribution",)
readonly_fields = ("date_tirage",) readonly_fields = ("date_tirage",)
search_fields = ['attribution__spectacle__title', search_fields = [
'seller__user__username', "attribution__spectacle__title",
'seller__user__first_name', "seller__user__username",
'seller__user__last_name'] "seller__user__first_name",
"seller__user__last_name",
]
actions = ['transfer', 'reinit'] actions = ["transfer", "reinit"]
actions_on_bottom = True actions_on_bottom = True
form = SpectacleReventeAdminForm form = SpectacleReventeAdminForm
@ -275,10 +294,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
attrib.save() attrib.save()
self.message_user( self.message_user(
request, request,
"%d attribution%s %s été transférée%s avec succès." % ( "%d attribution%s %s été transférée%s avec succès."
count, pluralize(count), % (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
pluralize(count, "a,ont"), pluralize(count)) )
)
transfer.short_description = "Transférer les reventes sélectionnées" transfer.short_description = "Transférer les reventes sélectionnées"
def reinit(self, request, queryset): def reinit(self, request, queryset):
@ -287,20 +306,15 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
""" """
count = queryset.count() count = queryset.count()
for revente in queryset.filter( for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()): attribution__spectacle__date__gte=timezone.now()
revente.date = timezone.now() - timedelta(hours=1) ):
revente.soldTo = None revente.reset(new_date=timezone.now() - timedelta(hours=1))
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
self.message_user( self.message_user(
request, request,
"%d attribution%s %s été réinitialisée%s avec succès." % ( "%d attribution%s %s été réinitialisée%s avec succès."
count, pluralize(count), % (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
pluralize(count, "a,ont"), pluralize(count)) )
)
reinit.short_description = "Réinitialiser les reventes sélectionnées" 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 import random
@ -22,7 +14,7 @@ class Algorithm(object):
show.requests show.requests
- on crée des tables de demandes pour chaque personne, afin de - on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings""" pouvoir modifier les rankings"""
self.max_group = 2*max(choice.priority for choice in choices) self.max_group = 2 * max(choice.priority for choice in choices)
self.shows = [] self.shows = []
showdict = {} showdict = {}
for show in shows: for show in shows:
@ -60,16 +52,19 @@ class Algorithm(object):
self.ranks[member][show] -= increment self.ranks[member][show] -= increment
def appendResult(self, l, member, show): def appendResult(self, l, member, show):
l.append((member, l.append(
self.ranks[member][show], (
self.origranks[member][show], member,
self.choices[member][show].double)) self.ranks[member][show],
self.origranks[member][show],
self.choices[member][show].double,
)
)
def __call__(self, seed): def __call__(self, seed):
random.seed(seed) random.seed(seed)
results = [] results = []
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True)
reverse=True)
for show in shows: for show in shows:
# On regroupe tous les gens ayant le même rang # On regroupe tous les gens ayant le même rang
groups = dict([(i, []) for i in range(1, self.max_group + 1)]) 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: if len(winners) + 1 < show.slots:
self.appendResult(winners, member, show) self.appendResult(winners, member, show)
self.appendResult(winners, member, show) self.appendResult(winners, member, show)
elif not self.choices[member][show].autoquit \ elif (
and len(winners) < show.slots: not self.choices[member][show].autoquit
and len(winners) < show.slots
):
self.appendResult(winners, member, show) self.appendResult(winners, member, show)
self.appendResult(losers, member, show) self.appendResult(losers, member, show)
else: else:

View file

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

View file

@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
from django import forms from django import forms
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from django.template import loader
from django.utils import timezone from django.utils import timezone
from bda.models import Attribution, Spectacle from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet): class InscriptionInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -18,9 +16,9 @@ class InscriptionInlineFormSet(BaseInlineFormSet):
# set once for all "spectacle" field choices # set once for all "spectacle" field choices
# - restrict choices to the spectacles of this tirage # - restrict choices to the spectacles of this tirage
# - force_choices avoid many db requests # - 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] 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): def force_choices(self, name, choices):
"""Set choices of a field. """Set choices of a field.
@ -32,7 +30,7 @@ class InscriptionInlineFormSet(BaseInlineFormSet):
for form in self.forms: for form in self.forms:
field = form.fields[name] field = form.fields[name]
if field.empty_label is not None: if field.empty_label is not None:
field.choices = [('', field.empty_label)] + choices field.choices = [("", field.empty_label)] + choices
else: else:
field.choices = choices field.choices = choices
@ -41,77 +39,146 @@ class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea()) token = forms.CharField(widget=forms.widgets.Textarea())
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): class TemplateLabelField(forms.ModelMultipleChoiceField):
"""
Extends ModelMultipleChoiceField to offer two more customization options :
- `label_from_instance` can be used with a template file
- the widget rendering template can be specified with `option_template_name`
"""
def __init__(
self,
label_template_name=None,
context_object_name="obj",
option_template_name=None,
*args,
**kwargs
):
super().__init__(*args, **kwargs)
self.label_template_name = label_template_name
self.context_object_name = context_object_name
if option_template_name is not None:
self.widget.option_template_name = option_template_name
def label_from_instance(self, obj): def label_from_instance(self, obj):
return "%s" % str(obj.spectacle) if self.label_template_name is None:
return super().label_from_instance(obj)
else:
return loader.render_to_string(
self.label_template_name, context={self.context_object_name: obj}
)
# Formulaires pour revente_manage
class ResellForm(forms.Form): class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields["attributions"] = TemplateLabelField(
participant.attribution_set queryset=participant.attribution_set.filter(
.filter(spectacle__date__gte=timezone.now()) spectacle__date__gte=timezone.now()
)
.exclude(revente__seller=participant) .exclude(revente__seller=participant)
.select_related('spectacle', 'spectacle__location', .select_related("spectacle", "spectacle__location", "participant__user"),
'participant__user') widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/attribution_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="attribution",
) )
class AnnulForm(forms.Form): class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(AnnulForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields["reventes"] = TemplateLabelField(
participant.attribution_set label="",
.filter(spectacle__date__gte=timezone.now(), queryset=participant.original_shows.filter(
revente__isnull=False, attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
revente__notif_sent=False, )
revente__soldTo__isnull=True) .select_related(
.select_related('spectacle', 'spectacle__location', "attribution__spectacle", "attribution__spectacle__location"
'participant__user') )
) .order_by("-date"),
widget=forms.CheckboxSelectMultiple,
required=False,
class InscriptionReventeForm(forms.Form): label_template_name="bda/forms/revente_self_label_table.html",
spectacles = forms.ModelMultipleChoiceField( option_template_name="bda/forms/checkbox_table.html",
queryset=Spectacle.objects.none(), context_object_name="revente",
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())
) )
class SoldForm(forms.Form): class SoldForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(SoldForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields["reventes"] = TemplateLabelField(
participant.attribution_set queryset=participant.original_shows.filter(soldTo__isnull=False)
.filter(revente__isnull=False, .exclude(soldTo=participant)
revente__soldTo__isnull=False) .select_related(
.exclude(revente__soldTo=participant) "attribution__spectacle", "attribution__spectacle__location"
.select_related('spectacle', 'spectacle__location', ),
'participant__user') widget=forms.CheckboxSelectMultiple,
label_template_name="bda/forms/revente_sold_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
# Formulaire pour revente_subscribe
class InscriptionReventeForm(forms.Form):
def __init__(self, tirage, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["spectacles"] = TemplateLabelField(
queryset=tirage.spectacle_set.select_related("location").filter(
date__gte=timezone.now()
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/spectacle_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="spectacle",
)
# Formulaires pour revente_tirages
class ReventeTirageAnnulForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=participant.entered.filter(soldTo__isnull=True).select_related(
"attribution__spectacle", "seller__user"
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_other_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
class ReventeTirageForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=(
SpectacleRevente.objects.filter(
notif_sent=True,
shotgun=False,
tirage_done=False,
attribution__spectacle__tirage=participant.tirage,
)
.exclude(confirmed_entry=participant)
.select_related("attribution__spectacle")
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_other_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
) )

View file

@ -5,17 +5,15 @@ Crée deux tirages de test et y inscrit les utilisateurs
import os import os
import random import random
from django.utils import timezone
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone
from gestioncof.management.base import MyBaseCommand from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
from bda.views import do_tirage from bda.views import do_tirage
from gestioncof.management.base import MyBaseCommand
# Où sont stockés les fichiers json # Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
'data')
class Command(MyBaseCommand): class Command(MyBaseCommand):
@ -27,27 +25,29 @@ class Command(MyBaseCommand):
# --- # ---
Tirage.objects.all().delete() Tirage.objects.all().delete()
Tirage.objects.bulk_create([ Tirage.objects.bulk_create(
Tirage( [
title="Tirage de test 1", Tirage(
ouverture=timezone.now()-timezone.timedelta(days=7), title="Tirage de test 1",
fermeture=timezone.now(), ouverture=timezone.now() - timezone.timedelta(days=7),
active=True fermeture=timezone.now(),
), active=True,
Tirage( ),
title="Tirage de test 2", Tirage(
ouverture=timezone.now(), title="Tirage de test 2",
fermeture=timezone.now()+timezone.timedelta(days=60), ouverture=timezone.now(),
active=True fermeture=timezone.now() + timezone.timedelta(days=60),
) active=True,
]) ),
]
)
tirages = Tirage.objects.all() tirages = Tirage.objects.all()
# --- # ---
# Salles # Salles
# --- # ---
locations = self.from_json('locations.json', DATA_DIR, Salle) locations = self.from_json("locations.json", DATA_DIR, Salle)
# --- # ---
# Spectacles # Spectacles
@ -60,15 +60,13 @@ class Command(MyBaseCommand):
""" """
show.tirage = random.choice(tirages) show.tirage = random.choice(tirages)
show.listing = bool(random.randint(0, 1)) show.listing = bool(random.randint(0, 1))
show.date = ( show.date = show.tirage.fermeture + timezone.timedelta(
show.tirage.fermeture days=random.randint(60, 90)
+ timezone.timedelta(days=random.randint(60, 90))
) )
show.location = random.choice(locations) show.location = random.choice(locations)
return show 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 # Inscriptions
@ -79,23 +77,19 @@ class Command(MyBaseCommand):
choices = [] choices = []
for user in User.objects.filter(profile__is_cof=True): for user in User.objects.filter(profile__is_cof=True):
for tirage in tirages: for tirage in tirages:
part, _ = Participant.objects.get_or_create( part, _ = Participant.objects.get_or_create(user=user, tirage=tirage)
user=user,
tirage=tirage
)
shows = random.sample( shows = random.sample(
list(tirage.spectacle_set.all()), list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
tirage.spectacle_set.count() // 2
) )
for (rank, show) in enumerate(shows): for (rank, show) in enumerate(shows):
choices.append(ChoixSpectacle( choices.append(
participant=part, ChoixSpectacle(
spectacle=show, participant=part,
priority=rank + 1, spectacle=show,
double_choice=random.choice( priority=rank + 1,
['1', 'double', 'autoquit'] double_choice=random.choice(["1", "double", "autoquit"]),
) )
)) )
ChoixSpectacle.objects.bulk_create(choices) ChoixSpectacle.objects.bulk_create(choices)
self.stdout.write("- {:d} inscriptions générées".format(len(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. Gestion en ligne de commande des reventes.
""" """
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.utils import timezone from django.utils import timezone
from bda.models import SpectacleRevente from bda.models import SpectacleRevente
class Command(BaseCommand): class Command(BaseCommand):
help = "Envoie les mails de notification et effectue " \ help = (
"les tirages au sort des reventes" "Envoie les mails de notification et effectue les tirages au sort des reventes"
)
leave_locale_alone = True leave_locale_alone = True
def handle(self, *args, **options): def handle(self, *args, **options):
now = timezone.now() now = timezone.now()
reventes = SpectacleRevente.objects.all() reventes = SpectacleRevente.objects.all()
for revente in reventes: for revente in reventes:
# Check si < 24h # Le spectacle est bientôt et on a pas encore envoyé de mail :
if (revente.attribution.spectacle.date <= # on met la place au shotgun et on prévient.
revente.date + timedelta(days=1)) and \ if revente.is_urgent and not revente.notif_sent:
now >= revente.date + timedelta(minutes=15) and \ if revente.can_notif:
not revente.notif_sent: self.stdout.write(str(now))
self.stdout.write(str(now)) revente.mail_shotgun()
revente.mail_shotgun() self.stdout.write(
self.stdout.write("Mail de disponibilité immédiate envoyé") "Mails de disponibilité immédiate envoyés "
# Check si délai de retrait dépassé "pour la revente [%s]" % revente
elif (now >= revente.date + timedelta(hours=1) and )
not revente.notif_sent):
# Le spectacle est dans plus longtemps : on prévient
elif revente.can_notif and not revente.notif_sent:
self.stdout.write(str(now)) self.stdout.write(str(now))
revente.send_notif() revente.send_notif()
self.stdout.write("Mail d'inscription à une revente envoyé") self.stdout.write(
# Check si tirage à faire "Mails d'inscription à la revente [%s] envoyés" % revente
elif (now >= revente.date_tirage and )
not revente.tirage_done):
# On fait le tirage
elif now >= revente.date_tirage and not revente.tirage_done:
self.stdout.write(str(now)) self.stdout.write(str(now))
revente.tirage() winner = revente.tirage()
self.stdout.write("Tirage effectué, mails envoyés") 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. Gestion en ligne de commande des mails de rappel.
""" """
from __future__ import unicode_literals
from datetime import timedelta from datetime import timedelta
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from bda.models import Spectacle from bda.models import Spectacle
class Command(BaseCommand): class Command(BaseCommand):
help = 'Envoie les mails de rappel des spectacles dont la date ' \ help = (
'approche.\nNe renvoie pas les mails déjà envoyés.' "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 leave_locale_alone = True
def handle(self, *args, **options): def handle(self, *args, **options):
now = timezone.now() now = timezone.now()
delay = timedelta(days=4) delay = timedelta(days=4)
shows = Spectacle.objects \ shows = (
.filter(date__range=(now, now+delay)) \ Spectacle.objects.filter(date__range=(now, now + delay))
.filter(tirage__active=True) \ .filter(tirage__active=True)
.filter(rappel_sent__isnull=True) \ .filter(rappel_sent__isnull=True)
.all() .all()
)
for show in shows: for show in shows:
show.send_rappel() show.send_rappel()
self.stdout.write( self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show)
'Mails de rappels pour %s envoyés avec succès.' % show)
if not shows: if not shows:
self.stdout.write('Aucun mail à envoyer.') self.stdout.write("Aucun mail à envoyer.")

View file

@ -1,108 +1,206 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Attribution', name="Attribution",
fields=[ 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( migrations.CreateModel(
name='ChoixSpectacle', name="ChoixSpectacle",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
('priority', models.PositiveIntegerField(verbose_name=b'Priorit\xc3\xa9')), "id",
('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')])), 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={ options={
'ordering': ('priority',), "ordering": ("priority",),
'verbose_name': 'voeu', "verbose_name": "voeu",
'verbose_name_plural': 'voeux', "verbose_name_plural": "voeux",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Participant', name="Participant",
fields=[ 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')), "id",
('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')])), 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( migrations.CreateModel(
name='Salle', name="Salle",
fields=[ 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')), "id",
('address', models.TextField(verbose_name=b'Adresse')), 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( migrations.CreateModel(
name='Spectacle', name="Spectacle",
fields=[ 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')), "id",
('date', models.DateTimeField(verbose_name=b'Date & heure')), models.AutoField(
('description', models.TextField(verbose_name=b'Description', blank=True)), verbose_name="ID",
('slots_description', models.TextField(verbose_name=b'Description des places', blank=True)), serialize=False,
('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)), auto_created=True,
('slots', models.IntegerField(verbose_name=b'Places')), primary_key=True,
('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')), ),
('location', models.ForeignKey(to='bda.Salle')), ),
("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={ options={
'ordering': ('priority', 'date', 'title'), "ordering": ("priority", "date", "title"),
'verbose_name': 'Spectacle', "verbose_name": "Spectacle",
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='participant', model_name="participant",
name='attributions', name="attributions",
field=models.ManyToManyField(related_name='attributed_to', through='bda.Attribution', to='bda.Spectacle'), field=models.ManyToManyField(
related_name="attributed_to",
through="bda.Attribution",
to="bda.Spectacle",
),
), ),
migrations.AddField( migrations.AddField(
model_name='participant', model_name="participant",
name='choices', name="choices",
field=models.ManyToManyField(related_name='chosen_by', through='bda.ChoixSpectacle', to='bda.Spectacle'), field=models.ManyToManyField(
related_name="chosen_by",
through="bda.ChoixSpectacle",
to="bda.Spectacle",
),
), ),
migrations.AddField( migrations.AddField(
model_name='participant', model_name="participant",
name='user', name="user",
field=models.OneToOneField(to=settings.AUTH_USER_MODEL), field=models.OneToOneField(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
), ),
migrations.AddField( migrations.AddField(
model_name='choixspectacle', model_name="choixspectacle",
name='participant', name="participant",
field=models.ForeignKey(to='bda.Participant'), field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='choixspectacle', model_name="choixspectacle",
name='spectacle', name="spectacle",
field=models.ForeignKey(related_name='participants', to='bda.Spectacle'), field=models.ForeignKey(
related_name="participants",
to="bda.Spectacle",
on_delete=models.CASCADE,
),
), ),
migrations.AddField( migrations.AddField(
model_name='attribution', model_name="attribution",
name='participant', name="participant",
field=models.ForeignKey(to='bda.Participant'), field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
), ),
migrations.AddField( migrations.AddField(
model_name='attribution', model_name="attribution",
name='spectacle', name="spectacle",
field=models.ForeignKey(related_name='attribues', to='bda.Spectacle'), field=models.ForeignKey(
related_name="attribues", to="bda.Spectacle", on_delete=models.CASCADE
),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='choixspectacle', name="choixspectacle", unique_together=set([("participant", "spectacle")])
unique_together=set([('participant', 'spectacle')]),
), ),
] ]

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings from django.conf import settings
from django.db import migrations, models
from django.utils import timezone from django.utils import timezone
@ -36,49 +36,77 @@ def fill_tirage_fields(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0001_initial")]
('bda', '0001_initial'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Tirage', name="Tirage",
fields=[ 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')), "id",
('ouverture', models.DateTimeField(verbose_name=b"Date et heure d'ouverture du tirage")), models.AutoField(
('fermeture', models.DateTimeField(verbose_name=b'Date et heure de fermerture du tirage')), verbose_name="ID",
('token', models.TextField(verbose_name=b'Graine du tirage', blank=True)), serialize=False,
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')), 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( migrations.AlterField(
model_name='participant', model_name="participant",
name='user', name="user",
field=models.ForeignKey(to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
), ),
# Create fields `spectacle` for `Participant` and `Spectacle` models. # Create fields `spectacle` for `Participant` and `Spectacle` models.
# These fields are not nullable, but we first create them as nullable # These fields are not nullable, but we first create them as nullable
# to give a default value for existing instances of these models. # to give a default value for existing instances of these models.
migrations.AddField( migrations.AddField(
model_name='participant', model_name="participant",
name='tirage', name="tirage",
field=models.ForeignKey(to='bda.Tirage', null=True), field=models.ForeignKey(
to="bda.Tirage", null=True, on_delete=models.CASCADE
),
), ),
migrations.AddField( migrations.AddField(
model_name='spectacle', model_name="spectacle",
name='tirage', name="tirage",
field=models.ForeignKey(to='bda.Tirage', null=True), field=models.ForeignKey(
to="bda.Tirage", null=True, on_delete=models.CASCADE
),
), ),
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop), migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
migrations.AlterField( migrations.AlterField(
model_name='participant', model_name="participant",
name='tirage', name="tirage",
field=models.ForeignKey(to='bda.Tirage'), field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
), ),
migrations.AlterField( migrations.AlterField(
model_name='spectacle', model_name="spectacle",
name='tirage', name="tirage",
field=models.ForeignKey(to='bda.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): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0002_add_tirage")]
('bda', '0002_add_tirage'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='spectacle', model_name="spectacle",
name='price', name="price",
field=models.FloatField(verbose_name=b"Prix d'une place"), field=models.FloatField(verbose_name=b"Prix d'une place"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='tirage', model_name="tirage",
name='active', name="active",
field=models.BooleanField(default=False, verbose_name=b'Tirage actif'), 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0003_update_tirage_and_spectacle")]
('bda', '0003_update_tirage_and_spectacle'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='spectacle', model_name="spectacle",
name='listing', name="listing",
field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'), field=models.BooleanField(
default=False, verbose_name=b"Les places sont sur listing"
),
preserve_default=False, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='spectacle', model_name="spectacle",
name='rappel_sent', name="rappel_sent",
field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True), 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0004_mails-rappel")]
('bda', '0004_mails-rappel'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='choixspectacle', model_name="choixspectacle",
name='priority', name="priority",
field=models.PositiveIntegerField(verbose_name='Priorit\xe9'), field=models.PositiveIntegerField(verbose_name="Priorit\xe9"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='spectacle', model_name="spectacle",
name='priority', name="priority",
field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'), field=models.IntegerField(default=1000, verbose_name="Priorit\xe9"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='spectacle', model_name="spectacle",
name='rappel_sent', name="rappel_sent",
field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True), 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 db_alias = schema_editor.connection.alias
for tirage in Tirage.objects.using(db_alias).all(): for tirage in Tirage.objects.using(db_alias).all():
if tirage.tokens: if tirage.tokens:
tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % ( tirage.tokens = 'Before %s\n"""%s"""\n' % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"), timezone.now().strftime("%y-%m-%d %H:%M:%S"),
tirage.tokens) tirage.tokens,
)
tirage.save() tirage.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0005_encoding")]
('bda', '0005_encoding'),
]
operations = [ operations = [
migrations.RenameField('tirage', 'token', 'tokens'), migrations.RenameField("tirage", "token", "tokens"),
migrations.AddField( migrations.AddField(
model_name='tirage', model_name="tirage",
name='enable_do_tirage', name="enable_do_tirage",
field=models.BooleanField( field=models.BooleanField(
default=False, default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9"
verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'), ),
), ),
migrations.RunPython(forwards_func, migrations.RunPython.noop), migrations.RunPython(forwards_func, migrations.RunPython.noop),
] ]

View file

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

View file

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

View file

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

View file

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

View file

@ -6,17 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0010_spectaclerevente_shotgun")]
('bda', '0010_spectaclerevente_shotgun'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='tirage', model_name="tirage",
name='appear_catalogue', name="appear_catalogue",
field=models.BooleanField( field=models.BooleanField(
default=False, default=False, verbose_name="Tirage à afficher dans le catalogue"
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,22 +1,22 @@
# -*- coding: utf-8 -*-
import calendar import calendar
import random import random
from datetime import timedelta 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.contrib.sites.models import Site
from django.core import mail
from django.db import models from django.db import models
from django.db.models import Count from django.db.models import Count
from django.contrib.auth.models import User from django.utils import formats, timezone
from django.conf import settings
from django.utils import timezone, formats
def get_generic_user(): def get_generic_user():
generic, _ = User.objects.get_or_create( generic, _ = User.objects.get_or_create(
username="bda_generic", 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 return generic
@ -28,15 +28,15 @@ class Tirage(models.Model):
tokens = models.TextField("Graine(s) du tirage", blank=True) tokens = models.TextField("Graine(s) du tirage", blank=True)
active = models.BooleanField("Tirage actif", default=False) active = models.BooleanField("Tirage actif", default=False)
appear_catalogue = models.BooleanField( appear_catalogue = models.BooleanField(
"Tirage à afficher dans le catalogue", "Tirage à afficher dans le catalogue", default=False
default=False
) )
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
default=False)
def __str__(self): def __str__(self):
return "%s - %s" % (self.title, formats.localize( return "%s - %s" % (
timezone.template_localtime(self.fermeture))) self.title,
formats.localize(timezone.template_localtime(self.fermeture)),
)
class Salle(models.Model): class Salle(models.Model):
@ -48,7 +48,7 @@ class Salle(models.Model):
class CategorieSpectacle(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): def __str__(self):
return self.name return self.name
@ -59,26 +59,27 @@ class CategorieSpectacle(models.Model):
class Spectacle(models.Model): class Spectacle(models.Model):
title = models.CharField("Titre", max_length=300) title = models.CharField("Titre", max_length=300)
category = models.ForeignKey(CategorieSpectacle, blank=True, null=True) category = models.ForeignKey(
CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True
)
date = models.DateTimeField("Date & heure") date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle) location = models.ForeignKey(Salle, on_delete=models.CASCADE)
vips = models.TextField('Personnalités', blank=True) vips = models.TextField("Personnalités", blank=True)
description = models.TextField("Description", blank=True) description = models.TextField("Description", blank=True)
slots_description = models.TextField("Description des places", blank=True) slots_description = models.TextField("Description des places", blank=True)
image = models.ImageField('Image', blank=True, null=True, image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/")
upload_to='imgs/shows/') ext_link = models.CharField(
ext_link = models.CharField('Lien vers le site du spectacle', blank=True, "Lien vers le site du spectacle", blank=True, max_length=500
max_length=500) )
price = models.FloatField("Prix d'une place") price = models.FloatField("Prix d'une place")
slots = models.IntegerField("Places") slots = models.IntegerField("Places")
tirage = models.ForeignKey(Tirage) tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
listing = models.BooleanField("Les places sont sur listing") listing = models.BooleanField("Les places sont sur listing")
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True)
null=True)
class Meta: class Meta:
verbose_name = "Spectacle" verbose_name = "Spectacle"
ordering = ("date", "title",) ordering = ("date", "title")
def timestamp(self): def timestamp(self):
return "%d" % calendar.timegm(self.date.utctimetuple()) return "%d" % calendar.timegm(self.date.utctimetuple())
@ -88,7 +89,7 @@ class Spectacle(models.Model):
self.title, self.title,
formats.localize(timezone.template_localtime(self.date)), formats.localize(timezone.template_localtime(self.date)),
self.location, self.location,
self.price self.price,
) )
def getImgUrl(self): def getImgUrl(self):
@ -97,7 +98,7 @@ class Spectacle(models.Model):
""" """
try: try:
return self.image.url return self.image.url
except: except Exception:
return None return None
def send_rappel(self): def send_rappel(self):
@ -107,19 +108,21 @@ class Spectacle(models.Model):
""" """
# On récupère la liste des participants + le BdA # On récupère la liste des participants + le BdA
members = list( members = list(
User.objects User.objects.filter(participant__attributions=self)
.filter(participant__attributions=self) .annotate(nb_attr=Count("id"))
.annotate(nb_attr=Count("id")).order_by() .order_by()
) )
bda_generic = get_generic_user() bda_generic = get_generic_user()
bda_generic.nb_attr = 1 bda_generic.nb_attr = 1
members.append(bda_generic) members.append(bda_generic)
# On écrit un mail personnalisé à chaque participant # On écrit un mail personnalisé à chaque participant
datatuple = [( datatuple = [
'bda-rappel', (
{'member': member, "nb_attr": member.nb_attr, 'show': self}, "bda-rappel",
settings.MAIL_DATA['rappels']['FROM'], {"member": member, "nb_attr": member.nb_attr, "show": self},
[member.email]) settings.MAIL_DATA["rappels"]["FROM"],
[member.email],
)
for member in members for member in members
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
@ -135,9 +138,9 @@ class Spectacle(models.Model):
class Quote(models.Model): class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle) spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
text = models.TextField('Citation') text = models.TextField("Citation")
author = models.CharField('Auteur', max_length=200) author = models.CharField("Auteur", max_length=200)
PAYMENT_TYPES = ( PAYMENT_TYPES = (
@ -149,129 +152,200 @@ PAYMENT_TYPES = (
class Participant(models.Model): class Participant(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(Spectacle, choices = models.ManyToManyField(
through="ChoixSpectacle", Spectacle, through="ChoixSpectacle", related_name="chosen_by"
related_name="chosen_by") )
attributions = models.ManyToManyField(Spectacle, attributions = models.ManyToManyField(
through="Attribution", Spectacle, through="Attribution", related_name="attributed_to"
related_name="attributed_to") )
paid = models.BooleanField("A payé", default=False) paid = models.BooleanField("A payé", default=False)
paymenttype = models.CharField("Moyen de paiement", paymenttype = models.CharField(
max_length=6, choices=PAYMENT_TYPES, "Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
blank=True) )
tirage = models.ForeignKey(Tirage) tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(Spectacle, choicesrevente = models.ManyToManyField(
related_name="subscribed", Spectacle, related_name="subscribed", blank=True
blank=True) )
def __str__(self): def __str__(self):
return "%s - %s" % (self.user, self.tirage.title) return "%s - %s" % (self.user, self.tirage.title)
DOUBLE_CHOICES = ( DOUBLE_CHOICES = (
("1", "1 place"), ("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"), ("double", "2 places si possible, 1 sinon"),
("double", "2 places sinon rien"), ("autoquit", "2 places sinon rien"),
) )
class ChoixSpectacle(models.Model): class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant) participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(Spectacle, related_name="participants") spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="participants"
)
priority = models.PositiveIntegerField("Priorité") priority = models.PositiveIntegerField("Priorité")
double_choice = models.CharField("Nombre de places", double_choice = models.CharField(
default="1", choices=DOUBLE_CHOICES, "Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10
max_length=10) )
def get_double(self): def get_double(self):
return self.double_choice != "1" return self.double_choice != "1"
double = property(get_double) double = property(get_double)
def get_autoquit(self): def get_autoquit(self):
return self.double_choice == "autoquit" return self.double_choice == "autoquit"
autoquit = property(get_autoquit) autoquit = property(get_autoquit)
def __str__(self): def __str__(self):
return "Vœux de %s pour %s" % ( return "Vœux de %s pour %s" % (
self.participant.user.get_full_name(), self.participant.user.get_full_name(),
self.spectacle.title) self.spectacle.title,
)
class Meta: class Meta:
ordering = ("priority",) ordering = ("priority",)
unique_together = (("participant", "spectacle",),) unique_together = (("participant", "spectacle"),)
verbose_name = "voeu" verbose_name = "voeu"
verbose_name_plural = "voeux" verbose_name_plural = "voeux"
class Attribution(models.Model): class Attribution(models.Model):
participant = models.ForeignKey(Participant) participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(Spectacle, related_name="attribues") spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="attribues"
)
given = models.BooleanField("Donnée", default=False) given = models.BooleanField("Donnée", default=False)
def __str__(self): def __str__(self):
return "%s -- %s, %s" % (self.participant.user, self.spectacle.title, return "%s -- %s, %s" % (
self.spectacle.date) self.participant.user,
self.spectacle.title,
self.spectacle.date,
)
class SpectacleRevente(models.Model): class SpectacleRevente(models.Model):
attribution = models.OneToOneField(Attribution, attribution = models.OneToOneField(
related_name="revente") Attribution, on_delete=models.CASCADE, related_name="revente"
date = models.DateTimeField("Date de mise en vente", )
default=timezone.now) date = models.DateTimeField("Date de mise en vente", default=timezone.now)
answered_mail = models.ManyToManyField(Participant, confirmed_entry = models.ManyToManyField(
related_name="wanted", Participant, related_name="entered", blank=True
blank=True) )
seller = models.ForeignKey(Participant, seller = models.ForeignKey(
related_name="original_shows", Participant,
verbose_name="Vendeur") on_delete=models.CASCADE,
soldTo = models.ForeignKey(Participant, blank=True, null=True, verbose_name="Vendeur",
verbose_name="Vendue à") related_name="original_shows",
)
soldTo = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
verbose_name="Vendue à",
blank=True,
null=True,
)
notif_sent = models.BooleanField("Notification envoyée", notif_sent = models.BooleanField("Notification envoyée", default=False)
default=False)
tirage_done = models.BooleanField("Tirage effectué", notif_time = models.DateTimeField(
default=False) "Moment d'envoi de la notification", blank=True, null=True
shotgun = models.BooleanField("Disponible immédiatement", )
default=False)
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 @property
def date_tirage(self): def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente.""" """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 remaining_time = (
- self.date - timedelta(hours=13)) self.attribution.spectacle.date - self.real_notif_time - self.min_margin
# 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 delay = min(remaining_time, self.max_wait_time)
return self.date + delay + timedelta(hours=1)
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): def __str__(self):
return "%s -- %s" % (self.seller, return "%s -- %s" % (self.seller, self.attribution.spectacle.title)
self.attribution.spectacle.title)
class Meta: class Meta:
verbose_name = "Revente" 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): def send_notif(self):
""" """
Envoie une notification pour indiquer la mise en vente d'une place sur Envoie une notification pour indiquer la mise en vente d'une place sur
BdA-Revente à tous les intéressés. BdA-Revente à tous les intéressés.
""" """
inscrits = self.attribution.spectacle.subscribed.select_related('user') inscrits = self.attribution.spectacle.subscribed.select_related("user")
datatuple = [( datatuple = [
'bda-revente', (
{ "bda-revente",
'member': participant.user, {
'show': self.attribution.spectacle, "member": participant.user,
'revente': self, "show": self.attribution.spectacle,
'site': Site.objects.get_current() "revente": self,
}, "site": Site.objects.get_current(),
settings.MAIL_DATA['revente']['FROM'], },
[participant.user.email]) settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
for participant in inscrits for participant in inscrits
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
self.notif_sent = True self.notif_sent = True
self.notif_time = timezone.now()
self.save() self.save()
def mail_shotgun(self): def mail_shotgun(self):
@ -279,72 +353,98 @@ class SpectacleRevente(models.Model):
Envoie un mail à toutes les personnes intéréssées par le spectacle pour Envoie un mail à toutes les personnes intéréssées par le spectacle pour
leur indiquer qu'il est désormais disponible au shotgun. leur indiquer qu'il est désormais disponible au shotgun.
""" """
inscrits = self.attribution.spectacle.subscribed.select_related('user') inscrits = self.attribution.spectacle.subscribed.select_related("user")
datatuple = [( datatuple = [
'bda-shotgun', (
{ "bda-shotgun",
'member': participant.user, {
'show': self.attribution.spectacle, "member": participant.user,
'site': Site.objects.get_current(), "show": self.attribution.spectacle,
}, "site": Site.objects.get_current(),
settings.MAIL_DATA['revente']['FROM'], },
[participant.user.email]) settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
for participant in inscrits for participant in inscrits
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
self.notif_sent = True self.notif_sent = True
self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde # Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True self.tirage_done = True
self.shotgun = True self.shotgun = True
self.save() self.save()
def tirage(self): def tirage(self, send_mails=True):
""" """
Lance le tirage au sort associé à la revente. Un gagnant est choisi Lance le tirage au sort associé à la revente. Un gagnant est choisi
parmis les personnes intéressées par le spectacle. Les personnes sont parmis les personnes intéressées par le spectacle. Les personnes sont
ensuites prévenues par mail du résultat du tirage. 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 spectacle = self.attribution.spectacle
seller = self.seller seller = self.seller
winner = None
if inscrits: if inscrits:
# Envoie un mail au gagnant et au vendeur # Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits) winner = random.choice(inscrits)
self.soldTo = winner self.soldTo = winner
datatuple = [] if send_mails:
context = { mails = []
'acheteur': winner.user,
'vendeur': seller.user,
'show': spectacle,
}
datatuple.append((
'bda-revente-winner',
context,
settings.MAIL_DATA['revente']['FROM'],
[winner.user.email],
))
datatuple.append((
'bda-revente-seller',
context,
settings.MAIL_DATA['revente']['FROM'],
[seller.user.email]
))
# Envoie un mail aux perdants context = {
for inscrit in inscrits: "acheteur": winner.user,
if inscrit != winner: "vendeur": seller.user,
new_context = dict(context) "show": spectacle,
new_context['acheteur'] = inscrit.user }
datatuple.append((
'bda-revente-loser', c_mails_qs = CustomMail.objects.filter(
new_context, shortname__in=[
settings.MAIL_DATA['revente']['FROM'], "bda-revente-winner",
[inscrit.user.email] "bda-revente-loser",
)) "bda-revente-seller",
send_mass_custom_mail(datatuple) ]
)
c_mails = {cm.shortname: cm for cm in c_mails_qs}
mails.append(
c_mails["bda-revente-winner"].get_message(
context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[winner.user.email],
)
)
mails.append(
c_mails["bda-revente-seller"].get_message(
context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[seller.user.email],
reply_to=[winner.user.email],
)
)
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context["acheteur"] = inscrit.user
mails.append(
c_mails["bda-revente-loser"].get_message(
new_context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[inscrit.user.email],
)
)
mail_conn = mail.get_connection()
mail_conn.send_messages(mails)
# Si personne ne veut de la place, elle part au shotgun # Si personne ne veut de la place, elle part au shotgun
else: else:
self.shotgun = True self.shotgun = True
self.tirage_done = True self.tirage_done = True
self.save() self.save()
return winner

View file

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

View file

@ -0,0 +1 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=attribution.spectacle %}

View file

@ -0,0 +1,4 @@
<tr>
<td><input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}></td>
{{ widget.label }}
</tr>

View file

@ -0,0 +1 @@
<td data-sort-value="{{ revente.date_tirage | date:"U" }}">{{ revente.date_tirage }}</td>

View file

@ -0,0 +1,3 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% with user=revente.seller.user %} <td>{{user.first_name}} {{user.last_name}}</td> {% endwith%}
{% include 'bda/forms/date_tirage.html' %}

View file

@ -0,0 +1,2 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% include 'bda/forms/date_tirage.html' %}

View file

@ -0,0 +1,4 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% with user=revente.soldTo.user %}
<td><a href="mailto:{{ user.email }}">{{user.first_name}} {{user.last_name}}</a></td>
{% endwith %}

View file

@ -0,0 +1,4 @@
<td>{{ spectacle.title }}</td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">{{ spectacle.price |floatformat }}€</td>

View file

@ -14,7 +14,7 @@
</tr></thead> </tr></thead>
<tbody class="bda_formset_content"> <tbody class="bda_formset_content">
{% endif %} {% 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 %} {% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %} {% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}"> <td class="bda-field-{{ field.name }}">

View file

@ -27,6 +27,14 @@ var django = {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-'); var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor); $(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++; total++;
$('#id_' + type + '-TOTAL_FORMS').val(total); $('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement); $(selector).after(newElement);
@ -44,6 +52,11 @@ var django = {
} else { } else {
deleteInput.attr("checked", true); deleteInput.attr("checked", true);
} }
} else {
// Reset the default values
var selects = $(form).find("select");
$(selects[0]).val("");
$(selects[1]).val("1");
} }
// callback // 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

@ -47,7 +47,7 @@
<div> <div>
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button> <button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
<pre id="export-salle" style="display:none">{% spaceless %} <pre id="export-salle" style="display:none">{% spaceless %}
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places {% for participant in participants %}{{ participant.name }} : {{ participant.nb_places }} place{{ participant.nb_places|pluralize }}
{% endfor %} {% endfor %}
{% endspaceless %}</pre> {% endspaceless %}</pre>
</div> </div>

View file

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

View file

@ -0,0 +1,122 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Gestion des places que je revends</h2>
{% if resell_exists %}
<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>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Date</th>
<th data-sort="int">Prix</th>
</tr>
</thead>
<tbody>
{% for checkbox in resellform.attributions %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<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_exists %}
<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 %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
</form>
<hr />
{% endif %}
{% if sold_exists %}
<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>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue à</th>
</tr>
</thead>
<tbody>
{% for checkbox in soldform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<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_exists and not annul_exists and not sold_exists %}
<p>Plus de reventes possibles !</p>
{% endif %}
<script type="text/javascript" src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script language="JavaScript">
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Places disponibles immédiatement</h2>
{% if spectacles %}
<table class="table table-striped stupidtable" id="bda-shotgun">
<thead>
<tr>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th></th>
</tr>
</thead>
<tbody>
{% for spectacle in spectacles %}
<tr>
{% include "bda/forms/spectacle_label_table.html" with spectacle=spectacle %}
<td class="button"><a role="button" class="btn btn-primary" href="{% url 'bda-revente-buy' spectacle.id %}">Racheter</a>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,65 @@
{% extends "base_title.html" %}
{% load staticfiles%}
{% 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 une
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>
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
</tr>
</thead>
<tbody>
{% for checkbox in form.spectacles %}{{ checkbox }}{% endfor %}
</tbody>
</table>
</div>
<input type="submit"
class="btn btn-primary"
value="S'inscrire pour les places sélectionnées">
</form>
<script type="text/javascript" src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length; i < n; i++) {
checkboxes[i].checked = check;
}
}
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -0,0 +1,100 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Tirages au sort de reventes</h2>
{% if annul_exists %}
<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>
Voici la liste des reventes auxquelles vous êtes inscrit·e ; si vous ne souhaitez plus participer au tirage au sort vous pouvez vous en désister.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue par</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="annul"
value="Se désister des tirages sélectionnés">
</div>
</form>
{% endif %}
<hr />
{% if sub_exists %}
<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 tirages en cours suivants.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue par</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in subform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="subscribe"
value="S'inscrire aux tirages sélectionnés">
</div>
</form>
{% endif %}
{% if not annul_exists and not sub_exists %}
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Aucune revente n'est active pour le moment !
</div>
{% endif %}
<script type="text/javascript" src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script language="JavaScript">
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -6,7 +6,7 @@
<p>Le tirage au sort de cette revente a déjà été effectué !</p> <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 <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 %} {% else %}
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p> <p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
{% endif %} {% 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

@ -1,105 +0,0 @@
import json
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.utils import timezone
from .models import Tirage, Spectacle, Salle, CategorieSpectacle
class TestBdAViews(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
Spectacle.objects.bulk_create([
Spectacle(
title="foo", date=timezone.now(), location=self.location,
price=0, slots=42, tirage=self.tirage, listing=False,
category=self.category
),
Spectacle(
title="bar", date=timezone.now(), location=self.location,
price=1, slots=142, tirage=self.tirage, listing=False,
category=self.category
),
Spectacle(
title="baz", date=timezone.now(), location=self.location,
price=2, slots=242, tirage=self.tirage, listing=False,
category=self.category
),
])
self.bda_user = User.objects.create_user(
username="bda_user", password="bda4ever"
)
self.bda_user.profile.is_cof = True
self.bda_user.profile.is_buro = True
self.bda_user.profile.save()
def bda_participants(self):
"""The BdA participants views can be queried"""
client = Client()
show = self.tirage.spectacle_set.first()
client.login(self.bda_user.username, "bda4ever")
tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id))
show_resp = client.get(
"/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
)
reminder_url = "/bda/mails-rappel/{}".format(show.id)
reminder_get_resp = client.get(reminder_url)
reminder_post_resp = client.post(reminder_url)
self.assertEqual(200, tirage_resp.status_code)
self.assertEqual(200, show_resp.status_code)
self.assertEqual(200, reminder_get_resp.status_code)
self.assertEqual(200, reminder_post_resp.status_code)
def test_catalogue(self):
"""Test the catalogue JSON API"""
client = Client()
# The `list` hook
resp = client.get("/bda/catalogue/list")
self.assertJSONEqual(
resp.content.decode("utf-8"),
[{"id": self.tirage.id, "title": self.tirage.title}]
)
# The `details` hook
resp = client.get(
"/bda/catalogue/details?id={}".format(self.tirage.id)
)
self.assertJSONEqual(
resp.content.decode("utf-8"),
{
"categories": [{
"id": self.category.id,
"name": self.category.name
}],
"locations": [{
"id": self.location.id,
"name": self.location.name
}],
}
)
# The `descriptions` hook
resp = client.get(
"/bda/catalogue/descriptions?id={}".format(self.tirage.id)
)
raw = resp.content.decode("utf-8")
try:
results = json.loads(raw)
except ValueError:
self.fail("Not valid JSON: {}".format(raw))
self.assertEqual(len(results), 3)
self.assertEqual(
{(s["title"], s["price"], s["slots"]) for s in results},
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)}
)

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

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

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

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

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

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

@ -0,0 +1,380 @@
import json
from datetime import timedelta
from django.test import TestCase
from django.utils import formats, timezone
from ..models import CategorieSpectacle, Participant, Salle
from .testcases import BdATestHelpers, BdAViewTestCaseMixin
from .utils import create_spectacle
class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-tirage-inscription"
http_methods = ["GET", "POST"]
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/bda/inscription/{}".format(self.tirage.id)
def test_get_opened(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertFalse(resp.context["messages"])
def test_get_closed_future(self):
self.tirage.ouverture = timezone.now() + timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=2)
self.tirage.save()
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Le tirage n'est pas encore ouvert : ouverture le {}".format(
formats.localize(timezone.template_localtime(self.tirage.ouverture))
),
[str(msg) for msg in resp.context["messages"]],
)
def test_get_closed_past(self):
self.tirage.ouverture = timezone.now() - timedelta(days=2)
self.tirage.fermeture = timezone.now() - timedelta(days=1)
self.tirage.save()
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertIn(
" C'est fini : tirage au sort dans la journée !",
[str(msg) for msg in resp.context["messages"]],
)
def get_base_post_data(self):
return {
"choixspectacle_set-TOTAL_FORMS": "3",
"choixspectacle_set-INITIAL_FORMS": "0",
"choixspectacle_set-MIN_NUM_FORMS": "0",
"choixspectacle_set-MAX_NUM_FORMS": "1000",
}
base_post_data = property(get_base_post_data)
def test_post(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
data = dict(
self.base_post_data,
**{
"choixspectacle_set-TOTAL_FORMS": "2",
"choixspectacle_set-0-id": "",
"choixspectacle_set-0-participant": "",
"choixspectacle_set-0-spectacle": str(self.show1.pk),
"choixspectacle_set-0-double_choice": "1",
"choixspectacle_set-0-priority": "2",
"choixspectacle_set-1-id": "",
"choixspectacle_set-1-participant": "",
"choixspectacle_set-1-spectacle": str(self.show2.pk),
"choixspectacle_set-1-double_choice": "autoquit",
"choixspectacle_set-1-priority": "1",
}
)
resp = self.client.post(self.url, data)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Votre inscription a été mise à jour avec succès !",
[str(msg) for msg in resp.context["messages"]],
)
participant = Participant.objects.get(
user=self.users["bda_member"], tirage=self.tirage
)
self.assertSetEqual(
set(
participant.choixspectacle_set.values_list(
"priority", "spectacle_id", "double_choice"
)
),
{(1, self.show2.pk, "autoquit"), (2, self.show1.pk, "1")},
)
def test_post_state_changed(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
data = {"dbstate": "different"}
resp = self.client.post(self.url, data)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Impossible d'enregistrer vos modifications : vous avez apporté d'autres "
"modifications entre temps.",
[str(msg) for msg in resp.context["messages"]],
)
class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-places-attribuees"
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/bda/places/{}".format(self.tirage.id)
class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-etat-places"
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/bda/etat-places/{}".format(self.tirage.id)
class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-tirage"
http_methods = ["GET", "POST"]
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/bda/tirage/{}".format(self.tirage.id)
def test_perform_tirage_disabled(self):
# Cannot be performed if disabled
self.tirage.enable_do_tirage = False
self.tirage.save()
resp = self.client.get(self.url)
self.assertTemplateUsed(resp, "tirage-failed.html")
def test_perform_tirage_opened_registrations(self):
# 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 = self.client.get(self.url)
self.assertTemplateUsed(resp, "tirage-failed.html")
def test_perform_tirage(self):
# Otherwise, perform the tirage
self.tirage.enable_do_tirage = True
self.tirage.fermeture = timezone.now()
self.tirage.save()
resp = self.client.get(self.url)
self.assertTemplateNotUsed(resp, "tirage-failed.html")
class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-liste-spectacles"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/bda/spectacles/{}".format(self.tirage.id)
class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-spectacle"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id, "spectacle_id": self.show1.id}
@property
def url_expected(self):
return "/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id)
class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-unpaid"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/bda/spectacles/unpaid/{}".format(self.tirage.id)
class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-rappels"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"spectacle_id": self.show1.id}
@property
def url_expected(self):
return "/bda/mails-rappel/{}".format(self.show1.id)
def test_post(self):
self.require_custommails()
resp = self.client.post(self.url)
self.assertEqual(200, resp.status_code)
# TODO: check that emails are sent
class DescriptionsSpectaclesViewTestCase(
BdATestHelpers, BdAViewTestCaseMixin, TestCase
):
url_name = "bda-descriptions"
auth_user = None
auth_forbidden = []
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.pk}
@property
def url_expected(self):
return "/bda/descriptions/{}".format(self.tirage.pk)
def test_get(self):
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertListEqual(
list(resp.context["shows"]), [self.show1, self.show2, self.show3]
)
def test_get_filter_category(self):
category1 = CategorieSpectacle.objects.create(name="Category 1")
category2 = CategorieSpectacle.objects.create(name="Category 2")
show1 = create_spectacle(category=category1, tirage=self.tirage)
show2 = create_spectacle(category=category2, tirage=self.tirage)
resp = self.client.get(self.url, {"category": "Category 1"})
self.assertEqual(resp.status_code, 200)
self.assertListEqual(list(resp.context["shows"]), [show1])
resp = self.client.get(self.url, {"category": "Category 2"})
self.assertEqual(resp.status_code, 200)
self.assertListEqual(list(resp.context["shows"]), [show2])
def test_get_filter_location(self):
location1 = Salle.objects.create(name="Location 1")
location2 = Salle.objects.create(name="Location 2")
show1 = create_spectacle(location=location1, tirage=self.tirage)
show2 = create_spectacle(location=location2, tirage=self.tirage)
resp = self.client.get(self.url, {"location": str(location1.pk)})
self.assertEqual(resp.status_code, 200)
self.assertListEqual(list(resp.context["shows"]), [show1])
resp = self.client.get(self.url, {"location": str(location2.pk)})
self.assertEqual(resp.status_code, 200)
self.assertListEqual(list(resp.context["shows"]), [show2])
class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
auth_user = None
auth_forbidden = []
bda_testdata = True
def test_api_list(self):
url_list = "/bda/catalogue/list"
resp = self.client.get(url_list)
self.assertJSONEqual(
resp.content.decode("utf-8"),
[{"id": self.tirage.id, "title": self.tirage.title}],
)
def test_api_details(self):
url_details = "/bda/catalogue/details?id={}".format(self.tirage.id)
resp = self.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}],
},
)
def test_api_descriptions(self):
url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id)
resp = self.client.get(url_descriptions)
raw = resp.content.decode("utf-8")
try:
results = json.loads(raw)
except ValueError:
self.fail("Not valid JSON: {}".format(raw))
self.assertEqual(len(results), 3)
self.assertEqual(
{(s["title"], s["price"], s["slots"]) for s in results},
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)},
)
class TestBdaRevente:
pass
# TODO

75
bda/tests/testcases.py Normal file
View file

@ -0,0 +1,75 @@
import os
from django.conf import settings
from django.core.management import call_command
from django.utils import timezone
from shared.tests.testcases import ViewTestCaseMixin
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
from .utils import create_user
class BdAViewTestCaseMixin(ViewTestCaseMixin):
def get_users_base(self):
return {
"bda_other": create_user(username="bda_other"),
"bda_member": create_user(username="bda_member", is_cof=True),
"bda_staff": create_user(username="bda_staff", is_cof=True, is_buro=True),
}
class BdATestHelpers:
bda_testdata = False
def setUp(self):
super().setUp()
if self.bda_testdata:
self.load_bda_testdata()
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 load_bda_testdata(self):
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
self.show1 = Spectacle.objects.create(
title="foo",
date=timezone.now(),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.show2 = Spectacle.objects.create(
title="bar",
date=timezone.now(),
location=self.location,
price=1,
slots=142,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.show3 = Spectacle.objects.create(
title="baz",
date=timezone.now(),
location=self.location,
price=2,
slots=242,
tirage=self.tirage,
listing=False,
category=self.category,
)

36
bda/tests/utils.py Normal file
View file

@ -0,0 +1,36 @@
from datetime import timedelta
from django.contrib.auth.models import User
from django.utils import timezone
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
def create_user(username, is_cof=False, is_buro=False):
user = User.objects.create_user(username=username, password=username)
user.profile.is_cof = is_cof
user.profile.is_buro = is_buro
user.profile.save()
return user
def user_is_cof(user):
return (user is not None) and user.profile.is_cof
def user_is_staff(user):
return (user is not None) and user.profile.is_buro
def create_spectacle(**kwargs):
defaults = {
"title": "Title",
"category": CategorieSpectacle.objects.first(),
"date": (timezone.now() + timedelta(days=7)).date(),
"location": Salle.objects.first(),
"price": 10.0,
"slots": 20,
"tirage": Tirage.objects.first(),
"listing": False,
}
return Spectacle.objects.create(**dict(defaults, **kwargs))

View file

@ -1,55 +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 django.conf.urls import url
from gestioncof.decorators import buro_required
from bda.views import SpectacleListView
from bda import views from bda import views
from bda.views import SpectacleListView
from gestioncof.decorators import buro_required
urlpatterns = [ urlpatterns = [
url(r'^inscription/(?P<tirage_id>\d+)$', url(
r"^inscription/(?P<tirage_id>\d+)$",
views.inscription, views.inscription,
name='bda-tirage-inscription'), name="bda-tirage-inscription",
url(r'^places/(?P<tirage_id>\d+)$', ),
views.places, url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
name="bda-places-attribuees"), url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
url(r'^revente/(?P<tirage_id>\d+)$', url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
views.revente, url(
name='bda-revente'), r"^spectacles/(?P<tirage_id>\d+)$",
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()), buro_required(SpectacleListView.as_view()),
name="bda-liste-spectacles"), name="bda-liste-spectacles",
url(r'^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$', ),
url(
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
views.spectacle, views.spectacle,
name="bda-spectacle"), name="bda-spectacle",
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$', ),
views.unpaid, url(r"^spectacles/unpaid/(?P<tirage_id>\d+)$", views.unpaid, name="bda-unpaid"),
name="bda-unpaid"), url(
url(r'^liste-revente/(?P<tirage_id>\d+)$', r"^spectacles/autocomplete$",
views.list_revente, views.spectacle_autocomplete,
name="bda-liste-revente"), name="bda-spectacle-autocomplete",
url(r'^buy-revente/(?P<spectacle_id>\d+)$', ),
views.buy_revente, url(
name="bda-buy-revente"), r"^participants/autocomplete$",
url(r'^revente-interested/(?P<revente_id>\d+)$', views.participant_autocomplete,
views.revente_interested, name="bda-participant-autocomplete",
name='bda-revente-interested'), ),
url(r'^revente-immediat/(?P<tirage_id>\d+)$', # 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, views.revente_shotgun,
name="bda-shotgun"), name="bda-revente-shotgun",
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', ),
views.send_rappel, url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
name="bda-rappels" url(
), r"^descriptions/(?P<tirage_id>\d+)$",
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, views.descriptions_spectacles,
name='bda-descriptions'), name="bda-descriptions",
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue, ),
name='bda-catalogue'), 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 import os
from channels.asgi import get_channel_layer from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ: if "DJANGO_SETTINGS_MODULE" not in os.environ:

View file

11
cof/locale/en/formats.py Normal file
View file

@ -0,0 +1,11 @@
# -*- encoding: utf-8 -*-
"""
English formatting.
"""
from __future__ import unicode_literals
DATETIME_FORMAT = r"l N j, Y \a\t P"
DATE_FORMAT = r"l N j, Y"
TIME_FORMAT = r"P"

View file

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

View file

@ -1,6 +1,3 @@
from channels.routing import include 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'),
]

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" """
Django common settings for cof project. Django common settings for cof project.
@ -7,6 +6,7 @@ the local development server should be here.
""" """
import os import os
import sys
try: try:
from . import secret from . import secret
@ -31,6 +31,7 @@ def import_secret(name):
SECRET_KEY = import_secret("SECRET_KEY") SECRET_KEY = import_secret("SECRET_KEY")
ADMINS = import_secret("ADMINS") ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL") SERVER_EMAIL = import_secret("SERVER_EMAIL")
EMAIL_HOST = import_secret("EMAIL_HOST")
DBNAME = import_secret("DBNAME") DBNAME = import_secret("DBNAME")
DBUSER = import_secret("DBUSER") DBUSER = import_secret("DBUSER")
@ -41,108 +42,115 @@ REDIS_DB = import_secret("REDIS_DB")
REDIS_HOST = import_secret("REDIS_HOST") REDIS_HOST = import_secret("REDIS_HOST")
REDIS_PORT = import_secret("REDIS_PORT") 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") KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
BASE_DIR = os.path.dirname( BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
) TESTING = sys.argv[1] == "test"
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'gestioncof', "shared",
'django.contrib.auth', "gestioncof",
'django.contrib.contenttypes', # Must be before 'django.contrib.admin'.
'django.contrib.sessions', # https://django-autocomplete-light.readthedocs.io/en/master/install.html
'django.contrib.sites', "dal",
'django.contrib.messages', "dal_select2",
'django.contrib.staticfiles', "django.contrib.auth",
'grappelli', "django.contrib.contenttypes",
'django.contrib.admin', "django.contrib.sessions",
'django.contrib.admindocs', "django.contrib.sites",
'bda', "django.contrib.messages",
'autocomplete_light', "django.contrib.staticfiles",
'captcha', "django.contrib.admin",
'django_cas_ng', "django.contrib.admindocs",
'bootstrapform', "bda",
'kfet', "petitscours",
'kfet.open', "captcha",
'channels', "django_cas_ng",
'widget_tweaks', "bootstrapform",
'custommail', "kfet",
'djconfig', "kfet.open",
'wagtail.wagtailforms', "channels",
'wagtail.wagtailredirects', "widget_tweaks",
'wagtail.wagtailembeds', "custommail",
'wagtail.wagtailsites', "djconfig",
'wagtail.wagtailusers', "wagtail.wagtailforms",
'wagtail.wagtailsnippets', "wagtail.wagtailredirects",
'wagtail.wagtaildocs', "wagtail.wagtailembeds",
'wagtail.wagtailimages', "wagtail.wagtailsites",
'wagtail.wagtailsearch', "wagtail.wagtailusers",
'wagtail.wagtailadmin', "wagtail.wagtailsnippets",
'wagtail.wagtailcore', "wagtail.wagtaildocs",
'wagtail.contrib.modeladmin', "wagtail.wagtailimages",
'wagtailmenus', "wagtail.wagtailsearch",
'modelcluster', "wagtail.wagtailadmin",
'taggit', "wagtail.wagtailcore",
'kfet.auth', "wagtail.contrib.modeladmin",
'kfet.cms', "wagtail.contrib.wagtailroutablepage",
"wagtailmenus",
"wagtail_modeltranslation",
"modelcluster",
"taggit",
"kfet.auth",
"kfet.cms",
"gestioncof.cms",
] ]
MIDDLEWARE_CLASSES = [
'django.contrib.sessions.middleware.SessionMiddleware', MIDDLEWARE = [
'django.middleware.common.CommonMiddleware', "corsheaders.middleware.CorsMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.middleware.common.CommonMiddleware",
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'kfet.auth.middleware.TemporaryAuthMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.auth.middleware.SessionAuthenticationMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "kfet.auth.middleware.TemporaryAuthMiddleware",
'django.middleware.security.SecurityMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'djconfig.middleware.DjConfigMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'wagtail.wagtailcore.middleware.SiteMiddleware', "django.middleware.security.SecurityMiddleware",
'wagtail.wagtailredirects.middleware.RedirectMiddleware', "djconfig.middleware.DjConfigMiddleware",
"wagtail.wagtailcore.middleware.SiteMiddleware",
"wagtail.wagtailredirects.middleware.RedirectMiddleware",
"django.middleware.locale.LocaleMiddleware",
] ]
ROOT_URLCONF = 'cof.urls' ROOT_URLCONF = "cof.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
'django.core.context_processors.i18n', "django.template.context_processors.i18n",
'django.core.context_processors.media', "django.template.context_processors.media",
'django.core.context_processors.static', "django.template.context_processors.static",
'wagtailmenus.context_processors.wagtailmenus', "wagtailmenus.context_processors.wagtailmenus",
'djconfig.context_processors.config', "djconfig.context_processors.config",
'gestioncof.shared.context_processor', "gestioncof.shared.context_processor",
'kfet.auth.context_processors.temporary_auth', "kfet.auth.context_processors.temporary_auth",
'kfet.context_processors.config', "kfet.context_processors.config",
], ]
}, },
}, }
] ]
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.postgresql_psycopg2', "ENGINE": "django.db.backends.postgresql_psycopg2",
'NAME': DBNAME, "NAME": DBNAME,
'USER': DBUSER, "USER": DBUSER,
'PASSWORD': DBPASSWD, "PASSWORD": DBPASSWD,
'HOST': os.environ.get('DBHOST', 'localhost'), "HOST": os.environ.get("DBHOST", "localhost"),
} }
} }
@ -150,9 +158,9 @@ DATABASES = {
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/ # 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 USE_I18N = True
@ -160,51 +168,63 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = (("fr", "Français"), ("en", "English"))
# Various additional settings # Various additional settings
SITE_ID = 1 SITE_ID = 1
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF" GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>" GRAPPELLI_ADMIN_TITLE = '<a href="/">GestioCOF</a>'
MAIL_DATA = { MAIL_DATA = {
'petits_cours': { "petits_cours": {
'FROM': "Le COF <cof@ens.fr>", "FROM": "Le COF <cof@ens.fr>",
'BCC': "archivescof@gmail.com", "BCC": "archivescof@gmail.com",
'REPLYTO': "cof@ens.fr"}, "REPLYTO": "cof@ens.fr",
'rappels': { },
'FROM': 'Le BdA <bda@ens.fr>', "rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"},
'REPLYTO': 'Le BdA <bda@ens.fr>'}, "revente": {
'revente': { "FROM": "BdA-Revente <bda-revente@ens.fr>",
'FROM': 'BdA-Revente <bda-revente@ens.fr>', "REPLYTO": "BdA-Revente <bda-revente@ens.fr>",
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'}, },
} }
LOGIN_URL = "cof-login" LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' CAS_SERVER_URL = "https://cas.eleves.ens.fr/"
CAS_VERSION = '3' CAS_VERSION = "2"
CAS_LOGIN_MSG = None CAS_LOGIN_MSG = None
CAS_IGNORE_REFERER = True CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/' CAS_REDIRECT_URL = "/"
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', "django.contrib.auth.backends.ModelBackend",
'gestioncof.shared.COFCASBackend', "gestioncof.shared.COFCASBackend",
'kfet.auth.backends.GenericBackend', "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 RECAPTCHA_USE_SSL = True
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
# Cache settings # Cache settings
CACHES = { CACHES = {
'default': { "default": {
'BACKEND': 'redis_cache.RedisCache', "BACKEND": "redis_cache.RedisCache",
'LOCATION': 'redis://:{passwd}@{host}:{port}/db' "LOCATION": "redis://:{passwd}@{host}:{port}/db".format(
.format(passwd=REDIS_PASSWD, host=REDIS_HOST, passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB
port=REDIS_PORT, db=REDIS_DB), ),
} }
} }
@ -215,20 +235,25 @@ CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "asgi_redis.RedisChannelLayer", "BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [( "hosts": [
"redis://:{passwd}@{host}:{port}/{db}" (
.format(passwd=REDIS_PASSWD, host=REDIS_HOST, "redis://:{passwd}@{host}:{port}/{db}".format(
port=REDIS_PORT, db=REDIS_DB) passwd=REDIS_PASSWD,
)], host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
)
)
]
}, },
"ROUTING": "cof.routing.routing", "ROUTING": "cof.routing.routing",
} }
} }
FORMAT_MODULE_PATH = 'cof.locale' FORMAT_MODULE_PATH = "cof.locale"
# Wagtail settings # Wagtail settings
WAGTAIL_SITE_NAME = 'GestioCOF' WAGTAIL_SITE_NAME = "GestioCOF"
WAGTAIL_ENABLE_UPDATE_CHECK = False WAGTAIL_ENABLE_UPDATE_CHECK = False
TAGGIT_CASE_INSENSITIVE = True 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 * # NOQA
from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES 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 DEBUG = True
if TESTING:
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# --- # ---
# Apache static/media config # Apache static/media config
# --- # ---
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATIC_ROOT = '/srv/gestiocof/static/' STATIC_ROOT = "/srv/gestiocof/static/"
MEDIA_ROOT = '/srv/gestiocof/media/' MEDIA_ROOT = "/srv/gestiocof/media/"
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
# --- # ---
# Debug tool bar # Debug tool bar
# --- # ---
def show_toolbar(request): def show_toolbar(request):
""" """
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
@ -34,13 +37,12 @@ def show_toolbar(request):
machine physique n'est pas forcément connue, et peut difficilement être machine physique n'est pas forcément connue, et peut difficilement être
mise dans les INTERNAL_IPS. mise dans les INTERNAL_IPS.
""" """
return DEBUG return DEBUG and not request.path.startswith("/admin/")
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE_CLASSES = ( if not TESTING:
["debug_panel.middleware.DebugPanelMiddleware"] INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
+ MIDDLEWARE_CLASSES
) MIDDLEWARE = ["debug_panel.middleware.DebugPanelMiddleware"] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar, DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}
}

View file

@ -8,21 +8,16 @@ import os
from .dev import * # NOQA from .dev import * # NOQA
from .dev import BASE_DIR from .dev import BASE_DIR
# Use sqlite for local development # Use sqlite for local development
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "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 # Use the default cache backend for local development
CACHES = { CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
}
}
# Use the default in memory asgi backend for local development # Use the default in memory asgi backend for local development
CHANNEL_LAYERS = { CHANNEL_LAYERS = {

View file

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

View file

@ -1,6 +1,7 @@
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 ADMINS = None
SERVER_EMAIL = "root@vagrant" SERVER_EMAIL = "root@vagrant"
EMAIL_HOST = "localhost"
DBUSER = "cof_gestion" DBUSER = "cof_gestion"
DBNAME = "cof_gestion" DBNAME = "cof_gestion"

View file

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

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

View file

@ -2,14 +2,16 @@ from django.apps import AppConfig
class GestioncofConfig(AppConfig): class GestioncofConfig(AppConfig):
name = 'gestioncof' name = "gestioncof"
verbose_name = "Gestion des adhérents du COF" verbose_name = "Gestion des adhérents du COF"
def ready(self): def ready(self):
from . import signals from . import signals # noqa
self.register_config() self.register_config()
def register_config(self): def register_config(self):
import djconfig import djconfig
from .forms import GestioncofConfigForm from .forms import GestioncofConfigForm
djconfig.register(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 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.decorators import buro_required
from gestioncof.models import CofProfile
class Clipper(object): class Clipper(object):
@ -21,68 +18,71 @@ class Clipper(object):
self.clipper = clipper self.clipper = clipper
self.fullname = fullname 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 @buro_required
def autocomplete(request): def autocomplete(request):
if "q" not in request.GET: if "q" not in request.GET:
raise Http404 raise Http404
q = request.GET['q'] q = request.GET["q"]
data = { data = {"q": q}
'q': q,
}
queries = {} queries = {}
bits = q.split() bits = q.split()
# Fetching data from User and CofProfile tables # Fetching data from User and CofProfile tables
queries['members'] = CofProfile.objects.filter(is_cof=True) queries["members"] = CofProfile.objects.filter(is_cof=True)
queries['users'] = User.objects.filter(profile__is_cof=False) queries["users"] = User.objects.filter(profile__is_cof=False)
for bit in bits: for bit in bits:
queries['members'] = queries['members'].filter( queries["members"] = queries["members"].filter(
Q(user__first_name__icontains=bit) Q(user__first_name__icontains=bit)
| Q(user__last_name__icontains=bit) | Q(user__last_name__icontains=bit)
| Q(user__username__icontains=bit) | Q(user__username__icontains=bit)
| Q(login_clipper__icontains=bit)) | Q(login_clipper__icontains=bit)
queries['users'] = queries['users'].filter( )
queries["users"] = queries["users"].filter(
Q(first_name__icontains=bit) Q(first_name__icontains=bit)
| Q(last_name__icontains=bit) | Q(last_name__icontains=bit)
| Q(username__icontains=bit)) | Q(username__icontains=bit)
queries['members'] = queries['members'].distinct() )
queries['users'] = queries['users'].distinct() queries["members"] = queries["members"].distinct()
queries["users"] = queries["users"].distinct()
# Clearing redundancies # Clearing redundancies
usernames = ( usernames = set(queries["members"].values_list("login_clipper", flat="True")) | set(
set(queries['members'].values_list('login_clipper', flat='True')) queries["users"].values_list("profile__login_clipper", flat="True")
| set(queries['users'].values_list('profile__login_clipper',
flat='True'))
) )
# Fetching data from the SPI # Fetching data from the SPI
if getattr(settings, 'LDAP_SERVER_URL', None): if getattr(settings, "LDAP_SERVER_URL", None):
# Fetching # Fetching
ldap_query = '(&{:s})'.format(''.join( ldap_query = "(&{:s})".format(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit) "".join(
for bit in bits if bit.isalnum() "(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=bit)
)) for bit in bits
if bit.isalnum()
)
)
if ldap_query != "(&)": if ldap_query != "(&)":
# If none of the bits were legal, we do not perform the query # If none of the bits were legal, we do not perform the query
entries = None entries = None
with Connection(settings.LDAP_SERVER_URL) as conn: with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search( conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"])
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
entries = conn.entries entries = conn.entries
# Clearing redundancies # Clearing redundancies
queries['clippers'] = [ queries["clippers"] = [
Clipper(entry.uid.value, entry.cn.value) Clipper(entry.uid.value, entry.cn.value)
for entry in entries for entry in entries
if entry.uid.value if entry.uid.value and entry.uid.value not in usernames
and entry.uid.value not in usernames
] ]
# Resulting data # Resulting data
data.update(queries) 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) return shortcuts.render(request, "autocomplete_user.html", data)

View file

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

View file

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

7
gestioncof/cms/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class COFCMSAppConfig(AppConfig):
name = "gestioncof.cms"
label = "cofcms"
verbose_name = "CMS COF"

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,940 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-01-20 19:10
from __future__ import unicode_literals
import django.db.models.deletion
import wagtail.wagtailcore.blocks
import wagtail.wagtailcore.fields
import wagtail.wagtailimages.blocks
from django.db import migrations, models
import gestioncof.cms.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("wagtailcore", "0033_remove_golive_expiry_help_text"),
("wagtailimages", "0019_delete_filter"),
]
operations = [
migrations.CreateModel(
name="COFActuIndexPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
(
"title_fr",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"title_en",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"slug_fr",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"slug_en",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"url_path_fr",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"url_path_en",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"seo_title_fr",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"seo_title_en",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"search_description_fr",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"search_description_en",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
],
options={
"verbose_name": "Index des actualités",
"verbose_name_plural": "Indexs des actualités",
},
bases=("wagtailcore.page", gestioncof.cms.models.COFActuIndexMixin),
),
migrations.CreateModel(
name="COFActuPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
(
"title_fr",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"title_en",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"slug_fr",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"slug_en",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"url_path_fr",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"url_path_en",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"seo_title_fr",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"seo_title_en",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"search_description_fr",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"search_description_en",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"chapo",
models.TextField(blank=True, verbose_name="Description rapide"),
),
(
"chapo_fr",
models.TextField(
blank=True, null=True, verbose_name="Description rapide"
),
),
(
"chapo_en",
models.TextField(
blank=True, null=True, verbose_name="Description rapide"
),
),
(
"body",
wagtail.wagtailcore.fields.RichTextField(verbose_name="Contenu"),
),
(
"body_fr",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Contenu"
),
),
(
"body_en",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Contenu"
),
),
(
"is_event",
models.BooleanField(default=True, verbose_name="Évènement"),
),
(
"date_start",
models.DateTimeField(verbose_name="Date et heure de début"),
),
(
"date_end",
models.DateTimeField(
blank=True,
default=None,
null=True,
verbose_name="Date et heure de fin",
),
),
(
"all_day",
models.BooleanField(default=False, verbose_name="Toute la journée"),
),
(
"image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.Image",
verbose_name="Image à la Une",
),
),
],
options={"verbose_name": "Actualité", "verbose_name_plural": "Actualités"},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="COFDirectoryEntryPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
(
"title_fr",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"title_en",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"slug_fr",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"slug_en",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"url_path_fr",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"url_path_en",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"seo_title_fr",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"seo_title_en",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"search_description_fr",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"search_description_en",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"body",
wagtail.wagtailcore.fields.RichTextField(
verbose_name="Description"
),
),
(
"body_fr",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Description"
),
),
(
"body_en",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Description"
),
),
(
"links",
wagtail.wagtailcore.fields.StreamField(
(
(
"lien",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"url",
wagtail.wagtailcore.blocks.URLBlock(
required=True
),
),
(
"texte",
wagtail.wagtailcore.blocks.CharBlock(),
),
)
),
),
(
"contact",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"email",
wagtail.wagtailcore.blocks.EmailBlock(
required=True
),
),
(
"texte",
wagtail.wagtailcore.blocks.CharBlock(),
),
)
),
),
)
),
),
(
"links_fr",
wagtail.wagtailcore.fields.StreamField(
(
(
"lien",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"url",
wagtail.wagtailcore.blocks.URLBlock(
required=True
),
),
(
"texte",
wagtail.wagtailcore.blocks.CharBlock(),
),
)
),
),
(
"contact",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"email",
wagtail.wagtailcore.blocks.EmailBlock(
required=True
),
),
(
"texte",
wagtail.wagtailcore.blocks.CharBlock(),
),
)
),
),
),
null=True,
),
),
(
"links_en",
wagtail.wagtailcore.fields.StreamField(
(
(
"lien",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"url",
wagtail.wagtailcore.blocks.URLBlock(
required=True
),
),
(
"texte",
wagtail.wagtailcore.blocks.CharBlock(),
),
)
),
),
(
"contact",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"email",
wagtail.wagtailcore.blocks.EmailBlock(
required=True
),
),
(
"texte",
wagtail.wagtailcore.blocks.CharBlock(),
),
)
),
),
),
null=True,
),
),
(
"image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.Image",
verbose_name="Image",
),
),
],
options={
"verbose_name": "Éntrée d'annuaire",
"verbose_name_plural": "Éntrées d'annuaire",
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="COFDirectoryPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
(
"title_fr",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"title_en",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"slug_fr",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"slug_en",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"url_path_fr",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"url_path_en",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"seo_title_fr",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"seo_title_en",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"search_description_fr",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"search_description_en",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"introduction",
wagtail.wagtailcore.fields.RichTextField(
verbose_name="Introduction"
),
),
(
"introduction_fr",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Introduction"
),
),
(
"introduction_en",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Introduction"
),
),
],
options={
"verbose_name": "Annuaire (clubs, partenaires, bons plans...)",
"verbose_name_plural": "Annuaires",
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="COFPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
(
"title_fr",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"title_en",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"slug_fr",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"slug_en",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"url_path_fr",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"url_path_en",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"seo_title_fr",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"seo_title_en",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"search_description_fr",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"search_description_en",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"body",
wagtail.wagtailcore.fields.StreamField(
(
(
"heading",
wagtail.wagtailcore.blocks.CharBlock(
classname="full title"
),
),
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
)
),
),
(
"body_fr",
wagtail.wagtailcore.fields.StreamField(
(
(
"heading",
wagtail.wagtailcore.blocks.CharBlock(
classname="full title"
),
),
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
),
null=True,
),
),
(
"body_en",
wagtail.wagtailcore.fields.StreamField(
(
(
"heading",
wagtail.wagtailcore.blocks.CharBlock(
classname="full title"
),
),
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
),
null=True,
),
),
],
options={
"verbose_name": "Page normale COF",
"verbose_name_plural": "Pages normales COF",
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="COFRootPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
(
"title_fr",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"title_en",
models.CharField(
help_text="The page title as you'd like it to be seen by the public",
max_length=255,
null=True,
verbose_name="title",
),
),
(
"slug_fr",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"slug_en",
models.SlugField(
allow_unicode=True,
help_text="The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/",
max_length=255,
null=True,
verbose_name="slug",
),
),
(
"url_path_fr",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"url_path_en",
models.TextField(
blank=True, editable=False, null=True, verbose_name="URL path"
),
),
(
"seo_title_fr",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"seo_title_en",
models.CharField(
blank=True,
help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.",
max_length=255,
null=True,
verbose_name="page title",
),
),
(
"search_description_fr",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"search_description_en",
models.TextField(
blank=True, null=True, verbose_name="search description"
),
),
(
"introduction",
wagtail.wagtailcore.fields.RichTextField(
verbose_name="Introduction"
),
),
(
"introduction_fr",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Introduction"
),
),
(
"introduction_en",
wagtail.wagtailcore.fields.RichTextField(
null=True, verbose_name="Introduction"
),
),
],
options={
"verbose_name": "Racine site du COF",
"verbose_name_plural": "Racines site du COF",
},
bases=("wagtailcore.page", gestioncof.cms.models.COFActuIndexMixin),
),
]

View file

@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-04-28 13:46
from __future__ import unicode_literals
import django.db.models.deletion
import wagtail.contrib.wagtailroutablepage.models
import wagtail.wagtailcore.blocks
import wagtail.wagtailcore.fields
import wagtail.wagtailimages.blocks
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtailcore", "0039_collectionviewrestriction"),
("cofcms", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="COFUtilPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
)
],
options={
"verbose_name": "Page utilitaire",
"verbose_name_plural": "Pages utilitaires",
},
bases=(
wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin,
"wagtailcore.page",
),
),
migrations.AlterModelOptions(
name="cofdirectoryentrypage",
options={
"verbose_name": "Entrée d'annuaire",
"verbose_name_plural": "Entrées d'annuaire",
},
),
migrations.AddField(
model_name="cofdirectorypage",
name="alphabetique",
field=models.BooleanField(
default=True, verbose_name="Tri par ordre alphabétique ?"
),
),
migrations.AlterField(
model_name="cofpage",
name="body",
field=wagtail.wagtailcore.fields.StreamField(
(
(
"heading",
wagtail.wagtailcore.blocks.CharBlock(classname="full title"),
),
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
(
"iframe",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"url",
wagtail.wagtailcore.blocks.URLBlock(
"Adresse de la page"
),
),
(
"height",
wagtail.wagtailcore.blocks.CharBlock(
"Hauteur (en pixels)"
),
),
)
),
),
)
),
),
migrations.AlterField(
model_name="cofpage",
name="body_en",
field=wagtail.wagtailcore.fields.StreamField(
(
(
"heading",
wagtail.wagtailcore.blocks.CharBlock(classname="full title"),
),
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
(
"iframe",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"url",
wagtail.wagtailcore.blocks.URLBlock(
"Adresse de la page"
),
),
(
"height",
wagtail.wagtailcore.blocks.CharBlock(
"Hauteur (en pixels)"
),
),
)
),
),
),
null=True,
),
),
migrations.AlterField(
model_name="cofpage",
name="body_fr",
field=wagtail.wagtailcore.fields.StreamField(
(
(
"heading",
wagtail.wagtailcore.blocks.CharBlock(classname="full title"),
),
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
(
"iframe",
wagtail.wagtailcore.blocks.StructBlock(
(
(
"url",
wagtail.wagtailcore.blocks.URLBlock(
"Adresse de la page"
),
),
(
"height",
wagtail.wagtailcore.blocks.CharBlock(
"Hauteur (en pixels)"
),
),
)
),
),
),
null=True,
),
),
]

View file

234
gestioncof/cms/models.py Normal file
View file

@ -0,0 +1,234 @@
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import models
from wagtail.contrib.wagtailroutablepage.models import RoutablePageMixin, route
from wagtail.wagtailadmin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.fields import RichTextField, StreamField
from wagtail.wagtailcore.models import Page
from wagtail.wagtailimages.blocks import ImageChooserBlock
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
# Page pouvant afficher des actualités
class COFActuIndexMixin:
@property
def actus(self):
actus = COFActuPage.objects.live().order_by("-date_start").descendant_of(self)
return actus
# Racine du site du COF
class COFRootPage(Page, COFActuIndexMixin):
introduction = RichTextField("Introduction")
content_panels = Page.content_panels + [
FieldPanel("introduction", classname="full")
]
subpage_types = ["COFActuIndexPage", "COFPage", "COFDirectoryPage", "COFUtilPage"]
class Meta:
verbose_name = "Racine site du COF"
verbose_name_plural = "Racines site du COF"
# Block iframe
class IFrameBlock(blocks.StructBlock):
url = blocks.URLBlock("Adresse de la page")
height = blocks.CharBlock("Hauteur (en pixels)")
class Meta:
verbose_name = "Page incluse (iframe, à utiliser avec précaution)"
verbose_name_plural = "Pages incluses (iframes, à utiliser avec précaution)"
template = "cofcms/iframe_block.html"
# Page lambda du site
class COFPage(Page):
body = StreamField(
[
("heading", blocks.CharBlock(classname="full title")),
("paragraph", blocks.RichTextBlock()),
("image", ImageChooserBlock()),
("iframe", IFrameBlock()),
]
)
content_panels = Page.content_panels + [StreamFieldPanel("body")]
subpage_types = ["COFDirectoryPage", "COFPage"]
parent_page_types = ["COFPage", "COFRootPage"]
class Meta:
verbose_name = "Page normale COF"
verbose_name_plural = "Pages normales COF"
# Actualités
class COFActuIndexPage(Page, COFActuIndexMixin):
subpage_types = ["COFActuPage"]
parent_page_types = ["COFRootPage"]
class Meta:
verbose_name = "Index des actualités"
verbose_name_plural = "Indexs des actualités"
def get_context(self, request):
context = super().get_context(request)
actus = COFActuPage.objects.live().descendant_of(self).order_by("-date_end")
page = request.GET.get("page")
paginator = Paginator(actus, 5)
try:
actus = paginator.page(page)
except PageNotAnInteger:
actus = paginator.page(1)
except EmptyPage:
actus = paginator.page(paginator.num_pages)
context["actus"] = actus
return context
class COFActuPage(RoutablePageMixin, Page):
chapo = models.TextField("Description rapide", blank=True)
body = RichTextField("Contenu")
image = models.ForeignKey(
"wagtailimages.Image",
verbose_name="Image à la Une",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
is_event = models.BooleanField("Évènement", default=True, blank=True)
date_start = models.DateTimeField("Date et heure de début")
date_end = models.DateTimeField(
"Date et heure de fin", blank=True, default=None, null=True
)
all_day = models.BooleanField("Toute la journée", default=False, blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("image"),
FieldPanel("chapo"),
FieldPanel("body", classname="full"),
FieldPanel("is_event"),
FieldPanel("date_start"),
FieldPanel("date_end"),
FieldPanel("all_day"),
]
subpage_types = []
parent_page_types = ["COFActuIndexPage"]
class Meta:
verbose_name = "Actualité"
verbose_name_plural = "Actualités"
# Annuaires (Clubs, partenaires, bonnes adresses)
class COFDirectoryPage(Page):
introduction = RichTextField("Introduction")
alphabetique = models.BooleanField(
"Tri par ordre alphabétique ?", default=True, blank=True
)
content_panels = Page.content_panels + [
FieldPanel("introduction"),
FieldPanel("alphabetique"),
]
subpage_types = ["COFActuPage", "COFDirectoryEntryPage"]
parent_page_types = ["COFRootPage", "COFPage"]
@property
def entries(self):
entries = COFDirectoryEntryPage.objects.live().descendant_of(self)
if self.alphabetique:
entries = entries.order_by("title")
return entries
class Meta:
verbose_name = "Annuaire (clubs, partenaires, bons plans...)"
verbose_name_plural = "Annuaires"
class COFDirectoryEntryPage(Page):
body = RichTextField("Description")
links = StreamField(
[
(
"lien",
blocks.StructBlock(
[
("url", blocks.URLBlock(required=True)),
("texte", blocks.CharBlock()),
]
),
),
(
"contact",
blocks.StructBlock(
[
("email", blocks.EmailBlock(required=True)),
("texte", blocks.CharBlock()),
]
),
),
]
)
image = models.ForeignKey(
"wagtailimages.Image",
verbose_name="Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
content_panels = Page.content_panels + [
ImageChooserPanel("image"),
FieldPanel("body", classname="full"),
StreamFieldPanel("links"),
]
subpage_types = []
parent_page_types = ["COFDirectoryPage"]
class Meta:
verbose_name = "Entrée d'annuaire"
verbose_name_plural = "Entrées d'annuaire"
# Pour le calendrier, ne doit pas être pris par ModelTranslation
class COFUtilPage(RoutablePageMixin, Page):
# Mini calendrier
@route(r"^calendar/(\d+)/(\d+)/$")
def calendar(self, request, year, month):
from .views import raw_calendar_view
return raw_calendar_view(request, int(year), int(month))
"""
ModelTranslation override le système des @route de wagtail, ce qui empêche
COFUtilPage d'être une page traduite pour pouvoir l'utiliser.
Ce qui fait planter `get_absolute_url` pour des problèmes d'héritage des
pages parentes (qui sont, elles, traduites).
Le seul moyen trouvé pour résoudre ce problème est de faire une autre
fonction à qui on fournit request en argument (donc pas un override de
get_absolute_url).
TODO : vérifier si ces problèmes ont été résolus dans les màj de wagtail
et modeltranslation
"""
def debugged_get_url(self, request):
parent = COFRootPage.objects.parent_of(self).live().first()
burl = parent.relative_url(request.site)
return burl + self.slug
class Meta:
verbose_name = "Page utilitaire"
verbose_name_plural = "Pages utilitaires"

View file

@ -0,0 +1,25 @@
require 'compass/import-once/activate'
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "css"
sass_dir = "sass"
images_dir = "images"
javascripts_dir = "js"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# If you prefer the indented syntax, you might want to regenerate this
# project again passing --syntax sass, or you can uncomment this:
# preferred_syntax = :sass
# and then run:
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

View file

@ -0,0 +1,5 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View file

@ -0,0 +1,3 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

View file

@ -0,0 +1,669 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
@import url("https://fonts.googleapis.com/css?family=Carter+One|Source+Sans+Pro:300,300i,700");
/* line 5, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font: inherit;
font-size: 100%;
vertical-align: baseline;
}
/* line 22, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
html {
line-height: 1;
}
/* line 24, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
ol, ul {
list-style: none;
}
/* line 26, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* line 28, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
caption, th, td {
text-align: left;
font-weight: normal;
vertical-align: middle;
}
/* line 30, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
q, blockquote {
quotes: none;
}
/* line 103, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
q:before, q:after, blockquote:before, blockquote:after {
content: "";
content: none;
}
/* line 32, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
a img {
border: none;
}
/* line 116, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
display: block;
}
/* line 12, ../sass/screen.scss */
*, *:after, *:before {
box-sizing: border-box;
}
/* line 16, ../sass/screen.scss */
body {
background: #fefefe;
font: 17px "Source Sans Pro", "sans-serif";
}
/* line 21, ../sass/screen.scss */
header {
background: #5B0012;
}
/* line 25, ../sass/screen.scss */
h1, h2 {
font-family: "Carter One", "serif";
color: #90001C;
}
/* line 30, ../sass/screen.scss */
h1 {
font-size: 2.3em;
}
/* line 34, ../sass/screen.scss */
h2 {
font-size: 1.6em;
}
/* line 38, ../sass/screen.scss */
a {
color: #CC9500;
text-decoration: none;
font-weight: bold;
}
/* line 44, ../sass/screen.scss */
h2 a {
font-weight: inherit;
color: inherit;
}
/* line 50, ../sass/screen.scss */
header a {
color: #fefefe;
}
/* line 53, ../sass/screen.scss */
header section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: stretch;
}
/* line 59, ../sass/screen.scss */
header section.bottom-menu {
justify-content: space-around;
text-align: center;
background: #90001C;
}
/* line 65, ../sass/screen.scss */
header h1 {
padding: 0 15px;
}
/* line 69, ../sass/screen.scss */
header nav ul {
display: inline-flex;
}
/* line 71, ../sass/screen.scss */
header nav ul li {
display: inline-block;
}
/* line 73, ../sass/screen.scss */
header nav ul li > * {
display: block;
padding: 10px 15px;
font-weight: bold;
}
/* line 78, ../sass/screen.scss */
header nav ul li > *:hover {
background: #280008;
}
/* line 84, ../sass/screen.scss */
header nav .lang-select {
display: inline-block;
height: 100%;
vertical-align: top;
position: relative;
}
/* line 90, ../sass/screen.scss */
header nav .lang-select:before {
content: "";
color: #fff;
position: absolute;
top: 0;
left: 0;
border-left: 1px solid #fff;
height: calc(100% - 20px);
margin: 10px 0;
padding-left: 10px;
}
/* line 102, ../sass/screen.scss */
header nav .lang-select a {
padding: 10px 20px;
display: block;
}
/* line 106, ../sass/screen.scss */
header nav .lang-select a img {
display: block;
width: auto;
max-height: 20px;
vertical-align: middle;
}
/* line 117, ../sass/screen.scss */
article {
line-height: 1.4;
}
/* line 119, ../sass/screen.scss */
article p, article ul {
margin: 0.4em 0;
}
/* line 122, ../sass/screen.scss */
article ul {
padding-left: 20px;
}
/* line 124, ../sass/screen.scss */
article ul li {
list-style: outside;
}
/* line 128, ../sass/screen.scss */
article:last-child {
margin-bottom: 30px;
}
/* line 133, ../sass/screen.scss */
.container {
max-width: 1000px;
margin: 0 auto;
position: relative;
}
/* line 138, ../sass/screen.scss */
.container .aside-wrap {
position: absolute;
top: 30px;
height: 100%;
width: 25%;
left: 6px;
}
/* line 145, ../sass/screen.scss */
.container .aside-wrap .aside {
color: #222;
position: fixed;
position: sticky;
top: 5px;
width: 100%;
background: #FFC500;
padding: 15px;
box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3);
}
/* line 155, ../sass/screen.scss */
.container .aside-wrap .aside h2 {
color: #fff;
}
/* line 159, ../sass/screen.scss */
.container .aside-wrap .aside .calendar {
margin: 0 auto;
display: block;
}
/* line 164, ../sass/screen.scss */
.container .aside-wrap .aside a {
color: #997000;
}
/* line 170, ../sass/screen.scss */
.container .content {
max-width: 900px;
margin-left: auto;
margin-right: 6px;
}
/* line 175, ../sass/screen.scss */
.container .content .intro {
border-bottom: 3px solid #7f7f7f;
margin: 20px 0;
margin-top: 5px;
padding: 15px 5px;
}
/* line 184, ../sass/screen.scss */
.container .content section article {
background: #fff;
padding: 20px 30px;
box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3);
border: 1px solid rgba(153, 118, 0, 0.1);
border-radius: 2px;
}
/* line 190, ../sass/screen.scss */
.container .content section article a {
color: #CC9500;
}
/* line 195, ../sass/screen.scss */
.container .content section article + h2 {
margin-top: 15px;
}
/* line 199, ../sass/screen.scss */
.container .content section article + article {
margin-top: 25px;
}
/* line 203, ../sass/screen.scss */
.container .content section .image {
margin: 15px 0;
text-align: center;
padding: 20px;
}
/* line 208, ../sass/screen.scss */
.container .content section .image img {
max-width: 100%;
height: auto;
box-shadow: -7px 7px 1px rgba(153, 118, 0, 0.2);
}
/* line 216, ../sass/screen.scss */
.container .content section.directory article.entry {
width: 80%;
max-width: 600px;
max-height: 100%;
position: relative;
margin-left: 6%;
}
/* line 223, ../sass/screen.scss */
.container .content section.directory article.entry .entry-image {
display: block;
float: right;
width: 150px;
background: #fff;
box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.2);
border-right: 1px solid rgba(153, 118, 0, 0.2);
border-top: 1px solid rgba(153, 118, 0, 0.2);
padding: 1px;
overflow: hidden;
margin-left: 10px;
margin-bottom: 10px;
transform: translateX(10px);
}
/* line 237, ../sass/screen.scss */
.container .content section.directory article.entry .entry-image img {
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
}
/* line 245, ../sass/screen.scss */
.container .content section.directory article.entry ul.links {
margin-top: 10px;
border-top: 1px solid #90001C;
padding-top: 10px;
}
/* line 253, ../sass/screen.scss */
.container .content section.actuhome {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: top;
}
/* line 259, ../sass/screen.scss */
.container .content section.actuhome article + article {
margin: 0;
}
/* line 263, ../sass/screen.scss */
.container .content section.actuhome article.actu {
position: relative;
background: none;
box-shadow: none;
border: none;
max-width: 400px;
min-width: 300px;
flex: 1;
}
/* line 272, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-header {
position: relative;
box-shadow: -4px 5px 1px rgba(153, 118, 0, 0.3);
border-right: 1px solid rgba(153, 118, 0, 0.2);
border-top: 1px solid rgba(153, 118, 0, 0.2);
min-height: 180px;
padding: 0;
margin: 0;
overflow: hidden;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
}
/* line 285, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-header h2 {
position: absolute;
width: 100%;
bottom: 0;
left: 0;
padding: 5px;
text-shadow: 0 0 5px rgba(153, 118, 0, 0.8);
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
}
/* line 293, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-header h2 a {
color: #fff;
}
/* line 299, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-misc {
background: white;
box-shadow: -2px 2px 1px rgba(153, 118, 0, 0.2);
border: 1px solid rgba(153, 118, 0, 0.2);
border-radius: 2px;
margin: 0 10px;
padding: 15px;
padding-top: 5px;
}
/* line 308, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-misc .actu-minical {
display: block;
}
/* line 311, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-misc .actu-dates {
display: block;
text-align: right;
font-size: 0.9em;
}
/* line 318, ../sass/screen.scss */
.container .content section.actuhome article.actu .actu-overlay {
display: block;
background: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5;
opacity: 0;
}
/* line 334, ../sass/screen.scss */
.container .content section.actulist article.actu {
display: flex;
width: 100%;
padding: 0;
}
/* line 339, ../sass/screen.scss */
.container .content section.actulist article.actu .actu-image {
width: 30%;
max-width: 200px;
background-size: cover;
background-position: center center;
}
/* line 345, ../sass/screen.scss */
.container .content section.actulist article.actu .actu-infos {
padding: 15px;
flex: 1;
}
/* line 349, ../sass/screen.scss */
.container .content section.actulist article.actu .actu-infos .actu-dates {
font-weight: bold;
font-size: 0.9em;
}
/* line 359, ../sass/screen.scss */
.container .aside-wrap + .content {
max-width: 70%;
}
/* line 364, ../sass/screen.scss */
.calendar {
color: rgba(0, 0, 0, 0.8);
width: 200px;
}
/* line 368, ../sass/screen.scss */
.calendar td, .calendar th {
text-align: center;
vertical-align: middle;
border: 2px solid transparent;
padding: 1px;
}
/* line 375, ../sass/screen.scss */
.calendar th {
font-weight: bold;
}
/* line 379, ../sass/screen.scss */
.calendar td {
font-size: 0.8em;
width: 28px;
height: 28px;
}
/* line 384, ../sass/screen.scss */
.calendar td.out {
opacity: 0.3;
}
/* line 387, ../sass/screen.scss */
.calendar td.today {
border-bottom-color: #000;
}
/* line 390, ../sass/screen.scss */
.calendar td:nth-child(7), .calendar td:nth-child(6) {
background: rgba(0, 0, 0, 0.2);
}
/* line 393, ../sass/screen.scss */
.calendar td.hasevent {
position: relative;
font-weight: bold;
color: #90001C;
font-size: 1em;
}
/* line 399, ../sass/screen.scss */
.calendar td.hasevent > a {
padding: 3px;
color: #90001C !important;
}
/* line 404, ../sass/screen.scss */
.calendar td.hasevent ul.cal-events {
text-align: left;
display: none;
position: absolute;
z-index: 2;
background: #fff;
width: 150px;
left: -30px;
margin-top: 10px;
padding: 5px;
background-color: #90001C;
}
/* line 417, ../sass/screen.scss */
.calendar td.hasevent ul.cal-events .datename {
display: none;
}
/* line 420, ../sass/screen.scss */
.calendar td.hasevent ul.cal-events:before {
top: -12px;
left: 38px;
content: "";
position: absolute;
border: 6px solid transparent;
border-bottom-color: #90001C;
}
/* line 428, ../sass/screen.scss */
.calendar td.hasevent ul.cal-events a {
color: #fff;
}
/* line 433, ../sass/screen.scss */
.calendar td.hasevent > a:hover {
background-color: #90001C;
color: #fff !important;
}
/* line 437, ../sass/screen.scss */
.calendar td.hasevent > a:hover + ul.cal-events {
display: block;
}
/* line 445, ../sass/screen.scss */
#calendar-wrap .details {
border-top: 1px solid #90001C;
margin-top: 15px;
padding-top: 10px;
}
/* line 450, ../sass/screen.scss */
#calendar-wrap .details li.datename {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 5px;
}
/* line 451, ../sass/screen.scss */
#calendar-wrap .details li.datename:after {
content: " :";
}
/* line 1, ../sass/_responsive.scss */
header .minimenu {
display: none;
}
@media only screen and (max-width: 600px) {
/* line 6, ../sass/_responsive.scss */
header {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100%;
max-height: 100vh;
height: 60px;
overflow: hidden;
}
/* line 16, ../sass/_responsive.scss */
header .minimenu {
display: block;
position: absolute;
right: 3px;
top: 3px;
}
/* line 23, ../sass/_responsive.scss */
header section {
display: block;
}
/* line 25, ../sass/_responsive.scss */
header section nav {
display: none;
}
/* line 31, ../sass/_responsive.scss */
header.expanded {
overflow: auto;
height: auto;
}
/* line 35, ../sass/_responsive.scss */
header.expanded nav {
display: block;
text-align: center;
}
/* line 38, ../sass/_responsive.scss */
header.expanded nav ul {
flex-wrap: wrap;
justify-content: right;
}
/* line 41, ../sass/_responsive.scss */
header.expanded nav ul li > * {
padding: 18px;
}
/* line 48, ../sass/_responsive.scss */
.container {
margin-top: 65px;
}
/* line 51, ../sass/_responsive.scss */
.container .content {
max-width: unset;
margin: 6px;
}
/* line 56, ../sass/_responsive.scss */
.container .content section article {
padding: 10px;
}
/* line 60, ../sass/_responsive.scss */
.container .content section .image {
padding: 0;
margin: 10px -6px;
}
/* line 65, ../sass/_responsive.scss */
.container .content section.directory article.entry {
width: 100%;
margin-left: 0;
}
/* line 72, ../sass/_responsive.scss */
.container .aside-wrap + .content {
max-width: unset;
margin-top: 120px;
}
/* line 77, ../sass/_responsive.scss */
.container .aside-wrap {
z-index: 3;
top: 60px;
position: fixed;
width: 100%;
margin: 0;
height: auto;
left: 0;
}
/* line 86, ../sass/_responsive.scss */
.container .aside-wrap .aside {
margin: 0;
padding: 0;
top: 0;
position: unset;
}
/* line 92, ../sass/_responsive.scss */
.container .aside-wrap .aside > h2 {
position: relative;
cursor: pointer;
padding: 5px 10px;
}
/* line 96, ../sass/_responsive.scss */
.container .aside-wrap .aside > h2:after {
content: "v";
font-family: "Source Sans Pro", "sans-serif";
font-weight: bold;
color: #CC9500;
position: absolute;
right: 10px;
}
/* line 106, ../sass/_responsive.scss */
.container .aside-wrap .aside:not(.expanded) .aside-content {
display: none;
}
/* line 111, ../sass/_responsive.scss */
.container .aside-wrap .aside ul {
text-align: center;
}
/* line 113, ../sass/_responsive.scss */
.container .aside-wrap .aside ul li {
display: inline-block;
}
/* line 115, ../sass/_responsive.scss */
.container .aside-wrap .aside ul li > * {
display: block;
padding: 15px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
<path fill="none" stroke="#FFFFFF" stroke-width="2" d="M47.062,41.393c0,3.131-2.538,5.669-5.669,5.669H8.608 c-3.131,0-5.669-2.538-5.669-5.669V8.608c0-3.131,2.538-5.669,5.669-5.669h32.785c3.131,0,5.669,2.538,5.669,5.669V41.393z"/>
<g>
<line fill="none" stroke="#FFFFFF" stroke-width="3" x1="10.826" y1="15" x2="40.241" y2="15"/>
<line fill="none" stroke="#FFFFFF" stroke-width="3" x1="10.826" y1="25" x2="40.241" y2="25"/>
<line fill="none" stroke="#FFFFFF" stroke-width="3" x1="10.826" y1="35" x2="40.241" y2="35"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 998 B

View file

@ -0,0 +1,32 @@
$(function(){
var mem = {};
var ctt = $("#calendar-wrap");
makeInteractive();
function makeInteractive () {
$(".cal-btn").on("click", loadCalendar);
$(".hasevent a").on("click", showEvents);
}
function loadCalendar () {
var url = $(this).attr("cal-dest");
if (mem[url] != undefined) {
ctt.html(mem[url]);
makeInteractive();
return;
}
ctt.innerText = "Chargement...";
ctt.load(url, function () {
mem[url] = this.innerHTML;
makeInteractive();
});
}
function showEvents() {
ctt.find(".details").remove();
ctt.append(
$("<div>", {class:"details"})
.html(this.nextElementSibling.outerHTML)
);
}
});

View file

@ -0,0 +1,13 @@
$(function() {
$(".facteur").on("click", function(){
var $this = $(this);
var sticker = $this.attr('data-mref')
.replace('pont', '.')
.replace('arbre', '@')
.replace(/(.)-/g, '$1');
var boite = $("<a>", {href:"ma"+"il"+"to:"+sticker}).text(sticker);
$(this).before(boite)
.remove();
})
});

View file

@ -0,0 +1,11 @@
$fond: #fefefe;
$bandeau: #5B0012;
$sousbandeau: #90001C;
$aside: #FFC500;
$titre: $sousbandeau;
$lien: #CC9500;
$headerlien: $fond;
$ombres: darken($aside, 20%);
$bodyfont: "Source Sans Pro", "sans-serif";
$headfont: "Carter One", "serif";

View file

@ -0,0 +1,124 @@
header .minimenu {
display: none;
}
@media only screen and (max-width: 600px) {
header {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100%;
max-height: 100vh;
height: 60px;
overflow: hidden;
.minimenu {
display: block;
position: absolute;
right: 3px;
top: 3px;
}
section {
display: block;
nav {
display: none;
}
}
}
header.expanded {
overflow: auto;
height: auto;
nav {
display: block;
text-align: center;
ul {
flex-wrap: wrap;
justify-content: right;
li > * {
padding: 18px;
}
}
}
}
.container {
margin-top: 65px;
.content {
max-width: unset;
margin: 6px;
section {
article {
padding: 10px;
}
.image {
padding: 0;
margin: 10px -6px;
}
&.directory article.entry {
width: 100%;
margin-left: 0;
}
}
}
.aside-wrap + .content {
max-width: unset;
margin-top: 120px;
}
.aside-wrap {
z-index: 3;
top: 60px;
position: fixed;
width: 100%;
margin: 0;
height: auto;
left: 0;
.aside {
margin: 0;
padding: 0;
top: 0;
position: unset;
& > h2 {
position: relative;
cursor: pointer;
padding: 5px 10px;
&:after {
content: "v";
font-family: $bodyfont;
font-weight: bold;
color: $lien;
position: absolute;
right: 10px;
}
}
&:not(.expanded) {
.aside-content {
display: none;
}
}
ul {
text-align: center;
li {
display: inline-block;
& > * {
display: block;
padding: 15px;
}
}
}
}
}
}
}

View file

@ -0,0 +1,460 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
@import url('https://fonts.googleapis.com/css?family=Carter+One|Source+Sans+Pro:300,300i,700');
@import "compass/reset";
@import "_colors";
*, *:after, *:before {
box-sizing: border-box;
}
body {
background: $fond;
font: 17px $bodyfont;
}
header {
background: $bandeau;
}
h1, h2 {
font-family: $headfont;
color: $titre;
}
h1 {
font-size: 2.3em;
}
h2 {
font-size: 1.6em;
}
a {
color: $lien;
text-decoration: none;
font-weight: bold;
}
h2 a {
font-weight: inherit;
color: inherit;
}
header {
a {
color: $headerlien;
}
section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: stretch;
&.bottom-menu {
justify-content: space-around;
text-align: center;
background: $sousbandeau;
}
}
h1 {
padding: 0 15px;
}
nav {
ul {
display: inline-flex;
li {
display: inline-block;
& > * {
display: block;
padding: 10px 15px;
font-weight: bold;
&:hover {
background: darken($bandeau, 10%);
}
}
}
}
.lang-select {
display: inline-block;
height: 100%;
vertical-align: top;
position: relative;
&:before {
content: "";
color: #fff;
position: absolute;
top: 0;
left: 0;
border-left: 1px solid #fff;
height: calc(100% - 20px);
margin: 10px 0;
padding-left: 10px;
}
a {
padding: 10px 20px;
display: block;
img {
display: block;
width: auto;
max-height: 20px;
vertical-align: middle;
}
}
}
}
}
article {
line-height: 1.4;
p, ul {
margin: 0.4em 0;
}
ul {
padding-left: 20px;
li {
list-style: outside;
}
}
&:last-child {
margin-bottom: 30px;
}
}
.container {
max-width: 1000px;
margin: 0 auto;
position: relative;
.aside-wrap {
position: absolute;
top: 30px;
height: 100%;
width: 25%;
left: 6px;
.aside {
color: #222;
position: fixed;
position: sticky;
top: 5px;
width: 100%;
background: $aside;
padding: 15px;
box-shadow: -4px 4px 1px rgba($ombres, 0.3);
h2 {
color: #fff;
}
.calendar {
margin: 0 auto;
display: block;
}
a {
color: darken($lien, 10%);
}
}
}
.content {
max-width: 900px;
margin-left: auto;
margin-right: 6px;
.intro {
border-bottom: 3px solid darken($fond, 50%);
margin: 20px 0;
margin-top: 5px;
padding: 15px 5px;
}
section {
article {
background: #fff;
padding: 20px 30px;;
box-shadow: -4px 4px 1px rgba($ombres, 0.3);
border: 1px solid rgba($ombres, 0.1);
border-radius: 2px;
a {
color: $lien;
}
}
article + h2 {
margin-top: 15px;
}
article + article {
margin-top: 25px;
}
.image {
margin: 15px 0;
text-align: center;
padding: 20px;
img {
max-width: 100%;
height: auto;
box-shadow: -7px 7px 1px rgba($ombres, 0.2);
}
}
&.directory {
article.entry {
width: 80%;
max-width: 600px;
max-height: 100%;
position: relative;
margin-left: 6%;
.entry-image {
display: block;
float: right;
width: 150px;
background: #fff;
box-shadow: -4px 4px 1px rgba($ombres, 0.2);
border-right: 1px solid rgba($ombres, 0.2);
border-top: 1px solid rgba($ombres, 0.2);
padding: 1px;
overflow: hidden;
margin-left: 10px;
margin-bottom: 10px;
transform: translateX(10px);
img {
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
}
}
ul.links {
margin-top: 10px;
border-top: 1px solid $sousbandeau;
padding-top: 10px;
}
}
}
&.actuhome {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: top;
article + article {
margin: 0;
}
article.actu {
position: relative;
background: none;
box-shadow: none;
border: none;
max-width: 400px;
min-width: 300px;
flex: 1;
.actu-header {
position: relative;
box-shadow: -4px 5px 1px rgba($ombres, 0.3);
border-right: 1px solid rgba($ombres, 0.2);
border-top: 1px solid rgba($ombres, 0.2);
min-height: 180px;
padding: 0;
margin: 0;
overflow: hidden;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
h2 {
position: absolute;
width: 100%;
bottom: 0;
left: 0;
padding: 5px;
text-shadow: 0 0 5px rgba($ombres, 0.8);
background: linear-gradient(to top, rgba(#000, 0.7), rgba(#000, 0));
a {
color: #fff;
}
}
}
.actu-misc {
background: lighten($fond, 15%);
box-shadow: -2px 2px 1px rgba($ombres, 0.2);
border: 1px solid rgba($ombres, 0.2);
border-radius: 2px;
margin: 0 10px;
padding: 15px;
padding-top: 5px;
.actu-minical {
display: block;
}
.actu-dates {
display: block;
text-align: right;
font-size: 0.9em;
}
}
.actu-overlay {
display: block;
background: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5;
opacity: 0;
}
}
}
&.actulist {
article.actu {
display:flex;
width: 100%;
padding: 0;
.actu-image {
width: 30%;
max-width: 200px;
background-size: cover;
background-position: center center;
}
.actu-infos {
padding: 15px;
flex: 1;
.actu-dates {
font-weight: bold;
font-size: 0.9em;
}
}
}
}
}
}
.aside-wrap + .content {
max-width: 70%;
}
}
.calendar {
color: rgba(#000, 0.8);
width: 200px;
td, th {
text-align: center;
vertical-align: middle;
border: 2px solid transparent;
padding: 1px;
}
th {
font-weight: bold;
}
td {
font-size: 0.8em;
width: 28px;
height: 28px;
&.out {
opacity: 0.3;
}
&.today {
border-bottom-color: #000;
}
&:nth-child(7), &:nth-child(6) {
background: rgba(#000, 0.2);
}
&.hasevent {
position: relative;
font-weight: bold;
color: $sousbandeau;
font-size: 1em;
& > a {
padding: 3px;
color: $sousbandeau !important;
}
ul.cal-events {
text-align: left;
display: none;
position: absolute;
z-index: 2;
background: #fff;
width: 150px;
left: -30px;
margin-top: 10px;
padding: 5px;
background-color: $sousbandeau;
.datename {
display: none;
}
&:before {
top: -12px;
left: 38px;
content: "";
position: absolute;
border: 6px solid transparent;
border-bottom-color: $sousbandeau;
}
a {
color: #fff;
}
}
& > a:hover {
background-color: $sousbandeau;
color: #fff !important;
& + ul.cal-events {
display: block;
}
}
}
}
}
#calendar-wrap .details {
border-top: 1px solid $sousbandeau;
margin-top: 15px;
padding-top: 10px;
li.datename {
&:after {
content: " :";
}
font-weight: bold;
font-size: 1.1em;
margin-bottom: 5px;
}
}
@import "_responsive";

View file

@ -0,0 +1,51 @@
{% load static menu_tags wagtailuserbar i18n wagtailcore_tags %}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Association des élèves de l'ENS Ulm{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{% static "cofcms/css/screen.css" %}"/>
{% block extra_head %}{% endblock %}
</head>
<body>
<header id="header">
<section class="top-menu">
<h1 class="cof"><a href="/">COF</a></h1>
<a class="minimenu" href="javascript:void(0)" onclick="document.getElementById('header').classList.toggle('expanded');"><img src="{% static "cofcms/images/minimenu.svg" %}"></a>
<nav>
{% flat_menu "cof-nav-ext" template="cofcms/base_nav.html" %}
</nav>
</section>
<section class="bottom-menu">
<nav>
{% flat_menu "cof-nav-int" template="cofcms/base_nav.html" apply_active_classes=True %}
{% get_current_language as curlang %}
<div class="lang-select">
{% if curlang == 'en' %}
{% language 'fr' %}
<a href="{% pageurl self %}" title="Français"><img src="{% static "cofcms/images/fr.png" %}"></a>
{% endlanguage %}
{% else %}
{% language 'en' %}
<a href="{% pageurl self %}" title="English"><img src="{% static "cofcms/images/en.png" %}"></a>
{% endlanguage %}
{% endif %}
</div>
</nav>
</section>
</header>
<div class="container">
{% block superaside %}{% endblock %}
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
{% wagtailuserbar %}
</body>
</html>

View file

@ -0,0 +1,12 @@
{% extends "cofcms/base.html" %}
{% block superaside %}
<div class="aside-wrap">
<div class="aside" id="aside">
<h2 onclick="document.getElementById('aside').classList.toggle('expanded')">{% block aside_title %}{% endblock %}</h2>
<div class="aside-content">
{% block aside %}{% endblock %}
</div>
</div>
</div>
{% endblock %}

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