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

This commit is contained in:
Qwann 2018-10-11 23:10:37 +02:00
commit 3b23c28965
206 changed files with 12313 additions and 7830 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ venv/
media/ media/
*.log *.log
*.sqlite3 *.sqlite3
.coverage
# PyCharm # PyCharm
.idea .idea

View file

@ -1,3 +1,5 @@
image: "python:3.5"
services: services:
- postgres:latest - postgres:latest
- redis:latest - redis:latest
@ -10,7 +12,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 +22,41 @@ 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
- python --version
script: script:
- python manage.py test - coverage run manage.py test
after_script:
- coverage report
cache:
key: test
paths:
- vendor/
# For GitLab CI to get coverage from build.
# Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
linters:
image: python:3.6
stage: test
before_script:
- mkdir -p vendor/pip
- pip install --upgrade black isort flake8
script:
- black --check .
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
# Print errors only
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
cache:
key: linters
paths:
- vendor/

106
.pre-commit.sh Executable file
View file

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

View file

@ -1,6 +1,7 @@
# GestioCOF # GestioCOF
![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg) [![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
@ -43,6 +44,16 @@ pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py cof/settings/secret.py 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 #### Fin d'installation

View file

@ -1 +0,0 @@

View file

@ -1,16 +1,24 @@
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 dal.autocomplete import ModelSelect2 from bda.models import (
Attribution,
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ CategorieSpectacle,
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente ChoixSpectacle,
Participant,
Quote,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
class ReadOnlyMixin(object): class ReadOnlyMixin(object):
@ -27,8 +35,8 @@ class ReadOnlyMixin(object):
class ChoixSpectacleAdminForm(forms.ModelForm): class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'participant': ModelSelect2(url='bda-participant-autocomplete'), "participant": ModelSelect2(url="bda-participant-autocomplete"),
'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'), "spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
} }
@ -43,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):
@ -70,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
@ -81,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')
) )
@ -94,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"
@ -108,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:
@ -143,24 +150,23 @@ 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):
@ -171,21 +177,26 @@ class AttributionAdminForm(forms.ModelForm):
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):
@ -193,13 +204,15 @@ class ChoixSpectacleAdmin(admin.ModelAdmin):
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):
@ -209,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['confirmed_entry'].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')
) )
@ -252,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):
@ -263,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
@ -284,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):
@ -296,14 +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.reset(new_date=timezone.now() - timedelta(hours=1)) revente.reset(new_date=timezone.now() - timedelta(hours=1))
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,5 +1,3 @@
from django.db.models import Max
import random import random
@ -16,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:
@ -54,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)])
@ -82,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

@ -6,7 +6,6 @@ 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)
@ -16,9 +15,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.
@ -30,7 +29,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
@ -53,128 +52,128 @@ class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
label = "{show}{suffix}" label = "{show}{suffix}"
suffix = "" suffix = ""
if self.own: if self.own:
# C'est notre propre revente : pas besoin de spécifier le vendeur # C'est notre propre revente : informations sur le statut
if obj.soldTo is not None: if obj.soldTo is not None:
suffix = " -- Vendue à {firstname} {lastname}".format( suffix = " -- Vendue à {firstname} {lastname}".format(
firstname=obj.soldTo.user.first_name, firstname=obj.soldTo.user.first_name,
lastname=obj.soldTo.user.last_name, lastname=obj.soldTo.user.last_name,
) )
elif obj.shotgun:
suffix = " -- Tirage infructueux"
elif obj.notif_sent:
suffix = " -- Inscriptions au tirage en cours"
else: else:
# Ce n'est pas à nous : on ne voit jamais l'acheteur # Ce n'est pas à nous : on ne voit jamais l'acheteur
suffix = " -- Vendue par {firstname} {lastname}".format( suffix = " -- Vendue par {firstname} {lastname}".format(
firstname=obj.seller.user.first_name, firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name
lastname=obj.seller.user.last_name, )
)
return label.format(show=str(obj.attribution.spectacle), return label.format(show=str(obj.attribution.spectacle), suffix=suffix)
suffix=suffix)
class ResellForm(forms.Form): class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField( attributions = AttributionModelMultipleChoiceField(
label='', label="",
queryset=Attribution.objects.none(), queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) required=False,
)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields["attributions"].queryset = (
participant.attribution_set participant.attribution_set.filter(spectacle__date__gte=timezone.now())
.filter(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')
) )
class AnnulForm(forms.Form): class AnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField( reventes = ReventeModelMultipleChoiceField(
own=True, own=True,
label='', label="",
queryset=Attribution.objects.none(), queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) required=False,
)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['reventes'].queryset = ( self.fields["reventes"].queryset = (
participant.original_shows participant.original_shows.filter(
.filter(attribution__spectacle__date__gte=timezone.now(), attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
notif_sent=False, )
soldTo__isnull=True) .select_related(
.select_related('attribution__spectacle', "attribution__spectacle", "attribution__spectacle__location"
'attribution__spectacle__location') )
.order_by("-date")
) )
class InscriptionReventeForm(forms.Form): class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField( spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(), queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) required=False,
)
def __init__(self, tirage, *args, **kwargs): def __init__(self, tirage, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['spectacles'].queryset = ( self.fields["spectacles"].queryset = tirage.spectacle_set.select_related(
tirage.spectacle_set "location"
.select_related('location') ).filter(date__gte=timezone.now())
.filter(date__gte=timezone.now())
)
class ReventeTirageAnnulForm(forms.Form): class ReventeTirageAnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField( reventes = ReventeModelMultipleChoiceField(
own=False, own=False,
label='', label="",
queryset=SpectacleRevente.objects.none(), queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False required=False,
) )
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['reventes'].queryset = ( self.fields["reventes"].queryset = participant.entered.filter(
participant.entered.filter(soldTo__isnull=True) soldTo__isnull=True
.select_related('attribution__spectacle', ).select_related("attribution__spectacle", "seller__user")
'seller__user')
)
class ReventeTirageForm(forms.Form): class ReventeTirageForm(forms.Form):
reventes = ReventeModelMultipleChoiceField( reventes = ReventeModelMultipleChoiceField(
own=False, own=False,
label='', label="",
queryset=SpectacleRevente.objects.none(), queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False required=False,
) )
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['reventes'].queryset = ( self.fields["reventes"].queryset = (
SpectacleRevente.objects.filter( SpectacleRevente.objects.filter(
notif_sent=True, notif_sent=True, shotgun=False, tirage_done=False
shotgun=False, )
tirage_done=False .exclude(confirmed_entry=participant)
).exclude(confirmed_entry=participant) .select_related("attribution__spectacle")
.select_related('attribution__spectacle')
) )
class SoldForm(forms.Form): class SoldForm(forms.Form):
reventes = ReventeModelMultipleChoiceField( reventes = ReventeModelMultipleChoiceField(
own=True, own=True,
label='', label="",
queryset=Attribution.objects.none(), queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple) widget=forms.CheckboxSelectMultiple,
)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['reventes'].queryset = ( self.fields["reventes"].queryset = (
participant.original_shows participant.original_shows.filter(soldTo__isnull=False)
.filter(soldTo__isnull=False)
.exclude(soldTo=participant) .exclude(soldTo=participant)
.select_related('attribution__spectacle', .select_related(
'attribution__spectacle__location') "attribution__spectacle", "attribution__spectacle__location"
)
) )

View file

@ -5,17 +5,15 @@ Crée deux tirages de test et y inscrit les utilisateurs
import os import 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

@ -4,12 +4,14 @@ Gestion en ligne de commande des reventes.
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):
@ -28,22 +30,18 @@ class Command(BaseCommand):
) )
# Le spectacle est dans plus longtemps : on prévient # Le spectacle est dans plus longtemps : on prévient
elif (revente.can_notif and not revente.notif_sent): 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( self.stdout.write(
"Mails d'inscription à la revente [%s] envoyés" "Mails d'inscription à la revente [%s] envoyés" % revente
% revente
) )
# On fait le tirage # On fait le tirage
elif (now >= revente.date_tirage and not revente.tirage_done): elif now >= revente.date_tirage and not revente.tirage_done:
self.stdout.write(str(now)) self.stdout.write(str(now))
winner = revente.tirage() winner = revente.tirage()
self.stdout.write( self.stdout.write("Tirage effectué pour la revente [%s]" % revente)
"Tirage effectué pour la revente [%s]"
% revente
)
if winner: if winner:
self.stdout.write("Gagnant : %s" % winner.user) self.stdout.write("Gagnant : %s" % winner.user)

View file

@ -3,27 +3,31 @@ Gestion en ligne de commande des mails de rappel.
""" """
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', on_delete=models.CASCADE)), ),
("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, on_delete=models.CASCADE), 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', on_delete=models.CASCADE), 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', on_delete=models.CASCADE), 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', on_delete=models.CASCADE), 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', on_delete=models.CASCADE), 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, on_delete=models.CASCADE), 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, on_delete=models.CASCADE), 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, on_delete=models.CASCADE), 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', on_delete=models.CASCADE), 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', on_delete=models.CASCADE), 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,91 +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(
on_delete=models.CASCADE, blank=True,
null=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),
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,69 +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(
on_delete=models.CASCADE, to="bda.Attribution", on_delete=models.CASCADE, related_name="revente"
related_name='revente'), ),
), ),
migrations.AddField( migrations.AddField(
model_name='spectaclerevente', model_name="spectaclerevente",
name='seller', name="seller",
field=models.ForeignKey(to='bda.Participant', field=models.ForeignKey(
on_delete=models.CASCADE, to="bda.Participant",
verbose_name='Vendeur', on_delete=models.CASCADE,
related_name='original_shows'), 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(
on_delete=models.CASCADE, to="bda.Participant",
verbose_name='Vendue à', null=True, on_delete=models.CASCADE,
blank=True), 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

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

View file

@ -14,40 +14,38 @@ def swap_double_choice(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0011_tirage_appear_catalogue")]
('bda', '0011_tirage_appear_catalogue'),
]
operations = [ operations = [
# Temporarily allow an extra "tmp" value for the `double_choice` field # Temporarily allow an extra "tmp" value for the `double_choice` field
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",
max_length=10, max_length=10,
default='1', default="1",
choices=[ choices=[
('tmp', 'tmp'), ("tmp", "tmp"),
('1', '1 place'), ("1", "1 place"),
('double', '2 places si possible, 1 sinon'), ("double", "2 places si possible, 1 sinon"),
('autoquit', '2 places sinon rien') ("autoquit", "2 places sinon rien"),
] ],
), ),
), ),
migrations.RunPython(swap_double_choice, migrations.RunPython.noop), migrations.RunPython(swap_double_choice, migrations.RunPython.noop),
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",
max_length=10, max_length=10,
default='1', default="1",
choices=[ choices=[
('1', '1 place'), ("1", "1 place"),
('double', '2 places si possible, 1 sinon'), ("double", "2 places si possible, 1 sinon"),
('autoquit', '2 places sinon rien') ("autoquit", "2 places sinon rien"),
] ],
), ),
), ),
] ]

View file

@ -7,10 +7,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
('bda', '0012_notif_time'),
('bda', '0012_swap_double_choice'),
]
operations = [ operations = []
]

View file

@ -1,23 +1,22 @@
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.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
from custommail.models import CustomMail
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
@ -29,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):
@ -49,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
@ -61,28 +60,26 @@ 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( category = models.ForeignKey(
CategorieSpectacle, on_delete=models.CASCADE, CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True
blank=True, null=True,
) )
date = models.DateTimeField("Date & heure") date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle, on_delete=models.CASCADE) 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, on_delete=models.CASCADE) 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())
@ -92,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):
@ -101,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):
@ -111,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)
@ -140,8 +139,8 @@ class Spectacle(models.Model):
class Quote(models.Model): class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE) 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 = (
@ -154,20 +153,20 @@ PAYMENT_TYPES = (
class Participant(models.Model): class Participant(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) 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, on_delete=models.CASCADE) 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)
@ -183,30 +182,32 @@ DOUBLE_CHOICES = (
class ChoixSpectacle(models.Model): class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE) participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey( spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, Spectacle, on_delete=models.CASCADE, related_name="participants"
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"
@ -214,48 +215,49 @@ class ChoixSpectacle(models.Model):
class Attribution(models.Model): class Attribution(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE) participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey( spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, Spectacle, on_delete=models.CASCADE, related_name="attribues"
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 = models.OneToOneField(
Attribution, on_delete=models.CASCADE, Attribution, on_delete=models.CASCADE, related_name="revente"
related_name="revente", )
date = models.DateTimeField("Date de mise en vente", default=timezone.now)
confirmed_entry = models.ManyToManyField(
Participant, related_name="entered", blank=True
) )
date = models.DateTimeField("Date de mise en vente",
default=timezone.now)
confirmed_entry = models.ManyToManyField(Participant,
related_name="entered",
blank=True)
seller = models.ForeignKey( seller = models.ForeignKey(
Participant, on_delete=models.CASCADE, Participant,
on_delete=models.CASCADE,
verbose_name="Vendeur", verbose_name="Vendeur",
related_name="original_shows", related_name="original_shows",
) )
soldTo = models.ForeignKey( soldTo = models.ForeignKey(
Participant, on_delete=models.CASCADE, Participant,
on_delete=models.CASCADE,
verbose_name="Vendue à", verbose_name="Vendue à",
blank=True, null=True, blank=True,
null=True,
) )
notif_sent = models.BooleanField("Notification envoyée", notif_sent = models.BooleanField("Notification envoyée", default=False)
default=False)
notif_time = models.DateTimeField("Moment d'envoi de la notification", notif_time = models.DateTimeField(
blank=True, null=True) "Moment d'envoi de la notification", blank=True, null=True
)
tirage_done = models.BooleanField("Tirage effectué", tirage_done = models.BooleanField("Tirage effectué", default=False)
default=False)
shotgun = models.BooleanField("Disponible immédiatement", shotgun = models.BooleanField("Disponible immédiatement", default=False)
default=False)
#### ####
# Some class attributes # Some class attributes
### ###
@ -282,8 +284,9 @@ class SpectacleRevente(models.Model):
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."""
remaining_time = (self.attribution.spectacle.date remaining_time = (
- self.real_notif_time - self.min_margin) self.attribution.spectacle.date - self.real_notif_time - self.min_margin
)
delay = min(remaining_time, self.max_wait_time) delay = min(remaining_time, self.max_wait_time)
@ -296,16 +299,14 @@ class SpectacleRevente(models.Model):
Plus précisément, on doit avoir min_margin + min_wait_time de marge. Plus précisément, on doit avoir min_margin + min_wait_time de marge.
""" """
spectacle_date = self.attribution.spectacle.date spectacle_date = self.attribution.spectacle.date
return (spectacle_date <= timezone.now() + self.min_margin return spectacle_date <= timezone.now() + self.min_margin + self.min_wait_time
+ self.min_wait_time)
@property @property
def can_notif(self): def can_notif(self):
return (timezone.now() >= self.date + self.remorse_time) 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"
@ -327,17 +328,19 @@ class SpectacleRevente(models.Model):
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)
@ -350,16 +353,18 @@ 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)
@ -389,30 +394,33 @@ class SpectacleRevente(models.Model):
mails = [] mails = []
context = { context = {
'acheteur': winner.user, "acheteur": winner.user,
'vendeur': seller.user, "vendeur": seller.user,
'show': spectacle, "show": spectacle,
} }
c_mails_qs = CustomMail.objects.filter(shortname__in=[ c_mails_qs = CustomMail.objects.filter(
'bda-revente-winner', 'bda-revente-loser', shortname__in=[
'bda-revente-seller', "bda-revente-winner",
]) "bda-revente-loser",
"bda-revente-seller",
]
)
c_mails = {cm.shortname: cm for cm in c_mails_qs} c_mails = {cm.shortname: cm for cm in c_mails_qs}
mails.append( mails.append(
c_mails['bda-revente-winner'].get_message( c_mails["bda-revente-winner"].get_message(
context, context,
from_email=settings.MAIL_DATA['revente']['FROM'], from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[winner.user.email], to=[winner.user.email],
) )
) )
mails.append( mails.append(
c_mails['bda-revente-seller'].get_message( c_mails["bda-revente-seller"].get_message(
context, context,
from_email=settings.MAIL_DATA['revente']['FROM'], from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[seller.user.email], to=[seller.user.email],
reply_to=[winner.user.email], reply_to=[winner.user.email],
) )
@ -422,12 +430,12 @@ class SpectacleRevente(models.Model):
for inscrit in inscrits: for inscrit in inscrits:
if inscrit != winner: if inscrit != winner:
new_context = dict(context) new_context = dict(context)
new_context['acheteur'] = inscrit.user new_context["acheteur"] = inscrit.user
mails.append( mails.append(
c_mails['bda-revente-loser'].get_message( c_mails["bda-revente-loser"].get_message(
new_context, new_context,
from_email=settings.MAIL_DATA['revente']['FROM'], from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[inscrit.user.email], to=[inscrit.user.email],
) )
) )

View file

@ -6,7 +6,7 @@
<h2>Gestion des places que je revends</h2> <h2>Gestion des places que je revends</h2>
{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %} {% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %}
{% if resellform.attributions %} {% if resell_attributions %}
<br /> <br />
<h3>Places non revendues</h3> <h3>Places non revendues</h3>
@ -29,34 +29,24 @@
<hr /> <hr />
{% endif %} {% endif %}
{% if annul_reventes or overdue %} {% if annul_reventes %}
<h3>Places en cours de revente</h3> <h3>Places en cours de revente</h3>
<form action="" method="post"> <form action="" method="post">
{% if annul_reventes %} <div class="bg-info text-info center-block">
<div class="bg-info text-info center-block"> <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se.
Vous pouvez annuler les places mises en vente il y a moins d'une heure. </div>
</div> {% csrf_token %}
{% endif %} <div class='form-group'>
{% csrf_token %} <div class='multiple-checkbox'>
<div class='form-group'> <ul>
<div class='multiple-checkbox'> {% for revente in annul_reventes %}
<ul> <li>{{ revente.tag }} {{ revente.choice_label }}</li>
{% for revente in annul_reventes %} {% endfor %}
<li>{{ revente.tag }} {{ revente.choice_label }}</li> </ul>
{% endfor %} </div>
{% for attrib in overdue %} </div>
<li> <input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
<input type="checkbox" style="visibility:hidden">
{{ attrib.spectacle }}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if annul_reventes %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form> </form>
<hr /> <hr />
@ -82,7 +72,7 @@
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button> <button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form> </form>
{% endif %} {% endif %}
{% if not resell_attributions and not annul_attributions and not overdue and not sold_reventes %} {% if not resell_attributions and not annul_reventes and not sold_reventes %}
<p>Plus de reventes possibles !</p> <p>Plus de reventes possibles !</p>
{% endif %} {% endif %}

View file

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

View file

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

View file

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

View file

@ -1,62 +1,75 @@
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'^etat-places/(?P<tirage_id>\d+)$', url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage),
views.etat_places, url(
name='bda-etat-places'), r"^spectacles/(?P<tirage_id>\d+)$",
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'^spectacles/autocomplete$', r"^spectacles/autocomplete$",
views.spectacle_autocomplete, views.spectacle_autocomplete,
name="bda-spectacle-autocomplete"), name="bda-spectacle-autocomplete",
url(r'^participants/autocomplete$', ),
url(
r"^participants/autocomplete$",
views.participant_autocomplete, views.participant_autocomplete,
name="bda-participant-autocomplete"), name="bda-participant-autocomplete",
),
# Urls BdA-Revente # Urls BdA-Revente
url(
url(r'^revente/(?P<tirage_id>\d+)/manage$', r"^revente/(?P<tirage_id>\d+)/manage$",
views.revente_manage, views.revente_manage,
name='bda-revente-manage'), name="bda-revente-manage",
url(r'^revente/(?P<tirage_id>\d+)/subscribe$', ),
url(
r"^revente/(?P<tirage_id>\d+)/subscribe$",
views.revente_subscribe, views.revente_subscribe,
name="bda-revente-subscribe"), name="bda-revente-subscribe",
url(r'^revente/(?P<tirage_id>\d+)/tirages$', ),
url(
r"^revente/(?P<tirage_id>\d+)/tirages$",
views.revente_tirages, views.revente_tirages,
name="bda-revente-tirages"), name="bda-revente-tirages",
url(r'^revente/(?P<spectacle_id>\d+)/buy$', ),
url(
r"^revente/(?P<spectacle_id>\d+)/buy$",
views.revente_buy, views.revente_buy,
name="bda-revente-buy"), name="bda-revente-buy",
url(r'^revente/(?P<revente_id>\d+)/confirm$', ),
url(
r"^revente/(?P<revente_id>\d+)/confirm$",
views.revente_confirm, views.revente_confirm,
name='bda-revente-confirm'), name="bda-revente-confirm",
url(r'^revente/(?P<tirage_id>\d+)/shotgun$', ),
url(
r"^revente/(?P<tirage_id>\d+)/shotgun$",
views.revente_shotgun, views.revente_shotgun,
name="bda-revente-shotgun"), name="bda-revente-shotgun",
),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
views.send_rappel, url(
name="bda-rappels" r"^descriptions/(?P<tirage_id>\d+)$",
), views.descriptions_spectacles,
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, name="bda-descriptions",
name='bda-descriptions'), ),
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue, url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-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

@ -2,4 +2,4 @@
Formats français. Formats français.
""" """
DATETIME_FORMAT = r'l j F Y \à H:i' DATETIME_FORMAT = r"l j F Y \à H:i"

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

View file

@ -46,112 +46,106 @@ 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' TESTING = sys.argv[1] == "test"
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'shared', "shared",
"gestioncof",
'gestioncof',
# Must be before 'django.contrib.admin'. # Must be before 'django.contrib.admin'.
# https://django-autocomplete-light.readthedocs.io/en/master/install.html # https://django-autocomplete-light.readthedocs.io/en/master/install.html
'dal', "dal",
'dal_select2', "dal_select2",
"django.contrib.auth",
'django.contrib.auth', "django.contrib.contenttypes",
'django.contrib.contenttypes', "django.contrib.sessions",
'django.contrib.sessions', "django.contrib.sites",
'django.contrib.sites', "django.contrib.messages",
'django.contrib.messages', "django.contrib.staticfiles",
'django.contrib.staticfiles', "django.contrib.admin",
'django.contrib.admin', "django.contrib.admindocs",
'django.contrib.admindocs', "bda",
"captcha",
'bda', "django_cas_ng",
'captcha', "bootstrapform",
'django_cas_ng', "kfet",
'bootstrapform', "kfet.open",
'kfet', "channels",
'kfet.open', "widget_tweaks",
'channels', "custommail",
'widget_tweaks', "djconfig",
'custommail', "wagtail.wagtailforms",
'djconfig', "wagtail.wagtailredirects",
'wagtail.wagtailforms', "wagtail.wagtailembeds",
'wagtail.wagtailredirects', "wagtail.wagtailsites",
'wagtail.wagtailembeds', "wagtail.wagtailusers",
'wagtail.wagtailsites', "wagtail.wagtailsnippets",
'wagtail.wagtailusers', "wagtail.wagtaildocs",
'wagtail.wagtailsnippets', "wagtail.wagtailimages",
'wagtail.wagtaildocs', "wagtail.wagtailsearch",
'wagtail.wagtailimages', "wagtail.wagtailadmin",
'wagtail.wagtailsearch', "wagtail.wagtailcore",
'wagtail.wagtailadmin', "wagtail.contrib.modeladmin",
'wagtail.wagtailcore', "wagtailmenus",
'wagtail.contrib.modeladmin', "modelcluster",
'wagtailmenus', "taggit",
'modelcluster', "kfet.auth",
'taggit', "kfet.cms",
'kfet.auth', "corsheaders",
'kfet.cms',
'corsheaders',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', "corsheaders.middleware.CorsMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', "django.contrib.auth.middleware.SessionAuthenticationMiddleware",
'kfet.auth.middleware.TemporaryAuthMiddleware', "kfet.auth.middleware.TemporaryAuthMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'djconfig.middleware.DjConfigMiddleware', "djconfig.middleware.DjConfigMiddleware",
'wagtail.wagtailcore.middleware.SiteMiddleware', "wagtail.wagtailcore.middleware.SiteMiddleware",
'wagtail.wagtailredirects.middleware.RedirectMiddleware', "wagtail.wagtailredirects.middleware.RedirectMiddleware",
] ]
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.template.context_processors.i18n', "django.template.context_processors.i18n",
'django.template.context_processors.media', "django.template.context_processors.media",
'django.template.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"),
} }
} }
@ -159,9 +153,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
@ -173,35 +167,35 @@ USE_TZ = True
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 = "3"
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",
) )
@ -214,21 +208,16 @@ AUTHENTICATION_BACKENDS = (
NOCAPTCHA = True NOCAPTCHA = True
RECAPTCHA_USE_SSL = True RECAPTCHA_USE_SSL = True
CORS_ORIGIN_WHITELIST = ( CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
'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), ),
} }
} }
@ -239,20 +228,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

@ -6,32 +6,30 @@ 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, TESTING 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: if TESTING:
PASSWORD_HASHERS = [ PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
'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
@ -41,13 +39,10 @@ def show_toolbar(request):
""" """
return DEBUG return DEBUG
if not TESTING: if not TESTING:
INSTALLED_APPS += ["debug_toolbar", "debug_panel"] INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE = [ MIDDLEWARE = ["debug_panel.middleware.DebugPanelMiddleware"] + MIDDLEWARE
"debug_panel.middleware.DebugPanelMiddleware"
] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}
'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

@ -8,21 +8,13 @@ import os
from .common import * # NOQA from .common import * # NOQA
from .common import BASE_DIR, import_secret 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/"

View file

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

View file

@ -6,111 +6,130 @@ from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
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,
petitcours_patterns,
surveys_patterns,
)
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(petitcours_patterns)),
# 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, ),
name="ext_login_view"), url(r"^cas/login$", django_cas_views.login, name="cas_login_view"),
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}), url(r"^cas/logout$", django_cas_views.logout),
url(r'^login$', gestioncof_views.login, name="cof-login"), url(r"^outsider/login$", gestioncof_views.login_ext, name="ext_login_view"),
url(r'^logout$', gestioncof_views.logout, name="cof-logout"), 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"),
name='profile'), url(
url(r'^outsider/password-change$', django_views.password_change, r"^outsider/password-change$",
name='password_change'), django_views.password_change,
url(r'^outsider/password-change-done$', 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"),
name='registration'), url(
url(r'^registration/clipper/(?P<login_clipper>[\w-]+)/' r"^registration/clipper/(?P<login_clipper>[\w-]+)/" r"(?P<fullname>.*)$",
r'(?P<fullname>.*)$', gestioncof_views.registration_form2,
gestioncof_views.registration_form2, name="clipper-registration"), name="clipper-registration",
url(r'^registration/user/(?P<username>.+)$', ),
gestioncof_views.registration_form2, name="user-registration"), url(
url(r'^registration/empty$', gestioncof_views.registration_form2, r"^registration/user/(?P<username>.+)$",
name="empty-registration"), 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(
name="cof.registration.autocomplete"), r"^autocomplete/registration$",
url(r'^user/autocomplete$', gestioncof_views.user_autocomplete, autocomplete,
name='cof-user-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"^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"),
name='utile_cof'), url(r"^utile_bda$", gestioncof_views.utile_bda, name="utile_bda"),
url(r'^utile_bda$', gestioncof_views.utile_bda, url(r"^utile_bda/bda_diff$", gestioncof_views.liste_bdadiff, name="ml_diffbda"),
name='utile_bda'), url(r"^utile_cof/diff_cof$", gestioncof_views.liste_diffcof, name="ml_diffcof"),
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff, url(
name="ml_diffbda"), r"^utile_bda/bda_revente$",
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof, gestioncof_views.liste_bdarevente,
name='ml_diffcof'), name="ml_bda_revente",
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente, ),
name="ml_bda_revente"), url(r"^k-fet/", include("kfet.urls")),
url(r'^k-fet/', include('kfet.urls')), url(r"^cms/", include(wagtailadmin_urls)),
url(r'^cms/', include(wagtailadmin_urls)), url(r"^documents/", include(wagtaildocs_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"),
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 += [url(r"", include(wagtail_urls))]
url(r'', include(wagtail_urls)),
]

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 _
from dal.autocomplete import ModelSelect2 from gestioncof.models import (
Club,
CofProfile,
Event,
EventCommentField,
EventOption,
EventOptionChoice,
EventRegistration,
Survey,
SurveyQuestion,
SurveyQuestionAnswer,
)
from gestioncof.petits_cours_models import (
PetitCoursAbility,
PetitCoursAttribution,
PetitCoursAttributionCounter,
PetitCoursDemande,
PetitCoursSubject,
)
def add_link_field(target_model='', field='', link_text=str, 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,27 +158,33 @@ 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):
@ -184,15 +193,15 @@ class UserProfileAdmin(UserAdmin):
return super().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,72 +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 EventRegistrationAdminForm(forms.ModelForm):
class Meta: class Meta:
widgets = { widgets = {"user": ModelSelect2(url="cof-user-autocomplete")}
'user': ModelSelect2(url='cof-user-autocomplete'),
}
class EventRegistrationAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin):
form = EventRegistrationAdminForm form = EventRegistrationAdminForm
list_display = ('__str__', 'event', 'user', 'paid') list_display = ("__str__", "event", "user", "paid")
list_filter = ('paid',) list_filter = ("paid",)
search_fields = ('user__username', 'user__first_name', 'user__last_name', search_fields = (
'user__email', 'event__title') "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().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
@ -294,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,13 +1,12 @@
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):
@ -19,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,14 +1,16 @@
import csv import csv
from django.apps import apps
from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponse, HttpResponseForbidden
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.apps import apps
def export(qs, fields=None): def export(qs, fields=None):
model = qs.model model = qs.model
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type="text/csv")
response['Content-Disposition'] = 'attachment; filename=%s.csv' \ response["Content-Disposition"] = "attachment; filename=%s.csv" % slugify(
% slugify(model.__name__) model.__name__
)
writer = csv.writer(response) writer = csv.writer(response)
# Write headers to CSV file # Write headers to CSV file
if fields: if fields:
@ -32,8 +34,9 @@ def export(qs, fields=None):
return response return response
def admin_list_export(request, model_name, app_label, queryset=None, def admin_list_export(
fields=None, list_display=True): request, model_name, app_label, queryset=None, fields=None, list_display=True
):
""" """
Put the following line in your urls.py BEFORE your admin include Put the following line in your urls.py BEFORE your admin include
(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/', (r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',

View file

@ -5,9 +5,10 @@ def is_cof(user):
try: try:
profile = user.profile profile = user.profile
return profile.is_cof return profile.is_cof
except: except Exception:
return False return False
cof_required = user_passes_test(is_cof) cof_required = user_passes_test(is_cof)
@ -15,7 +16,8 @@ def is_buro(user):
try: try:
profile = user.profile profile = user.profile
return profile.is_buro return profile.is_buro
except: except Exception:
return False return False
buro_required = user_passes_test(is_buro) buro_required = user_passes_test(is_buro)

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import re
from django import template from django import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
import re
register = template.Library() register = template.Library()
@ -12,6 +12,7 @@ def key(d, key_name):
value = d[key_name] value = d[key_name]
except KeyError: except KeyError:
from django.conf import settings from django.conf import settings
value = settings.TEMPLATE_STRING_IF_INVALID value = settings.TEMPLATE_STRING_IF_INVALID
return value return value
@ -19,16 +20,15 @@ def key(d, key_name):
def highlight_text(text, q): def highlight_text(text, q):
q2 = "|".join(re.escape(word) for word in q.split()) q2 = "|".join(re.escape(word) for word in q.split())
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE) pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
return mark_safe(re.sub(pattern, return mark_safe(
r"<span class='highlight'>\g<filter></span>", re.sub(pattern, r"<span class='highlight'>\g<filter></span>", text)
text)) )
@register.filter @register.filter
def highlight_user(user, q): def highlight_user(user, q):
if user.first_name and user.last_name: if user.first_name and user.last_name:
text = "%s %s (<tt>%s</tt>)" % (user.first_name, user.last_name, text = "%s %s (<tt>%s</tt>)" % (user.first_name, user.last_name, user.username)
user.username)
else: else:
text = user.username text = user.username
return highlight_text(text, q) return highlight_text(text, q)

View file

@ -12,17 +12,17 @@ from gestioncof.models import CofProfile, User
class SimpleTest(TestCase): class SimpleTest(TestCase):
def test_delete_user(self): def test_delete_user(self):
u = User(username='foo', first_name='foo', last_name='bar') u = User(username="foo", first_name="foo", last_name="bar")
# to each user there's a cofprofile associated # to each user there's a cofprofile associated
u.save() u.save()
self.assertTrue(CofProfile.objects.filter(user__username='foo').exists()) self.assertTrue(CofProfile.objects.filter(user__username="foo").exists())
# there's no point in having a cofprofile without a user associated. # there's no point in having a cofprofile without a user associated.
u.delete() u.delete()
self.assertFalse(CofProfile.objects.filter(user__username='foo').exists()) self.assertFalse(CofProfile.objects.filter(user__username="foo").exists())
# there's no point in having a user without a cofprofile associated. # there's no point in having a user without a cofprofile associated.
u.save() u.save()
CofProfile.objects.get(user__username='foo').delete() CofProfile.objects.get(user__username="foo").delete()
self.assertFalse(User.objects.filter(username='foo').exists()) self.assertFalse(User.objects.filter(username="foo").exists())

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin
from .utils import create_user, create_member, create_staff from .utils import create_member, create_staff, create_user
class ViewTestCaseMixin(BaseViewTestCaseMixin): class ViewTestCaseMixin(BaseViewTestCaseMixin):
@ -18,7 +18,7 @@ class ViewTestCaseMixin(BaseViewTestCaseMixin):
def get_users_base(self): def get_users_base(self):
return { return {
'user': create_user('user'), "user": create_user("user"),
'member': create_member('member'), "member": create_member("member"),
'staff': create_staff('staff'), "staff": create_staff("staff"),
} }

View file

@ -7,28 +7,35 @@ def _create_user(username, is_cof=False, is_staff=False, attrs=None):
if attrs is None: if attrs is None:
attrs = {} attrs = {}
password = attrs.pop('password', username) password = attrs.pop("password", username)
user_keys = [ user_keys = ["first_name", "last_name", "email", "is_staff", "is_superuser"]
'first_name', 'last_name', 'email', 'is_staff', 'is_superuser',
]
user_attrs = {k: v for k, v in attrs.items() if k in user_keys} user_attrs = {k: v for k, v in attrs.items() if k in user_keys}
profile_keys = [ profile_keys = [
'is_cof', 'login_clipper', 'phone', 'occupation', 'departement', "is_cof",
'type_cotiz', 'mailing_cof', 'mailing_bda', 'mailing_bda_revente', "login_clipper",
'comments', 'is_buro', 'petit_cours_accept', "phone",
'petit_cours_remarques', "occupation",
"departement",
"type_cotiz",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"comments",
"is_buro",
"petit_cours_accept",
"petit_cours_remarques",
] ]
profile_attrs = {k: v for k, v in attrs.items() if k in profile_keys} profile_attrs = {k: v for k, v in attrs.items() if k in profile_keys}
if is_cof: if is_cof:
profile_attrs['is_cof'] = True profile_attrs["is_cof"] = True
if is_staff: if is_staff:
# At the moment, admin is accessible by COF staff. # At the moment, admin is accessible by COF staff.
user_attrs['is_staff'] = True user_attrs["is_staff"] = True
profile_attrs['is_buro'] = True profile_attrs["is_buro"] = True
user = User(username=username, **user_attrs) user = User(username=username, **user_attrs)
user.set_password(password) user.set_password(password)
@ -56,6 +63,6 @@ def create_staff(username, attrs=None):
def create_root(username, attrs=None): def create_root(username, attrs=None):
if attrs is None: if attrs is None:
attrs = {} attrs = {}
attrs.setdefault('is_staff', True) attrs.setdefault("is_staff", True)
attrs.setdefault('is_superuser', True) attrs.setdefault("is_superuser", True)
return _create_user(username, attrs=attrs) return _create_user(username, attrs=attrs)

View file

@ -1,67 +1,87 @@
from django.conf.urls import url from django.conf.urls import url
from gestioncof.petits_cours_views import DemandeListView, DemandeDetailView
from gestioncof import views, petits_cours_views from gestioncof import petits_cours_views, views
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
from gestioncof.petits_cours_views import DemandeDetailView, DemandeListView
export_patterns = [ export_patterns = [
url(r'^members$', views.export_members, url(r"^members$", views.export_members, name="cof.membres_export"),
name='cof.membres_export'), url(
url(r'^mega/avecremarques$', views.export_mega_remarksonly, r"^mega/avecremarques$",
name='cof.mega_export_remarks'), views.export_mega_remarksonly,
url(r'^mega/participants$', views.export_mega_participants, name="cof.mega_export_remarks",
name='cof.mega_export_participants'), ),
url(r'^mega/orgas$', views.export_mega_orgas, url(
name='cof.mega_export_orgas'), r"^mega/participants$",
views.export_mega_participants,
name="cof.mega_export_participants",
),
url(r"^mega/orgas$", views.export_mega_orgas, name="cof.mega_export_orgas"),
# url(r'^mega/(?P<type>.+)$', views.export_mega_bytype), # url(r'^mega/(?P<type>.+)$', views.export_mega_bytype),
url(r'^mega$', views.export_mega, url(r"^mega$", views.export_mega, name="cof.mega_export"),
name='cof.mega_export'),
] ]
petitcours_patterns = [ petitcours_patterns = [
url(r'^inscription$', petits_cours_views.inscription, url(
name='petits-cours-inscription'), r"^inscription$",
url(r'^demande$', petits_cours_views.demande, petits_cours_views.inscription,
name='petits-cours-demande'), name="petits-cours-inscription",
url(r'^demande-raw$', petits_cours_views.demande_raw, ),
name='petits-cours-demande-raw'), url(r"^demande$", petits_cours_views.demande, name="petits-cours-demande"),
url(r'^demandes$', url(
r"^demande-raw$",
petits_cours_views.demande_raw,
name="petits-cours-demande-raw",
),
url(
r"^demandes$",
buro_required(DemandeListView.as_view()), buro_required(DemandeListView.as_view()),
name='petits-cours-demandes-list'), name="petits-cours-demandes-list",
url(r'^demandes/(?P<pk>\d+)$', ),
url(
r"^demandes/(?P<pk>\d+)$",
buro_required(DemandeDetailView.as_view()), buro_required(DemandeDetailView.as_view()),
name='petits-cours-demande-details'), name="petits-cours-demande-details",
url(r'^demandes/(?P<demande_id>\d+)/traitement$', ),
url(
r"^demandes/(?P<demande_id>\d+)/traitement$",
petits_cours_views.traitement, petits_cours_views.traitement,
name='petits-cours-demande-traitement'), name="petits-cours-demande-traitement",
url(r'^demandes/(?P<demande_id>\d+)/retraitement$', ),
url(
r"^demandes/(?P<demande_id>\d+)/retraitement$",
petits_cours_views.retraitement, petits_cours_views.retraitement,
name='petits-cours-demande-retraitement'), name="petits-cours-demande-retraitement",
),
] ]
surveys_patterns = [ surveys_patterns = [
url(r'^(?P<survey_id>\d+)/status$', views.survey_status, url(
name='survey.details.status'), r"^(?P<survey_id>\d+)/status$",
url(r'^(?P<survey_id>\d+)$', views.survey, views.survey_status,
name='survey.details'), name="survey.details.status",
),
url(r"^(?P<survey_id>\d+)$", views.survey, name="survey.details"),
] ]
events_patterns = [ events_patterns = [
url(r'^(?P<event_id>\d+)$', views.event, url(r"^(?P<event_id>\d+)$", views.event, name="event.details"),
name='event.details'), url(r"^(?P<event_id>\d+)/status$", views.event_status, name="event.details.status"),
url(r'^(?P<event_id>\d+)/status$', views.event_status,
name='event.details.status'),
] ]
calendar_patterns = [ calendar_patterns = [
url(r'^subscription$', views.calendar, url(r"^subscription$", views.calendar, name="calendar"),
name='calendar'), url(
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics, r"^(?P<token>[a-z0-9-]+)/calendar.ics$", views.calendar_ics, name="calendar.ics"
name='calendar.ics'), ),
] ]
clubs_patterns = [ clubs_patterns = [
url(r'^membres/(?P<name>\w+)', views.membres_club, name='membres-club'), url(r"^membres/(?P<name>\w+)", views.membres_club, name="membres-club"),
url(r'^liste', views.liste_clubs, name='liste-clubs'), url(r"^liste", views.liste_clubs, name="liste-clubs"),
url(r'^change_respo/(?P<club_name>\w+)/(?P<user_id>\d+)', url(
views.change_respo, name='change-respo'), r"^change_respo/(?P<club_name>\w+)/(?P<user_id>\d+)",
views.change_respo,
name="change-respo",
),
] ]

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
from django.forms.widgets import Widget
from django.forms.utils import flatatt from django.forms.utils import flatatt
from django.forms.widgets import Widget
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -13,8 +13,8 @@ class TriStateCheckbox(Widget):
def render(self, name, value, attrs=None, choices=()): def render(self, name, value, attrs=None, choices=()):
if value is None: if value is None:
value = 'none' value = "none"
attrs['value'] = value attrs["value"] = value
final_attrs = self.build_attrs(self.attrs, attrs) final_attrs = self.build_attrs(self.attrs, attrs)
output = ["<span class=\"tristate\"%s></span>" % flatatt(final_attrs)] output = ['<span class="tristate"%s></span>' % flatatt(final_attrs)]
return mark_safe('\n'.join(output)) return mark_safe("\n".join(output))

View file

@ -1 +1 @@
default_app_config = 'kfet.apps.KFetConfig' default_app_config = "kfet.apps.KFetConfig"

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -2,7 +2,7 @@ from django.apps import AppConfig
class KFetConfig(AppConfig): class KFetConfig(AppConfig):
name = 'kfet' name = "kfet"
verbose_name = "Application K-Fêt" verbose_name = "Application K-Fêt"
def ready(self): def ready(self):
@ -11,4 +11,5 @@ class KFetConfig(AppConfig):
def register_config(self): def register_config(self):
import djconfig import djconfig
from kfet.forms import KFetConfigForm from kfet.forms import KFetConfigForm
djconfig.register(KFetConfigForm) djconfig.register(KFetConfigForm)

View file

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

View file

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

View file

@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from kfet.models import Account, GenericTeamToken from kfet.models import Account, GenericTeamToken
from .utils import get_kfet_generic_user from .utils import get_kfet_generic_user
@ -12,11 +13,7 @@ class BaseKFetBackend:
Add extra select related up to Account. Add extra select related up to Account.
""" """
try: try:
return ( return User.objects.select_related("profile__account_kfet").get(pk=user_id)
User.objects
.select_related('profile__account_kfet')
.get(pk=user_id)
)
except User.DoesNotExist: except User.DoesNotExist:
return None return None

View file

@ -2,9 +2,6 @@ from django.contrib.auth.context_processors import PermWrapper
def temporary_auth(request): def temporary_auth(request):
if hasattr(request, 'real_user'): if hasattr(request, "real_user"):
return { return {"user": request.real_user, "perms": PermWrapper(request.real_user)}
'user': request.real_user,
'perms': PermWrapper(request.real_user),
}
return {} return {}

View file

@ -5,15 +5,12 @@ from django.forms import widgets
class KFetPermissionsField(forms.ModelMultipleChoiceField): class KFetPermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
queryset = Permission.objects.filter( queryset = Permission.objects.filter(
content_type__in=ContentType.objects.filter(app_label="kfet"), content_type__in=ContentType.objects.filter(app_label="kfet")
) )
super().__init__( super().__init__(
queryset=queryset, queryset=queryset, widget=widgets.CheckboxSelectMultiple, *args, **kwargs
widget=widgets.CheckboxSelectMultiple,
*args, **kwargs
) )
def label_from_instance(self, obj): def label_from_instance(self, obj):

View file

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

View file

@ -12,21 +12,19 @@ class TemporaryAuthMiddleware:
values from CofProfile and Account of this user. values from CofProfile and Account of this user.
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
if request.user.is_authenticated: if request.user.is_authenticated:
# avoid multiple db accesses in views and templates # avoid multiple db accesses in views and templates
request.user = ( request.user = User.objects.select_related("profile__account_kfet").get(
User.objects pk=request.user.pk
.select_related('profile__account_kfet')
.get(pk=request.user.pk)
) )
temp_request_user = AccountBackend().authenticate( temp_request_user = AccountBackend().authenticate(
request, request, kfet_password=self.get_kfet_password(request)
kfet_password=self.get_kfet_password(request),
) )
if temp_request_user: if temp_request_user:
@ -36,7 +34,4 @@ class TemporaryAuthMiddleware:
return self.get_response(request) return self.get_response(request)
def get_kfet_password(self, request): def get_kfet_password(self, request):
return ( return request.META.get("HTTP_KFETPASSWORD") or request.POST.get("KFETPASSWORD")
request.META.get('HTTP_KFETPASSWORD') or
request.POST.get('KFETPASSWORD')
)

View file

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

View file

@ -3,7 +3,6 @@ from django.utils.crypto import get_random_string
class GenericTeamTokenManager(models.Manager): class GenericTeamTokenManager(models.Manager):
def create_token(self): def create_token(self):
token = get_random_string(50) token = get_random_string(50)
while self.filter(token=token).exists(): while self.filter(token=token).exists():

View file

@ -19,22 +19,26 @@ def suggest_auth_generic(sender, request, user, **kwargs):
- logged in user is a kfet staff member (except the generic user). - logged in user is a kfet staff member (except the generic user).
""" """
# Filter against the next page. # Filter against the next page.
if not(hasattr(request, 'GET') and 'next' in request.GET): if not (hasattr(request, "GET") and "next" in request.GET):
return return
next_page = request.GET['next'] next_page = request.GET["next"]
generic_url = reverse('kfet.login.generic') generic_url = reverse("kfet.login.generic")
if not('k-fet' in next_page and not next_page.startswith(generic_url)): if not ("k-fet" in next_page and not next_page.startswith(generic_url)):
return return
# Filter against the logged in user. # Filter against the logged in user.
if not(user.has_perm('kfet.is_team') and user != get_kfet_generic_user()): if not (user.has_perm("kfet.is_team") and user != get_kfet_generic_user()):
return return
# Seems legit to add message. # Seems legit to add message.
text = _("K-Fêt — Ouvrir une session partagée ?") text = _("K-Fêt — Ouvrir une session partagée ?")
messages.info(request, mark_safe( messages.info(
'<a href="#" data-url="{}" onclick="submit_url(this)">{}</a>' request,
.format(generic_url, text) mark_safe(
)) '<a href="#" data-url="{}" onclick="submit_url(this)">{}</a>'.format(
generic_url, text
)
),
)

View file

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

View file

@ -23,12 +23,9 @@ def setup_kfet_generic_user(**kwargs):
""" """
generic = get_kfet_generic_user() generic = get_kfet_generic_user()
generic.user_permissions.add( generic.user_permissions.add(
Permission.objects.get( Permission.objects.get(content_type__app_label="kfet", codename="is_team")
content_type__app_label='kfet',
codename='is_team',
)
) )
def hash_password(password): def hash_password(password):
return hashlib.sha256(password.encode('utf-8')).hexdigest() return hashlib.sha256(password.encode("utf-8")).hexdigest()

View file

@ -1,17 +1,17 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Prefetch from django.db.models import Prefetch
from django.http import QueryDict from django.http import QueryDict
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import View
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView
from .forms import GroupForm from .forms import GroupForm
@ -30,28 +30,33 @@ class GenericLoginView(View):
provider, which can be external. Session is unusable as it will be cleared provider, which can be external. Session is unusable as it will be cleared
on logout. on logout.
""" """
TOKEN_COOKIE_NAME = 'kfettoken'
@method_decorator(require_http_methods(['GET', 'POST'])) TOKEN_COOKIE_NAME = "kfettoken"
@method_decorator(require_http_methods(["GET", "POST"]))
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None)
if not token: if not token:
if not request.user.has_perm('kfet.is_team'): if not request.user.has_perm("kfet.is_team"):
return redirect_to_login(request.get_full_path()) return redirect_to_login(request.get_full_path())
if request.method == 'POST': if request.method == "POST":
# Step 1: set token and logout user. # Step 1: set token and logout user.
return self.prepare_auth() return self.prepare_auth()
else: else:
# GET request should not change server/client states. Send a # GET request should not change server/client states. Send a
# confirmation template to emit a POST request. # confirmation template to emit a POST request.
return render(request, 'kfet/confirm_form.html', { return render(
'title': _("Ouvrir une session partagée"), request,
'text': _( "kfet/confirm_form.html",
"Êtes-vous sûr·e de vouloir ouvrir une session " {
"partagée ?" "title": _("Ouvrir une session partagée"),
), "text": _(
}) "Êtes-vous sûr·e de vouloir ouvrir une session "
"partagée ?"
),
},
)
else: else:
# Step 2: validate token. # Step 2: validate token.
return self.validate_auth(token) return self.validate_auth(token)
@ -62,20 +67,19 @@ class GenericLoginView(View):
# Prepare callback of logout. # Prepare callback of logout.
here_url = reverse(login_generic) here_url = reverse(login_generic)
if 'next' in self.request.GET: if "next" in self.request.GET:
# Keep given next page. # Keep given next page.
here_qd = QueryDict(mutable=True) here_qd = QueryDict(mutable=True)
here_qd['next'] = self.request.GET['next'] here_qd["next"] = self.request.GET["next"]
here_url += '?{}'.format(here_qd.urlencode()) here_url += "?{}".format(here_qd.urlencode())
logout_url = reverse('cof-logout') logout_url = reverse("cof-logout")
logout_qd = QueryDict(mutable=True) logout_qd = QueryDict(mutable=True)
logout_qd['next'] = here_url logout_qd["next"] = here_url
logout_url += '?{}'.format(logout_qd.urlencode(safe='/')) logout_url += "?{}".format(logout_qd.urlencode(safe="/"))
resp = redirect(logout_url) resp = redirect(logout_url)
resp.set_signed_cookie( resp.set_signed_cookie(self.TOKEN_COOKIE_NAME, token.token, httponly=True)
self.TOKEN_COOKIE_NAME, token.token, httponly=True)
return resp return resp
def validate_auth(self, token): def validate_auth(self, token):
@ -85,9 +89,9 @@ class GenericLoginView(View):
if user: if user:
# Log in generic user. # Log in generic user.
login(self.request, user) login(self.request, user)
messages.success(self.request, _( messages.success(
"K-Fêt — Ouverture d'une session partagée." self.request, _("K-Fêt — Ouverture d'une session partagée.")
)) )
resp = redirect(self.get_next_url()) resp = redirect(self.get_next_url())
else: else:
# Try again. # Try again.
@ -98,39 +102,34 @@ class GenericLoginView(View):
return resp return resp
def get_next_url(self): def get_next_url(self):
return self.request.GET.get('next', reverse('kfet.kpsul')) return self.request.GET.get("next", reverse("kfet.kpsul"))
login_generic = GenericLoginView.as_view() login_generic = GenericLoginView.as_view()
@permission_required('kfet.manage_perms') @permission_required("kfet.manage_perms")
def account_group(request): def account_group(request):
user_pre = Prefetch( user_pre = Prefetch(
'user_set', "user_set", queryset=User.objects.select_related("profile__account_kfet")
queryset=User.objects.select_related('profile__account_kfet'),
) )
groups = ( groups = Group.objects.filter(name__icontains="K-Fêt").prefetch_related(
Group.objects "permissions", user_pre
.filter(name__icontains='K-Fêt')
.prefetch_related('permissions', user_pre)
) )
return render(request, 'kfet/account_group.html', { return render(request, "kfet/account_group.html", {"groups": groups})
'groups': groups,
})
class AccountGroupCreate(SuccessMessageMixin, CreateView): class AccountGroupCreate(SuccessMessageMixin, CreateView):
model = Group model = Group
template_name = 'kfet/account_group_form.html' template_name = "kfet/account_group_form.html"
form_class = GroupForm form_class = GroupForm
success_message = 'Nouveau groupe : %(name)s' success_message = "Nouveau groupe : %(name)s"
success_url = reverse_lazy('kfet.account.group') success_url = reverse_lazy("kfet.account.group")
class AccountGroupUpdate(SuccessMessageMixin, UpdateView): class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
queryset = Group.objects.filter(name__icontains='K-Fêt') queryset = Group.objects.filter(name__icontains="K-Fêt")
template_name = 'kfet/account_group_form.html' template_name = "kfet/account_group_form.html"
form_class = GroupForm form_class = GroupForm
success_message = 'Groupe modifié : %(name)s' success_message = "Groupe modifié : %(name)s"
success_url = reverse_lazy('kfet.account.group') success_url = reverse_lazy("kfet.account.group")

View file

@ -1,8 +1,8 @@
from ldap3 import Connection
from django.shortcuts import render
from django.http import Http404
from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.db.models import Q
from django.http import Http404
from django.shortcuts import render
from ldap3 import Connection
from gestioncof.models import User from gestioncof.models import User
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
@ -25,81 +25,80 @@ def account_create(request):
raise Http404 raise Http404
q = request.GET.get("q") q = request.GET.get("q")
if (len(q) == 0): if len(q) == 0:
return render(request, "kfet/account_create_autocomplete.html") return render(request, "kfet/account_create_autocomplete.html")
data = {'q': q} data = {"q": q}
queries = {} queries = {}
search_words = q.split() search_words = q.split()
# Fetching data from User, CofProfile and Account tables # Fetching data from User, CofProfile and Account tables
queries['kfet'] = Account.objects queries["kfet"] = Account.objects
queries['users_cof'] = User.objects.filter(profile__is_cof=True) queries["users_cof"] = User.objects.filter(profile__is_cof=True)
queries['users_notcof'] = User.objects.filter(profile__is_cof=False) queries["users_notcof"] = User.objects.filter(profile__is_cof=False)
for word in search_words: for word in search_words:
queries['kfet'] = queries['kfet'].filter( queries["kfet"] = queries["kfet"].filter(
Q(cofprofile__user__username__icontains=word) Q(cofprofile__user__username__icontains=word)
| Q(cofprofile__user__first_name__icontains=word) | Q(cofprofile__user__first_name__icontains=word)
| Q(cofprofile__user__last_name__icontains=word) | Q(cofprofile__user__last_name__icontains=word)
) )
queries['users_cof'] = queries['users_cof'].filter( queries["users_cof"] = queries["users_cof"].filter(
Q(username__icontains=word) Q(username__icontains=word)
| Q(first_name__icontains=word) | Q(first_name__icontains=word)
| Q(last_name__icontains=word) | Q(last_name__icontains=word)
) )
queries['users_notcof'] = queries['users_notcof'].filter( queries["users_notcof"] = queries["users_notcof"].filter(
Q(username__icontains=word) Q(username__icontains=word)
| Q(first_name__icontains=word) | Q(first_name__icontains=word)
| Q(last_name__icontains=word) | Q(last_name__icontains=word)
) )
# Clearing redundancies # Clearing redundancies
queries['kfet'] = queries['kfet'].distinct() queries["kfet"] = queries["kfet"].distinct()
usernames = set( usernames = set(
queries['kfet'].values_list('cofprofile__user__username', flat=True)) queries["kfet"].values_list("cofprofile__user__username", flat=True)
queries['kfet'] = [ )
(account, account.cofprofile.user) queries["kfet"] = [
for account in queries['kfet'] (account, account.cofprofile.user) for account in queries["kfet"]
] ]
queries['users_cof'] = \ queries["users_cof"] = (
queries['users_cof'].exclude(username__in=usernames).distinct() queries["users_cof"].exclude(username__in=usernames).distinct()
queries['users_notcof'] = \ )
queries['users_notcof'].exclude(username__in=usernames).distinct() queries["users_notcof"] = (
usernames |= set( queries["users_notcof"].exclude(username__in=usernames).distinct()
queries['users_cof'].values_list('username', flat=True)) )
usernames |= set( usernames |= set(queries["users_cof"].values_list("username", flat=True))
queries['users_notcof'].values_list('username', flat=True)) usernames |= set(queries["users_notcof"].values_list("username", 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=word) "".join(
for word in search_words if word.isalnum() "(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=word)
)) for word in search_words
if word.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 render(request, "kfet/account_create_autocomplete.html", data) return render(request, "kfet/account_create_autocomplete.html", data)
@ -111,17 +110,19 @@ def account_search(request):
q = request.GET.get("q") q = request.GET.get("q")
words = q.split() words = q.split()
data = {'q': q} data = {"q": q}
for word in words: for word in words:
query = Account.objects.filter( query = Account.objects.filter(
Q(cofprofile__user__username__icontains=word) | Q(cofprofile__user__username__icontains=word)
Q(cofprofile__user__first_name__icontains=word) | | Q(cofprofile__user__first_name__icontains=word)
Q(cofprofile__user__last_name__icontains=word) | Q(cofprofile__user__last_name__icontains=word)
).distinct() ).distinct()
query = [(account.trigramme, account.cofprofile.user.get_full_name()) query = [
for account in query] (account.trigramme, account.cofprofile.user.get_full_name())
for account in query
]
data['accounts'] = query data["accounts"] = query
return render(request, 'kfet/account_search_autocomplete.html', data) return render(request, "kfet/account_search_autocomplete.html", data)

View file

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

View file

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

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