From 4bd2562edf5a82c6d58a4c805bdbe4ff31483b6b Mon Sep 17 00:00:00 2001 From: Hugo Roussille Date: Wed, 13 Sep 2017 15:57:57 +0200 Subject: [PATCH 001/122] django-cors-headers for cross-domain AJAX --- cof/settings/common.py | 2 ++ requirements.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/cof/settings/common.py b/cof/settings/common.py index ba0b6044..fba32743 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -91,9 +91,11 @@ INSTALLED_APPS = [ 'modelcluster', 'taggit', 'kfet.cms', + 'corsheaders', ] MIDDLEWARE_CLASSES = [ + 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', diff --git a/requirements.txt b/requirements.txt index f3964212..b28b939e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ channels==1.1.5 python-dateutil wagtail==1.10.* wagtailmenus==2.2.* +django-cors-headers==2.1.0 # Production tools wheel From a4eedbc1a60c902301ef48559a58caf804ad5624 Mon Sep 17 00:00:00 2001 From: Hugo Roussille Date: Wed, 13 Sep 2017 18:21:34 +0200 Subject: [PATCH 002/122] Whitelist bda and cof apps for cross-domain --- cof/settings/common.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cof/settings/common.py b/cof/settings/common.py index fba32743..bac17653 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -197,6 +197,11 @@ AUTHENTICATION_BACKENDS = ( RECAPTCHA_USE_SSL = True +CORS_ORIGIN_REGEX_WHITELIST = ( + 'bda.ens.fr', + 'cof.ens.fr', +) + # Cache settings CACHES = { From 732e47707e96bb7047438edb1791511e89918fa1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 17:25:58 +0200 Subject: [PATCH 003/122] Add unsubscribe option + list of current draws --- bda/forms.py | 41 +++++++++++++++++++-- bda/templates/bda/revente-tirages.html | 28 +++++++++++++++ bda/urls.py | 11 +++--- bda/views.py | 50 +++++++++++++++++++++++++- gestioncof/templates/home.html | 7 ++-- 5 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 bda/templates/bda/revente-tirages.html diff --git a/bda/forms.py b/bda/forms.py index c0417d1e..139ef45d 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -4,7 +4,7 @@ from django import forms from django.forms.models import BaseInlineFormSet from django.utils import timezone -from bda.models import Attribution, Spectacle +from bda.models import Attribution, Spectacle, SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): @@ -45,6 +45,9 @@ class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): return "%s" % str(obj.spectacle) +class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): + def label_from_instance(self, obj): + return "%s" % str(obj.attribution.spectacle) class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( @@ -63,7 +66,6 @@ class ResellForm(forms.Form): 'participant__user') ) - class AnnulForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', @@ -83,7 +85,6 @@ class AnnulForm(forms.Form): 'participant__user') ) - class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( queryset=Spectacle.objects.none(), @@ -98,6 +99,40 @@ class InscriptionReventeForm(forms.Form): .filter(date__gte=timezone.now()) ) +class ReventeTirageAnnulForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + participant.wanted.filter(soldTo__isnull=True) + .select_related('attribution__spectacle') + ) + + +class ReventeTirageForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + SpectacleRevente.objects.filter( + notif_sent=True, + shotgun=False, + tirage_done=False + ).exclude(answered_mail=participant) + .select_related('attribution__spectacle') + ) class SoldForm(forms.Form): attributions = AttributionModelMultipleChoiceField( diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente-tirages.html new file mode 100644 index 00000000..bd738673 --- /dev/null +++ b/bda/templates/bda/revente-tirages.html @@ -0,0 +1,28 @@ +{% extends "base_title.html" %} +{% load bootstrap %} + +{% block realcontent %} + +

Tirages au sort de reventes

+{% if annulform.reventes %} +

Mes inscriptions

+
+ {% csrf_token %} + {{annulform|bootstrap}} +
+ +
+
+{% endif %} +
+{% if subform.reventes %} +

Tirages en cours

+
+ {% csrf_token %} + {{subform|bootstrap}} +
+ +
+
+{% endif %} +{% endblock %} diff --git a/bda/urls.py b/bda/urls.py index 876c84ea..51dd4235 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -32,16 +32,19 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), - url(r'^liste-revente/(?P\d+)$', + url(r'^revente/(?P\d+)/list$', views.list_revente, name="bda-liste-revente"), - url(r'^buy-revente/(?P\d+)$', + url(r'^revente/(?P\d+)/tirages$', + views.revente_tirages, + name="bda-revente-tirages"), + url(r'^revente/(?P\d+)/buy$', views.buy_revente, name="bda-buy-revente"), - url(r'^revente-interested/(?P\d+)$', + url(r'^revente/(?P\d+)/interested$', views.revente_interested, name='bda-revente-interested'), - url(r'^revente-immediat/(?P\d+)$', + url(r'^revente/(?P\d+)/immediat$', views.revente_shotgun, name="bda-shotgun"), url(r'^mails-rappel/(?P\d+)$', diff --git a/bda/views.py b/bda/views.py index 84b6c9d3..4b75c116 100644 --- a/bda/views.py +++ b/bda/views.py @@ -30,7 +30,7 @@ from bda.models import ( from bda.algorithm import Algorithm from bda.forms import ( TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, - InscriptionInlineFormSet, + InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm ) @@ -377,6 +377,7 @@ def revente(request, tirage_id): if not created: revente.seller = participant revente.date = timezone.now() + revente.wanted = Participant.objects.none() revente.soldTo = None revente.notif_sent = False revente.tirage_done = False @@ -442,6 +443,53 @@ def revente(request, tirage_id): "annulform": annulform, "resellform": resellform}) +@login_required +def revente_tirages(request, tirage_id): + tirage = get_object_or_404(Tirage, id=tirage_id) + participant, _ = Participant.objects.get_or_create( + user=request.user, tirage=tirage) + unsub = 0 + subform = ReventeTirageForm(participant, prefix="subscribe") + annulform = ReventeTirageAnnulForm(participant, prefix="annul") + + if request.method == 'POST': + if "subscribe" in request.POST: + subform = ReventeTirageForm(participant, request.POST, + prefix="subscribe") + if subform.is_valid(): + sub = 0 + reventes = subform.cleaned_data['reventes'] + for revente in reventes: + revente.answered_mail.add(participant) + sub += 1 + if sub > 0: + plural = "s" if sub > 1 else "" + messages.success( + request, + "Tu as bien été inscrit à {} revente{}" + .format(sub, plural) + ) + elif "annul" in request.POST: + annulform = ReventeTirageAnnulForm(participant, request.POST, + prefix="annul") + if annulform.is_valid(): + unsub = 0 + reventes = annulform.cleaned_data['reventes'] + for revente in reventes: + revente.answered_mail.remove(participant) + unsub += 1 + if unsub > 0: + plural = "s" if unsub > 1 else "" + messages.success( + request, + "Tu as bien été désinscrit de {} revente{}" + .format(unsub, plural) + ) + + return render(request, "bda/revente-tirages.html", + {"annulform": annulform, "subform": subform}) + + @login_required def revente_interested(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index acc04f30..f7ca57b5 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -43,9 +43,10 @@
  • État des demandes
  • {% else %}
  • Mes places
  • -
  • Revendre une place
  • -
  • S'inscrire à BdA-Revente
  • -
  • Places disponibles immédiatement
  • +
  • Gestion de mes reventes
  • +
  • Reventes en cours
  • +
  • S'inscrire à BdA-Revente
  • +
  • Places disponibles immédiatement
  • {% endif %} {% endfor %} From e74dbb11f1556a4ffb3cc42f83686a3a27cf5f45 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 18:39:45 +0200 Subject: [PATCH 004/122] Organize revente files and function names --- .../revente/confirm-shotgun.html} | 0 .../revente/confirmed.html} | 0 .../revente/mail-success.html} | 0 .../{reventes.html => revente/manage.html} | 0 .../revente/none.html} | 0 .../revente/notpaid.html} | 0 .../revente/shotgun.html} | 2 +- .../subscribe.html} | 0 .../tirages.html} | 0 .../revente/wrongtime.html} | 2 +- bda/urls.py | 30 +++++++++++-------- bda/views.py | 30 +++++++++---------- gestioncof/management/data/custommail.json | 2 +- gestioncof/templates/home.html | 6 ++-- 14 files changed, 38 insertions(+), 34 deletions(-) rename bda/templates/{revente-confirm.html => bda/revente/confirm-shotgun.html} (100%) rename bda/templates/{bda-interested.html => bda/revente/confirmed.html} (100%) rename bda/templates/{bda-success.html => bda/revente/mail-success.html} (100%) rename bda/templates/bda/{reventes.html => revente/manage.html} (100%) rename bda/templates/{bda-no-revente.html => bda/revente/none.html} (100%) rename bda/templates/{bda-notpaid.html => bda/revente/notpaid.html} (100%) rename bda/templates/{bda-shotgun.html => bda/revente/shotgun.html} (83%) rename bda/templates/bda/{liste-reventes.html => revente/subscribe.html} (100%) rename bda/templates/bda/{revente-tirages.html => revente/tirages.html} (100%) rename bda/templates/{bda-wrongtime.html => bda/revente/wrongtime.html} (86%) diff --git a/bda/templates/revente-confirm.html b/bda/templates/bda/revente/confirm-shotgun.html similarity index 100% rename from bda/templates/revente-confirm.html rename to bda/templates/bda/revente/confirm-shotgun.html diff --git a/bda/templates/bda-interested.html b/bda/templates/bda/revente/confirmed.html similarity index 100% rename from bda/templates/bda-interested.html rename to bda/templates/bda/revente/confirmed.html diff --git a/bda/templates/bda-success.html b/bda/templates/bda/revente/mail-success.html similarity index 100% rename from bda/templates/bda-success.html rename to bda/templates/bda/revente/mail-success.html diff --git a/bda/templates/bda/reventes.html b/bda/templates/bda/revente/manage.html similarity index 100% rename from bda/templates/bda/reventes.html rename to bda/templates/bda/revente/manage.html diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda/revente/none.html similarity index 100% rename from bda/templates/bda-no-revente.html rename to bda/templates/bda/revente/none.html diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda/revente/notpaid.html similarity index 100% rename from bda/templates/bda-notpaid.html rename to bda/templates/bda/revente/notpaid.html diff --git a/bda/templates/bda-shotgun.html b/bda/templates/bda/revente/shotgun.html similarity index 83% rename from bda/templates/bda-shotgun.html rename to bda/templates/bda/revente/shotgun.html index e10fae00..fae36c04 100644 --- a/bda/templates/bda-shotgun.html +++ b/bda/templates/bda/revente/shotgun.html @@ -5,7 +5,7 @@ {% if shotgun %}
      {% for spectacle in shotgun %} -
    • {{spectacle}}
    • +
    • {{spectacle}}
    • {% endfor %} {% else %}

      Pas de places disponibles immédiatement, désolé !

      diff --git a/bda/templates/bda/liste-reventes.html b/bda/templates/bda/revente/subscribe.html similarity index 100% rename from bda/templates/bda/liste-reventes.html rename to bda/templates/bda/revente/subscribe.html diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente/tirages.html similarity index 100% rename from bda/templates/bda/revente-tirages.html rename to bda/templates/bda/revente/tirages.html diff --git a/bda/templates/bda-wrongtime.html b/bda/templates/bda/revente/wrongtime.html similarity index 86% rename from bda/templates/bda-wrongtime.html rename to bda/templates/bda/revente/wrongtime.html index dfafb05f..18c417a2 100644 --- a/bda/templates/bda-wrongtime.html +++ b/bda/templates/bda/revente/wrongtime.html @@ -6,7 +6,7 @@

      Le tirage au sort de cette revente a déjà été effectué !

      Si personne n'était intéressé, elle est maintenant disponible - ici.

      + ici.

      {% else %}

      Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !

      {% endif %} diff --git a/bda/urls.py b/bda/urls.py index 51dd4235..7588187c 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -16,9 +16,6 @@ urlpatterns = [ url(r'^places/(?P\d+)$', views.places, name="bda-places-attribuees"), - url(r'^revente/(?P\d+)$', - views.revente, - name='bda-revente'), url(r'^etat-places/(?P\d+)$', views.etat_places, name='bda-etat-places'), @@ -32,21 +29,28 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), - url(r'^revente/(?P\d+)/list$', - views.list_revente, - name="bda-liste-revente"), + + # Urls BdA-Revente + + url(r'^revente/(?P\d+)/manage$', + views.revente_manage, + name='bda-revente-manage'), + url(r'^revente/(?P\d+)/subscribe$', + views.revente_subscribe, + name="bda-revente-subscribe"), url(r'^revente/(?P\d+)/tirages$', views.revente_tirages, name="bda-revente-tirages"), url(r'^revente/(?P\d+)/buy$', - views.buy_revente, - name="bda-buy-revente"), - url(r'^revente/(?P\d+)/interested$', - views.revente_interested, - name='bda-revente-interested'), - url(r'^revente/(?P\d+)/immediat$', + views.revente_buy, + name="bda-revente-buy"), + url(r'^revente/(?P\d+)/confirm$', + views.revente_confirm, + name='bda-revente-confirm'), + url(r'^revente/(?P\d+)/shotgun$', views.revente_shotgun, - name="bda-shotgun"), + name="bda-revente-shotgun"), + url(r'^mails-rappel/(?P\d+)$', views.send_rappel, name="bda-rappels" diff --git a/bda/views.py b/bda/views.py index 4b75c116..c0e64230 100644 --- a/bda/views.py +++ b/bda/views.py @@ -349,13 +349,13 @@ def tirage(request, tirage_id): @login_required -def revente(request, tirage_id): +def revente_manage(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) if not participant.paid: - return render(request, "bda-notpaid.html", {}) + return render(request, "bda/revente/notpaid.html", {}) resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') @@ -438,7 +438,7 @@ def revente(request, tirage_id): .filter( Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) - return render(request, "bda/reventes.html", + return render(request, "bda/revente/manage.html", {'tirage': tirage, 'overdue': overdue, "soldform": soldform, "annulform": annulform, "resellform": resellform}) @@ -486,27 +486,27 @@ def revente_tirages(request, tirage_id): .format(unsub, plural) ) - return render(request, "bda/revente-tirages.html", + return render(request, "bda/revente/tirages.html", {"annulform": annulform, "subform": subform}) @login_required -def revente_interested(request, revente_id): +def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=revente.attribution.spectacle.tirage) if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: - return render(request, "bda-wrongtime.html", + return render(request, "bda/revente/wrongtime.html", {"revente": revente}) revente.answered_mail.add(participant) - return render(request, "bda-interested.html", + return render(request, "bda/revente/confirmed.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @login_required -def list_revente(request, tirage_id): +def revente_subscribe(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=tirage) @@ -560,11 +560,11 @@ def list_revente(request, tirage_id): ) messages.info(request, msg, extra_tags="safe") - return render(request, "bda/liste-reventes.html", {"form": form}) + return render(request, "bda/revente/subscribe.html", {"form": form}) @login_required -def buy_revente(request, spectacle_id): +def revente_buy(request, spectacle_id): spectacle = get_object_or_404(Spectacle, id=spectacle_id) tirage = spectacle.tirage participant, _ = Participant.objects.get_or_create( @@ -578,13 +578,13 @@ def buy_revente(request, spectacle_id): own_reventes = reventes.filter(seller=participant) if len(own_reventes) > 0: own_reventes[0].delete() - return HttpResponseRedirect(reverse("bda-shotgun", + return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id])) reventes_shotgun = reventes.filter(shotgun=True) if not reventes_shotgun: - return render(request, "bda-no-revente.html", {}) + return render(request, "bda/revente/none.html", {}) if request.POST: revente = random.choice(reventes_shotgun) @@ -601,11 +601,11 @@ def buy_revente(request, spectacle_id): [revente.seller.user.email], context=context, ) - return render(request, "bda-success.html", + return render(request, "bda/revente/mail-success.html", {"seller": revente.attribution.participant.user, "spectacle": spectacle}) - return render(request, "revente-confirm.html", + return render(request, "bda/revente/confirm-shotgun.html", {"spectacle": spectacle, "user": request.user}) @@ -629,7 +629,7 @@ def revente_shotgun(request, tirage_id): ) shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0] - return render(request, "bda-shotgun.html", + return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun}) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index 9ee9b1ea..bf59e5f6 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -151,7 +151,7 @@ "shortname": "bda-revente", "subject": "{{ show }}", "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" } }, { diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index f7ca57b5..943ef780 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -43,10 +43,10 @@
    • État des demandes
    • {% else %}
    • Mes places
    • -
    • Gestion de mes reventes
    • +
    • Gestion de mes reventes
    • Reventes en cours
    • -
    • S'inscrire à BdA-Revente
    • -
    • Places disponibles immédiatement
    • +
    • S'inscrire à BdA-Revente
    • +
    • Places disponibles immédiatement
    • {% endif %}
    {% endfor %} From 919bcd197d077767bdc07355a70a84d39f8ebecf Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 18:59:30 +0200 Subject: [PATCH 005/122] Small code QoL improvements --- bda/models.py | 10 ++++++++++ bda/views.py | 10 ++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bda/models.py b/bda/models.py index 41462d70..5533e3bb 100644 --- a/bda/models.py +++ b/bda/models.py @@ -252,6 +252,16 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" + def reset(self): + """Réinitialise la revente pour permettre une remise sur le marché""" + self.seller = self.attribution.participant + self.date = timezone.now() + self.answered_mail.clear() + self.soldTo = None + self.notif_sent = False + self.tirage_done = False + self.shotgun = False + def send_notif(self): """ Envoie une notification pour indiquer la mise en vente d'une place sur diff --git a/bda/views.py b/bda/views.py index c0e64230..311d530a 100644 --- a/bda/views.py +++ b/bda/views.py @@ -375,13 +375,7 @@ def revente_manage(request, tirage_id): attribution=attribution, defaults={'seller': participant}) if not created: - revente.seller = participant - revente.date = timezone.now() - revente.wanted = Participant.objects.none() - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - revente.shotgun = False + revente.reset() context = { 'vendeur': participant.user, 'show': attribution.spectacle, @@ -495,7 +489,7 @@ def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=revente.attribution.spectacle.tirage) - if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: + if not revente.notif_sent or revente.shotgun: return render(request, "bda/revente/wrongtime.html", {"revente": revente}) From 1b0e4285ecbc7ea224cb7fad9c725365d4a9ba01 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:26:07 +0200 Subject: [PATCH 006/122] Reverse match fix --- gestioncof/management/data/custommail.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index bf59e5f6..029c03e0 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -161,7 +161,7 @@ "shortname": "bda-shotgun", "subject": "{{ show }}", "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" } }, { From 684603709e90e471f5528ab35f1efc2011145fb5 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:30:11 +0200 Subject: [PATCH 007/122] Class attributes and properties + more verbose log SpectacleRevente gets brand new properties and attributes to simplify code ; also, manage_reventes command output is more verbose --- bda/management/commands/manage_reventes.py | 48 ++++++++++++++-------- bda/models.py | 45 +++++++++++++++++--- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 0302ec4b..5a767604 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -6,7 +6,6 @@ Gestion en ligne de commande des reventes. from __future__ import unicode_literals -from datetime import timedelta from django.core.management import BaseCommand from django.utils import timezone from bda.models import SpectacleRevente @@ -21,23 +20,36 @@ class Command(BaseCommand): now = timezone.now() reventes = SpectacleRevente.objects.all() for revente in reventes: - # Check si < 24h - if (revente.attribution.spectacle.date <= - revente.date + timedelta(days=1)) and \ - now >= revente.date + timedelta(minutes=15) and \ - not revente.notif_sent: - self.stdout.write(str(now)) - revente.mail_shotgun() - self.stdout.write("Mail de disponibilité immédiate envoyé") - # Check si délai de retrait dépassé - elif (now >= revente.date + timedelta(hours=1) and - not revente.notif_sent): + # Le spectacle est bientôt et on a pas encore envoyé de mail : + # on met la place au shotgun et on prévient. + if revente.is_urgent and not revente.notif_sent: + if revente.can_notif: + self.stdout.write(str(now)) + revente.mail_shotgun() + self.stdout.write( + "Mails de disponibilité immédiate envoyés " + "pour la revente [%s]" % revente + ) + + # Le spectacle est dans plus longtemps : on prévient + elif (revente.can_notif and not revente.notif_sent): self.stdout.write(str(now)) revente.send_notif() - self.stdout.write("Mail d'inscription à une revente envoyé") - # Check si tirage à faire - elif (now >= revente.date_tirage and - not revente.tirage_done): + self.stdout.write( + "Mails d'inscription à la revente [%s] envoyés" + % revente + ) + + # On fait le tirage + elif (now >= revente.date_tirage and not revente.tirage_done): self.stdout.write(str(now)) - revente.tirage() - self.stdout.write("Tirage effectué, mails envoyés") + winner = revente.tirage() + self.stdout.write( + "Tirage effectué pour la revente [%s]" + % revente + ) + + if winner: + self.stdout.write("Gagnant : %s" % winner.user) + else: + self.stdout.write("Pas de gagnant ; place au shotgun") diff --git a/bda/models.py b/bda/models.py index 5533e3bb..b2882900 100644 --- a/bda/models.py +++ b/bda/models.py @@ -233,17 +233,46 @@ class SpectacleRevente(models.Model): default=False) shotgun = models.BooleanField("Disponible immédiatement", default=False) + #### + # Some class attributes + ### + # TODO : settings ? + + # Temps minimum entre le tirage et le spectacle + min_margin = timedelta(days=5) + + # Temps entre la création d'une revente et l'envoi du mail + remorse_time = timedelta(hours=1) + + # Temps min/max d'attente avant le tirage + max_wait_time = timedelta(days=3) + min_wait_time = timedelta(days=1) @property def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - # L'acheteur doit être connu au plus 12h avant le spectacle + notif_time = self.date + self.remorse_time + remaining_time = (self.attribution.spectacle.date - - self.date - timedelta(hours=13)) - # Au minimum, on attend 2 jours avant le tirage - delay = min(remaining_time, timedelta(days=2)) - # Le vendeur a aussi 1h pour changer d'avis - return self.date + delay + timedelta(hours=1) + - notif_time - self.min_margin) + + delay = min(remaining_time, self.max_wait_time) + + return notif_time + delay + + @property + def is_urgent(self): + """ + Renvoie True iff la revente doit être mise au shotgun directement. + Plus précisément, on doit avoir min_margin + min_wait_time de marge. + """ + spectacle_date = self.attribution.spectacle.date + return (spectacle_date <= timezone.now() + self.min_margin + + self.min_wait_time) + + @property + def can_notif(self): + return (timezone.now() >= self.date + self.remorse_time) def __str__(self): return "%s -- %s" % (self.seller, @@ -353,8 +382,12 @@ class SpectacleRevente(models.Model): [inscrit.user.email] )) send_mass_custom_mail(datatuple) + + return winner + # Si personne ne veut de la place, elle part au shotgun else: self.shotgun = True + return None self.tirage_done = True self.save() From 6a6549e0d72937d9f5adaf7bf43ff090150b4891 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:52:25 +0200 Subject: [PATCH 008/122] Add notif time In case of a gestioCOF bug, we keep the notification time in memory to still do the drawing 1-3 days after. --- bda/admin.py | 6 +++--- bda/forms.py | 4 ++-- bda/migrations/0012_notif_time.py | 28 ++++++++++++++++++++++++++++ bda/models.py | 29 +++++++++++++++++++++-------- bda/views.py | 14 +++++++------- 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 bda/migrations/0012_notif_time.py diff --git a/bda/admin.py b/bda/admin.py index 60d3c1ba..4f5d821a 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -225,7 +225,7 @@ class SpectacleReventeAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['answered_mail'].queryset = ( + self.fields['confirmed_entry'].queryset = ( Participant.objects .select_related('user', 'tirage') ) @@ -292,8 +292,8 @@ class SpectacleReventeAdmin(admin.ModelAdmin): revente.soldTo = None revente.notif_sent = False revente.tirage_done = False - if revente.answered_mail: - revente.answered_mail.clear() + if revente.confirmed_entry: + revente.confirmed_entry.clear() revente.save() self.message_user( request, diff --git a/bda/forms.py b/bda/forms.py index 139ef45d..11d05b0e 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -110,7 +110,7 @@ class ReventeTirageAnnulForm(forms.Form): def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['reventes'].queryset = ( - participant.wanted.filter(soldTo__isnull=True) + participant.entered.filter(soldTo__isnull=True) .select_related('attribution__spectacle') ) @@ -130,7 +130,7 @@ class ReventeTirageForm(forms.Form): notif_sent=True, shotgun=False, tirage_done=False - ).exclude(answered_mail=participant) + ).exclude(confirmed_entry=participant) .select_related('attribution__spectacle') ) diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py new file mode 100644 index 00000000..be66efd1 --- /dev/null +++ b/bda/migrations/0012_notif_time.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0011_tirage_appear_catalogue'), + ] + + operations = [ + migrations.RemoveField( + model_name='spectaclerevente', + name='answered_mail', + ), + migrations.AddField( + model_name='spectaclerevente', + name='confirmed_entry', + field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), + ), + migrations.AddField( + model_name='spectaclerevente', + name='notif_time', + field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True), + ), + ] diff --git a/bda/models.py b/bda/models.py index b2882900..2ad47dbf 100644 --- a/bda/models.py +++ b/bda/models.py @@ -218,9 +218,9 @@ class SpectacleRevente(models.Model): related_name="revente") date = models.DateTimeField("Date de mise en vente", default=timezone.now) - answered_mail = models.ManyToManyField(Participant, - related_name="wanted", - blank=True) + confirmed_entry = models.ManyToManyField(Participant, + related_name="entered", + blank=True) seller = models.ForeignKey(Participant, related_name="original_shows", verbose_name="Vendeur") @@ -229,8 +229,13 @@ class SpectacleRevente(models.Model): notif_sent = models.BooleanField("Notification envoyée", default=False) + + notif_time = models.DateTimeField("Moment d'envoi de la notification", + blank=True, null=True) + tirage_done = models.BooleanField("Tirage effectué", default=False) + shotgun = models.BooleanField("Disponible immédiatement", default=False) #### @@ -248,17 +253,23 @@ class SpectacleRevente(models.Model): max_wait_time = timedelta(days=3) min_wait_time = timedelta(days=1) + @property + def real_notif_time(self): + if self.notif_time: + return self.notif_time + else: + return self.date + self.remorse_time + @property def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - notif_time = self.date + self.remorse_time remaining_time = (self.attribution.spectacle.date - - notif_time - self.min_margin) + - self.real_notif_time - self.min_margin) delay = min(remaining_time, self.max_wait_time) - return notif_time + delay + return self.real_notif_time + delay @property def is_urgent(self): @@ -285,7 +296,7 @@ class SpectacleRevente(models.Model): """Réinitialise la revente pour permettre une remise sur le marché""" self.seller = self.attribution.participant self.date = timezone.now() - self.answered_mail.clear() + self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False self.tirage_done = False @@ -311,6 +322,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() self.save() def mail_shotgun(self): @@ -332,6 +344,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() # Flag inutile, sauf si l'horloge interne merde self.tirage_done = True self.shotgun = True @@ -343,7 +356,7 @@ class SpectacleRevente(models.Model): parmis les personnes intéressées par le spectacle. Les personnes sont ensuites prévenues par mail du résultat du tirage. """ - inscrits = list(self.answered_mail.all()) + inscrits = list(self.confirmed_entry.all()) spectacle = self.attribution.spectacle seller = self.seller diff --git a/bda/views.py b/bda/views.py index 311d530a..6ed22b21 100644 --- a/bda/views.py +++ b/bda/views.py @@ -420,8 +420,8 @@ def revente_manage(request, tirage_id): revente.notif_sent = False revente.tirage_done = False revente.shotgun = False - if revente.answered_mail: - revente.answered_mail.clear() + if revente.confirmed_entry: + revente.confirmed_entry.clear() revente.save() overdue = participant.attribution_set.filter( @@ -454,7 +454,7 @@ def revente_tirages(request, tirage_id): sub = 0 reventes = subform.cleaned_data['reventes'] for revente in reventes: - revente.answered_mail.add(participant) + revente.confirmed_entry.add(participant) sub += 1 if sub > 0: plural = "s" if sub > 1 else "" @@ -470,7 +470,7 @@ def revente_tirages(request, tirage_id): unsub = 0 reventes = annulform.cleaned_data['reventes'] for revente in reventes: - revente.answered_mail.remove(participant) + revente.confirmed_entry.remove(participant) unsub += 1 if unsub > 0: plural = "s" if unsub > 1 else "" @@ -493,7 +493,7 @@ def revente_confirm(request, revente_id): return render(request, "bda/revente/wrongtime.html", {"revente": revente}) - revente.answered_mail.add(participant) + revente.confirmed_entry.add(participant) return render(request, "bda/revente/confirmed.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @@ -526,12 +526,12 @@ def revente_subscribe(request, tirage_id): # la revente ayant le moins d'inscrits min_resell = ( qset.filter(shotgun=False) - .annotate(nb_subscribers=Count('answered_mail')) + .annotate(nb_subscribers=Count('confirmed_entry')) .order_by('nb_subscribers') .first() ) if min_resell is not None: - min_resell.answered_mail.add(participant) + min_resell.confirmed_entry.add(participant) inscrit_revente.append(spectacle) success = True else: From 785555c05cc874dfc2a9542608c0e94baffccc2e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 26 Oct 2017 12:40:11 +0200 Subject: [PATCH 009/122] Misc fixes --- bda/forms.py | 10 ++++++++-- bda/migrations/0012_notif_time.py | 7 ++++--- bda/models.py | 11 ++++++----- bda/views.py | 33 +++++++++++-------------------- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 11d05b0e..2929f771 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -43,11 +43,13 @@ class TokenForm(forms.Form): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.spectacle) + return str(obj.spectacle) + class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.attribution.spectacle) + return str(obj.attribution.spectacle) + class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( @@ -66,6 +68,7 @@ class ResellForm(forms.Form): 'participant__user') ) + class AnnulForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', @@ -85,6 +88,7 @@ class AnnulForm(forms.Form): 'participant__user') ) + class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( queryset=Spectacle.objects.none(), @@ -99,6 +103,7 @@ class InscriptionReventeForm(forms.Form): .filter(date__gte=timezone.now()) ) + class ReventeTirageAnnulForm(forms.Form): reventes = ReventeModelMultipleChoiceField( label='', @@ -134,6 +139,7 @@ class ReventeTirageForm(forms.Form): .select_related('attribution__spectacle') ) + class SoldForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py index be66efd1..ee777e35 100644 --- a/bda/migrations/0012_notif_time.py +++ b/bda/migrations/0012_notif_time.py @@ -11,11 +11,12 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( + migrations.RenameField( model_name='spectaclerevente', - name='answered_mail', + old_name='answered_mail', + new_name='confirmed_entry', ), - migrations.AddField( + migrations.AlterField( model_name='spectaclerevente', name='confirmed_entry', field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), diff --git a/bda/models.py b/bda/models.py index 2ad47dbf..59827621 100644 --- a/bda/models.py +++ b/bda/models.py @@ -168,6 +168,7 @@ class Participant(models.Model): def __str__(self): return "%s - %s" % (self.user, self.tirage.title) + DOUBLE_CHOICES = ( ("1", "1 place"), ("autoquit", "2 places si possible, 1 sinon"), @@ -292,15 +293,16 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" - def reset(self): + def reset(self, new_date=timezone.now()): """Réinitialise la revente pour permettre une remise sur le marché""" self.seller = self.attribution.participant - self.date = timezone.now() + self.date = new_date self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False self.tirage_done = False self.shotgun = False + self.save() def send_notif(self): """ @@ -396,11 +398,10 @@ class SpectacleRevente(models.Model): )) send_mass_custom_mail(datatuple) - return winner - # Si personne ne veut de la place, elle part au shotgun else: + winner = None self.shotgun = True - return None self.tirage_done = True self.save() + return winner diff --git a/bda/views.py b/bda/views.py index 6ed22b21..fb1a2e82 100644 --- a/bda/views.py +++ b/bda/views.py @@ -5,7 +5,6 @@ import random import hashlib import time import json -from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail, send_custom_mail from custommail.models import CustomMail from django.shortcuts import render, get_object_or_404 @@ -14,6 +13,7 @@ from django.contrib import messages from django.db import transaction from django.core import serializers from django.db.models import Count, Q, Prefetch +from django.template.defaultfilters import pluralize from django.forms.models import inlineformset_factory from django.http import ( HttpResponseBadRequest, HttpResponseRedirect, JsonResponse @@ -376,6 +376,7 @@ def revente_manage(request, tirage_id): defaults={'seller': participant}) if not created: revente.reset() + context = { 'vendeur': participant.user, 'show': attribution.spectacle, @@ -414,15 +415,10 @@ def revente_manage(request, tirage_id): attributions = soldform.cleaned_data['attributions'] for attribution in attributions: if attribution.spectacle.date > timezone.now(): - revente = attribution.revente - revente.date = timezone.now() - timedelta(minutes=65) - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - revente.shotgun = False - if revente.confirmed_entry: - revente.confirmed_entry.clear() - revente.save() + # On antidate pour envoyer le mail plus vite + new_date = (timezone.now() + - SpectacleRevente.remorse_time) + revente.reset(new_date=new_date) overdue = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), @@ -442,7 +438,6 @@ def revente_tirages(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=tirage) - unsub = 0 subform = ReventeTirageForm(participant, prefix="subscribe") annulform = ReventeTirageAnnulForm(participant, prefix="annul") @@ -451,33 +446,29 @@ def revente_tirages(request, tirage_id): subform = ReventeTirageForm(participant, request.POST, prefix="subscribe") if subform.is_valid(): - sub = 0 reventes = subform.cleaned_data['reventes'] + count = reventes.count() for revente in reventes: revente.confirmed_entry.add(participant) - sub += 1 - if sub > 0: - plural = "s" if sub > 1 else "" + if count > 0: messages.success( request, "Tu as bien été inscrit à {} revente{}" - .format(sub, plural) + .format(count, pluralize(count)) ) elif "annul" in request.POST: annulform = ReventeTirageAnnulForm(participant, request.POST, prefix="annul") if annulform.is_valid(): - unsub = 0 reventes = annulform.cleaned_data['reventes'] + count = reventes.count() for revente in reventes: revente.confirmed_entry.remove(participant) - unsub += 1 - if unsub > 0: - plural = "s" if unsub > 1 else "" + if count > 0: messages.success( request, "Tu as bien été désinscrit de {} revente{}" - .format(unsub, plural) + .format(count, pluralize(count)) ) return render(request, "bda/revente/tirages.html", From f18959c0a1d643fab3b08921a004f158d4ba4720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 1 Nov 2017 17:26:40 +0100 Subject: [PATCH 010/122] BdA-Revente: meaningful names, some help tests --- bda/templates/bda/revente/manage.html | 58 +++++++++++++++++++----- bda/templates/bda/revente/subscribe.html | 39 ++++++++++------ bda/templates/bda/revente/tirages.html | 40 ++++++++++++---- bda/views.py | 18 ++++++++ gestioncof/static/css/cof.css | 11 +++++ gestioncof/templates/home.html | 6 +-- 6 files changed, 136 insertions(+), 36 deletions(-) diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index 0912babb..8162d55d 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -3,50 +3,84 @@ {% block realcontent %} -

    Revente de place

    +

    Gestion des places que je revends

    {% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %} {% if resellform.attributions %} +
    +

    Places non revendues

    - {% csrf_token %} - {{resellform|bootstrap}} +
    + + Cochez les places que vous souhaitez revendre, et validez. Vous aurez + ensuite 1h pour changer d'avis avant que la revente soit confirmée et + que les notifications soient envoyées aux intéressé·e·s. +
    +
    + {% csrf_token %} + {{ resellform|bootstrap }} +
    + +
    {% endif %} -
    + {% if annul_attributions or overdue %}

    Places en cours de revente

    + {% if annul_attributions %} +
    + + Vous pouvez annuler les places mises en vente il y a moins d'une heure. +
    + {% endif %} {% csrf_token %}
      {% for attrib in annul_attributions %} -
    • {{attrib.tag}} {{attrib.choice_label}}
    • +
    • {{ attrib.tag }} {{ attrib.choice_label }}
    • {% endfor %} {% for attrib in overdue %}
    • - {{attrib.spectacle}} + {{ attrib.spectacle }}
    • {% endfor %} +
    +
    +
    {% if annul_attributions %} {% endif %}
    + +
    {% endif %} -
    + {% if sold_attributions %}

    Places revendues

    -
    + +
    + + Pour chaque revente, vous devez soit l'annuler soit la confirmer pour + transférer la place la place à la personne tirée au sort. + + L'annulation sert par exemple à pouvoir remettre la place en jeu si + vous ne parvenez pas à entrer en contact avec la personne tirée au + sort. +
    +
    {% csrf_token %} - {{soldform|bootstrap}} - - - + {{ soldform|bootstrap }} +
    + + + {% endif %} {% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}

    Plus de reventes possibles !

    diff --git a/bda/templates/bda/revente/subscribe.html b/bda/templates/bda/revente/subscribe.html index fcf57345..9a193908 100644 --- a/bda/templates/bda/revente/subscribe.html +++ b/bda/templates/bda/revente/subscribe.html @@ -4,28 +4,41 @@ {% block realcontent %}

    Inscriptions pour BdA-Revente

    +
    + + Cochez les spectacles pour lesquels vous souhaitez recevoir un + notification quand une place est disponible en revente.
    + Lorsque vous validez vos choix, si un tirage au sort est en cours pour + un des spectacles que vous avez sélectionné, vous serez automatiquement + inscrit à ce tirage. +
    +
    {% csrf_token %}
    -

    Spectacles

    -
    - - + + -
    -
      - {% for checkbox in form.spectacles %} -
    • {{checkbox}}
    • - {%endfor%} -
    -
    +
    +
      + {% for checkbox in form.spectacles %} +
    • {{ checkbox }}
    • + {% endfor %} +
    +
    - +
    + diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html index 0ea96c8f..a31cc3cf 100644 --- a/kfet/templates/kfet/category.html +++ b/kfet/templates/kfet/category.html @@ -17,12 +17,15 @@ {% block main %}
    - +
    - + diff --git a/kfet/templates/kfet/checkout.html b/kfet/templates/kfet/checkout.html index 96373c49..c2c5e4bc 100644 --- a/kfet/templates/kfet/checkout.html +++ b/kfet/templates/kfet/checkout.html @@ -24,13 +24,16 @@ {% block main %}
    -
    Nom Nombre d'articlesPeut être majoréePeut être majorée
    +
    - - + + @@ -43,8 +46,12 @@ - - + + {% endfor %} diff --git a/kfet/templates/kfet/checkout_read.html b/kfet/templates/kfet/checkout_read.html index 12ba7e64..37a6e173 100644 --- a/kfet/templates/kfet/checkout_read.html +++ b/kfet/templates/kfet/checkout_read.html @@ -14,10 +14,13 @@ {% if not statements %} Pas de relevé {% else %} -
    Nom BalanceDéb. valid.Fin valid.Déb. valid.Fin valid. Protégée
    {{ checkout.balance}}€{{ checkout.valid_from }}{{ checkout.valid_to }} + {{ checkout.valid_from|date:'d/m/Y H:i' }} + + {{ checkout.valid_to|date:'d/m/Y H:i' }} + {{ checkout.is_protected|yesno }}
    +
    - + @@ -25,9 +28,9 @@ {% for statement in statements %} - diff --git a/kfet/templates/kfet/inventory.html b/kfet/templates/kfet/inventory.html index 237c45cd..f05dc32a 100644 --- a/kfet/templates/kfet/inventory.html +++ b/kfet/templates/kfet/inventory.html @@ -17,10 +17,13 @@ {% block main %}
    -
    Date/heureDate/heure Montant pris Montant laissé Erreur
    + - {{ statement.at }} + {{ statement.at|date:'d/m/Y H:i' }} {{ statement.amount_taken }}
    +
    - + @@ -28,9 +31,9 @@ {% for inventory in inventories %} - diff --git a/kfet/templates/kfet/inventory_read.html b/kfet/templates/kfet/inventory_read.html index dd5ceb2c..1edc21e0 100644 --- a/kfet/templates/kfet/inventory_read.html +++ b/kfet/templates/kfet/inventory_read.html @@ -27,7 +27,10 @@ {% block main %}
    -
    DateDate Par Nb articles
    + - {{ inventory.at }} + {{ inventory.at|date:'d/m/Y H:i' }} {{ inventory.by }}
    +
    @@ -36,25 +39,28 @@ - - {% for inventoryart in inventoryarts %} - {% ifchanged inventoryart.article.category %} - - - - {% endifchanged %} - - - - - + {% regroup inventoryarts by article.category as category_list %} + {% for category in category_list %} + + + - {% endfor %} - + + + {% for inventoryart in category.list %} + + + + + + + {% endfor %} + + {% endfor %}
    ArticleErreur
    {{ inventoryart.article.category.name }}
    - - {{ inventoryart.article.name }} - - {{ inventoryart.stock_old }}{{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
    {{ category.grouper.name }}
    + + {{ inventoryart.article.name }} + + {{ inventoryart.stock_old }}{{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
    diff --git a/kfet/templates/kfet/order.html b/kfet/templates/kfet/order.html index 53ef1bfb..0e4ed868 100644 --- a/kfet/templates/kfet/order.html +++ b/kfet/templates/kfet/order.html @@ -55,11 +55,14 @@

    Liste des commandes

    - +
    - - + + @@ -74,9 +77,9 @@ {% endif %} - diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index d95cafe3..e2e7c4cd 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -11,60 +11,79 @@ {% csrf_token %}
    -
    DateDate Fournisseur Inventaire
    + - {{ order.at }} + {{ order.at|date:'d/m/Y H:i' }} {{ order.supplier }}
    +
    - - - - + + + + - - - + + + {% for label in scale.get_labels %} - + {% endfor %} - - {% for form in formset %} - {% ifchanged form.category %} - - - - {% endifchanged %} - - {{ form.article }} - - {% for v_chunk in form.v_all %} - - {% endfor %} - - - - - - - + {% regroup formset by category_name as category_list %} + {% for category in category_list %} + + + - {% endfor %} - + + + {% for form in category.list %} + + {{ form.article }} + + {% for v_chunk in form.v_all %} + + {% endfor %} + + + + + + + + + {% endfor %} + + {% endfor %}
    ArticleVentes - - V. moy.
    - -
    E.T.
    - -
    Prév.
    - -
    + Ventes + + + V. moy. +
    + +
    + E.T. +
    + +
    + Prév. +
    + +
    StockBox
    - -
    Rec.
    - -
    Commande + Box +
    + +
    + Rec. +
    + +
    + Commande +
    {{ label }}{{ label }}
    {{ form.category_name }}
    {{ form.name }}{{ v_chunk }}{{ form.v_moy }}{{ form.v_et }}{{ form.v_prev }}{{ form.stock }}{{ form.box_capacity|default:"" }}{{ form.c_rec }}{{ form.quantity_ordered | add_class:"form-control" }}
    {{ category.grouper }}
    {{ form.name }}{{ v_chunk }}{{ form.v_moy }}{{ form.v_et }}{{ form.v_prev }}{{ form.stock }}{{ form.box_capacity|default:"" }}{{ form.c_rec }}{{ form.quantity_ordered|add_class:"form-control" }}
    {{ formset.management_form }} diff --git a/kfet/templates/kfet/order_read.html b/kfet/templates/kfet/order_read.html index 69b74420..9241c394 100644 --- a/kfet/templates/kfet/order_read.html +++ b/kfet/templates/kfet/order_read.html @@ -42,7 +42,10 @@

    Détails

    - +
    @@ -51,32 +54,35 @@ - - {% for orderart in orderarts %} - {% ifchanged orderart.article.category %} - - - - {% endifchanged %} - - - - - + {% regroup orderarts by article.category as category_list %} + {% for category in category_list %} + + + - {% endfor %} - + + + {% for orderart in category.list %} + + + + + + + {% endfor %} + + {% endfor %}
    ArticleReçu
    {{ orderart.article.category.name }}
    - - {{ orderart.article.name }} - - {{ orderart.quantity_ordered }} - {% if orderart.article.box_capacity %} - {# c'est une division ! #} - {% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %} - {% endif %} - - {{ orderart.quantity_received|default_if_none:'' }} -
    {{ category.grouper.name }}
    + + {{ orderart.article.name }} + + {{ orderart.quantity_ordered }} + {% if orderart.article.box_capacity %} + {# c'est une division ! #} + {% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %} + {% endif %} + + {{ orderart.quantity_received|default_if_none:'' }} +
    diff --git a/kfet/views.py b/kfet/views.py index f1dd6834..f0665ef6 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1839,7 +1839,7 @@ def order_create(request, pk): else: formset = cls_formset(initial=initial) - scale.label_fmt = "S -{rev_i}" + scale.label_fmt = "S-{rev_i}" return render(request, 'kfet/order_create.html', { 'supplier': supplier, From 212528011acd58b14dc64859b288d9b33caf5750 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 19 Dec 2017 11:40:02 +0100 Subject: [PATCH 013/122] Add some tests --- bda/models.py | 63 +++++++++++++++++------------------ bda/tests/test_revente.py | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 bda/tests/test_revente.py diff --git a/bda/models.py b/bda/models.py index 59827621..1c081a56 100644 --- a/bda/models.py +++ b/bda/models.py @@ -352,7 +352,7 @@ class SpectacleRevente(models.Model): self.shotgun = True self.save() - def tirage(self): + def tirage(self, send_mails=True): """ Lance le tirage au sort associé à la revente. Un gagnant est choisi parmis les personnes intéressées par le spectacle. Les personnes sont @@ -366,37 +366,38 @@ class SpectacleRevente(models.Model): # Envoie un mail au gagnant et au vendeur winner = random.choice(inscrits) self.soldTo = winner - datatuple = [] - context = { - 'acheteur': winner.user, - 'vendeur': seller.user, - 'show': spectacle, - } - datatuple.append(( - 'bda-revente-winner', - context, - settings.MAIL_DATA['revente']['FROM'], - [winner.user.email], - )) - datatuple.append(( - 'bda-revente-seller', - context, - settings.MAIL_DATA['revente']['FROM'], - [seller.user.email] - )) + if send_mails: + datatuple = [] + context = { + 'acheteur': winner.user, + 'vendeur': seller.user, + 'show': spectacle, + } + datatuple.append(( + 'bda-revente-winner', + context, + settings.MAIL_DATA['revente']['FROM'], + [winner.user.email], + )) + datatuple.append(( + 'bda-revente-seller', + context, + settings.MAIL_DATA['revente']['FROM'], + [seller.user.email] + )) - # Envoie un mail aux perdants - for inscrit in inscrits: - if inscrit != winner: - new_context = dict(context) - new_context['acheteur'] = inscrit.user - datatuple.append(( - 'bda-revente-loser', - new_context, - settings.MAIL_DATA['revente']['FROM'], - [inscrit.user.email] - )) - send_mass_custom_mail(datatuple) + # Envoie un mail aux perdants + for inscrit in inscrits: + if inscrit != winner: + new_context = dict(context) + new_context['acheteur'] = inscrit.user + datatuple.append(( + 'bda-revente-loser', + new_context, + settings.MAIL_DATA['revente']['FROM'], + [inscrit.user.email] + )) + send_mass_custom_mail(datatuple) # Si personne ne veut de la place, elle part au shotgun else: diff --git a/bda/tests/test_revente.py b/bda/tests/test_revente.py new file mode 100644 index 00000000..8ef7be19 --- /dev/null +++ b/bda/tests/test_revente.py @@ -0,0 +1,69 @@ +from django.contrib.auth.models import User +from django.test import TestCase, Client +from django.utils import timezone + +from datetime import timedelta + +from bda.models import (Tirage, Spectacle, Salle, CategorieSpectacle, + SpectacleRevente, Attribution, Participant) + + +class TestModels(TestCase): + def setUp(self): + self.tirage = Tirage.objects.create( + title="Tirage test", + appear_catalogue=True, + ouverture=timezone.now(), + fermeture=timezone.now() + ) + self.category = CategorieSpectacle.objects.create(name="Category") + self.location = Salle.objects.create(name="here") + self.spectacle_soon = Spectacle.objects.create( + title="foo", date=timezone.now()+timedelta(days=1), + location=self.location, price=0, slots=42, + tirage=self.tirage, listing=False, category=self.category + ) + self.spectacle_later = Spectacle.objects.create( + title="bar", date=timezone.now()+timedelta(days=30), + location=self.location, price=0, slots=42, + tirage=self.tirage, listing=False, category=self.category + ) + + user_buyer = User.objects.create_user( + username="bda_buyer", password="testbuyer" + ) + user_seller = User.objects.create_user( + username="bda_seller", password="testseller" + ) + self.buyer = Participant.objects.create( + user=user_buyer, tirage=self.tirage + ) + self.seller = Participant.objects.create( + user=user_seller, tirage=self.tirage + ) + + self.attr_soon = Attribution.objects.create( + participant=self.seller, spectacle=self.spectacle_soon + ) + self.attr_later = Attribution.objects.create( + participant=self.seller, spectacle=self.spectacle_later + ) + self.revente_soon = SpectacleRevente.objects.create( + seller=self.seller, + attribution=self.attr_soon + ) + self.revente_later = SpectacleRevente.objects.create( + seller=self.seller, + attribution=self.attr_later + ) + + def test_urgent(self): + self.assertTrue(self.revente_soon.is_urgent) + self.assertFalse(self.revente_later.is_urgent) + + def test_tirage(self): + self.revente_soon.confirmed_entry.add(self.buyer) + + self.assertEqual(self.revente_soon.tirage(send_mails=False), + self.buyer) + self.assertIsNone(self.revente_later.tirage(send_mails=False)) From f1bbade002663adff353ba477a6a9889580416b8 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 19 Dec 2017 12:40:50 +0100 Subject: [PATCH 014/122] Better labels for `revente` objects The label for the ReventeModelMultipleChoiceField now depends on a `own` parameter, which determines if we display the seller or the buyer's name. --- bda/forms.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 2929f771..14e91ce2 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -47,8 +47,29 @@ class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): + def __init__(self, *args, own=True, **kwargs): + super().__init__(*args, **kwargs) + self.own = own + def label_from_instance(self, obj): - return str(obj.attribution.spectacle) + label = "{show}{suffix}" + suffix = "" + if self.own: + # C'est notre propre revente : pas besoin de spécifier le vendeur + if obj.soldTo is not None: + suffix = " -- Vendue à {firstname} {lastname}".format( + firstname=obj.soldTo.user.first_name, + lastname=obj.soldTo.user.last_name, + ) + else: + # Ce n'est pas à nous : on ne voit jamais l'acheteur + suffix = " -- Vendue par {firstname} {lastname}".format( + firstname=obj.seller.user.first_name, + lastname=obj.seller.user.last_name, + ) + + return label.format(show=str(obj.attribution.spectacle), + suffix=suffix) class ResellForm(forms.Form): @@ -106,6 +127,7 @@ class InscriptionReventeForm(forms.Form): class ReventeTirageAnnulForm(forms.Form): reventes = ReventeModelMultipleChoiceField( + own=False, label='', queryset=SpectacleRevente.objects.none(), widget=forms.CheckboxSelectMultiple, @@ -116,12 +138,14 @@ class ReventeTirageAnnulForm(forms.Form): super().__init__(*args, **kwargs) self.fields['reventes'].queryset = ( participant.entered.filter(soldTo__isnull=True) - .select_related('attribution__spectacle') + .select_related('attribution__spectacle', + 'seller__user') ) class ReventeTirageForm(forms.Form): reventes = ReventeModelMultipleChoiceField( + own=False, label='', queryset=SpectacleRevente.objects.none(), widget=forms.CheckboxSelectMultiple, From 1783196a9c137d52fcfe033a97c7c09bf2cec140 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 19 Dec 2017 12:41:50 +0100 Subject: [PATCH 015/122] Management view only deals with Revente objects Except for Revente creation, every form is now handled with revente objects, to use the display option in the previous commit. --- bda/forms.py | 34 +++++++++++++------------- bda/templates/bda/revente/manage.html | 16 ++++++------ bda/templates/bda/revente/tirages.html | 4 +-- bda/views.py | 20 +++++++-------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 14e91ce2..90b0359f 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -91,7 +91,8 @@ class ResellForm(forms.Form): class AnnulForm(forms.Form): - attributions = AttributionModelMultipleChoiceField( + reventes = ReventeModelMultipleChoiceField( + own=True, label='', queryset=Attribution.objects.none(), widget=forms.CheckboxSelectMultiple, @@ -99,14 +100,13 @@ class AnnulForm(forms.Form): def __init__(self, participant, *args, **kwargs): super(AnnulForm, self).__init__(*args, **kwargs) - self.fields['attributions'].queryset = ( - participant.attribution_set - .filter(spectacle__date__gte=timezone.now(), - revente__isnull=False, - revente__notif_sent=False, - revente__soldTo__isnull=True) - .select_related('spectacle', 'spectacle__location', - 'participant__user') + self.fields['reventes'].queryset = ( + participant.original_shows + .filter(attribution__spectacle__date__gte=timezone.now(), + notif_sent=False, + soldTo__isnull=True) + .select_related('attribution__spectacle', + 'attribution__spectacle__location') ) @@ -165,18 +165,18 @@ class ReventeTirageForm(forms.Form): class SoldForm(forms.Form): - attributions = AttributionModelMultipleChoiceField( + reventes = ReventeModelMultipleChoiceField( + own=True, label='', queryset=Attribution.objects.none(), widget=forms.CheckboxSelectMultiple) def __init__(self, participant, *args, **kwargs): super(SoldForm, self).__init__(*args, **kwargs) - self.fields['attributions'].queryset = ( - participant.attribution_set - .filter(revente__isnull=False, - revente__soldTo__isnull=False) - .exclude(revente__soldTo=participant) - .select_related('spectacle', 'spectacle__location', - 'participant__user') + self.fields['reventes'].queryset = ( + participant.original_shows + .filter(soldTo__isnull=False) + .exclude(soldTo=participant) + .select_related('attribution__spectacle', + 'attribution__spectacle__location') ) diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index 8162d55d..cf0ba80e 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -4,7 +4,7 @@ {% block realcontent %}

    Gestion des places que je revends

    -{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %} +{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %} {% if resellform.attributions %}
    @@ -29,10 +29,10 @@
    {% endif %} -{% if annul_attributions or overdue %} +{% if annul_reventes or overdue %}

    Places en cours de revente

    - {% if annul_attributions %} + {% if annul_reventes %}
    Vous pouvez annuler les places mises en vente il y a moins d'une heure. @@ -42,8 +42,8 @@
      - {% for attrib in annul_attributions %} -
    • {{ attrib.tag }} {{ attrib.choice_label }}
    • + {% for revente in annul_reventes %} +
    • {{ revente.tag }} {{ revente.choice_label }}
    • {% endfor %} {% for attrib in overdue %}
    • @@ -54,7 +54,7 @@
    - {% if annul_attributions %} + {% if annul_reventes %} {% endif %} @@ -62,7 +62,7 @@
    {% endif %} -{% if sold_attributions %} +{% if sold_reventes %}

    Places revendues

    @@ -82,7 +82,7 @@ {% endif %} -{% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %} +{% if not resell_attributions and not annul_attributions and not overdue and not sold_reventes %}

    Plus de reventes possibles !

    {% endif %} diff --git a/bda/templates/bda/revente/tirages.html b/bda/templates/bda/revente/tirages.html index 91d39c90..b7017806 100644 --- a/bda/templates/bda/revente/tirages.html +++ b/bda/templates/bda/revente/tirages.html @@ -11,7 +11,7 @@
    Vous pouvez vous désinscrire des reventes suivantes tant que le tirage n'a - pas eu lieu + pas eu lieu.
    {% csrf_token %} @@ -34,7 +34,7 @@
    - Vous pouvez vous inscrire aux tirage en cours suivants + Vous pouvez vous inscrire aux tirage en cours suivants.
    {% csrf_token %} diff --git a/bda/views.py b/bda/views.py index 67fa2486..4cb35c52 100644 --- a/bda/views.py +++ b/bda/views.py @@ -401,18 +401,18 @@ def revente_manage(request, tirage_id): elif 'annul' in request.POST: annulform = AnnulForm(participant, request.POST, prefix='annul') if annulform.is_valid(): - attributions = annulform.cleaned_data["attributions"] - for attribution in attributions: - attribution.revente.delete() + reventes = annulform.cleaned_data["reventes"] + for revente in reventes: + revente.delete() # On confirme une vente en transférant la place à la personne qui a # gagné le tirage elif 'transfer' in request.POST: soldform = SoldForm(participant, request.POST, prefix='sold') if soldform.is_valid(): - attributions = soldform.cleaned_data['attributions'] - for attribution in attributions: - attribution.participant = attribution.revente.soldTo - attribution.save() + reventes = soldform.cleaned_data['reventes'] + for reventes in reventes: + revente.attribution.participant = revente.soldTo + revente.attribution.save() # On annule la revente après le tirage au sort (par exemple si # la personne qui a gagné le tirage ne se manifeste pas). La place est @@ -420,9 +420,9 @@ def revente_manage(request, tirage_id): elif 'reinit' in request.POST: soldform = SoldForm(participant, request.POST, prefix='sold') if soldform.is_valid(): - attributions = soldform.cleaned_data['attributions'] - for attribution in attributions: - if attribution.spectacle.date > timezone.now(): + reventes = soldform.cleaned_data['reventes'] + for revente in reventes: + if revente.attribution.spectacle.date > timezone.now(): # On antidate pour envoyer le mail plus vite new_date = (timezone.now() - SpectacleRevente.remorse_time) From 9a8773978c52defaf98dd7fd58babd4961273dd5 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 19 Dec 2017 12:50:20 +0100 Subject: [PATCH 016/122] Use new method in admin --- bda/admin.py | 8 +------- bda/models.py | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/bda/admin.py b/bda/admin.py index 4f5d821a..2d289b71 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -288,13 +288,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin): count = queryset.count() for revente in queryset.filter( attribution__spectacle__date__gte=timezone.now()): - revente.date = timezone.now() - timedelta(hours=1) - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - if revente.confirmed_entry: - revente.confirmed_entry.clear() - revente.save() + revente.reset(new_date=timezone.now() - timedelta(hours=1)) self.message_user( request, "%d attribution%s %s été réinitialisée%s avec succès." % ( diff --git a/bda/models.py b/bda/models.py index c01fe727..722a3ef7 100644 --- a/bda/models.py +++ b/bda/models.py @@ -300,6 +300,7 @@ class SpectacleRevente(models.Model): self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False + self.notif_time = None self.tirage_done = False self.shotgun = False self.save() From 475f1adec57ae2206d19a5bd49effee9433d2a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Jan 2018 12:23:24 +0100 Subject: [PATCH 017/122] Remove "coding: utf8" line --- bda/admin.py | 2 -- bda/algorithm.py | 2 -- bda/autocomplete_light_registry.py | 2 -- bda/forms.py | 2 -- bda/management/commands/manage_reventes.py | 2 -- bda/management/commands/sendrappels.py | 2 -- bda/models.py | 2 -- bda/urls.py | 2 -- bda/views.py | 2 -- cof/locale/fr/formats.py | 2 -- cof/settings/common.py | 1 - cof/urls.py | 2 -- gestioncof/autocomplete.py | 2 -- gestioncof/autocomplete_light_registry.py | 2 -- gestioncof/csv_views.py | 2 -- gestioncof/decorators.py | 2 -- gestioncof/management/commands/syncmails.py | 1 - gestioncof/petits_cours_forms.py | 2 -- gestioncof/petits_cours_models.py | 2 -- gestioncof/petits_cours_views.py | 2 -- gestioncof/templatetags/utils.py | 2 -- gestioncof/tests.py | 1 - gestioncof/urls.py | 2 -- gestioncof/widgets.py | 2 -- kfet/apps.py | 2 -- kfet/auth/backends.py | 1 - kfet/auth/middleware.py | 1 - kfet/auth/tests.py | 1 - kfet/autocomplete.py | 2 -- kfet/config.py | 2 -- kfet/consumers.py | 2 -- kfet/context_processors.py | 2 -- kfet/decorators.py | 2 -- kfet/forms.py | 2 -- kfet/models.py | 2 -- kfet/routing.py | 2 -- kfet/statistic.py | 2 -- kfet/templatetags/kfet_tags.py | 2 -- kfet/tests/test_config.py | 2 -- kfet/tests/test_statistic.py | 2 -- kfet/urls.py | 2 -- kfet/views.py | 2 -- 42 files changed, 78 deletions(-) diff --git a/bda/admin.py b/bda/admin.py index 60d3c1ba..4736ce2d 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import autocomplete_light from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail diff --git a/bda/algorithm.py b/bda/algorithm.py index 7f18ce18..7d6ab2f0 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import division from __future__ import print_function from __future__ import unicode_literals diff --git a/bda/autocomplete_light_registry.py b/bda/autocomplete_light_registry.py index 6c2f3ea6..774e5c2b 100644 --- a/bda/autocomplete_light_registry.py +++ b/bda/autocomplete_light_registry.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import division from __future__ import print_function from __future__ import unicode_literals diff --git a/bda/forms.py b/bda/forms.py index c0417d1e..3b0dd5bd 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django import forms from django.forms.models import BaseInlineFormSet from django.utils import timezone diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 0302ec4b..f5dee265 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Gestion en ligne de commande des reventes. """ diff --git a/bda/management/commands/sendrappels.py b/bda/management/commands/sendrappels.py index 88cf9d5c..8fbdb31c 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Gestion en ligne de commande des mails de rappel. """ diff --git a/bda/models.py b/bda/models.py index 41462d70..42c3b3ef 100644 --- a/bda/models.py +++ b/bda/models.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import calendar import random from datetime import timedelta diff --git a/bda/urls.py b/bda/urls.py index 876c84ea..52e74a67 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import division from __future__ import print_function from __future__ import unicode_literals diff --git a/bda/views.py b/bda/views.py index 84b6c9d3..7109443a 100644 --- a/bda/views.py +++ b/bda/views.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from collections import defaultdict import random import hashlib diff --git a/cof/locale/fr/formats.py b/cof/locale/fr/formats.py index 710fa6ed..ec63d8cc 100644 --- a/cof/locale/fr/formats.py +++ b/cof/locale/fr/formats.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- - """ Formats français. """ diff --git a/cof/settings/common.py b/cof/settings/common.py index a2ea3f5e..4c573d95 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Django common settings for cof project. diff --git a/cof/urls.py b/cof/urls.py index f62d5f01..4599d332 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Fichier principal de configuration des urls du projet GestioCOF """ diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 968398fd..1d60cd78 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from ldap3 import Connection from django import shortcuts diff --git a/gestioncof/autocomplete_light_registry.py b/gestioncof/autocomplete_light_registry.py index 4c62d995..6e04022f 100644 --- a/gestioncof/autocomplete_light_registry.py +++ b/gestioncof/autocomplete_light_registry.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import autocomplete_light from django.contrib.auth.models import User diff --git a/gestioncof/csv_views.py b/gestioncof/csv_views.py index c1d82aca..a4f3c028 100644 --- a/gestioncof/csv_views.py +++ b/gestioncof/csv_views.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import division from __future__ import print_function from __future__ import unicode_literals diff --git a/gestioncof/decorators.py b/gestioncof/decorators.py index a1263ce3..3875b77d 100644 --- a/gestioncof/decorators.py +++ b/gestioncof/decorators.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.contrib.auth.decorators import user_passes_test diff --git a/gestioncof/management/commands/syncmails.py b/gestioncof/management/commands/syncmails.py index 1d3dddb8..ba61dcf4 100644 --- a/gestioncof/management/commands/syncmails.py +++ b/gestioncof/management/commands/syncmails.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Import des mails de GestioCOF dans la base de donnée """ diff --git a/gestioncof/petits_cours_forms.py b/gestioncof/petits_cours_forms.py index dfb7a263..c0770afc 100644 --- a/gestioncof/petits_cours_forms.py +++ b/gestioncof/petits_cours_forms.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from captcha.fields import ReCaptchaField from django import forms diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py index d9ea9668..06199a01 100644 --- a/gestioncof/petits_cours_models.py +++ b/gestioncof/petits_cours_models.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from functools import reduce from django.db import models diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index 087c9cef..5854a927 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import json from datetime import datetime from custommail.shortcuts import render_custom_mail diff --git a/gestioncof/templatetags/utils.py b/gestioncof/templatetags/utils.py index 76bc6003..5afd8cfa 100644 --- a/gestioncof/templatetags/utils.py +++ b/gestioncof/templatetags/utils.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import division from __future__ import print_function from __future__ import unicode_literals diff --git a/gestioncof/tests.py b/gestioncof/tests.py index 66043daf..f99b0fcb 100644 --- a/gestioncof/tests.py +++ b/gestioncof/tests.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ This file demonstrates writing tests using the unittest module. These will pass when you run "manage.py test". diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 57c2e8f2..02814673 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.conf.urls import url from gestioncof.petits_cours_views import DemandeListView, DemandeDetailView from gestioncof import views, petits_cours_views diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index 758fc4ad..a44e93b0 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import division from __future__ import print_function from __future__ import unicode_literals diff --git a/kfet/apps.py b/kfet/apps.py index 4f114c37..a18dd905 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import * diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index c6ad21b2..d8ef3001 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib.auth import get_user_model from kfet.models import Account, GenericTeamToken diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 748ce4dd..388be4fc 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib.auth import get_user_model from .backends import AccountBackend diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index c2f183cd..3a61daa2 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from unittest import mock from django.core import signing diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index c4886180..0d1904d6 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from ldap3 import Connection from django.shortcuts import render from django.http import Http404 diff --git a/kfet/config.py b/kfet/config.py index 76da5a79..f248b370 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.core.exceptions import ValidationError from django.db import models diff --git a/kfet/consumers.py b/kfet/consumers.py index 0f447d2d..a53bbb72 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin diff --git a/kfet/context_processors.py b/kfet/context_processors.py index 04feec81..89678f62 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from kfet.config import kfet_config diff --git a/kfet/decorators.py b/kfet/decorators.py index 0c8a1a76..66c9d71c 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.contrib.auth.decorators import user_passes_test diff --git a/kfet/forms.py b/kfet/forms.py index 963e4254..5cfef918 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from datetime import timedelta from decimal import Decimal diff --git a/kfet/models.py b/kfet/models.py index b1e351d5..08ca4490 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.db import models from django.core.urlresolvers import reverse from django.core.validators import RegexValidator diff --git a/kfet/routing.py b/kfet/routing.py index 54de69ae..f1305d4b 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from channels.routing import include, route_class from . import consumers diff --git a/kfet/statistic.py b/kfet/statistic.py index 3f32807e..0aba4dda 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from datetime import date, datetime, time, timedelta from dateutil.relativedelta import relativedelta diff --git a/kfet/templatetags/kfet_tags.py b/kfet/templatetags/kfet_tags.py index f5cd3848..68b74738 100644 --- a/kfet/templatetags/kfet_tags.py +++ b/kfet/templatetags/kfet_tags.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import re from django import template diff --git a/kfet/tests/test_config.py b/kfet/tests/test_config.py index 03c9cf3c..43497ca8 100644 --- a/kfet/tests/test_config.py +++ b/kfet/tests/test_config.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from decimal import Decimal from django.test import TestCase diff --git a/kfet/tests/test_statistic.py b/kfet/tests/test_statistic.py index d8db7ec8..93de27a0 100644 --- a/kfet/tests/test_statistic.py +++ b/kfet/tests/test_statistic.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from unittest.mock import patch from django.test import TestCase, Client diff --git a/kfet/urls.py b/kfet/urls.py index f39299a5..96fd4ddf 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from django.conf.urls import include, url from django.contrib.auth.decorators import permission_required diff --git a/kfet/views.py b/kfet/views.py index f1dd6834..5c52637d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import ast from urllib.parse import urlencode From 57411ab46f9da69f0f8d392f6b08785f878d98a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Jan 2018 12:32:42 +0100 Subject: [PATCH 018/122] Remove __future__ imports --- bda/algorithm.py | 4 ---- bda/autocomplete_light_registry.py | 4 ---- bda/management/commands/manage_reventes.py | 2 -- bda/management/commands/sendrappels.py | 2 -- bda/urls.py | 4 ---- cof/locale/fr/formats.py | 2 -- gestioncof/csv_views.py | 4 ---- gestioncof/templatetags/utils.py | 4 ---- gestioncof/tests.py | 4 ---- gestioncof/widgets.py | 4 ---- kfet/apps.py | 2 -- requirements.txt | 1 - 12 files changed, 37 deletions(-) diff --git a/bda/algorithm.py b/bda/algorithm.py index 7d6ab2f0..f0f48ad9 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,7 +1,3 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django.db.models import Max import random diff --git a/bda/autocomplete_light_registry.py b/bda/autocomplete_light_registry.py index 774e5c2b..7aa43b07 100644 --- a/bda/autocomplete_light_registry.py +++ b/bda/autocomplete_light_registry.py @@ -1,7 +1,3 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - import autocomplete_light from bda.models import Participant, Spectacle diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index f5dee265..23bb7ae6 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -2,8 +2,6 @@ Gestion en ligne de commande des reventes. """ -from __future__ import unicode_literals - from datetime import timedelta from django.core.management import BaseCommand from django.utils import timezone diff --git a/bda/management/commands/sendrappels.py b/bda/management/commands/sendrappels.py index 8fbdb31c..82889f80 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -2,8 +2,6 @@ Gestion en ligne de commande des mails de rappel. """ -from __future__ import unicode_literals - from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone diff --git a/bda/urls.py b/bda/urls.py index 52e74a67..8a27fed0 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -1,7 +1,3 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django.conf.urls import url from gestioncof.decorators import buro_required from bda.views import SpectacleListView diff --git a/cof/locale/fr/formats.py b/cof/locale/fr/formats.py index ec63d8cc..4b47ce3d 100644 --- a/cof/locale/fr/formats.py +++ b/cof/locale/fr/formats.py @@ -2,6 +2,4 @@ Formats français. """ -from __future__ import unicode_literals - DATETIME_FORMAT = r'l j F Y \à H:i' diff --git a/gestioncof/csv_views.py b/gestioncof/csv_views.py index a4f3c028..733768dc 100644 --- a/gestioncof/csv_views.py +++ b/gestioncof/csv_views.py @@ -1,7 +1,3 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - import csv from django.http import HttpResponse, HttpResponseForbidden from django.template.defaultfilters import slugify diff --git a/gestioncof/templatetags/utils.py b/gestioncof/templatetags/utils.py index 5afd8cfa..2b732aec 100644 --- a/gestioncof/templatetags/utils.py +++ b/gestioncof/templatetags/utils.py @@ -1,7 +1,3 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django import template from django.utils.safestring import mark_safe diff --git a/gestioncof/tests.py b/gestioncof/tests.py index f99b0fcb..85673edd 100644 --- a/gestioncof/tests.py +++ b/gestioncof/tests.py @@ -5,10 +5,6 @@ when you run "manage.py test". Replace this with more appropriate tests for your application. """ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django.test import TestCase from gestioncof.models import CofProfile, User diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index a44e93b0..134ddd80 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -1,7 +1,3 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django.forms.widgets import Widget from django.forms.utils import flatatt from django.utils.safestring import mark_safe diff --git a/kfet/apps.py b/kfet/apps.py index a18dd905..8d8170e9 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -1,5 +1,3 @@ -from __future__ import (absolute_import, division, - print_function, unicode_literals) from builtins import * from django.apps import AppConfig diff --git a/requirements.txt b/requirements.txt index 1591656d..c9c6c4c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ asgiref==1.1.1 daphne==1.3.0 asgi-redis==1.3.0 statistics==1.0.3.5 -future==0.15.2 django-widget-tweaks==1.4.1 git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail ldap3 From 97eed06b6fdd1e808e5add5200a52ae8429cd8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Jan 2018 12:33:30 +0100 Subject: [PATCH 019/122] Remove builtins imports --- kfet/apps.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kfet/apps.py b/kfet/apps.py index 8d8170e9..7a6c97a2 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -1,7 +1,6 @@ -from builtins import * - from django.apps import AppConfig + class KFetConfig(AppConfig): name = 'kfet' verbose_name = "Application K-Fêt" From 62d8c2ffaf765e1af33767eb6aa8e85ad4cc019a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Jan 2018 12:37:00 +0100 Subject: [PATCH 020/122] remove @py2_unicode_compat + six --- kfet/models.py | 8 +++----- requirements.txt | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index 08ca4490..4d30d719 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,11 +1,11 @@ +from functools import reduce + from django.db import models from django.core.urlresolvers import reverse from django.core.validators import RegexValidator from django.contrib.auth.models import User from gestioncof.models import CofProfile -from django.utils.six.moves import reduce from django.utils import timezone -from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from django.db import transaction from django.db.models import F @@ -370,7 +370,7 @@ class CheckoutTransfer(models.Model): amount = models.DecimalField( max_digits = 6, decimal_places = 2) -@python_2_unicode_compatible + class CheckoutStatement(models.Model): by = models.ForeignKey( Account, on_delete = models.PROTECT, @@ -439,7 +439,6 @@ class CheckoutStatement(models.Model): super(CheckoutStatement, self).save(*args, **kwargs) -@python_2_unicode_compatible class ArticleCategory(models.Model): name = models.CharField("nom", max_length=45) has_addcost = models.BooleanField("majorée", default=True, @@ -452,7 +451,6 @@ class ArticleCategory(models.Model): return self.name -@python_2_unicode_compatible class Article(models.Model): name = models.CharField("nom", max_length = 45) is_sold = models.BooleanField("en vente", default = True) diff --git a/requirements.txt b/requirements.txt index c9c6c4c0..914eca1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ django-recaptcha==1.0.5 django-redis-cache==1.7.1 psycopg2 Pillow==3.3.0 -six==1.10.0 unicodecsv==0.14.1 icalendar==3.10 django-bootstrap-form==3.2.1 From e9b901337e0d7f547ba25848d8957c8f34f660d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Jan 2018 12:13:15 +0100 Subject: [PATCH 021/122] A few tests for BdA views --- bda/tests.py | 105 ------------------- bda/tests/__init__.py | 0 bda/tests/test_views.py | 222 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 105 deletions(-) delete mode 100644 bda/tests.py create mode 100644 bda/tests/__init__.py create mode 100644 bda/tests/test_views.py diff --git a/bda/tests.py b/bda/tests.py deleted file mode 100644 index 97a220c9..00000000 --- a/bda/tests.py +++ /dev/null @@ -1,105 +0,0 @@ -import json - -from django.contrib.auth.models import User -from django.test import TestCase, Client -from django.utils import timezone - -from .models import Tirage, Spectacle, Salle, CategorieSpectacle - - -class TestBdAViews(TestCase): - def setUp(self): - self.tirage = Tirage.objects.create( - title="Test tirage", - appear_catalogue=True, - ouverture=timezone.now(), - fermeture=timezone.now(), - ) - self.category = CategorieSpectacle.objects.create(name="Category") - self.location = Salle.objects.create(name="here") - Spectacle.objects.bulk_create([ - Spectacle( - title="foo", date=timezone.now(), location=self.location, - price=0, slots=42, tirage=self.tirage, listing=False, - category=self.category - ), - Spectacle( - title="bar", date=timezone.now(), location=self.location, - price=1, slots=142, tirage=self.tirage, listing=False, - category=self.category - ), - Spectacle( - title="baz", date=timezone.now(), location=self.location, - price=2, slots=242, tirage=self.tirage, listing=False, - category=self.category - ), - ]) - - self.bda_user = User.objects.create_user( - username="bda_user", password="bda4ever" - ) - self.bda_user.profile.is_cof = True - self.bda_user.profile.is_buro = True - self.bda_user.profile.save() - - def bda_participants(self): - """The BdA participants views can be queried""" - client = Client() - show = self.tirage.spectacle_set.first() - - client.login(self.bda_user.username, "bda4ever") - tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id)) - show_resp = client.get( - "/bda/spectacles/{}/{}".format(self.tirage.id, show.id) - ) - reminder_url = "/bda/mails-rappel/{}".format(show.id) - reminder_get_resp = client.get(reminder_url) - reminder_post_resp = client.post(reminder_url) - self.assertEqual(200, tirage_resp.status_code) - self.assertEqual(200, show_resp.status_code) - self.assertEqual(200, reminder_get_resp.status_code) - self.assertEqual(200, reminder_post_resp.status_code) - - def test_catalogue(self): - """Test the catalogue JSON API""" - client = Client() - - # The `list` hook - resp = client.get("/bda/catalogue/list") - self.assertJSONEqual( - resp.content.decode("utf-8"), - [{"id": self.tirage.id, "title": self.tirage.title}] - ) - - # The `details` hook - resp = client.get( - "/bda/catalogue/details?id={}".format(self.tirage.id) - ) - self.assertJSONEqual( - resp.content.decode("utf-8"), - { - "categories": [{ - "id": self.category.id, - "name": self.category.name - }], - "locations": [{ - "id": self.location.id, - "name": self.location.name - }], - } - ) - - # The `descriptions` hook - resp = client.get( - "/bda/catalogue/descriptions?id={}".format(self.tirage.id) - ) - raw = resp.content.decode("utf-8") - try: - results = json.loads(raw) - except ValueError: - self.fail("Not valid JSON: {}".format(raw)) - self.assertEqual(len(results), 3) - self.assertEqual( - {(s["title"], s["price"], s["slots"]) for s in results}, - {("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)} - ) diff --git a/bda/tests/__init__.py b/bda/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py new file mode 100644 index 00000000..125d5d57 --- /dev/null +++ b/bda/tests/test_views.py @@ -0,0 +1,222 @@ +import json + +from datetime import timedelta +from unittest import mock + +from django.contrib.auth.models import User +from django.template.defaultfilters import urlencode +from django.test import TestCase, Client +from django.utils import timezone + +from ..models import Tirage, Spectacle, Salle, CategorieSpectacle + + +def create_user(username, is_cof=False, is_buro=False): + user = User.objects.create_user(username=username, password=username) + user.profile.is_cof = is_cof + user.profile.is_buro = is_buro + user.profile.save() + return user + + +def user_is_cof(user): + return (user is not None) and user.profile.is_cof + + +def user_is_staff(user): + return (user is not None) and user.profile.is_buro + + +class BdATestHelpers: + def setUp(self): + # Some user with different access privileges + staff = create_user(username="bda_staff", is_cof=True, is_buro=True) + staff_c = Client() + staff_c.force_login(staff) + + member = create_user(username="bda_member", is_cof=True) + member_c = Client() + member_c.force_login(member) + + other = create_user(username="bda_other") + other_c = Client() + other_c.force_login(other) + + self.client_matrix = [ + (staff, staff_c), + (member, member_c), + (other, other_c), + (None, Client()) + ] + + def 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 + return "/login?next={}".format(urlencode(urlencode(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 + BdATestHelpers.setUp(self) + # Some BdA stuff + self.tirage = Tirage.objects.create( + title="Test tirage", + appear_catalogue=True, + ouverture=timezone.now(), + fermeture=timezone.now(), + ) + self.category = CategorieSpectacle.objects.create(name="Category") + self.location = Salle.objects.create(name="here") + Spectacle.objects.bulk_create([ + Spectacle( + title="foo", date=timezone.now(), location=self.location, + price=0, slots=42, tirage=self.tirage, listing=False, + category=self.category + ), + Spectacle( + title="bar", date=timezone.now(), location=self.location, + price=1, slots=142, tirage=self.tirage, listing=False, + category=self.category + ), + Spectacle( + title="baz", date=timezone.now(), location=self.location, + price=2, slots=242, tirage=self.tirage, listing=False, + category=self.category + ), + ]) + + def test_bda_inscriptions(self): + # TODO: test the form + url = "/bda/inscription/{}".format(self.tirage.id) + self.check_restricted_access(url) + + def test_bda_places(self): + url = "/bda/places/{}".format(self.tirage.id) + self.check_restricted_access(url) + + def test_etat_places(self): + url = "/bda/etat-places/{}".format(self.tirage.id) + self.check_restricted_access(url) + + def test_perform_tirage(self): + # Only staff member can perform a tirage + url = "/bda/tirage/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + _, staff_c = self.client_matrix[0] + # Cannot be performed if disabled + self.tirage.enable_do_tirage = False + self.tirage.save() + resp = staff_c.get(url) + self.assertTemplateUsed(resp, "tirage-failed.html") + # Cannot be performed if registrations are still open + self.tirage.enable_do_tirage = True + self.tirage.fermeture = timezone.now() + timedelta(seconds=3600) + self.tirage.save() + resp = staff_c.get(url) + self.assertTemplateUsed(resp, "tirage-failed.html") + # Otherwise, perform the tirage + self.tirage.fermeture = timezone.now() + self.tirage.save() + resp = staff_c.get(url) + self.assertTemplateNotUsed(resp, "tirage-failed.html") + + def test_spectacles_list(self): + url = "/bda/spectacles/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + def test_spectacle_detail(self): + show = self.tirage.spectacle_set.first() + url = "/bda/spectacles/{}/{}".format(self.tirage.id, show.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + def test_tirage_unpaid(self): + url = "/bda/spectacles/unpaid/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + def test_send_reminders(self): + # Just get the page + url = "/bda/mails-rappel/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + # Actually send the reminder emails + # TODO: first load the emails into the database + _, staff_c = self.client_matrix[0] + resp = staff_c.post(url) + self.assertEqual(200, resp.status_code) + # TODO: check that emails are sent + + def test_catalogue_api(self): + url_list = "/bda/catalogue/list" + url_details = "/bda/catalogue/details?id={}".format(self.tirage.id) + url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id) + + # Anyone can get + def anyone_can_get(url): + self.check_restricted_access(url, validate_user=lambda user: True) + + anyone_can_get(url_list) + anyone_can_get(url_details) + anyone_can_get(url_descriptions) + + # The resulting JSON contains the information + _, client = self.client_matrix[0] + + # List + resp = client.get(url_list) + self.assertJSONEqual( + resp.content.decode("utf-8"), + [{"id": self.tirage.id, "title": self.tirage.title}] + ) + + # Details + resp = client.get(url_details) + self.assertJSONEqual( + resp.content.decode("utf-8"), + { + "categories": [{ + "id": self.category.id, + "name": self.category.name + }], + "locations": [{ + "id": self.location.id, + "name": self.location.name + }], + } + ) + + # Descriptions + resp = client.get(url_descriptions) + raw = resp.content.decode("utf-8") + try: + results = json.loads(raw) + except ValueError: + self.fail("Not valid JSON: {}".format(raw)) + self.assertEqual(len(results), 3) + self.assertEqual( + {(s["title"], s["price"], s["slots"]) for s in results}, + {("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)} + ) + + +class TestBdaRevente: + pass + # TODO From c80e63415b0e550aff236d5a2e0861e46a6cc8bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jan 2018 14:30:33 +0100 Subject: [PATCH 022/122] Load custommails before bda tests --- bda/tests/test_views.py | 6 +- gestioncof/management/commands/syncmails.py | 127 ++++++++++---------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 125d5d57..038297d8 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -49,6 +49,10 @@ class BdATestHelpers: (None, Client()) ] + def require_custommails(self): + from gestioncof.management.commands import syncmails + syncmails.load_from_file() + def check_restricted_access(self, url, validate_user=user_is_cof, redirect_url=None): def craft_redirect_url(user): if redirect_url: @@ -154,11 +158,11 @@ class TestBdAViews(BdATestHelpers, TestCase): self.check_restricted_access(url, validate_user=user_is_staff) def test_send_reminders(self): + self.require_custommails() # Just get the page url = "/bda/mails-rappel/{}".format(self.tirage.id) self.check_restricted_access(url, validate_user=user_is_staff) # Actually send the reminder emails - # TODO: first load the emails into the database _, staff_c = self.client_matrix[0] resp = staff_c.post(url) self.assertEqual(200, resp.status_code) diff --git a/gestioncof/management/commands/syncmails.py b/gestioncof/management/commands/syncmails.py index 1d3dddb8..0308a949 100644 --- a/gestioncof/management/commands/syncmails.py +++ b/gestioncof/management/commands/syncmails.py @@ -11,6 +11,70 @@ from django.core.management.base import BaseCommand from django.contrib.contenttypes.models import ContentType +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): + 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 + 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) + + log('{synced:d} mails synchronized {unchanged:d} unchanged'.format(**status)) + + class Command(BaseCommand): help = ("Va chercher les données mails de GestioCOF stocké au format json " "dans /gestioncof/management/data/custommails.json. Le format des " @@ -22,65 +86,4 @@ class Command(BaseCommand): "remplacer par le nouveau résultat de la commande précédente.") def handle(self, *args, **options): - path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - 'data', 'custommail.json') - with open(path, 'r') as jsonfile: - mail_data = json.load(jsonfile) - - # On se souvient à quel objet correspond quel pk du json - assoc = {'types': {}, 'mails': {}} - status = {'synced': 0, 'unchanged': 0} - - for obj in mail_data: - fields = obj['fields'] - - # Pour les trois types d'objets : - # - On récupère les objets référencés par les clefs étrangères - # - On crée l'objet si nécessaire - # - On le stocke éventuellement dans les deux dictionnaires définis - # plus haut - - # Variable types - if obj['model'] == 'custommail.variabletype': - fields['inner1'] = assoc['types'].get(fields['inner1']) - fields['inner2'] = assoc['types'].get(fields['inner2']) - if fields['kind'] == 'model': - fields['content_type'] = ( - ContentType.objects - .get_by_natural_key(*fields['content_type']) - ) - var_type, _ = Type.objects.get_or_create(**fields) - assoc['types'][obj['pk']] = var_type - - # Custom mails - if obj['model'] == 'custommail.custommail': - mail = None - try: - mail = CustomMail.objects.get( - shortname=fields['shortname']) - status['unchanged'] += 1 - except CustomMail.DoesNotExist: - mail = CustomMail.objects.create(**fields) - status['synced'] += 1 - self.stdout.write( - 'SYNCED {:s}'.format(fields['shortname'])) - assoc['mails'][obj['pk']] = mail - - # Variables - if obj['model'] == 'custommail.custommailvariable': - fields['custommail'] = assoc['mails'].get(fields['custommail']) - fields['type'] = assoc['types'].get(fields['type']) - try: - Variable.objects.get( - custommail=fields['custommail'], - name=fields['name'] - ) - except Variable.DoesNotExist: - Variable.objects.create(**fields) - - # C'est agréable d'avoir le résultat affiché - self.stdout.write( - '{synced:d} mails synchronized {unchanged:d} unchanged' - .format(**status) - ) + load_from_file(log=self.stdout.write) From 5086128f16e801a0ff0e0cf186f8e511f4611ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jan 2018 14:43:54 +0100 Subject: [PATCH 023/122] Fix ill-formed url in tests --- bda/tests/test_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 038297d8..3ee46792 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -160,7 +160,8 @@ class TestBdAViews(BdATestHelpers, TestCase): def test_send_reminders(self): self.require_custommails() # Just get the page - url = "/bda/mails-rappel/{}".format(self.tirage.id) + 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] From 91119f68bc5deef627baa47ec7fb80e56c0bb514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 10 Jan 2018 17:34:41 +0100 Subject: [PATCH 024/122] =?UTF-8?q?Ne=20pas=20oublier=20avant=20de=20passe?= =?UTF-8?q?r=20en=20prod=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_PROD.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 TODO_PROD.md diff --git a/TODO_PROD.md b/TODO_PROD.md new file mode 100644 index 00000000..1a7d0736 --- /dev/null +++ b/TODO_PROD.md @@ -0,0 +1 @@ +- Changer les urls dans les mails "bda-revente" et "bda-shotgun" From 52fd49616dab6515ab6e113ccb405878c59f18dd Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 10 Jan 2018 20:14:27 +0100 Subject: [PATCH 025/122] Fix model test --- bda/tests/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/tests/test_models.py b/bda/tests/test_models.py index d9242b3f..95ce8646 100644 --- a/bda/tests/test_models.py +++ b/bda/tests/test_models.py @@ -67,7 +67,7 @@ class SpectacleReventeTests(TestCase): revente = self.rev wanted_by = [self.p1, self.p2, self.p3] - revente.answered_mail = wanted_by + revente.confirmed_entry = wanted_by with mock.patch('bda.models.random.choice') as mc: # Set winner to self.p1. From 6059ca067b96f3eb2071f022a39ef2d7d1cc5cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 15 Jan 2018 05:26:33 +0100 Subject: [PATCH 026/122] Speed up tests ~20% less using MD5 and force_login in kfet testcase. ~77% less by disabling the debug tollbar. --- cof/settings/common.py | 3 +++ cof/settings/dev.py | 22 ++++++++++++++-------- kfet/tests/testcases.py | 8 +------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index 48242fc3..708e90a0 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -7,6 +7,7 @@ the local development server should be here. """ import os +import sys try: from . import secret @@ -53,6 +54,8 @@ BASE_DIR = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) +TESTING = sys.argv[1] == 'test' + # Application definition INSTALLED_APPS = [ diff --git a/cof/settings/dev.py b/cof/settings/dev.py index 6e1f6b11..114f37da 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -4,13 +4,18 @@ The settings that are not listed here are imported from .common """ from .common import * # NOQA -from .common import INSTALLED_APPS, MIDDLEWARE +from .common import INSTALLED_APPS, MIDDLEWARE, TESTING EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' DEBUG = True +if TESTING: + PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.MD5PasswordHasher', + ] + # --- # Apache static/media config @@ -36,12 +41,13 @@ def show_toolbar(request): """ return DEBUG -INSTALLED_APPS += ["debug_toolbar", "debug_panel"] +if not TESTING: + INSTALLED_APPS += ["debug_toolbar", "debug_panel"] -MIDDLEWARE = [ - "debug_panel.middleware.DebugPanelMiddleware" -] + MIDDLEWARE + MIDDLEWARE = [ + "debug_panel.middleware.DebugPanelMiddleware" + ] + MIDDLEWARE -DEBUG_TOOLBAR_CONFIG = { - 'SHOW_TOOLBAR_CALLBACK': show_toolbar, -} + DEBUG_TOOLBAR_CONFIG = { + 'SHOW_TOOLBAR_CALLBACK': show_toolbar, + } diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index 3ea428c3..aa2fb1b6 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -245,13 +245,7 @@ class ViewTestCaseMixin(TestCaseMixin): self.register_user(label, user) if self.auth_user: - # The wrapper is a sanity check. - self.assertTrue( - self.client.login( - username=self.auth_user, - password=self.auth_user, - ) - ) + self.client.force_login(self.users[self.auth_user]) def tearDown(self): del self.users_base From e23e1bdba6c6ddbc4e80e23b2099c7e6e99788f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 15 Jan 2018 16:52:46 +0100 Subject: [PATCH 027/122] kfet -- Add test to check the choices of checkouts in K-Psul Particularly, it adds a regression test for #184. --- kfet/tests/test_forms.py | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 kfet/tests/test_forms.py diff --git a/kfet/tests/test_forms.py b/kfet/tests/test_forms.py new file mode 100644 index 00000000..e946d39d --- /dev/null +++ b/kfet/tests/test_forms.py @@ -0,0 +1,48 @@ +import datetime +from unittest import mock + +from django.test import TestCase +from django.utils import timezone + +from kfet.forms import KPsulCheckoutForm +from kfet.models import Checkout + +from .utils import create_user + + +class KPsulCheckoutFormTests(TestCase): + + def setUp(self): + self.now = timezone.now() + + user = create_user() + + self.c1 = Checkout.objects.create( + name='C1', balance=10, + created_by=user.profile.account_kfet, + valid_from=self.now, + valid_to=self.now + datetime.timedelta(days=1), + ) + + self.form = KPsulCheckoutForm() + + def test_checkout(self): + checkout_f = self.form.fields['checkout'] + self.assertListEqual(list(checkout_f.choices), [ + ('', '---------'), + (self.c1.pk, 'C1'), + ]) + + @mock.patch('django.utils.timezone.now') + def test_checkout_valid(self, mock_now): + """ + Checkout are filtered using the current datetime. + Regression test for #184. + """ + self.now += datetime.timedelta(days=2) + mock_now.return_value = self.now + + form = KPsulCheckoutForm() + + checkout_f = form.fields['checkout'] + self.assertListEqual(list(checkout_f.choices), [('', '---------')]) From 525bb4d16dc7057fa746d5e66c9c8ebb336466b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 15 Jan 2018 16:56:38 +0100 Subject: [PATCH 028/122] kfet -- Fix available checkouts in K-Psul The checkout validity is checked using the current datetime (when requesting the kpsul page). --- kfet/forms.py | 16 ++++++++-------- kfet/models.py | 9 +++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 963e4254..26774b1c 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -296,17 +296,17 @@ class KPsulAccountForm(forms.ModelForm): class KPsulCheckoutForm(forms.Form): checkout = forms.ModelChoiceField( - queryset=( - Checkout.objects - .filter( - is_protected=False, - valid_from__lte=timezone.now(), - valid_to__gte=timezone.now(), - ) - ), + queryset=None, widget=forms.Select(attrs={'id': 'id_checkout_select'}), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Create the queryset on form instanciation to use the current time. + self.fields['checkout'].queryset = ( + Checkout.objects.is_valid().filter(is_protected=False)) + class KPsulOperationForm(forms.ModelForm): article = forms.ModelChoiceField( diff --git a/kfet/models.py b/kfet/models.py index deee76eb..c9de48bc 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -341,6 +341,13 @@ class AccountNegative(models.Model): return self.start + kfet_config.overdraft_duration +class CheckoutQuerySet(models.QuerySet): + + def is_valid(self): + now = timezone.now() + return self.filter(valid_from__lte=now, valid_to__gte=now) + + class Checkout(models.Model): created_by = models.ForeignKey( Account, on_delete = models.PROTECT, @@ -353,6 +360,8 @@ class Checkout(models.Model): default = 0) is_protected = models.BooleanField(default = False) + objects = CheckoutQuerySet.as_manager() + def get_absolute_url(self): return reverse('kfet.checkout.read', kwargs={'pk': self.pk}) From 478f56d94b6db8a185bbd0dd821147c06a2bfafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 16 Jan 2018 15:59:17 +0100 Subject: [PATCH 029/122] kfet -- Create initial statement on checkout save - Why? Because it should be the actual behavior. - To allow using arithmetic operations with values of DecimalField when object are not retrieved from DB, some strings are replaced by Decimal or int. If you wonder why it's not automatically done, see: https://code.djangoproject.com/ticket/27825 --- kfet/models.py | 16 ++++++++++++++++ kfet/tests/test_models.py | 37 ++++++++++++++++++++++++++++++++++++- kfet/tests/test_views.py | 19 ++++++++++++------- kfet/views.py | 10 +--------- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index deee76eb..3f38cc44 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -362,6 +362,22 @@ class Checkout(models.Model): def __str__(self): return self.name + def save(self, *args, **kwargs): + created = self.pk is None + + ret = super().save(*args, **kwargs) + + if created: + self.statements.create( + amount_taken=0, + balance_old=self.balance, + balance_new=self.balance, + by=self.created_by, + ) + + return ret + + class CheckoutTransfer(models.Model): from_checkout = models.ForeignKey( Checkout, on_delete = models.PROTECT, diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py index ea132acd..727cac4e 100644 --- a/kfet/tests/test_models.py +++ b/kfet/tests/test_models.py @@ -1,7 +1,12 @@ +import datetime + from django.contrib.auth import get_user_model from django.test import TestCase +from django.utils import timezone -from kfet.models import Account +from kfet.models import Account, Checkout + +from .utils import create_user User = get_user_model() @@ -23,3 +28,33 @@ class AccountTests(TestCase): with self.assertRaises(Account.DoesNotExist): Account.objects.get_by_password('bernard') + + +class CheckoutTests(TestCase): + + def setUp(self): + self.now = timezone.now() + + self.u = create_user() + self.u_acc = self.u.profile.account_kfet + + self.c = Checkout( + created_by=self.u_acc, + valid_from=self.now, + valid_to=self.now + datetime.timedelta(days=1), + ) + + def test_initial_statement(self): + """A statement is added with initial balance on creation.""" + self.c.balance = 10 + self.c.save() + + st = self.c.statements.get() + self.assertEqual(st.balance_new, 10) + self.assertEqual(st.amount_taken, 0) + self.assertEqual(st.amount_error, 0) + + # Saving again doesn't create a new statement. + self.c.save() + + self.assertEqual(self.c.statements.count(), 1) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 41ed8b5c..40f895a1 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -746,12 +746,16 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase): def setUp(self): super().setUp() - self.checkout = Checkout.objects.create( - name='Checkout', - created_by=self.accounts['team'], - valid_from=self.now, - valid_to=self.now + timedelta(days=5), - ) + + with mock.patch('django.utils.timezone.now') as mock_now: + mock_now.return_value = self.now + + self.checkout = Checkout.objects.create( + name='Checkout', balance=Decimal('10'), + created_by=self.accounts['team'], + valid_from=self.now, + valid_to=self.now + timedelta(days=1), + ) def test_ok(self): r = self.client.get(self.url) @@ -794,7 +798,7 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase): name='Checkout', valid_from=self.now, valid_to=self.now + timedelta(days=5), - balance='3.14', + balance=Decimal('3.14'), is_protected=False, created_by=self.accounts['team'], ) @@ -864,6 +868,7 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context['checkoutstatements'], map(repr, expected_statements), + ordered=False, ) diff --git a/kfet/views.py b/kfet/views.py index 1fe9ac22..2b69684d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -528,15 +528,7 @@ class CheckoutCreate(SuccessMessageMixin, CreateView): # Creating form.instance.created_by = self.request.user.profile.account_kfet - checkout = form.save() - - # Création d'un relevé avec balance initiale - CheckoutStatement.objects.create( - checkout = checkout, - by = self.request.user.profile.account_kfet, - balance_old = checkout.balance, - balance_new = checkout.balance, - amount_taken = 0) + form.save() return super(CheckoutCreate, self).form_valid(form) From 42e762bc4a07cde8eee2a9e12ef9e51ce1b75322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 16 Jan 2018 16:22:52 +0100 Subject: [PATCH 030/122] Py3 allows to shorten super() --- bda/admin.py | 2 +- bda/forms.py | 8 ++++---- bda/views.py | 2 +- gestioncof/admin.py | 4 ++-- gestioncof/forms.py | 28 +++++++++++++------------- gestioncof/petits_cours_forms.py | 4 ++-- gestioncof/petits_cours_views.py | 2 +- gestioncof/widgets.py | 2 +- kfet/forms.py | 20 +++++++++---------- kfet/models.py | 8 ++++---- kfet/views.py | 34 ++++++++++++++++---------------- 11 files changed, 57 insertions(+), 57 deletions(-) diff --git a/bda/admin.py b/bda/admin.py index 0e796f57..5511cf85 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -164,7 +164,7 @@ class AttributionAdminForm(forms.ModelForm): ) def clean(self): - cleaned_data = super(AttributionAdminForm, self).clean() + cleaned_data = super().clean() participant = cleaned_data.get("participant") spectacle = cleaned_data.get("spectacle") if participant and spectacle: diff --git a/bda/forms.py b/bda/forms.py index 3b0dd5bd..a363c36e 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -52,7 +52,7 @@ class ResellForm(forms.Form): required=False) def __init__(self, participant, *args, **kwargs): - super(ResellForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['attributions'].queryset = ( participant.attribution_set .filter(spectacle__date__gte=timezone.now()) @@ -70,7 +70,7 @@ class AnnulForm(forms.Form): required=False) def __init__(self, participant, *args, **kwargs): - super(AnnulForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['attributions'].queryset = ( participant.attribution_set .filter(spectacle__date__gte=timezone.now(), @@ -89,7 +89,7 @@ class InscriptionReventeForm(forms.Form): required=False) def __init__(self, tirage, *args, **kwargs): - super(InscriptionReventeForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['spectacles'].queryset = ( tirage.spectacle_set .select_related('location') @@ -104,7 +104,7 @@ class SoldForm(forms.Form): widget=forms.CheckboxSelectMultiple) def __init__(self, participant, *args, **kwargs): - super(SoldForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['attributions'].queryset = ( participant.attribution_set .filter(revente__isnull=False, diff --git a/bda/views.py b/bda/views.py index 4d18a043..dace3c51 100644 --- a/bda/views.py +++ b/bda/views.py @@ -628,7 +628,7 @@ class SpectacleListView(ListView): return categories def get_context_data(self, **kwargs): - context = super(SpectacleListView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['tirage_id'] = self.tirage.id context['tirage_name'] = self.tirage.title return context diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 51969822..54a6a5a0 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -181,7 +181,7 @@ class UserProfileAdmin(UserAdmin): def get_fieldsets(self, request, user=None): if not request.user.is_superuser: return self.staff_fieldsets - return super(UserProfileAdmin, self).get_fieldsets(request, user) + return super().get_fieldsets(request, user) def save_model(self, request, user, form, change): cof_group, created = Group.objects.get_or_create(name='COF') @@ -267,7 +267,7 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin): class ClubAdminForm(forms.ModelForm): def clean(self): - cleaned_data = super(ClubAdminForm, self).clean() + cleaned_data = super().clean() respos = cleaned_data.get('respos') members = cleaned_data.get('membres') for respo in respos.all(): diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 2124b7c8..0d1db499 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -18,7 +18,7 @@ class EventForm(forms.Form): event = kwargs.pop("event") self.event = event current_choices = kwargs.pop("current_choices", None) - super(EventForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) choices = {} if current_choices: for choice in current_choices.all(): @@ -60,7 +60,7 @@ class SurveyForm(forms.Form): def __init__(self, *args, **kwargs): survey = kwargs.pop("survey") current_answers = kwargs.pop("current_answers", None) - super(SurveyForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) answers = {} if current_answers: for answer in current_answers.all(): @@ -100,7 +100,7 @@ class SurveyForm(forms.Form): class SurveyStatusFilterForm(forms.Form): def __init__(self, *args, **kwargs): survey = kwargs.pop("survey") - super(SurveyStatusFilterForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) for question in survey.questions.all(): for answer in question.answers.all(): name = "question_%d_answer_%d" % (question.id, answer.id) @@ -129,7 +129,7 @@ class SurveyStatusFilterForm(forms.Form): class EventStatusFilterForm(forms.Form): def __init__(self, *args, **kwargs): event = kwargs.pop("event") - super(EventStatusFilterForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) for option in event.options.all(): for choice in option.choices.all(): name = "option_%d_choice_%d" % (option.id, choice.id) @@ -175,12 +175,12 @@ class UserProfileForm(forms.ModelForm): last_name = forms.CharField(label=_('Nom'), max_length=30) def __init__(self, *args, **kw): - super(UserProfileForm, self).__init__(*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(UserProfileForm, self).save(*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() @@ -193,7 +193,7 @@ class UserProfileForm(forms.ModelForm): class RegistrationUserForm(forms.ModelForm): def __init__(self, *args, **kw): - super(RegistrationUserForm, self).__init__(*args, **kw) + super().__init__(*args, **kw) self.fields['username'].help_text = "" class Meta: @@ -219,8 +219,7 @@ class RegistrationPassUserForm(RegistrationUserForm): return pass2 def save(self, commit=True, *args, **kwargs): - user = super(RegistrationPassUserForm, self).save(commit, *args, - **kwargs) + user = super().save(commit, *args, **kwargs) user.set_password(self.cleaned_data['password2']) if commit: user.save() @@ -229,7 +228,7 @@ class RegistrationPassUserForm(RegistrationUserForm): class RegistrationProfileForm(forms.ModelForm): def __init__(self, *args, **kw): - super(RegistrationProfileForm, self).__init__(*args, **kw) + super().__init__(*args, **kw) self.fields['mailing_cof'].initial = True self.fields['mailing_bda'].initial = True self.fields['mailing_bda_revente'].initial = True @@ -274,7 +273,7 @@ class AdminEventForm(forms.Form): kwargs["initial"] = {"status": "wait"} else: kwargs["initial"] = {"status": "no"} - super(AdminEventForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) choices = {} for choice in current_choices: if choice.event_option.id not in choices: @@ -337,14 +336,15 @@ class BaseEventRegistrationFormset(BaseFormSet): self.events = kwargs.pop('events') self.current_registrations = kwargs.pop('current_registrations', None) self.extra = len(self.events) - super(BaseEventRegistrationFormset, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _construct_form(self, index, **kwargs): kwargs['event'] = self.events[index] if self.current_registrations is not None: kwargs['current_registration'] = self.current_registrations[index] - return super(BaseEventRegistrationFormset, self)._construct_form( - index, **kwargs) + return super()._construct_form(index, **kwargs) + + EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset) diff --git a/gestioncof/petits_cours_forms.py b/gestioncof/petits_cours_forms.py index c0770afc..e8f067bf 100644 --- a/gestioncof/petits_cours_forms.py +++ b/gestioncof/petits_cours_forms.py @@ -10,7 +10,7 @@ from gestioncof.petits_cours_models import PetitCoursDemande, PetitCoursAbility class BaseMatieresFormSet(BaseInlineFormSet): def clean(self): - super(BaseMatieresFormSet, self).clean() + super().clean() if any(self.errors): # Don't bother validating the formset unless each form is # valid on its own @@ -34,7 +34,7 @@ class DemandeForm(ModelForm): captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'}) def __init__(self, *args, **kwargs): - super(DemandeForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['matieres'].help_text = '' class Meta: diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index ec3d1a58..6b8c8610 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -42,7 +42,7 @@ class DemandeDetailView(DetailView): context_object_name = "demande" def get_context_data(self, **kwargs): - context = super(DemandeDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) obj = self.object context['attributions'] = obj.petitcoursattribution_set.all() return context diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index 134ddd80..1741cfec 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe class TriStateCheckbox(Widget): def __init__(self, attrs=None, choices=()): - super(TriStateCheckbox, self).__init__(attrs) + super().__init__(attrs) # choices can be any iterable, but we may need to render this widget # multiple times. Thus, collapse it into a list so it can be consumed # more than once. diff --git a/kfet/forms.py b/kfet/forms.py index 5cfef918..417a51a7 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -42,7 +42,7 @@ class AccountForm(forms.ModelForm): # Surcharge pour passer data à Account.save() def save(self, data = {}, *args, **kwargs): - obj = super(AccountForm, self).save(commit = False, *args, **kwargs) + obj = super().save(commit = False, *args, **kwargs) obj.save(data = data) return obj @@ -91,7 +91,7 @@ class AccountPwdForm(forms.Form): raise ValidationError("Mot de passe trop court") if pwd1 != pwd2: raise ValidationError("Les mots de passes sont différents") - super(AccountPwdForm, self).clean() + super().clean() class CofForm(forms.ModelForm): def clean_is_cof(self): @@ -195,7 +195,7 @@ class CheckoutStatementCreateForm(forms.ModelForm): or self.cleaned_data['balance_200'] is None or self.cleaned_data['balance_500'] is None): raise ValidationError("Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp (et t'as pas idée de comment c'est long de vérifier que t'as mis des valeurs de partout...)") - super(CheckoutStatementCreateForm, self).clean() + super().clean() class CheckoutStatementUpdateForm(forms.ModelForm): class Meta: @@ -236,7 +236,7 @@ class ArticleForm(forms.ModelForm): required = False) def __init__(self, *args, **kwargs): - super(ArticleForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.instance.pk: self.initial['suppliers'] = self.instance.suppliers.values_list('pk', flat=True) @@ -250,7 +250,7 @@ class ArticleForm(forms.ModelForm): category, _ = ArticleCategory.objects.get_or_create(name=category_new) self.cleaned_data['category'] = category - super(ArticleForm, self).clean() + super().clean() class Meta: model = Article @@ -321,7 +321,7 @@ class KPsulOperationForm(forms.ModelForm): } def clean(self): - super(KPsulOperationForm, self).clean() + super().clean() type_ope = self.cleaned_data.get('type') amount = self.cleaned_data.get('amount') article = self.cleaned_data.get('article') @@ -364,7 +364,7 @@ class AddcostForm(forms.Form): raise ValidationError('Compte invalide') else: self.cleaned_data['amount'] = 0 - super(AddcostForm, self).clean() + super().clean() # ----- @@ -462,7 +462,7 @@ class InventoryArticleForm(forms.Form): stock_new = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): - super(InventoryArticleForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if 'initial' in kwargs: self.name = kwargs['initial']['name'] self.stock_old = kwargs['initial']['stock_old'] @@ -484,7 +484,7 @@ class OrderArticleForm(forms.Form): quantity_ordered = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): - super(OrderArticleForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if 'initial' in kwargs: self.name = kwargs['initial']['name'] self.stock = kwargs['initial']['stock'] @@ -514,7 +514,7 @@ class OrderArticleToInventoryForm(forms.Form): quantity_received = forms.IntegerField() def __init__(self, *args, **kwargs): - super(OrderArticleToInventoryForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if 'initial' in kwargs: self.name = kwargs['initial']['name'] self.category = kwargs['initial']['category'] diff --git a/kfet/models.py b/kfet/models.py index e5aed2b7..5cd4b757 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -255,7 +255,7 @@ class Account(models.Model): cof.save() if data: self.cofprofile = cof - super(Account, self).save(*args, **kwargs) + super().save(*args, **kwargs) def change_pwd(self, clear_password): from .auth.utils import hash_password @@ -423,7 +423,7 @@ class CheckoutStatement(models.Model): self.balance_new + self.amount_taken - self.balance_old) with transaction.atomic(): Checkout.objects.filter(pk=checkout_id).update(balance=self.balance_new) - super(CheckoutStatement, self).save(*args, **kwargs) + super().save(*args, **kwargs) else: self.amount_error = ( self.balance_new + self.amount_taken - self.balance_old) @@ -437,7 +437,7 @@ class CheckoutStatement(models.Model): and last_statement.balance_new != self.balance_new): Checkout.objects.filter(pk=self.checkout_id).update( balance=F('balance') - last_statement.balance_new + self.balance_new) - super(CheckoutStatement, self).save(*args, **kwargs) + super().save(*args, **kwargs) class ArticleCategory(models.Model): @@ -538,7 +538,7 @@ class InventoryArticle(models.Model): # d'erreur if not self.inventory.order: self.stock_error = self.stock_new - self.stock_old - super(InventoryArticle, self).save(*args, **kwargs) + super().save(*args, **kwargs) class Supplier(models.Model): diff --git a/kfet/views.py b/kfet/views.py index ef1885b3..df25c95e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -493,7 +493,7 @@ class AccountNegativeList(ListView): context_object_name = 'negatives' def get_context_data(self, **kwargs): - context = super(AccountNegativeList, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) real_balances = (neg.account.real_balance for neg in self.object_list) context['negatives_sum'] = sum(real_balances) return context @@ -536,7 +536,7 @@ class CheckoutCreate(SuccessMessageMixin, CreateView): balance_new = checkout.balance, amount_taken = 0) - return super(CheckoutCreate, self).form_valid(form) + return super().form_valid(form) # Checkout - Read @@ -546,7 +546,7 @@ class CheckoutRead(DetailView): context_object_name = 'checkout' def get_context_data(self, **kwargs): - context = super(CheckoutRead, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['statements'] = context['checkout'].statements.order_by('-at') return context @@ -565,7 +565,7 @@ class CheckoutUpdate(SuccessMessageMixin, UpdateView): form.add_error(None, 'Permission refusée') return self.form_invalid(form) # Updating - return super(CheckoutUpdate, self).form_valid(form) + return super().form_valid(form) # ----- # Checkout Statement views @@ -617,7 +617,7 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView): at = self.object.at) def get_context_data(self, **kwargs): - context = super(CheckoutStatementCreate, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout']) context['checkout'] = checkout return context @@ -633,7 +633,7 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView): form.instance.balance_new = getAmountBalance(form.cleaned_data) form.instance.checkout_id = self.kwargs['pk_checkout'] form.instance.by = self.request.user.profile.account_kfet - return super(CheckoutStatementCreate, self).form_valid(form) + return super().form_valid(form) class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): model = CheckoutStatement @@ -645,7 +645,7 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']}) def get_context_data(self, **kwargs): - context = super(CheckoutStatementUpdate, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout']) context['checkout'] = checkout return context @@ -657,7 +657,7 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): return self.form_invalid(form) # Updating form.instance.amount_taken = getAmountTaken(form.instance) - return super(CheckoutStatementUpdate, self).form_valid(form) + return super().form_valid(form) # ----- # Category views @@ -689,7 +689,7 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView): return self.form_invalid(form) # Updating - return super(CategoryUpdate, self).form_valid(form) + return super().form_valid(form) # ----- # Article views @@ -756,7 +756,7 @@ class ArticleCreate(SuccessMessageMixin, CreateView): ) # Creating - return super(ArticleCreate, self).form_valid(form) + return super().form_valid(form) # Article - Read @@ -766,7 +766,7 @@ class ArticleRead(DetailView): context_object_name = 'article' def get_context_data(self, **kwargs): - context = super(ArticleRead, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) inventoryarts = (InventoryArticle.objects .filter(article=self.object) .select_related('inventory') @@ -818,7 +818,7 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): article=article, supplier=supplier) # Updating - return super(ArticleUpdate, self).form_valid(form) + return super().form_valid(form) # ----- @@ -1709,7 +1709,7 @@ class InventoryRead(DetailView): context_object_name = 'inventory' def get_context_data(self, **kwargs): - context = super(InventoryRead, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) inventoryarticles = (InventoryArticle.objects .select_related('article', 'article__category') .filter(inventory = self.object) @@ -1727,7 +1727,7 @@ class OrderList(ListView): context_object_name = 'orders' def get_context_data(self, **kwargs): - context = super(OrderList, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['suppliers'] = Supplier.objects.order_by('name') return context @@ -1856,7 +1856,7 @@ class OrderRead(DetailView): context_object_name = 'order' def get_context_data(self, **kwargs): - context = super(OrderRead, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) orderarticles = (OrderArticle.objects .select_related('article', 'article__category') .filter(order=self.object) @@ -2010,7 +2010,7 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): form.add_error(None, 'Permission refusée') return self.form_invalid(form) # Updating - return super(SupplierUpdate, self).form_valid(form) + return super().form_valid(form) # ========== @@ -2274,7 +2274,7 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): @method_decorator(login_required) def dispatch(self, *args, **kwargs): - return super(AccountStatBalance, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) # ------------------------ From 776ff28141dc96e2ab13e8b35bb71f42637dae3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 17:48:00 +0100 Subject: [PATCH 031/122] cof -- Add helpers to test cof views. --- gestioncof/tests/__init__.py | 0 gestioncof/{tests.py => tests/test_legacy.py} | 0 gestioncof/tests/testcases.py | 24 ++ gestioncof/tests/utils.py | 51 +++ shared/tests/testcases.py | 294 ++++++++++++++++++ 5 files changed, 369 insertions(+) create mode 100644 gestioncof/tests/__init__.py rename gestioncof/{tests.py => tests/test_legacy.py} (100%) create mode 100644 gestioncof/tests/testcases.py create mode 100644 gestioncof/tests/utils.py create mode 100644 shared/tests/testcases.py diff --git a/gestioncof/tests/__init__.py b/gestioncof/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/tests.py b/gestioncof/tests/test_legacy.py similarity index 100% rename from gestioncof/tests.py rename to gestioncof/tests/test_legacy.py diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py new file mode 100644 index 00000000..b53f2866 --- /dev/null +++ b/gestioncof/tests/testcases.py @@ -0,0 +1,24 @@ +from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin + +from .utils import create_user, create_member, create_staff + + +class ViewTestCaseMixin(BaseViewTestCaseMixin): + """ + TestCase extension to ease testing of cof views. + + Most information can be found in the base parent class doc. + This class performs some changes to users management, detailed below. + + During setup, three users are created: + - 'user': a basic user without any permission, + - 'member': (profile.is_cof is True), + - 'staff': (profile.is_cof is True) && (profile.is_buro is True). + """ + + def get_users_base(self): + return { + 'user': create_user('user'), + 'member': create_member('member'), + 'staff': create_staff('staff'), + } diff --git a/gestioncof/tests/utils.py b/gestioncof/tests/utils.py new file mode 100644 index 00000000..8d55680a --- /dev/null +++ b/gestioncof/tests/utils.py @@ -0,0 +1,51 @@ +from django.contrib.auth import get_user_model + +User = get_user_model() + + +def _create_user(username, is_cof=False, is_staff=False, attrs=None): + if attrs is None: + attrs = {} + + password = attrs.pop('password', username) + + user_keys = ['first_name', 'last_name', 'email', 'is_staff'] + user_attrs = {k: v for k, v in attrs.items() if k in user_keys} + + profile_keys = [ + 'is_cof', 'login_clipper', 'phone', '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} + + if is_cof: + profile_attrs['is_cof'] = True + + if is_staff: + # At the moment, admin is accessible by COF staff. + user_attrs['is_staff'] = True + profile_attrs['is_buro'] = True + + user = User(username=username, **user_attrs) + user.set_password(password) + user.save() + + for k, v in profile_attrs.items(): + setattr(user.profile, k, v) + user.profile.save() + + return user + + +def create_user(username, attrs=None): + return _create_user(username, attrs=attrs) + + +def create_member(username, attrs=None): + return _create_user(username, is_cof=True, attrs=attrs) + + +def create_staff(username, attrs=None): + return _create_user(username, is_cof=True, is_staff=True, attrs=attrs) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py new file mode 100644 index 00000000..15792383 --- /dev/null +++ b/shared/tests/testcases.py @@ -0,0 +1,294 @@ +from unittest import mock +from urllib.parse import parse_qs, urlparse + +from django.contrib.auth import get_user_model +from django.core.urlresolvers import reverse +from django.http import QueryDict +from django.test import Client +from django.utils import timezone +from django.utils.functional import cached_property + +User = get_user_model() + + +class TestCaseMixin: + + def assertForbidden(self, response): + """ + Test that the response (retrieved with a Client) is a denial of access. + + The response should verify one of the following: + - its HTTP response code is 403, + - it redirects to the login page with a GET parameter named 'next' + whose value is the url of the requested page. + + """ + request = response.wsgi_request + + try: + try: + # Is this an HTTP Forbidden response ? + self.assertEqual(response.status_code, 403) + except AssertionError: + # A redirection to the login view is fine too. + + # Let's build the login url with the 'next' param on current + # page. + full_path = request.get_full_path() + + querystring = QueryDict(mutable=True) + querystring['next'] = full_path + + login_url = '{}?{}'.format( + reverse('cof-login'), querystring.urlencode(safe='/')) + + # We don't focus on what the login view does. + # So don't fetch the redirect. + self.assertRedirects( + response, login_url, + fetch_redirect_response=False, + ) + except AssertionError: + raise AssertionError( + "%(http_method)s request at %(path)s should be forbidden for " + "%(username)s user.\n" + "Response isn't 403, nor a redirect to login view. Instead, " + "response code is %(code)d." % { + 'http_method': request.method, + 'path': request.get_full_path(), + 'username': ( + "'{}'".format(request.user) + if request.user.is_authenticated() + else 'anonymous' + ), + 'code': response.status_code, + } + ) + + def assertUrlsEqual(self, actual, expected): + """ + Test that the url 'actual' is as 'expected'. + + Arguments: + actual (str): Url to verify. + expected: Two forms are accepted. + * (str): Expected url. Strings equality is checked. + * (dict): Its keys must be attributes of 'urlparse(actual)'. + Equality is checked for each present key, except for + 'query' which must be a dict of the expected query string + parameters. + + """ + if type(expected) == dict: + parsed = urlparse(actual) + for part, expected_part in expected.items(): + if part == 'query': + self.assertDictEqual( + parse_qs(parsed.query), + expected.get('query', {}), + ) + else: + self.assertEqual(getattr(parsed, part), expected_part) + else: + self.assertEqual(actual, expected) + + +class ViewTestCaseMixin(TestCaseMixin): + """ + TestCase extension to ease tests of kfet views. + + + Urls concerns + ------------- + + # Basic usage + + Attributes: + url_name (str): Name of view under test, as given to 'reverse' + function. + url_args (list, optional): Will be given to 'reverse' call. + url_kwargs (dict, optional): Same. + url_expcted (str): What 'reverse' should return given previous + attributes. + + View url can then be accessed at the 'url' attribute. + + # Advanced usage + + If multiple combinations of url name, args, kwargs can be used for a view, + it is possible to define 'urls_conf' attribute. It must be a list whose + each item is a dict defining arguments for 'reverse' call ('name', 'args', + 'kwargs' keys) and its expected result ('expected' key). + + The reversed urls can be accessed at the 't_urls' attribute. + + + Users concerns + -------------- + + During setup, the following users are created: + - 'user': a basic user without any permission, + - 'root': a superuser, account trigramme: 200. + Their password is their username. + + One can create additionnal users with 'get_users_extra' method, or prevent + these users to be created with 'get_users_base' method. See these two + methods for further informations. + + By using 'register_user' method, these users can then be accessed at + 'users' attribute by their label. + + A user label can be given to 'auth_user' attribute. The related user is + then authenticated on self.client during test setup. Its value defaults to + 'None', meaning no user is authenticated. + + + Automated tests + --------------- + + # Url reverse + + Based on url-related attributes/properties, the test 'test_urls' checks + that expected url is returned by 'reverse' (once with basic url usage and + each for advanced usage). + + # Forbidden responses + + The 'test_forbidden' test verifies that each user, from labels of + 'auth_forbidden' attribute, can't access the url(s), i.e. response should + be a 403, or a redirect to login view. + + Tested HTTP requests are given by 'http_methods' attribute. Additional data + can be given by defining an attribute '_data'. + + """ + url_name = None + url_expected = None + + http_methods = ['GET'] + + auth_user = None + auth_forbidden = [] + + def setUp(self): + """ + Warning: Do not forget to call super().setUp() in subclasses. + """ + # 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) + + # A test can mock 'django.utils.timezone.now' and give this as return + # value. E.g. it is useful if the test checks values of 'auto_now' or + # 'auto_now_add' fields. + self.now = timezone.now() + + # Register of User instances. + self.users = {} + + for label, user in dict(self.users_base, **self.users_extra).items(): + self.register_user(label, user) + + if self.auth_user: + # The wrapper is a sanity check. + self.assertTrue( + self.client.login( + username=self.auth_user, + password=self.auth_user, + ) + ) + + def tearDown(self): + del self.users_base + del self.users_extra + + def get_users_base(self): + """ + Dict of . + + Note: Don't access yourself this property. Use 'users_base' attribute + which cache the returned value from here. + It allows to give functions calls, which creates users instances, as + values here. + + """ + return { + 'user': User.objects.create_user('user', '', 'user'), + 'root': User.objects.create_superuser('root', '', 'root'), + } + + @cached_property + def users_base(self): + return self.get_users_base() + + def get_users_extra(self): + """ + Dict of . + + Note: Don't access yourself this property. Use 'users_base' attribute + which cache the returned value from here. + It allows to give functions calls, which create users instances, as + values here. + + """ + return {} + + @cached_property + def users_extra(self): + return self.get_users_extra() + + def register_user(self, label, user): + self.users[label] = user + + def get_user(self, label): + if self.auth_user is not None: + return self.auth_user + return self.auth_user_mapping.get(label) + + @property + def urls_conf(self): + return [{ + 'name': self.url_name, + 'args': getattr(self, 'url_args', []), + 'kwargs': getattr(self, 'url_kwargs', {}), + 'expected': self.url_expected, + }] + + @property + def t_urls(self): + return [ + reverse( + url_conf['name'], + args=url_conf.get('args', []), + kwargs=url_conf.get('kwargs', {}), + ) + for url_conf in self.urls_conf] + + @property + def url(self): + return self.t_urls[0] + + def test_urls(self): + for url, conf in zip(self.t_urls, self.urls_conf): + self.assertEqual(url, conf['expected']) + + def test_forbidden(self): + for method in self.http_methods: + for user in self.auth_forbidden: + for url in self.t_urls: + self.check_forbidden(method, url, user) + + def check_forbidden(self, method, url, user=None): + method = method.lower() + client = Client() + if user is not None: + client.login(username=user, password=user) + + send_request = getattr(client, method) + data = getattr(self, '{}_data'.format(method), {}) + + r = send_request(url, data) + self.assertForbidden(r) From 57de31d59a82906b935237f533844397efa5e4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 17:57:43 +0100 Subject: [PATCH 032/122] cof -- Add tests for survey views --- gestioncof/tests/test_views.py | 178 +++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..b8860811 --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,178 @@ +from django.contrib import messages +from django.contrib.messages import get_messages +from django.contrib.messages.storage.base import Message +from django.test import TestCase + +from gestioncof.models import Survey, SurveyAnswer +from gestioncof.tests.testcases import ViewTestCaseMixin + + +class SurveyViewTests(ViewTestCaseMixin, TestCase): + url_name = 'survey.details' + + http_methods = ['GET', 'POST'] + + auth_user = 'user' + auth_forbidden = [None] + + post_expected_message = Message(messages.SUCCESS, ( + "Votre réponse a bien été enregistrée ! Vous pouvez cependant la " + "modifier jusqu'à la fin du sondage." + )) + + @property + def url_kwargs(self): + return {'survey_id': self.s.pk} + + @property + def url_expected(self): + return '/survey/{}'.format(self.s.pk) + + def setUp(self): + super().setUp() + + self.s = Survey.objects.create(title='Title') + + self.q1 = self.s.questions.create(question='Question 1 ?') + self.q2 = self.s.questions.create( + question='Question 2 ?', + multi_answers=True, + ) + + self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1') + self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2') + self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1') + self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2') + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_new(self): + r = self.client.post(self.url, { + 'question_{}'.format(self.q1.pk): [str(self.qa1.pk)], + 'question_{}'.format(self.q2.pk): [ + str(self.qa3.pk), str(self.qa4.pk), + ], + }) + + self.assertEqual(r.status_code, 200) + self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) + + a = self.s.surveyanswer_set.get(user=self.users['user']) + self.assertQuerysetEqual( + a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]), + ordered=False, + ) + + def test_post_edit(self): + a = self.s.surveyanswer_set.create(user=self.users['user']) + a.answers.add(self.qa1, self.qa1, self.qa4) + + r = self.client.post(self.url, { + 'question_{}'.format(self.q1.pk): [], + 'question_{}'.format(self.q2.pk): [str(self.qa3.pk)], + }) + + self.assertEqual(r.status_code, 200) + self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) + + a.refresh_from_db() + self.assertQuerysetEqual( + a.answers.all(), map(repr, [self.qa3]), + ordered=False, + ) + + def test_post_delete(self): + a = self.s.surveyanswer_set.create(user=self.users['user']) + a.answers.add(self.qa1, self.qa4) + + r = self.client.post(self.url, {'delete': '1'}) + + self.assertEqual(r.status_code, 200) + expected_message = Message( + messages.SUCCESS, "Votre réponse a bien été supprimée") + self.assertIn(expected_message, get_messages(r.wsgi_request)) + + with self.assertRaises(SurveyAnswer.DoesNotExist): + a.refresh_from_db() + + def test_forbidden_closed(self): + self.s.survey_open = False + self.s.save() + + r = self.client.get(self.url) + + self.assertNotEqual(r.status_code, 200) + + def test_forbidden_old(self): + self.s.old = True + self.s.save() + + r = self.client.get(self.url) + + self.assertNotEqual(r.status_code, 200) + + +class SurveyStatusViewTests(ViewTestCaseMixin, TestCase): + url_name = 'survey.details.status' + + http_methods = ['GET', 'POST'] + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + @property + def url_kwargs(self): + return {'survey_id': self.s.pk} + + @property + def url_expected(self): + return '/survey/{}/status'.format(self.s.pk) + + def setUp(self): + super().setUp() + + self.s = Survey.objects.create(title='Title') + + self.q1 = self.s.questions.create(question='Question 1 ?') + self.q2 = self.s.questions.create( + question='Question 2 ?', + multi_answers=True, + ) + + self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1') + self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2') + self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1') + self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2') + + self.a1 = self.s.surveyanswer_set.create(user=self.users['user']) + self.a1.answers.add(self.qa1) + self.a2 = self.s.surveyanswer_set.create(user=self.users['member']) + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def _get_qa_filter_name(self, qa): + return 'question_{}_answer_{}'.format(qa.survey_question.pk, qa.pk) + + def _test_filters(self, filters, expected): + r = self.client.post(self.url, { + self._get_qa_filter_name(qa): v for qa, v in filters + }) + + self.assertEqual(r.status_code, 200) + self.assertQuerysetEqual( + r.context['user_answers'], map(repr, expected), + ordered=False, + ) + + def test_filter_none(self): + self._test_filters([(self.qa1, 'none')], [self.a1, self.a2]) + + def test_filter_yes(self): + self._test_filters([(self.qa1, 'yes')], [self.a1]) + + def test_filter_no(self): + self._test_filters([(self.qa1, 'no')], [self.a2]) From 8675948d9e849acc028e79c78f7aed92b68101ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 18:01:36 +0100 Subject: [PATCH 033/122] cof -- Fix urls naming in survey templates --- gestioncof/templates/gestioncof/survey.html | 2 +- gestioncof/templates/survey_status.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gestioncof/templates/gestioncof/survey.html b/gestioncof/templates/gestioncof/survey.html index ccf447ef..9d4d67b3 100644 --- a/gestioncof/templates/gestioncof/survey.html +++ b/gestioncof/templates/gestioncof/survey.html @@ -8,7 +8,7 @@ {% if survey.details %}

    {{ survey.details }}

    {% endif %} - + {% csrf_token %} {{ form | bootstrap}} diff --git a/gestioncof/templates/survey_status.html b/gestioncof/templates/survey_status.html index 831a07bb..0e630c6e 100644 --- a/gestioncof/templates/survey_status.html +++ b/gestioncof/templates/survey_status.html @@ -11,7 +11,7 @@ {% endif %}

    Filtres

    {% include "tristate_js.html" %} - + {% csrf_token %} {{ form.as_p }} From ce734990776fe25f62be96b05ad589027bde0a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 18:15:57 +0100 Subject: [PATCH 034/122] Fix use of Widget.build_attrs in TriStateCheckbox Signature changed in Django 1.11. --- gestioncof/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index 758fc4ad..cbc9cd93 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -20,6 +20,7 @@ class TriStateCheckbox(Widget): def render(self, name, value, attrs=None, choices=()): if value is None: value = 'none' - final_attrs = self.build_attrs(attrs, value=value) + attrs['value'] = value + final_attrs = self.build_attrs(self.attrs, attrs) output = ["" % flatatt(final_attrs)] return mark_safe('\n'.join(output)) From f5b280896fcaac00934e2341552ca4f3e21bda05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 18:36:03 +0100 Subject: [PATCH 035/122] cof -- Add tests for event views --- gestioncof/tests/test_views.py | 163 +++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..a3c423d3 --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,163 @@ +from django.contrib import messages +from django.contrib.messages import get_messages +from django.contrib.messages.storage.base import Message +from django.test import TestCase + +from gestioncof.models import Event +from gestioncof.tests.testcases import ViewTestCaseMixin + + +class EventViewTests(ViewTestCaseMixin, TestCase): + url_name = 'event.details' + + http_methods = ['GET', 'POST'] + + auth_user = 'user' + auth_forbidden = [None] + + post_expected_message = Message(messages.SUCCESS, ( + "Votre inscription a bien été enregistrée ! Vous pouvez cependant la " + "modifier jusqu'à la fin des inscriptions." + )) + + @property + def url_kwargs(self): + return {'event_id': self.e.pk} + + @property + def url_expected(self): + return '/event/{}'.format(self.e.pk) + + def setUp(self): + super().setUp() + + self.e = Event.objects.create() + + self.ecf1 = self.e.commentfields.create(name='Comment Field 1') + self.ecf2 = self.e.commentfields.create( + name='Comment Field 2', fieldtype='char', + ) + + self.o1 = self.e.options.create(name='Option 1') + self.o2 = self.e.options.create(name='Option 2', multi_choices=True) + + self.oc1 = self.o1.choices.create(value='O1 - Choice 1') + self.oc2 = self.o1.choices.create(value='O1 - Choice 2') + self.oc3 = self.o2.choices.create(value='O2 - Choice 1') + self.oc4 = self.o2.choices.create(value='O2 - Choice 2') + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_new(self): + r = self.client.post(self.url, { + 'option_{}'.format(self.o1.pk): [str(self.oc1.pk)], + 'option_{}'.format(self.o2.pk): [ + str(self.oc3.pk), str(self.oc4.pk), + ], + }) + + self.assertEqual(r.status_code, 200) + self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) + + er = self.e.eventregistration_set.get(user=self.users['user']) + self.assertQuerysetEqual( + er.options.all(), map(repr, [self.oc1, self.oc3, self.oc4]), + ordered=False, + ) + # TODO: Make the view care about comments. + # self.assertQuerysetEqual( + # er.comments.all(), map(repr, []), + # ordered=False, + # ) + + def test_post_edit(self): + er = self.e.eventregistration_set.create(user=self.users['user']) + er.options.add(self.oc1, self.oc3, self.oc4) + er.comments.create( + commentfield=self.ecf1, content='Comment 1', + ) + + r = self.client.post(self.url, { + 'option_{}'.format(self.o1.pk): [], + 'option_{}'.format(self.o2.pk): [str(self.oc3.pk)], + }) + + self.assertEqual(r.status_code, 200) + self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) + + er.refresh_from_db() + self.assertQuerysetEqual( + er.options.all(), map(repr, [self.oc3]), + ordered=False, + ) + # TODO: Make the view care about comments. + # self.assertQuerysetEqual( + # er.comments.all(), map(repr, []), + # ordered=False, + # ) + + +class EventStatusViewTests(ViewTestCaseMixin, TestCase): + url_name = 'event.details.status' + + http_methods = ['GET', 'POST'] + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + @property + def url_kwargs(self): + return {'event_id': self.e.pk} + + @property + def url_expected(self): + return '/event/{}/status'.format(self.e.pk) + + def setUp(self): + super().setUp() + + self.e = Event.objects.create() + + self.cf1 = self.e.commentfields.create(name='Comment Field 1') + self.cf2 = self.e.commentfields.create( + name='Comment Field 2', fieldtype='char', + ) + + self.o1 = self.e.options.create(name='Option 1') + self.o2 = self.e.options.create(name='Option 2', multi_choices=True) + + self.oc1 = self.o1.choices.create(value='O1 - Choice 1') + self.oc2 = self.o1.choices.create(value='O1 - Choice 2') + self.oc3 = self.o2.choices.create(value='O2 - Choice 1') + self.oc4 = self.o2.choices.create(value='O2 - Choice 2') + + self.er1 = self.e.eventregistration_set.create(user=self.users['user']) + self.er1.options.add(self.oc1) + self.er2 = self.e.eventregistration_set.create( + user=self.users['member'], + ) + + def _get_oc_filter_name(self, oc): + return 'option_{}_choice_{}'.format(oc.event_option.pk, oc.pk) + + def _test_filters(self, filters, expected): + r = self.client.post(self.url, { + self._get_oc_filter_name(oc): v for oc, v in filters + }) + + self.assertEqual(r.status_code, 200) + self.assertQuerysetEqual( + r.context['user_choices'], map(repr, expected), + ordered=False, + ) + + def test_filter_none(self): + self._test_filters([(self.oc1, 'none')], [self.er1, self.er2]) + + def test_filter_yes(self): + self._test_filters([(self.oc1, 'yes')], [self.er1]) + + def test_filter_no(self): + self._test_filters([(self.oc1, 'no')], [self.er2]) From a6f52cfdc561fa436c474706c3f770805c43954f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 18:38:34 +0100 Subject: [PATCH 036/122] cof -- Fix urls naming in event template --- gestioncof/templates/event_status.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/templates/event_status.html b/gestioncof/templates/event_status.html index 2a09b820..40bda7db 100644 --- a/gestioncof/templates/event_status.html +++ b/gestioncof/templates/event_status.html @@ -11,7 +11,7 @@ {% endif %} {% include "tristate_js.html" %}

    Filtres

    - + {% csrf_token %} {{ form.as_p }} From dfb9ccb0af1b8700823e929d85806c06c769979a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 Jan 2018 18:15:57 +0100 Subject: [PATCH 037/122] Fix use of Widget.build_attrs in TriStateCheckbox Signature changed in Django 1.11. --- gestioncof/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index 758fc4ad..cbc9cd93 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -20,6 +20,7 @@ class TriStateCheckbox(Widget): def render(self, name, value, attrs=None, choices=()): if value is None: value = 'none' - final_attrs = self.build_attrs(attrs, value=value) + attrs['value'] = value + final_attrs = self.build_attrs(self.attrs, attrs) output = ["" % flatatt(final_attrs)] return mark_safe('\n'.join(output)) From 7160a9c954aa1767fc329f35c0fd08f8230b12ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 20 Jan 2018 16:14:55 +0100 Subject: [PATCH 038/122] cof -- Add tests for registration views --- gestioncof/autocomplete.py | 7 + gestioncof/tests/test_views.py | 339 +++++++++++++++++++++++++++++++++ shared/tests/testcases.py | 32 ++++ 3 files changed, 378 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 968398fd..9263dc50 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -21,6 +21,13 @@ class Clipper(object): self.clipper = clipper self.fullname = fullname + def __str__(self): + return '{} ({})'.format(self.clipper, self.fullname) + + def __eq__(self, other): + return ( + self.clipper == other.clipper and self.fullname == other.fullname) + @buro_required def autocomplete(request): diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..60f93c9d --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,339 @@ +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.contrib.messages.api import get_messages +from django.contrib.messages.storage.base import Message +from django.core import mail +from django.core.management import call_command +from django.test import TestCase, override_settings + +from gestioncof.autocomplete import Clipper +from gestioncof.models import Event +from gestioncof.tests.testcases import ViewTestCaseMixin + +from custommail.models import CustomMail + +from .utils import create_user, create_member + +User = get_user_model() + + +class RegistrationViewTests(ViewTestCaseMixin, TestCase): + url_name = 'registration' + url_expected = '/registration' + + http_methods = ['GET', 'POST'] + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def requires_mails(self): + call_command('syncmails', verbosity=0) + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + @property + def _minimal_data(self): + return { + 'first_name': '', + 'last_name': '', + 'email': '', + + # 'is_cof': '1', + 'login_clipper': '', + 'phone': '', + 'occupation': '1A', + 'departement': '', + 'type_cotiz': 'normalien', + 'comments': '', + + # 'user_exists': '1', + + 'events-TOTAL_FORMS': '0', + 'events-INITIAL_FORMS': '0', + 'events-MIN_NUM_FORMS': '0', + 'events-MAX_NUM_FORMS': '1000', + } + + def test_post_new(self): + self.requires_mails() + + r = self.client.post(self.url, dict(self._minimal_data, **{ + 'username': 'username', + 'first_name': 'first', + 'last_name': 'last', + 'email': 'username@mail.net', + 'is_cof': '1', + })) + + self.assertEqual(r.status_code, 200) + u = User.objects.get(username='username') + expected_message = Message(messages.SUCCESS, ( + "L'inscription de first last (username@mail.net) a été " + "enregistrée avec succès.\n" + "Il est désormais membre du COF n°{} !" + .format(u.pk) + )) + self.assertIn(expected_message, get_messages(r.wsgi_request)) + + self.assertEqual(u.first_name, 'first') + self.assertEqual(u.last_name, 'last') + self.assertEqual(u.email, 'username@mail.net') + + def test_post_edit(self): + self.requires_mails() + u = self.users['user'] + + r = self.client.post(self.url, dict(self._minimal_data, **{ + 'username': 'user', + 'first_name': 'first', + 'last_name': 'last', + 'email': 'user@mail.net', + 'is_cof': '1', + 'user_exists': '1', + })) + + self.assertEqual(r.status_code, 200) + u.refresh_from_db() + expected_message = Message(messages.SUCCESS, ( + "L'inscription de first last (user@mail.net) a été " + "enregistrée avec succès.\n" + "Il est désormais membre du COF n°{} !" + .format(u.pk) + )) + self.assertIn(expected_message, get_messages(r.wsgi_request)) + + self.assertEqual(u.first_name, 'first') + self.assertEqual(u.last_name, 'last') + self.assertEqual(u.email, 'user@mail.net') + + def _test_mail_welcome(self, was_cof, is_cof, expect_mail): + self.requires_mails() + u = self.users['member'] if was_cof else self.users['user'] + + data = dict(self._minimal_data, **{ + 'username': u.username, + 'email': 'user@mail.net', + 'user_exists': '1', + }) + if is_cof: + data['is_cof'] = '1' + self.client.post(self.url, data) + + u.refresh_from_db() + + def _is_sent(): + cm = CustomMail.objects.get(shortname='welcome') + welcome_msg = cm.get_message({'member': u}) + for m in mail.outbox: + if m.subject == welcome_msg.subject: + return True + return False + + self.assertEqual(_is_sent(), expect_mail) + + def test_mail_welcome_0(self): + self._test_mail_welcome(was_cof=False, is_cof=False, expect_mail=False) + + def test_mail_welcome_1(self): + self._test_mail_welcome(was_cof=False, is_cof=True, expect_mail=True) + + def test_mail_welcome_2(self): + self._test_mail_welcome(was_cof=True, is_cof=False, expect_mail=False) + + def test_mail_welcome_3(self): + self._test_mail_welcome(was_cof=True, is_cof=True, expect_mail=False) + + def test_events(self): + e = Event.objects.create() + + cf1 = e.commentfields.create(name='Comment Field 1') + cf2 = e.commentfields.create( + name='Comment Field 2', fieldtype='char', + ) + + o1 = e.options.create(name='Option 1') + o2 = e.options.create(name='Option 2', multi_choices=True) + + oc1 = o1.choices.create(value='O1 - Choice 1') + oc2 = o1.choices.create(value='O1 - Choice 2') + oc3 = o2.choices.create(value='O2 - Choice 1') + oc4 = o2.choices.create(value='O2 - Choice 2') + + self.client.post(self.url, dict(self._minimal_data, **{ + 'username': 'user', + 'user_exists': '1', + 'events-TOTAL_FORMS': '1', + 'events-INITIAL_FORMS': '0', + 'events-MIN_NUM_FORMS': '0', + 'events-MAX_NUM_FORMS': '1000', + 'events-0-status': 'paid', + 'events-0-option_{}'.format(o1.pk): [str(oc1.pk)], + 'events-0-option_{}'.format(o2.pk): [str(oc3.pk)], + 'events-0-comment_{}'.format(cf1.pk): 'comment 1', + 'events-0-comment_{}'.format(cf2.pk): '', + })) + + er = e.eventregistration_set.get(user=self.users['user']) + self.assertQuerysetEqual( + er.options.all(), map(repr, [oc1, oc3]), + ordered=False, + ) + self.assertCountEqual(er.comments.values_list('content', flat=True), [ + 'comment 1', + ]) + + +class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): + urls_conf = [ + { + 'name': 'empty-registration', + 'expected': '/registration/empty', + }, + { + 'name': 'user-registration', + 'kwargs': {'username': 'user'}, + 'expected': '/registration/user/user', + }, + { + 'name': 'clipper-registration', + 'kwargs': { + 'login_clipper': 'uid', + 'fullname': 'First Last1 Last2', + }, + 'expected': '/registration/clipper/uid/First%20Last1%20Last2', + }, + ] + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def get_initial(self, form, name): + return form.get_initial_for_field(form.fields[name], name) + + def test_empty(self): + r = self.client.get(self.t_urls[0]) + + user_form = r.context['user_form'] + profile_form = r.context['profile_form'] + events_form = r.context['event_formset'] + clubs_form = r.context['clubs_form'] + + def test_username(self): + u = self.users['user'] + u.first_name = 'first' + u.last_name = 'last' + u.save() + + r = self.client.get(self.t_urls[1]) + + user_form = r.context['user_form'] + profile_form = r.context['profile_form'] + events_form = r.context['event_formset'] + clubs_form = r.context['clubs_form'] + + self.assertEqual(self.get_initial(user_form, 'username'), 'user') + self.assertEqual(self.get_initial(user_form, 'first_name'), 'first') + self.assertEqual(self.get_initial(user_form, 'last_name'), 'last') + + def test_clipper(self): + r = self.client.get(self.t_urls[2]) + + user_form = r.context['user_form'] + profile_form = r.context['profile_form'] + events_form = r.context['event_formset'] + clubs_form = r.context['clubs_form'] + + self.assertEqual(self.get_initial(user_form, 'first_name'), 'First') + self.assertEqual( + self.get_initial(user_form, 'last_name'), 'Last1 Last2') + self.assertEqual( + self.get_initial(user_form, 'email'), 'uid@clipper.ens.fr') + self.assertEqual( + self.get_initial(profile_form, 'login_clipper'), 'uid') + + +@override_settings(LDAP_SERVER_URL='ldap_url') +class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): + url_name = 'cof.registration.autocomplete' + url_expected = '/autocomplete/registration' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def setUp(self): + super().setUp() + + self.u1 = create_user('uu_u1', attrs={ + 'first_name': 'abc', 'last_name': 'xyz', + }) + self.u2 = create_user('uu_u2', attrs={ + 'first_name': 'wyz', 'last_name': 'abd', + }) + self.m1 = create_member('uu_m1', attrs={ + 'first_name': 'ebd', 'last_name': 'wyv', + }) + + self.mockLDAP([]) + + def _test( + self, query, expected_users, expected_members, expected_clippers, + ): + r = self.client.get(self.url, {'q': query}) + + self.assertEqual(r.status_code, 200) + + self.assertQuerysetEqual( + r.context['users'], map(repr, expected_users), + ordered=False, + ) + self.assertQuerysetEqual( + r.context['members'], + map(lambda u: repr(u.profile), expected_members), + ordered=False, + ) + self.assertCountEqual( + map(str, r.context.get('clippers', [])), + map(str, expected_clippers), + ) + + def test_username(self): + self._test('uu', [self.u1, self.u2], [self.m1], []) + + def test_firstname(self): + self._test('ab', [self.u1, self.u2], [], []) + + def test_lastname(self): + self._test('wy', [self.u2], [self.m1], []) + + def test_multi_query(self): + self._test('wy bd', [self.u2], [self.m1], []) + + def test_clipper(self): + mock_ldap = self.mockLDAP([('uid', 'first last')]) + + self._test('aa bb', [], [], [Clipper('uid', 'first last')]) + + mock_ldap.search.assert_called_once_with( + 'dc=spi,dc=ens,dc=fr', + '(&(|(cn=*aa*)(uid=*aa*))(|(cn=*bb*)(uid=*bb*)))', + attributes=['uid', 'cn'], + ) + + def test_clipper_escaped(self): + mock_ldap = self.mockLDAP([]) + + self._test('; & | (', [], [], []) + + mock_ldap.search.assert_not_called() + + def test_clipper_no_duplicate(self): + self.mockLDAP([('uid', 'uu_u1')]) + + self._test('uu u1', [self.u1], [], [Clipper('uid', 'uu_u1')]) + + self.u1.profile.login_clipper = 'uid' + self.u1.profile.save() + + self._test('uu u1', [self.u1], [], []) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 15792383..03e63e6b 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -92,6 +92,38 @@ class TestCaseMixin: else: self.assertEqual(actual, expected) + def mockLDAP(self, results): + class Elt: + def __init__(self, value): + self.value = value + + class Entry: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, Elt(v)) + + results_as_ldap = [ + Entry(uid=uid, cn=name) for uid, name in results + ] + + mock_connection = mock.MagicMock() + mock_connection.entries = results_as_ldap + + # Connection is used as a context manager. + mock_context_manager = mock.MagicMock() + mock_context_manager.return_value.__enter__.return_value = ( + mock_connection + ) + + patcher = mock.patch( + 'gestioncof.autocomplete.Connection', + new=mock_context_manager, + ) + patcher.start() + self.addCleanup(patcher.stop) + + return mock_connection + class ViewTestCaseMixin(TestCaseMixin): """ From 0921f32e4c1b16567cc85a1ed4859738384b3968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 20 Jan 2018 16:17:57 +0100 Subject: [PATCH 039/122] cof -- Fix urls naming related to registration --- cof/urls.py | 3 ++- gestioncof/templates/gestioncof/registration_form.html | 2 +- gestioncof/templates/registration.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index e6e5d313..7ef80f3b 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -70,7 +70,8 @@ urlpatterns = [ url(r'^registration/empty$', gestioncof_views.registration_form2, name="empty-registration"), # Autocompletion - url(r'^autocomplete/registration$', autocomplete), + url(r'^autocomplete/registration$', autocomplete, + name='cof.registration.autocomplete'), url(r'^user/autocomplete$', gestioncof_views.user_autocomplete, name='cof-user-autocomplete'), # Interface admin diff --git a/gestioncof/templates/gestioncof/registration_form.html b/gestioncof/templates/gestioncof/registration_form.html index 8668152b..37f24cff 100644 --- a/gestioncof/templates/gestioncof/registration_form.html +++ b/gestioncof/templates/gestioncof/registration_form.html @@ -7,7 +7,7 @@ {% else %}

    Inscription d'un nouveau compte (extérieur ?)

    {% endif %} - + {% csrf_token %} {{ user_form | bootstrap }} diff --git a/gestioncof/templates/registration.html b/gestioncof/templates/registration.html index 8f05dfb0..2d7552a1 100644 --- a/gestioncof/templates/registration.html +++ b/gestioncof/templates/registration.html @@ -18,7 +18,7 @@ // On attend que la page soit prête pour executer le code $(document).ready(function() { $('input#search_autocomplete').yourlabsAutocomplete({ - url: '{% url 'gestioncof.autocomplete.autocomplete' %}', + url: '{% url 'cof.registration.autocomplete' %}', minimumCharacters: 3, id: 'search_autocomplete', choiceSelector: 'li:has(a)', From c239f28f171fc899ca9b6824e003e24833bf482f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 20 Jan 2018 17:02:23 +0100 Subject: [PATCH 040/122] syncmails should be able to be silent --- gestioncof/management/commands/syncmails.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/gestioncof/management/commands/syncmails.py b/gestioncof/management/commands/syncmails.py index 1d3dddb8..74ad152d 100644 --- a/gestioncof/management/commands/syncmails.py +++ b/gestioncof/management/commands/syncmails.py @@ -63,8 +63,9 @@ class Command(BaseCommand): except CustomMail.DoesNotExist: mail = CustomMail.objects.create(**fields) status['synced'] += 1 - self.stdout.write( - 'SYNCED {:s}'.format(fields['shortname'])) + if options['verbosity']: + self.stdout.write( + 'SYNCED {:s}'.format(fields['shortname'])) assoc['mails'][obj['pk']] = mail # Variables @@ -79,8 +80,9 @@ class Command(BaseCommand): except Variable.DoesNotExist: Variable.objects.create(**fields) - # C'est agréable d'avoir le résultat affiché - self.stdout.write( - '{synced:d} mails synchronized {unchanged:d} unchanged' - .format(**status) - ) + 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) + ) From 4084444dc3c1fd5f5af2e5714cee2fc163da1180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 20 Jan 2018 17:29:15 +0100 Subject: [PATCH 041/122] Fix autocomplete in registration views. django-autocomplete-light v3.x doesn't include anymore the $('').yourlabsAutocomplete() function, leading to issues in cof registration and kfet account creation views. Adding jquery-autocomplete-light fixes these issues. See: - (dal) https://github.com/yourlabs/django-autocomplete-light - (jal) https://github.com/yourlabs/jquery-autocomplete-light --- cof/settings/common.py | 2 + gestioncof/templates/registration.html | 2 +- kfet/templates/kfet/account_create.html | 2 +- .../3.5.0/dist/jquery.autocomplete-light.js | 1698 +++++++++++++++++ .../dist/jquery.autocomplete-light.min.js | 9 + 5 files changed, 1711 insertions(+), 2 deletions(-) create mode 100644 shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.js create mode 100644 shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js diff --git a/cof/settings/common.py b/cof/settings/common.py index 48242fc3..aec299c1 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -56,6 +56,8 @@ BASE_DIR = os.path.dirname( # Application definition INSTALLED_APPS = [ + 'shared', + 'gestioncof', # Must be before 'django.contrib.admin'. diff --git a/gestioncof/templates/registration.html b/gestioncof/templates/registration.html index 8f05dfb0..99ab3e73 100644 --- a/gestioncof/templates/registration.html +++ b/gestioncof/templates/registration.html @@ -4,7 +4,7 @@ {% block page_size %}col-sm-8{% endblock %} {% block extra_head %} - + {% endblock %} {% block realcontent %} diff --git a/kfet/templates/kfet/account_create.html b/kfet/templates/kfet/account_create.html index 59fc1d56..b09713df 100644 --- a/kfet/templates/kfet/account_create.html +++ b/kfet/templates/kfet/account_create.html @@ -5,7 +5,7 @@ {% block header-title %}Création d'un compte{% endblock %} {% block extra_head %} - + {% endblock %} {% block main %} diff --git a/shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.js b/shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.js new file mode 100644 index 00000000..a916bff5 --- /dev/null +++ b/shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.js @@ -0,0 +1,1698 @@ +/* + * jquery-autocomplete-light - v3.5.0 + * Dead simple autocompletion and widgets for jQuery + * http://yourlabs.org + * + * Made by James Pic + * Under MIT License + */ +/* +Here is the list of the major difference with other autocomplete scripts: + +- don't do anything but fire a signal when a choice is selected: it's +left as an exercise to the developer to implement whatever he wants when +that happens +- don't generate the autocomplete HTML, it should be generated by the server + +Let's establish the vocabulary used in this script, so that we speak the +same language: + +- The text input element is "input", +- The box that contains a list of choices is "box", +- Each result in the "autocomplete" is a "choice", +- With a capital A, "Autocomplete", is the class or an instance of the +class. + +Here is a fantastic schema in ASCII art: + + +---------------------+ <----- Input + | Your city name ? <---------- Placeholder + +---------------------+ + | Paris, France | <----- Autocomplete + | Paris, TX, USA | + | Paris, TN, USA | + | Paris, KY, USA <------------ Choice + | Paris, IL, USA | + +---------------------+ + +This script defines three signals: + +- hilightChoice: when a choice is hilight, or that the user +navigates into a choice with the keyboard, +- dehilightChoice: when a choice was hilighed, and that the user +navigates into another choice with the keyboard or mouse, +- selectChoice: when the user clicks on a choice, or that he pressed +enter on a hilighted choice. + +They all work the same, here's a trivial example: + + $('#your-autocomplete').bind( + 'selectChoice', + function(e, choice, autocomplete) { + alert('You selected: ' + choice.html()); + } + ); + +Note that 'e' is the variable containing the event object. + +Also, note that this script is composed of two main parts: + +- The Autocomplete class that handles all interaction, defined as +`Autocomplete`, +- The jQuery plugin that manages Autocomplete instance, defined as +`$.fn.yourlabsAutocomplete` +*/ + +if (window.isOpera === undefined) { + var isOpera = (navigator.userAgent.indexOf('Opera')>=0) && parseFloat(navigator.appVersion); +} + +if (window.isIE === undefined) { + var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split('MSIE ')[1].split(';')[0]); +} + +if (window.findPosX === undefined) { + window.findPosX = function(obj) { + var curleft = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft); + obj = obj.offsetParent; + } + // IE offsetParent does not include the top-level + if (isIE && obj.parentElement){ + curleft += obj.offsetLeft - obj.scrollLeft; + } + } else if (obj.x) { + curleft += obj.x; + } + return curleft; + } +} + +if (window.findPosY === undefined) { + window.findPosY = function(obj) { + var curtop = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop); + obj = obj.offsetParent; + } + // IE offsetParent does not include the top-level + if (isIE && obj.parentElement){ + curtop += obj.offsetTop - obj.scrollTop; + } + } else if (obj.y) { + curtop += obj.y; + } + return curtop; + } +} + +// Our class will live in the yourlabs global namespace. +if (window.yourlabs === undefined) window.yourlabs = {}; + +// Fix #25: Prevent accidental inclusion of autocomplete_light/static.html +if (window.yourlabs.Autocomplete !== undefined) + console.log('WARNING ! You are loading autocomplete.js **again**.'); + +yourlabs.getInternetExplorerVersion = function() +// Returns the version of Internet Explorer or a -1 +// (indicating the use of another browser). +{ + var rv = -1; // Return value assumes failure. + if (navigator.appName === 'Microsoft Internet Explorer') + { + var ua = navigator.userAgent; + var re = new RegExp('MSIE ([0-9]{1,}[.0-9]{0,})'); + if (re.exec(ua) !== null) + rv = parseFloat( RegExp.$1 ); + } + return rv; +}; + +$.fn.yourlabsRegistry = function(key, value) { + var ie = yourlabs.getInternetExplorerVersion(); + + if (ie === -1 || ie > 8) { + // If not on IE8 and friends, that's all we need to do. + return value === undefined ? this.data(key) : this.data(key, value); + } + + if ($.fn.yourlabsRegistry.data === undefined) { + $.fn.yourlabsRegistry.data = {}; + } + + if ($.fn.yourlabsRegistry.guid === undefined) { + $.fn.yourlabsRegistry.guid = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( + /[xy]/g, + function(c) { + var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); + return v.toString(16); + } + ); + }; + } + + var attributeName = 'data-yourlabs-' + key + '-registry-id'; + var id = this.attr(attributeName); + + if (id === undefined) { + id = $.fn.yourlabsRegistry.guid(); + this.attr(attributeName, id); + } + + if (value !== undefined) { + $.fn.yourlabsRegistry.data[id] = value; + } + + return $.fn.yourlabsRegistry.data[id]; +}; + +/* +The autocomplete class constructor: + +- takes a takes a text input element as argument, +- sets attributes and methods for this instance. + +The reason you want to learn about all this script is that you will then be +able to override any variable or function in it on a case-per-case basis. +However, overriding is the job of the jQuery plugin so the procedure is +described there. +*/ +yourlabs.Autocomplete = function (input) { + /* + The text input element that should have an autocomplete. + */ + this.input = input; + + // The value of the input. It is kept as an attribute for optimisation + // purposes. + this.value = null; + + /* + It is possible to wait until a certain number of characters have been + typed in the input before making a request to the server, to limit the + number of requests. + + However, you may want the autocomplete to behave like a select. If you + want that a simple click shows the autocomplete, set this to 0. + */ + this.minimumCharacters = 2; + + /* + In a perfect world, we would hide the autocomplete when the input looses + focus (on blur). But in reality, if the user clicks on a choice, the + input looses focus, and that would hide the autocomplete, *before* we + can intercept the click on the choice. + + When the input looses focus, wait for this number of milliseconds before + hiding the autocomplete. + */ + this.hideAfter = 200; + + /* + Normally the autocomplete box aligns with the left edge of the input. To + align with the right edge of the input instead, change this variable. + */ + this.alignRight = false; + + /* + The server should have a URL that takes the input value, and responds + with the list of choices as HTML. In most cases, an absolute URL is + better. + */ + this.url = false; + + /* + Although this script will make sure that it doesn't have multiple ajax + requests at the time, it also supports debouncing. + + Set a number of milliseconds here, it is the number of milliseconds that it + will wait before querying the server. The higher it is, the less it will + spam the server but the more the user will wait. + */ + this.xhrWait = 200; + + /* + As the server responds with plain HTML, we need a selector to find the + choices that it contains. + + For example, if the URL returns an HTML body where every result is in a + div of class "choice", then this should be set to '.choice'. + */ + this.choiceSelector = '.choice'; + + /* + When the user hovers a choice, it is nice to hilight it, for + example by changing it's background color. That's the job of CSS code. + + However, the CSS can not depend on the :hover because the user can + hilight choices with the keyboard by pressing the up and down + keys. + + To counter that problem, we specify a particular class that will be set + on a choice when it's 'hilighted', and unset when it's + 'dehilighted'. + */ + this.hilightClass = 'hilight'; + + /* + You can set this variable to true if you want the first choice + to be hilighted by default. + */ + this.autoHilightFirst = false; + + + /* + You can set this variable to false in order to allow opening of results + in new tabs or windows + */ + this.bindMouseDown = true; + + /* + The value of the input is passed to the server via a GET variable. This + is the name of the variable. + */ + this.queryVariable = 'q'; + + /* + This dict will also be passed to the server as GET variables. + + If this autocomplete depends on another user defined value, then the + other user defined value should be set in this dict. + + Consider a country select and a city autocomplete. The city autocomplete + should only fetch city choices that are in the selected country. To + achieve this, update the data with the value of the country select: + + $('select[name=country]').change(function() { + $('city[name=country]').yourlabsAutocomplete().data = { + country: $(this).val(), + } + }); + */ + this.data = {}; + + /* + To avoid several requests to be pending at the same time, the current + request is aborted before a new one is sent. This attribute will hold the + current XMLHttpRequest. + */ + this.xhr = false; + + /* + fetch() keeps a copy of the data sent to the server in this attribute. This + avoids double fetching the same autocomplete. + */ + this.lastData = {}; + + // The autocomplete box HTML. + this.box = $(''); + + /* + We'll append the box to the container and calculate an absolute position + every time the autocomplete is shown in the fixPosition method. + + By default, this traverses this.input's parents to find the nearest parent + with an 'absolute' or 'fixed' position. This prevents scrolling issues. If + we can't find a parent that would be correct to append to, default to + . + */ + this.container = this.input.parents().filter(function() { + var position = $(this).css('position'); + return position === 'absolute' || position === 'fixed'; + }).first(); + if (!this.container.length) this.container = $('body'); +}; + +/* +Rather than directly setting up the autocomplete (DOM events etc ...) in +the constructor, setup is done in this method. This allows to: + +- instanciate an Autocomplete, +- override attribute/methods of the instance, +- and *then* setup the instance. + */ +yourlabs.Autocomplete.prototype.initialize = function() { + var ie = yourlabs.getInternetExplorerVersion(); + + this.input + .on('blur.autocomplete', $.proxy(this.inputBlur, this)) + .on('focus.autocomplete', $.proxy(this.inputClick, this)) + .on('keydown.autocomplete', $.proxy(this.inputKeyup, this)); + + $(window).on('resize', $.proxy(function() { + if (this.box.is(':visible')) this.fixPosition(); + }, this)); + + // Currently, our positioning doesn't work well in Firefox. Since it's not + // the first option on mobile phones and small devices, we'll hide the bug + // until this is fixed. + if (/Firefox/i.test(navigator.userAgent)) + $(window).on('scroll', $.proxy(this.hide, this)); + + if (ie === -1 || ie > 9) { + this.input.on('input.autocomplete', $.proxy(this.refresh, this)); + } + else + { + var events = [ + 'keyup.autocomplete', + 'keypress.autocomplete', + 'cut.autocomplete', + 'paste.autocomplete' + ] + + this.input.on(events.join(' '), function($e) { + $.proxy(this.inputKeyup, this); + }) + } + + /* + Bind mouse events to fire signals. Because the same signals will be + sent if the user uses keyboard to work with the autocomplete. + */ + this.box + .on('mouseenter', this.choiceSelector, $.proxy(this.boxMouseenter, this)) + .on('mouseleave', this.choiceSelector, $.proxy(this.boxMouseleave, this)); + if(this.bindMouseDown){ + this.box + .on('mousedown', this.choiceSelector, $.proxy(this.boxClick, this)); + } + + /* + Initially - empty data queried + */ + this.data[this.queryVariable] = ''; +}; + +// Unbind callbacks on input. +yourlabs.Autocomplete.prototype.destroy = function(input) { + input + .unbind('blur.autocomplete') + .unbind('focus.autocomplete') + .unbind('input.autocomplete') + .unbind('keydown.autocomplete') + .unbind('keypress.autocomplete') + .unbind('keyup.autocomplete') +}; + +yourlabs.Autocomplete.prototype.inputBlur = function(e) { + window.setTimeout($.proxy(this.hide, this), this.hideAfter); +}; + +yourlabs.Autocomplete.prototype.inputClick = function(e) { + if (this.value === null) + this.value = this.getQuery(); + + if (this.value.length >= this.minimumCharacters) + this.show(); +}; + +// When mouse enters the box: +yourlabs.Autocomplete.prototype.boxMouseenter = function(e) { + // ... the first thing we want is to send the dehilight signal + // for any hilighted choice ... + var current = this.box.find('.' + this.hilightClass); + + this.input.trigger('dehilightChoice', + [current, this]); + + // ... and then sent the hilight signal for the choice. + this.input.trigger('hilightChoice', + [$(e.currentTarget), this]); +}; + +// When mouse leaves the box: +yourlabs.Autocomplete.prototype.boxMouseleave = function(e) { + // Send dehilightChoice when the mouse leaves a choice. + this.input.trigger('dehilightChoice', + [this.box.find('.' + this.hilightClass), this]); +}; + +// When mouse clicks in the box: +yourlabs.Autocomplete.prototype.boxClick = function(e) { + var current = this.box.find('.' + this.hilightClass); + + this.input.trigger('selectChoice', [current, this]); +}; + +// Return the value to pass to this.queryVariable. +yourlabs.Autocomplete.prototype.getQuery = function() { + // Return the input's value by default. + return this.input.val(); +}; + +yourlabs.Autocomplete.prototype.inputKeyup = function(e) { + if (!this.input.is(':visible')) + // Don't handle keypresses on hidden inputs (ie. with limited choices) + return; + + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + case 16: // shift + case 17: // ctrl + case 18: // alt + this.move(e); + break; + + case 9: // tab + case 13: // enter + if (!this.box.is(':visible')) return; + + var choice = this.box.find('.' + this.hilightClass); + + if (!choice.length) { + // Don't get in the way, let the browser submit form or focus + // on next element. + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.input.trigger('selectChoice', [choice, this]); + break; + + case 27: // escape + if (!this.box.is(':visible')) return; + this.hide(); + break; + + default: + this.refresh(); + } +}; + +// This function is in charge of ensuring that a relevant autocomplete is +// shown. +yourlabs.Autocomplete.prototype.show = function(html) { + // First recalculate the absolute position since the autocomplete may + // have changed position. + this.fixPosition(); + + // Is autocomplete empty ? + var empty = $.trim(this.box.find(this.choiceSelector)).length === 0; + + // If the inner container is empty or data has changed and there is no + // current pending request, rely on fetch(), which should show the + // autocomplete as soon as it's done fetching. + if ((this.hasChanged() || empty) && !this.xhr) { + this.fetch(); + return; + } + + // And actually, fetch() will call show() with the response + // body as argument. + if (html !== undefined) { + this.box.html(html); + this.fixPosition(); + } + + // Don't display empty boxes. + if (this.box.is(':empty')) { + if (this.box.is(':visible')) { + this.hide(); + } + return; + } + + var current = this.box.find('.' + this.hilightClass); + var first = this.box.find(this.choiceSelector + ':first'); + if (first && !current.length && this.autoHilightFirst) { + first.addClass(this.hilightClass); + } + + // Show the inner and outer container only if necessary. + if (!this.box.is(':visible')) { + this.box.css('display', 'block'); + this.fixPosition(); + } +}; + +// This function is in charge of the opposite. +yourlabs.Autocomplete.prototype.hide = function() { + this.box.hide(); +}; + +// This function is in charge of hilighting the right result from keyboard +// navigation. +yourlabs.Autocomplete.prototype.move = function(e) { + if (this.value === null) + this.value = this.getQuery(); + + // If the autocomplete should not be displayed then return. + if (this.value.length < this.minimumCharacters) return true; + + // The current choice if any. + var current = this.box.find('.' + this.hilightClass); + + // Prevent default browser behaviours on TAB and RETURN if a choice is + // hilighted. + if ($.inArray(e.keyCode, [9,13]) > -1 && current.length) { + e.preventDefault(); + } + + // If not KEY_UP or KEY_DOWN, then return. + // NOTE: with Webkit, both keyCode and charCode are set to 38/40 for &/(. + // charCode is 0 for arrow keys. + // Ref: http://stackoverflow.com/a/12046935/15690 + var way; + if (e.keyCode === 38 && !e.charCode) way = 'up'; + else if (e.keyCode === 40 && !e.charCode) way = 'down'; + else return; + + // The first and last choices. If the user presses down on the last + // choice, then the first one will be hilighted. + var first = this.box.find(this.choiceSelector + ':first'); + var last = this.box.find(this.choiceSelector + ':last'); + + // The choice that should be hilighted after the move. + var target; + + // The autocomplete must be shown so that the user sees what choice + // he is hilighting. + this.show(); + + // If a choice is currently hilighted: + if (current.length) { + if (way === 'up') { + // The target choice becomes the first previous choice. + target = current.prevAll(this.choiceSelector + ':first'); + + // If none, then the last choice becomes the target. + if (!target.length) target = last; + } else { + // The target choice becomes the first next** choice. + target = current.nextAll(this.choiceSelector + ':first'); + + // If none, then the first choice becomes the target. + if (!target.length) target = first; + } + + // Trigger dehilightChoice on the currently hilighted choice. + this.input.trigger('dehilightChoice', + [current, this]); + } else { + target = way === 'up' ? last : first; + } + + // Avoid moving the cursor in the input. + e.preventDefault(); + + // Trigger hilightChoice on the target choice. + this.input.trigger('hilightChoice', + [target, this]); +}; + +/* +Calculate and set the outer container's absolute positionning. We're copying +the system from Django admin's JS widgets like the date calendar, which means: + +- the autocomplete box is an element appended to this.co, +- +*/ +yourlabs.Autocomplete.prototype.fixPosition = function() { + var el = this.input.get(0); + + var zIndex = this.input.parents().filter(function() { + return $(this).css('z-index') !== 'auto' && $(this).css('z-index') !== '0'; + }).first().css('z-index'); + + var absolute_parent = this.input.parents().filter(function(){ + return $(this).css('position') === 'absolute'; + }).get(0); + + var top = (findPosY(el) + this.input.outerHeight()) + 'px'; + var left = findPosX(el) + 'px'; + + if(absolute_parent !== undefined){ + var parentTop = findPosY(absolute_parent); + var parentLeft = findPosX(absolute_parent); + var inputBottom = findPosY(el) + this.input.outerHeight(); + var inputLeft = findPosX(el); + top = (inputBottom - parentTop) + 'px'; + left = (inputLeft - parentLeft) + 'px'; + } + + if (this.alignRight) { + left = (findPosX(el) + el.scrollLeft - (this.box.outerWidth() - this.input.outerWidth())) + 'px'; + } + + this.box.appendTo(this.container).css({ + position: 'absolute', + minWidth: parseInt(this.input.outerWidth()), + top: top, + left: left, + zIndex: zIndex + }); +}; + +// Proxy fetch(), with some sanity checks. +yourlabs.Autocomplete.prototype.refresh = function() { + // Set the new current value. + this.value = this.getQuery(); + + // If the input doesn't contain enought characters then abort, else fetch. + if (this.value.length < this.minimumCharacters) + this.hide(); + else + this.fetch(); +}; + +// Return true if the data for this query has changed from last query. +yourlabs.Autocomplete.prototype.hasChanged = function() { + for(var key in this.data) { + if (!(key in this.lastData) || this.data[key] !== this.lastData[key]) { + return true; + } + } + return false; +}; + +// Manage requests to this.url. +yourlabs.Autocomplete.prototype.fetch = function() { + // Add the current value to the data dict. + this.data[this.queryVariable] = this.value; + + // Ensure that this request is different from the previous one + if (!this.hasChanged()) { + // Else show the same box again. + this.show(); + return; + } + + this.lastData = {}; + for(var key in this.data) { + this.lastData[key] = this.data[key]; + } + + // Abort any unsent requests. + if (this.xhr && this.xhr.readyState === 0) this.xhr.abort(); + + // Abort any request that we planned to make. + if (this.timeoutId) clearTimeout(this.timeoutId); + + // Make an asynchronous GET request to this.url in this.xhrWait ms + this.timeoutId = setTimeout($.proxy(this.makeXhr, this), this.xhrWait); +}; + +// Wrapped ajax call to use with setTimeout in fetch(). +yourlabs.Autocomplete.prototype.makeXhr = function() { + this.input.addClass('xhr-pending'); + + this.xhr = $.ajax(this.url, { + type: 'GET', + data: this.data, + complete: $.proxy(this.fetchComplete, this) + }); +}; + +// Callback for the ajax response. +yourlabs.Autocomplete.prototype.fetchComplete = function(jqXHR, textStatus) { + if (this.xhr === jqXHR) { + // Current request finished. + this.xhr = false; + } else { + // Ignore response from earlier request. + return; + } + + // Current request done, nothing else pending. + this.input.removeClass('xhr-pending'); + + if (textStatus === 'abort') return; + this.show(jqXHR.responseText); +}; + +/* +The jQuery plugin that manages Autocomplete instances across the various +inputs. It is named 'yourlabsAutocomplete' rather than just 'autocomplete' +to live happily with other plugins that may define an autocomplete() jQuery +plugin. + +It takes an array as argument, the array may contain any attribute or +function that should override the Autocomplete builtin. For example: + + $('input#your-autocomplete').yourlabsAutocomplete({ + url: '/some/url/', + hide: function() { + this.outerContainer + }, + }) + +Also, it implements a simple identity map, which means that: + + // First call for an input instanciates the Autocomplete instance + $('input#your-autocomplete').yourlabsAutocomplete({ + url: '/some/url/', + }); + + // Other calls return the previously created Autocomplete instance + $('input#your-autocomplete').yourlabsAutocomplete().data = { + newData: $('#foo').val(), + } + +To destroy an autocomplete, call yourlabsAutocomplete('destroy'). +*/ +$.fn.yourlabsAutocomplete = function(overrides) { + if (this.length < 1) { + // avoid crashing when called on a non existing element + return; + } + + overrides = overrides ? overrides : {}; + var autocomplete = this.yourlabsRegistry('autocomplete'); + + if (overrides === 'destroy') { + if (autocomplete) { + autocomplete.destroy(this); + this.removeData('autocomplete'); + } + return; + } + + // Disable the browser's autocomplete features on that input. + this.attr('autocomplete', 'off'); + + // If no Autocomplete instance is defined for this id, make one. + if (autocomplete === undefined) { + // Instanciate Autocomplete. + autocomplete = new yourlabs.Autocomplete(this); + + // Extend the instance with data-autocomplete-* overrides + for (var key in this.data()) { + if (!key) continue; + if (key.substr(0, 12) !== 'autocomplete' || key === 'autocomplete') + continue; + var newKey = key.replace('autocomplete', ''); + newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1); + autocomplete[newKey] = this.data(key); + } + + // Extend the instance with overrides. + autocomplete = $.extend(autocomplete, overrides); + + if (!autocomplete.url) { + alert('Autocomplete needs a url !'); + return; + } + + this.yourlabsRegistry('autocomplete', autocomplete); + + // All set, call initialize(). + autocomplete.initialize(); + } + + // Return the Autocomplete instance for this id from the registry. + return autocomplete; +}; + +// Binding some default behaviors. +$(document).ready(function() { + function removeHilightClass(e, choice, autocomplete) { + choice.removeClass(autocomplete.hilightClass); + } + $(document).bind('hilightChoice', function(e, choice, autocomplete) { + choice.addClass(autocomplete.hilightClass); + }); + $(document).bind('dehilightChoice', removeHilightClass); + $(document).bind('selectChoice', removeHilightClass); + $(document).bind('selectChoice', function(e, choice, autocomplete) { + autocomplete.hide(); + }); +}); + +$(document).ready(function() { + /* Credit: django.contrib.admin (BSD) */ + + var showAddAnotherPopup = function(triggeringLink) { + var name = triggeringLink.attr( 'id' ).replace(/^add_/, ''); + name = id_to_windowname(name); + href = triggeringLink.attr( 'href' ); + + if (href.indexOf('?') === -1) { + href += '?'; + } + + href += '&winName=' + name; + + var height = 500; + var width = 800; + var left = (screen.width/2)-(width/2); + var top = (screen.height/2)-(height/2); + var win = window.open(href, name, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=no, width='+width+', height='+height+', top='+top+', left='+left) + + function removeOverlay() { + if (win.closed) { + $('#yourlabs_overlay').remove(); + } else { + setTimeout(removeOverlay, 500); + } + } + + $('body').append('
    = 0; start--) { + if (value[start] === ',') { + break; + } + } + start = start < 0 ? 0 : start; + + // find end of word + for(var end=position; end <= value.length - 1; end++) { + if (value[end] === ',') { + break; + } + } + + while(value[start] === ',' || value[start] === ' ') start++; + while(value[end] === ',' || value[end] === ' ') end--; + + return [start, end + 1]; +} + +// TextWidget ties an input with an autocomplete. +yourlabs.TextWidget = function(input) { + this.input = input; + this.autocompleteOptions = { + getQuery: function() { + return this.input.getCursorWord(); + } + } +} + +// The widget is in charge of managing its Autocomplete. +yourlabs.TextWidget.prototype.initializeAutocomplete = function() { + this.autocomplete = this.input.yourlabsAutocomplete( + this.autocompleteOptions); + + // Add a class to ease css selection of autocompletes for widgets + this.autocomplete.box.addClass( + 'autocomplete-light-text-widget'); +}; + +// Bind Autocomplete.selectChoice signal to TextWidget.selectChoice() +yourlabs.TextWidget.prototype.bindSelectChoice = function() { + this.input.bind('selectChoice', function(e, choice) { + if (!choice.length) + return // placeholder: create choice here + + $(this).yourlabsTextWidget().selectChoice(choice); + }); +}; + +// Called when a choice is selected from the Autocomplete. +yourlabs.TextWidget.prototype.selectChoice = function(choice) { + var inputValue = this.input.val(); + var choiceValue = this.getValue(choice); + var positions = this.input.getCursorWordPositions(); + + var newValue = inputValue.substring(0, positions[0]); + newValue += choiceValue; + newValue += inputValue.substring(positions[1]); + + this.input.val(newValue); + this.input.focus(); +} + +// Return the value of an HTML choice, used to fill the input. +yourlabs.TextWidget.prototype.getValue = function(choice) { + return $.trim(choice.text()); +} + +// Initialize the widget. +yourlabs.TextWidget.prototype.initialize = function() { + this.initializeAutocomplete(); + this.bindSelectChoice(); +} + +// Destroy the widget. Takes a widget element because a cloned widget element +// will be dirty, ie. have wrong .input and .widget properties. +yourlabs.TextWidget.prototype.destroy = function(input) { + input + .unbind('selectChoice') + .yourlabsAutocomplete('destroy'); +} + +// TextWidget factory, registry and destroyer, as jQuery extension. +$.fn.yourlabsTextWidget = function(overrides) { + var widget; + overrides = overrides ? overrides : {}; + + if (overrides === 'destroy') { + widget = this.data('widget'); + if (widget) { + widget.destroy(this); + this.removeData('widget'); + } + return + } + + if (this.data('widget') === undefined) { + // Instanciate the widget + widget = new yourlabs.TextWidget(this); + + // Pares data-* + var data = this.data(); + var dataOverrides = { + autocompleteOptions: { + // workaround a display bug + minimumCharacters: 0, + getQuery: function() { + // Override getQuery since we need the autocomplete to filter + // choices based on the word the cursor is on, rather than the full + // input value. + return this.input.getCursorWord(); + } + } + }; + for (var key in data) { + if (!key) continue; + + if (key.substr(0, 12) === 'autocomplete') { + if (key === 'autocomplete') continue; + + var newKey = key.replace('autocomplete', ''); + newKey = newKey.replace(newKey[0], newKey[0].toLowerCase()) + dataOverrides.autocompleteOptions[newKey] = data[key]; + } else { + dataOverrides[key] = data[key]; + } + } + + // Allow attribute overrides + widget = $.extend(widget, dataOverrides); + + // Allow javascript object overrides + widget = $.extend(widget, overrides); + + this.data('widget', widget); + + // Setup for usage + widget.initialize(); + + // Widget is ready + widget.input.attr('data-widget-ready', 1); + widget.input.trigger('widget-ready'); + } + + return this.data('widget'); +} + +$(document).ready(function() { + $('body').on('initialize', 'input[data-widget-bootstrap=text]', function() { + /* + Only setup autocompletes on inputs which have + data-widget-bootstrap=text, if you want to initialize some + autocompletes with custom code, then set + data-widget-boostrap=yourbootstrap or something like that. + */ + $(this).yourlabsTextWidget(); + }); + + // Solid initialization, usage:: + // + // $(document).bind('yourlabsTextWidgetReady', function() { + // $('body').on('initialize', 'input[data-widget-bootstrap=text]', function() { + // $(this).yourlabsTextWidget({ + // yourCustomArgs: // ... + // }) + // }); + // }); + $(document).trigger('yourlabsTextWidgetReady'); + + $('.autocomplete-light-text-widget:not([id*="__prefix__"])').each(function() { + $(this).trigger('initialize'); + }); + + $(document).bind('DOMNodeInserted', function(e) { + var widget = $(e.target).find('.autocomplete-light-text-widget'); + + if (!widget.length) { + widget = $(e.target).is('.autocomplete-light-text-widget') ? $(e.target) : false; + + if (!widget) { + return; + } + } + + // Ignore inserted autocomplete box elements. + if (widget.is('.yourlabs-autocomplete')) { + return; + } + + // Ensure that the newly added widget is clean, in case it was cloned. + widget.yourlabsWidget('destroy'); + widget.find('input').yourlabsAutocomplete('destroy'); + + widget.trigger('initialize'); + }); +}) + +/* +Widget complements Autocomplete by enabling autocompletes to be used as +value holders. It looks very much like Autocomplete in its design. Thus, it +is recommended to read the source of Autocomplete first. + +Widget behaves like the autocomplete in facebook profile page, which all +users should be able to use. + +Behind the scenes, Widget maintains a normal hidden select which makes it +simple to play with on the server side like on the client side. If a value +is added and selected in the select element, then it is added to the deck, +and vice-versa. + +It needs some elements, and established vocabulary: + +- ".autocomplete-light-widget" element wraps all the HTML necessary for the + widget, +- ".deck" contains the list of selected choice(s) as HTML, +- "input" should be the text input that has the Autocomplete, +- "select" a (optionnaly multiple) select +- ".remove" a (preferabely hidden) element that contains a value removal + indicator, like an "X" sign or a trashcan icon, it is used to prefix every + children of the deck +- ".choice-template" a (preferabely hidden) element that contains the template + for choices which are added directly in the select, as they should be + copied in the deck, + +To avoid complexity, this script relies on extra HTML attributes, and +particularely one called 'data-value'. Learn more about data attributes: +http://dev.w3.org/html5/spec/global-attributes.html#embedding-custom-non-visible-data-with-the-data-attributes + +When a choice is selected from the Autocomplete, its element is cloned +and appended to the deck - "deck" contains "choices". It is important that the +choice elements of the autocomplete all contain a data-value attribute. +The value of data-value is used to fill the selected options in the hidden +select field. + +If choices may not all have a data-value attribute, then you can +override Widget.getValue() to implement your own logic. +*/ + +// Our class will live in the yourlabs global namespace. +if (window.yourlabs === undefined) window.yourlabs = {}; + +/* +Instanciate a Widget. +*/ +yourlabs.Widget = function(widget) { + // These attributes where described above. + this.widget = widget; + this.input = this.widget.find('input[data-autocomplete-url]'); + this.select = this.widget.find('select'); + this.deck = this.widget.find('.deck'); + this.choiceTemplate = this.widget.find('.choice-template .choice'); + + // The number of choices that the user may select with this widget. Set 0 + // for no limit. In the case of a foreign key you want to set it to 1. + this.maximumValues = 0; + + // Clear input when choice made? 1 for yes, 0 for no + this.clearInputOnSelectChoice = '1'; +} + +// When a choice is selected from the autocomplete of this widget, +// getValue() is called to add and select the option in the select. +yourlabs.Widget.prototype.getValue = function(choice) { + return choice.attr('data-value'); +}; + +// The widget is in charge of managing its Autocomplete. +yourlabs.Widget.prototype.initializeAutocomplete = function() { + this.autocomplete = this.input.yourlabsAutocomplete() + + // Add a class to ease css selection of autocompletes for widgets + this.autocomplete.box.addClass('autocomplete-light-widget'); +}; + +// Bind Autocomplete.selectChoice signal to Widget.selectChoice() +yourlabs.Widget.prototype.bindSelectChoice = function() { + this.input.bind('selectChoice', function(e, choice) { + if (!choice.length) + return // placeholder: create choice here + + var widget = $(this).parents('.autocomplete-light-widget' + ).yourlabsWidget(); + + widget.selectChoice(choice); + + widget.widget.trigger('widgetSelectChoice', [choice, widget]); + }); +}; + +// Called when a choice is selected from the Autocomplete. +yourlabs.Widget.prototype.selectChoice = function(choice) { + // Get the value for this choice. + var value = this.getValue(choice); + + if (!value) { + if (window.console) console.log('yourlabs.Widget.getValue failed'); + return; + } + + this.freeDeck(); + this.addToDeck(choice, value); + this.addToSelect(choice, value); + + var index = $(':input:visible').index(this.input); + this.resetDisplay(); + + if (this.clearInputOnSelectChoice === '1') { + this.input.val(''); + this.autocomplete.value = ''; + } + + if (this.input.is(':visible')) { + this.input.focus(); + } else { + var next = $(':input:visible:eq('+ index +')'); + next.focus(); + } + + if (! this.select.is('[multiple]')) { + this.input.prop('disabled', true); + } +} + +// Unselect a value if the maximum number of selected values has been +// reached. +yourlabs.Widget.prototype.freeDeck = function() { + var slots = this.maximumValues - this.deck.children().length; + + if (this.maximumValues && slots < 1) { + // We'll remove the first choice which is supposed to be the oldest + var choice = $(this.deck.children()[0]); + + this.deselectChoice(choice); + } +} + +// Empty the search input and hide it if maximumValues has been reached. +yourlabs.Widget.prototype.resetDisplay = function() { + var selected = this.select.find('option:selected').length; + + if (this.maximumValues && selected === this.maximumValues) { + this.input.hide(); + } else { + this.input.show(); + } + + this.deck.show(); + + // Also fix the position if the autocomplete is shown. + if (this.autocomplete.box.is(':visible')) this.autocomplete.fixPosition(); +} + +yourlabs.Widget.prototype.deckChoiceHtml = function(choice, value) { + var deckChoice = choice.clone(); + + this.addRemove(deckChoice); + + return deckChoice; +} + +yourlabs.Widget.prototype.optionChoice = function(option) { + var optionChoice = this.choiceTemplate.clone(); + + var target = optionChoice.find('.append-option-html'); + + if (target.length) { + target.append(option.html()); + } else { + optionChoice.html(option.html()); + } + + return optionChoice; +} + +yourlabs.Widget.prototype.addRemove = function(choices) { + var removeTemplate = this.widget.find('.remove:last') + .clone().css('display', 'inline-block'); + + var target = choices.find('.prepend-remove'); + + if (target.length) { + target.prepend(removeTemplate); + } else { + // Add the remove icon to each choice + choices.prepend(removeTemplate); + } +} + +// Add a selected choice of a given value to the deck. +yourlabs.Widget.prototype.addToDeck = function(choice, value) { + var existing_choice = this.deck.find('[data-value="'+value+'"]'); + + // Avoid duplicating choices in the deck. + if (!existing_choice.length) { + var deckChoice = this.deckChoiceHtml(choice); + + // In case getValue() actually **created** the value, for example + // with a post request. + deckChoice.attr('data-value', value); + + this.deck.append(deckChoice); + } +} + +// Add a selected choice of a given value to the deck. +yourlabs.Widget.prototype.addToSelect = function(choice, value) { + var option = this.select.find('option[value="'+value+'"]'); + + if (! option.length) { + this.select.append( + ''); + option = this.select.find('option[value="'+value+'"]'); + } + + option.attr('selected', 'selected'); + + this.select.trigger('change'); + this.updateAutocompleteExclude(); +} + +// Called when the user clicks .remove in a deck choice. +yourlabs.Widget.prototype.deselectChoice = function(choice) { + var value = this.getValue(choice); + + this.select.find('option[value="'+value+'"]').remove(); + this.select.trigger('change'); + + choice.remove(); + + if (this.deck.children().length === 0) { + this.deck.hide(); + } + + this.updateAutocompleteExclude(); + this.resetDisplay(); + + this.input.prop('disabled', false); + + this.widget.trigger('widgetDeselectChoice', [choice, this]); +}; + +yourlabs.Widget.prototype.updateAutocompleteExclude = function() { + var widget = this; + var choices = this.deck.find(this.autocomplete.choiceSelector); + + this.autocomplete.data.exclude = $.map(choices, function(choice) { + return widget.getValue($(choice)); + }); +} + +yourlabs.Widget.prototype.initialize = function() { + this.initializeAutocomplete(); + + // Working around firefox tempering form values after reload + var widget = this; + this.deck.find(this.autocomplete.choiceSelector).each(function() { + var value = widget.getValue($(this)); + var option = widget.select.find('option[value="'+value+'"]'); + if (!option.prop('selected')) option.prop('selected', true); + }); + + var choices = this.deck.find( + this.input.yourlabsAutocomplete().choiceSelector); + + this.addRemove(choices); + this.resetDisplay(); + + if (widget.select.val() && ! this.select.is('[multiple]')) { + this.input.prop('disabled', true); + } + + this.bindSelectChoice(); +} + +// Destroy the widget. Takes a widget element because a cloned widget element +// will be dirty, ie. have wrong .input and .widget properties. +yourlabs.Widget.prototype.destroy = function(widget) { + widget.find('input') + .unbind('selectChoice') + .yourlabsAutocomplete('destroy'); +} + +// Get or create or destroy a widget instance. +// +// On first call, yourlabsWidget() will instanciate a widget applying all +// passed overrides. +// +// On later calls, yourlabsWidget() will return the previously created widget +// instance, which is stored in widget.data('widget'). +// +// Calling yourlabsWidget('destroy') will destroy the widget. Useful if the +// element was blindly cloned with .clone(true) for example. +$.fn.yourlabsWidget = function(overrides) { + overrides = overrides ? overrides : {}; + + var widget = this.yourlabsRegistry('widget'); + + if (overrides === 'destroy') { + if (widget) { + widget.destroy(this); + this.removeData('widget'); + } + return + } + + if (widget === undefined) { + // Instanciate the widget + widget = new yourlabs.Widget(this); + + // Extend the instance with data-widget-* overrides + for (var key in this.data()) { + if (!key) continue; + if (key.substr(0, 6) !== 'widget' || key === 'widget') continue; + var newKey = key.replace('widget', ''); + newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1); + widget[newKey] = this.data(key); + } + + // Allow javascript object overrides + widget = $.extend(widget, overrides); + + $(this).yourlabsRegistry('widget', widget); + + // Setup for usage + widget.initialize(); + + // Widget is ready + widget.widget.attr('data-widget-ready', 1); + widget.widget.trigger('widget-ready'); + } + + return widget; +} + +$(document).ready(function() { + $('body').on('initialize', '.autocomplete-light-widget[data-widget-bootstrap=normal]', function() { + /* + Only setup widgets which have data-widget-bootstrap=normal, if you want to + initialize some Widgets with custom code, then set + data-widget-boostrap=yourbootstrap or something like that. + */ + $(this).yourlabsWidget(); + }); + + // Call Widget.deselectChoice when .remove is clicked + $('body').on('click', '.autocomplete-light-widget .deck .remove', function() { + var widget = $(this).parents('.autocomplete-light-widget' + ).yourlabsWidget(); + + var selector = widget.input.yourlabsAutocomplete().choiceSelector; + var choice = $(this).parents(selector); + + widget.deselectChoice(choice); + }); + + // Solid initialization, usage: + // + // + // $(document).bind('yourlabsWidgetReady', function() { + // $('.your.autocomplete-light-widget').on('initialize', function() { + // $(this).yourlabsWidget({ + // yourCustomArgs: // ... + // }) + // }); + // }); + $(document).trigger('yourlabsWidgetReady'); + + $('.autocomplete-light-widget:not([id*="__prefix__"])').each(function() { + $(this).trigger('initialize'); + }); + + $(document).bind('DOMNodeInserted', function(e) { + /* + Support values added directly in the select via js (ie. choices created in + modal or popup). + + For this, we listen to DOMNodeInserted and intercept insert of ') + */ + var widget; + + if ($(e.target).is('option')) { // added an option ? + widget = $(e.target).parents('.autocomplete-light-widget'); + + if (!widget.length) { + return; + } + + widget = widget.yourlabsWidget(); + var option = $(e.target); + var value = option.attr('value'); + var choice = widget.deck.find('[data-value="'+value+'"]'); + + if (!choice.length) { + var deckChoice = widget.optionChoice(option); + + deckChoice.attr('data-value', value); + + widget.selectChoice(deckChoice); + } + } else { // added a widget ? + var notReady = '.autocomplete-light-widget:not([data-widget-ready])' + widget = $(e.target).find(notReady); + + if (!widget.length) { + return; + } + + // Ignore inserted autocomplete box elements. + if (widget.is('.yourlabs-autocomplete')) { + return; + } + + // Ensure that the newly added widget is clean, in case it was + // cloned with data. + widget.yourlabsWidget('destroy'); + widget.find('input').yourlabsAutocomplete('destroy'); + + // added a widget: initialize the widget. + widget.trigger('initialize'); + } + }); + + var ie = yourlabs.getInternetExplorerVersion(); + if (ie !== -1 && ie < 9) { + observe = [ + '.autocomplete-light-widget:not([data-yourlabs-skip])', + '.autocomplete-light-widget option:not([data-yourlabs-skip])' + ].join(); + $(observe).attr('data-yourlabs-skip', 1); + + var ieDOMNodeInserted = function() { + // http://msdn.microsoft.com/en-us/library/ms536957 + $(observe).each(function() { + $(document).trigger(jQuery.Event('DOMNodeInserted', {target: $(this)})); + $(this).attr('data-yourlabs-skip', 1); + }); + + setTimeout(ieDOMNodeInserted, 500); + } + setTimeout(ieDOMNodeInserted, 500); + } + +}); diff --git a/shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js b/shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js new file mode 100644 index 00000000..a5bc6774 --- /dev/null +++ b/shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js @@ -0,0 +1,9 @@ +/* + * jquery-autocomplete-light - v3.5.0 + * Dead simple autocompletion and widgets for jQuery + * http://yourlabs.org + * + * Made by James Pic + * Under MIT License + */ +if(void 0===window.isOpera)var isOpera=navigator.userAgent.indexOf("Opera")>=0&&parseFloat(navigator.appVersion);if(void 0===window.isIE)var isIE=document.all&&!isOpera&&parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]);void 0===window.findPosX&&(window.findPosX=function(a){var b=0;if(a.offsetParent){for(;a.offsetParent;)b+=a.offsetLeft-(isOpera?0:a.scrollLeft),a=a.offsetParent;isIE&&a.parentElement&&(b+=a.offsetLeft-a.scrollLeft)}else a.x&&(b+=a.x);return b}),void 0===window.findPosY&&(window.findPosY=function(a){var b=0;if(a.offsetParent){for(;a.offsetParent;)b+=a.offsetTop-(isOpera?0:a.scrollTop),a=a.offsetParent;isIE&&a.parentElement&&(b+=a.offsetTop-a.scrollTop)}else a.y&&(b+=a.y);return b}),void 0===window.yourlabs&&(window.yourlabs={}),void 0!==window.yourlabs.Autocomplete&&console.log("WARNING ! You are loading autocomplete.js **again**."),yourlabs.getInternetExplorerVersion=function(){var a=-1;if("Microsoft Internet Explorer"===navigator.appName){var b=navigator.userAgent;null!==new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})").exec(b)&&(a=parseFloat(RegExp.$1))}return a},$.fn.yourlabsRegistry=function(a,b){var c=yourlabs.getInternetExplorerVersion();if(-1===c||c>8)return void 0===b?this.data(a):this.data(a,b);void 0===$.fn.yourlabsRegistry.data&&($.fn.yourlabsRegistry.data={}),void 0===$.fn.yourlabsRegistry.guid&&($.fn.yourlabsRegistry.guid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0;return("x"===a?b:3&b|8).toString(16)})});var d="data-yourlabs-"+a+"-registry-id",e=this.attr(d);return void 0===e&&(e=$.fn.yourlabsRegistry.guid(),this.attr(d,e)),void 0!==b&&($.fn.yourlabsRegistry.data[e]=b),$.fn.yourlabsRegistry.data[e]},yourlabs.Autocomplete=function(a){this.input=a,this.value=null,this.minimumCharacters=2,this.hideAfter=200,this.alignRight=!1,this.url=!1,this.xhrWait=200,this.choiceSelector=".choice",this.hilightClass="hilight",this.autoHilightFirst=!1,this.bindMouseDown=!0,this.queryVariable="q",this.data={},this.xhr=!1,this.lastData={},this.box=$(''),this.container=this.input.parents().filter(function(){var a=$(this).css("position");return"absolute"===a||"fixed"===a}).first(),this.container.length||(this.container=$("body"))},yourlabs.Autocomplete.prototype.initialize=function(){var a=yourlabs.getInternetExplorerVersion();if(this.input.on("blur.autocomplete",$.proxy(this.inputBlur,this)).on("focus.autocomplete",$.proxy(this.inputClick,this)).on("keydown.autocomplete",$.proxy(this.inputKeyup,this)),$(window).on("resize",$.proxy(function(){this.box.is(":visible")&&this.fixPosition()},this)),/Firefox/i.test(navigator.userAgent)&&$(window).on("scroll",$.proxy(this.hide,this)),-1===a||a>9)this.input.on("input.autocomplete",$.proxy(this.refresh,this));else{var b=["keyup.autocomplete","keypress.autocomplete","cut.autocomplete","paste.autocomplete"];this.input.on(b.join(" "),function(a){$.proxy(this.inputKeyup,this)})}this.box.on("mouseenter",this.choiceSelector,$.proxy(this.boxMouseenter,this)).on("mouseleave",this.choiceSelector,$.proxy(this.boxMouseleave,this)),this.bindMouseDown&&this.box.on("mousedown",this.choiceSelector,$.proxy(this.boxClick,this)),this.data[this.queryVariable]=""},yourlabs.Autocomplete.prototype.destroy=function(a){a.unbind("blur.autocomplete").unbind("focus.autocomplete").unbind("input.autocomplete").unbind("keydown.autocomplete").unbind("keypress.autocomplete").unbind("keyup.autocomplete")},yourlabs.Autocomplete.prototype.inputBlur=function(a){window.setTimeout($.proxy(this.hide,this),this.hideAfter)},yourlabs.Autocomplete.prototype.inputClick=function(a){null===this.value&&(this.value=this.getQuery()),this.value.length>=this.minimumCharacters&&this.show()},yourlabs.Autocomplete.prototype.boxMouseenter=function(a){var b=this.box.find("."+this.hilightClass);this.input.trigger("dehilightChoice",[b,this]),this.input.trigger("hilightChoice",[$(a.currentTarget),this])},yourlabs.Autocomplete.prototype.boxMouseleave=function(a){this.input.trigger("dehilightChoice",[this.box.find("."+this.hilightClass),this])},yourlabs.Autocomplete.prototype.boxClick=function(a){var b=this.box.find("."+this.hilightClass);this.input.trigger("selectChoice",[b,this])},yourlabs.Autocomplete.prototype.getQuery=function(){return this.input.val()},yourlabs.Autocomplete.prototype.inputKeyup=function(a){if(this.input.is(":visible"))switch(a.keyCode){case 40:case 38:case 16:case 17:case 18:this.move(a);break;case 9:case 13:if(!this.box.is(":visible"))return;var b=this.box.find("."+this.hilightClass);if(!b.length)return;a.preventDefault(),a.stopPropagation(),this.input.trigger("selectChoice",[b,this]);break;case 27:if(!this.box.is(":visible"))return;this.hide();break;default:this.refresh()}},yourlabs.Autocomplete.prototype.show=function(a){this.fixPosition();var b=0===$.trim(this.box.find(this.choiceSelector)).length;if((this.hasChanged()||b)&&!this.xhr)return void this.fetch();if(void 0!==a&&(this.box.html(a),this.fixPosition()),this.box.is(":empty"))return void(this.box.is(":visible")&&this.hide());var c=this.box.find("."+this.hilightClass),d=this.box.find(this.choiceSelector+":first");d&&!c.length&&this.autoHilightFirst&&d.addClass(this.hilightClass),this.box.is(":visible")||(this.box.css("display","block"),this.fixPosition())},yourlabs.Autocomplete.prototype.hide=function(){this.box.hide()},yourlabs.Autocomplete.prototype.move=function(a){if(null===this.value&&(this.value=this.getQuery()),this.value.length-1&&b.length&&a.preventDefault();var c;if(38!==a.keyCode||a.charCode){if(40!==a.keyCode||a.charCode)return;c="down"}else c="up";var d,e=this.box.find(this.choiceSelector+":first"),f=this.box.find(this.choiceSelector+":last");this.show(),b.length?("up"===c?(d=b.prevAll(this.choiceSelector+":first"),d.length||(d=f)):(d=b.nextAll(this.choiceSelector+":first"),d.length||(d=e)),this.input.trigger("dehilightChoice",[b,this])):d="up"===c?f:e,a.preventDefault(),this.input.trigger("hilightChoice",[d,this])},yourlabs.Autocomplete.prototype.fixPosition=function(){var a=this.input.get(0),b=this.input.parents().filter(function(){return"auto"!==$(this).css("z-index")&&"0"!==$(this).css("z-index")}).first().css("z-index"),c=this.input.parents().filter(function(){return"absolute"===$(this).css("position")}).get(0),d=findPosY(a)+this.input.outerHeight()+"px",e=findPosX(a)+"px";if(void 0!==c){var f=findPosY(c),g=findPosX(c),h=findPosY(a)+this.input.outerHeight(),i=findPosX(a);d=h-f+"px",e=i-g+"px"}this.alignRight&&(e=findPosX(a)+a.scrollLeft-(this.box.outerWidth()-this.input.outerWidth())+"px"),this.box.appendTo(this.container).css({position:"absolute",minWidth:parseInt(this.input.outerWidth()),top:d,left:e,zIndex:b})},yourlabs.Autocomplete.prototype.refresh=function(){this.value=this.getQuery(),this.value.length=0&&","!==b[c];c--);c=c<0?0:c;for(var d=a;d<=b.length-1&&","!==b[d];d++);for(;","===b[c]||" "===b[c];)c++;for(;","===b[d]||" "===b[d];)d--;return[c,d+1]},yourlabs.TextWidget=function(a){this.input=a,this.autocompleteOptions={getQuery:function(){return this.input.getCursorWord()}}},yourlabs.TextWidget.prototype.initializeAutocomplete=function(){this.autocomplete=this.input.yourlabsAutocomplete(this.autocompleteOptions),this.autocomplete.box.addClass("autocomplete-light-text-widget")},yourlabs.TextWidget.prototype.bindSelectChoice=function(){this.input.bind("selectChoice",function(a,b){b.length&&$(this).yourlabsTextWidget().selectChoice(b)})},yourlabs.TextWidget.prototype.selectChoice=function(a){var b=this.input.val(),c=this.getValue(a),d=this.input.getCursorWordPositions(),e=b.substring(0,d[0]);e+=c,e+=b.substring(d[1]),this.input.val(e),this.input.focus()},yourlabs.TextWidget.prototype.getValue=function(a){return $.trim(a.text())},yourlabs.TextWidget.prototype.initialize=function(){this.initializeAutocomplete(),this.bindSelectChoice()},yourlabs.TextWidget.prototype.destroy=function(a){a.unbind("selectChoice").yourlabsAutocomplete("destroy")},$.fn.yourlabsTextWidget=function(a){var b;if("destroy"===(a=a||{}))return void((b=this.data("widget"))&&(b.destroy(this),this.removeData("widget")));if(void 0===this.data("widget")){b=new yourlabs.TextWidget(this);var c=this.data(),d={autocompleteOptions:{minimumCharacters:0,getQuery:function(){return this.input.getCursorWord()}}};for(var e in c)if(e)if("autocomplete"===e.substr(0,12)){if("autocomplete"===e)continue;var f=e.replace("autocomplete","");f=f.replace(f[0],f[0].toLowerCase()),d.autocompleteOptions[f]=c[e]}else d[e]=c[e];b=$.extend(b,d),b=$.extend(b,a),this.data("widget",b),b.initialize(),b.input.attr("data-widget-ready",1),b.input.trigger("widget-ready")}return this.data("widget")},$(document).ready(function(){$("body").on("initialize","input[data-widget-bootstrap=text]",function(){$(this).yourlabsTextWidget()}),$(document).trigger("yourlabsTextWidgetReady"),$('.autocomplete-light-text-widget:not([id*="__prefix__"])').each(function(){$(this).trigger("initialize")}),$(document).bind("DOMNodeInserted",function(a){var b=$(a.target).find(".autocomplete-light-text-widget");(b.length||(b=!!$(a.target).is(".autocomplete-light-text-widget")&&$(a.target)))&&(b.is(".yourlabs-autocomplete")||(b.yourlabsWidget("destroy"),b.find("input").yourlabsAutocomplete("destroy"),b.trigger("initialize")))})}),void 0===window.yourlabs&&(window.yourlabs={}),yourlabs.Widget=function(a){this.widget=a,this.input=this.widget.find("input[data-autocomplete-url]"),this.select=this.widget.find("select"),this.deck=this.widget.find(".deck"),this.choiceTemplate=this.widget.find(".choice-template .choice"),this.maximumValues=0,this.clearInputOnSelectChoice="1"},yourlabs.Widget.prototype.getValue=function(a){return a.attr("data-value")},yourlabs.Widget.prototype.initializeAutocomplete=function(){this.autocomplete=this.input.yourlabsAutocomplete(),this.autocomplete.box.addClass("autocomplete-light-widget")},yourlabs.Widget.prototype.bindSelectChoice=function(){this.input.bind("selectChoice",function(a,b){if(b.length){var c=$(this).parents(".autocomplete-light-widget").yourlabsWidget();c.selectChoice(b),c.widget.trigger("widgetSelectChoice",[b,c])}})},yourlabs.Widget.prototype.selectChoice=function(a){var b=this.getValue(a);if(!b)return void(window.console&&console.log("yourlabs.Widget.getValue failed"));this.freeDeck(),this.addToDeck(a,b),this.addToSelect(a,b);var c=$(":input:visible").index(this.input);if(this.resetDisplay(),"1"===this.clearInputOnSelectChoice&&(this.input.val(""),this.autocomplete.value=""),this.input.is(":visible"))this.input.focus();else{$(":input:visible:eq("+c+")").focus()}this.select.is("[multiple]")||this.input.prop("disabled",!0)},yourlabs.Widget.prototype.freeDeck=function(){var a=this.maximumValues-this.deck.children().length;if(this.maximumValues&&a<1){var b=$(this.deck.children()[0]);this.deselectChoice(b)}},yourlabs.Widget.prototype.resetDisplay=function(){var a=this.select.find("option:selected").length;this.maximumValues&&a===this.maximumValues?this.input.hide():this.input.show(),this.deck.show(),this.autocomplete.box.is(":visible")&&this.autocomplete.fixPosition()},yourlabs.Widget.prototype.deckChoiceHtml=function(a,b){var c=a.clone();return this.addRemove(c),c},yourlabs.Widget.prototype.optionChoice=function(a){var b=this.choiceTemplate.clone(),c=b.find(".append-option-html");return c.length?c.append(a.html()):b.html(a.html()),b},yourlabs.Widget.prototype.addRemove=function(a){var b=this.widget.find(".remove:last").clone().css("display","inline-block"),c=a.find(".prepend-remove");c.length?c.prepend(b):a.prepend(b)},yourlabs.Widget.prototype.addToDeck=function(a,b){if(!this.deck.find('[data-value="'+b+'"]').length){var c=this.deckChoiceHtml(a);c.attr("data-value",b),this.deck.append(c)}},yourlabs.Widget.prototype.addToSelect=function(a,b){var c=this.select.find('option[value="'+b+'"]');c.length||(this.select.append(''),c=this.select.find('option[value="'+b+'"]')),c.attr("selected","selected"),this.select.trigger("change"),this.updateAutocompleteExclude()},yourlabs.Widget.prototype.deselectChoice=function(a){var b=this.getValue(a);this.select.find('option[value="'+b+'"]').remove(),this.select.trigger("change"),a.remove(),0===this.deck.children().length&&this.deck.hide(),this.updateAutocompleteExclude(),this.resetDisplay(),this.input.prop("disabled",!1),this.widget.trigger("widgetDeselectChoice",[a,this])},yourlabs.Widget.prototype.updateAutocompleteExclude=function(){var a=this,b=this.deck.find(this.autocomplete.choiceSelector);this.autocomplete.data.exclude=$.map(b,function(b){return a.getValue($(b))})},yourlabs.Widget.prototype.initialize=function(){this.initializeAutocomplete();var a=this;this.deck.find(this.autocomplete.choiceSelector).each(function(){var b=a.getValue($(this)),c=a.select.find('option[value="'+b+'"]');c.prop("selected")||c.prop("selected",!0)});var b=this.deck.find(this.input.yourlabsAutocomplete().choiceSelector);this.addRemove(b),this.resetDisplay(),a.select.val()&&!this.select.is("[multiple]")&&this.input.prop("disabled",!0),this.bindSelectChoice()},yourlabs.Widget.prototype.destroy=function(a){a.find("input").unbind("selectChoice").yourlabsAutocomplete("destroy")},$.fn.yourlabsWidget=function(a){a=a||{};var b=this.yourlabsRegistry("widget");if("destroy"===a)return void(b&&(b.destroy(this),this.removeData("widget")));if(void 0===b){b=new yourlabs.Widget(this);for(var c in this.data())if(c&&"widget"===c.substr(0,6)&&"widget"!==c){var d=c.replace("widget","");d=d.charAt(0).toLowerCase()+d.slice(1),b[d]=this.data(c)}b=$.extend(b,a),$(this).yourlabsRegistry("widget",b),b.initialize(),b.widget.attr("data-widget-ready",1),b.widget.trigger("widget-ready")}return b},$(document).ready(function(){$("body").on("initialize",".autocomplete-light-widget[data-widget-bootstrap=normal]",function(){$(this).yourlabsWidget()}),$("body").on("click",".autocomplete-light-widget .deck .remove",function(){var a=$(this).parents(".autocomplete-light-widget").yourlabsWidget(),b=a.input.yourlabsAutocomplete().choiceSelector,c=$(this).parents(b);a.deselectChoice(c)}),$(document).trigger("yourlabsWidgetReady"),$('.autocomplete-light-widget:not([id*="__prefix__"])').each(function(){$(this).trigger("initialize")}),$(document).bind("DOMNodeInserted",function(a){var b;if($(a.target).is("option")){if(b=$(a.target).parents(".autocomplete-light-widget"),!b.length)return;b=b.yourlabsWidget();var c=$(a.target),d=c.attr("value");if(!b.deck.find('[data-value="'+d+'"]').length){var e=b.optionChoice(c);e.attr("data-value",d),b.selectChoice(e)}}else{if(b=$(a.target).find(".autocomplete-light-widget:not([data-widget-ready])"),!b.length)return;if(b.is(".yourlabs-autocomplete"))return;b.yourlabsWidget("destroy"),b.find("input").yourlabsAutocomplete("destroy"),b.trigger("initialize")}});var a=yourlabs.getInternetExplorerVersion();if(-1!==a&&a<9){observe=[".autocomplete-light-widget:not([data-yourlabs-skip])",".autocomplete-light-widget option:not([data-yourlabs-skip])"].join(),$(observe).attr("data-yourlabs-skip",1);var b=function(){$(observe).each(function(){$(document).trigger(jQuery.Event("DOMNodeInserted",{target:$(this)})),$(this).attr("data-yourlabs-skip",1)}),setTimeout(b,500)};setTimeout(b,500)}}); \ No newline at end of file From a5071aa2576ae7117f78290656e32a4fa4d53619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 20 Jan 2018 22:24:13 +0100 Subject: [PATCH 042/122] cof -- Add tests for club views --- gestioncof/tests/test_views.py | 122 +++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..738fe353 --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,122 @@ +from django.test import Client, TestCase +from django.urls import reverse + +from gestioncof.models import Club +from gestioncof.tests.testcases import ViewTestCaseMixin + +from .utils import create_user + + +class ClubListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'liste-clubs' + url_expected = '/clubs/liste' + + auth_user = 'member' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + + self.c1 = Club.objects.create(name='Club1') + self.c2 = Club.objects.create(name='Club2') + + m = self.users['member'] + self.c1.membres.add(m) + self.c1.respos.add(m) + + def test_as_member(self): + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertEqual(r.context['owned_clubs'].get(), self.c1) + self.assertEqual(r.context['other_clubs'].get(), self.c2) + + def test_as_staff(self): + u = self.users['staff'] + c = Client() + c.force_login(u) + + r = c.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertQuerysetEqual( + r.context['owned_clubs'], map(repr, [self.c1, self.c2]), + ordered=False, + ) + + +class ClubMembersViewTests(ViewTestCaseMixin, TestCase): + url_name = 'membres-club' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + @property + def url_kwargs(self): + return {'name': self.c.name} + + @property + def url_expected(self): + return '/clubs/membres/{}'.format(self.c.name) + + def setUp(self): + super().setUp() + + self.u1 = create_user('u1') + self.u2 = create_user('u2') + + self.c = Club.objects.create(name='Club') + self.c.membres.add(self.u1, self.u2) + self.c.respos.add(self.u1) + + def test_as_staff(self): + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertEqual(r.context['members_no_respo'].get(), self.u2) + + def test_as_respo(self): + u = self.users['user'] + self.c.respos.add(u) + + c = Client() + c.force_login(u) + r = c.get(self.url) + + self.assertEqual(r.status_code, 200) + + +class ClubChangeRespoViewTests(ViewTestCaseMixin, TestCase): + url_name = 'change-respo' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + @property + def url_kwargs(self): + return {'club_name': self.c.name, 'user_id': self.users['user'].pk} + + @property + def url_expected(self): + return '/clubs/change_respo/{}/{}'.format( + self.c.name, self.users['user'].pk, + ) + + def setUp(self): + super().setUp() + + self.c = Club.objects.create(name='Club') + + def test(self): + u = self.users['user'] + expected_redirect = reverse('membres-club', kwargs={ + 'name': self.c.name, + }) + self.c.membres.add(u) + + r = self.client.get(self.url) + self.assertRedirects(r, expected_redirect) + self.assertIn(u, self.c.respos.all()) + + self.client.get(self.url) + self.assertNotIn(u, self.c.respos.all()) From bbe46645f75b123989f71c07054091c9df9e3a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 20 Jan 2018 22:24:25 +0100 Subject: [PATCH 043/122] cof -- Fix the club list view --- gestioncof/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index 5dfee83f..4e0cb080 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -566,7 +566,7 @@ def liste_clubs(request): if request.user.profile.is_buro: data = {'owned_clubs': clubs.all()} else: - data = {'owned_clubs': request.user.clubs_geres, + data = {'owned_clubs': request.user.clubs_geres.all(), 'other_clubs': clubs.exclude(respos=request.user)} return render(request, 'liste_clubs.html', data) From 7e0ecd8e0f707f912b66cb438778048b877e203e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Jan 2018 17:51:23 +0100 Subject: [PATCH 044/122] Add assertion to check ical data is as expected --- shared/tests/testcases.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 15792383..14a5fbfe 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -8,6 +8,8 @@ from django.test import Client from django.utils import timezone from django.utils.functional import cached_property +import icalendar + User = get_user_model() @@ -92,6 +94,40 @@ class TestCaseMixin: else: self.assertEqual(actual, expected) + def _test_event_equal(self, event, exp): + for k, v_desc in exp.items(): + if isinstance(v_desc, tuple): + v_getter = v_desc[0] + v = v_desc[1] + else: + v_getter = lambda v: v + v = v_desc + if v_getter(event[k.upper()]) != v: + return False + return True + + def _find_event(self, ev, l): + for i, elt in enumerate(l): + if self._test_event_equal(ev, elt): + return elt, i + return False, -1 + + def assertCalEqual(self, ical_content, expected): + remaining = expected.copy() + unexpected = [] + + cal = icalendar.Calendar.from_ical(ical_content) + + for ev in cal.walk('vevent'): + found, i_found = self._find_event(ev, remaining) + if found: + remaining.pop(i_found) + else: + unexpected.append(ev) + + self.assertListEqual(unexpected, []) + self.assertListEqual(remaining, []) + class ViewTestCaseMixin(TestCaseMixin): """ From 2e6a54c7dbb8cff5077d35230e063712aa90b3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Jan 2018 17:52:53 +0100 Subject: [PATCH 045/122] cof -- Add tests for calendar views --- gestioncof/tests/test_views.py | 151 +++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..e33fe2f2 --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,151 @@ +import uuid +from datetime import timedelta + +from django.contrib import messages +from django.contrib.messages.api import get_messages +from django.contrib.messages.storage.base import Message +from django.test import TestCase + +from bda.models import Salle, Tirage +from gestioncof.models import CalendarSubscription +from gestioncof.tests.testcases import ViewTestCaseMixin + + +class CalendarViewTests(ViewTestCaseMixin, TestCase): + url_name = 'calendar' + url_expected = '/calendar/subscription' + + auth_user = 'member' + auth_forbidden = [None, 'user'] + + post_expected_message = Message( + messages.SUCCESS, "Calendrier mis à jour avec succès.") + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_new(self): + r = self.client.post(self.url, { + 'subscribe_to_events': True, + 'subscribe_to_my_shows': True, + 'other_shows': [], + }) + + self.assertEqual(r.status_code, 200) + self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) + cs = self.users['member'].calendarsubscription + self.assertTrue(cs.subscribe_to_events) + self.assertTrue(cs.subscribe_to_my_shows) + + def test_post_edit(self): + u = self.users['member'] + token = uuid.uuid4() + cs = CalendarSubscription.objects.create(token=token, user=u) + + r = self.client.post(self.url, { + 'other_shows': [], + }) + + self.assertEqual(r.status_code, 200) + self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) + cs.refresh_from_db() + self.assertEqual(cs.token, token) + self.assertFalse(cs.subscribe_to_events) + self.assertFalse(cs.subscribe_to_my_shows) + + def test_post_other_shows(self): + t = Tirage.objects.create( + ouverture=self.now, + fermeture=self.now, + active=True, + ) + l = Salle.objects.create() + s = t.spectacle_set.create( + date=self.now, price=3.5, slots=20, location=l, listing=True) + + r = self.client.post(self.url, {'other_shows': [str(s.pk)]}) + + self.assertEqual(r.status_code, 200) + + +class CalendarICSViewTests(ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.calendar_ics' + + auth_user = None + auth_forbidden = [] + + @property + def url_kwargs(self): + return {'token': self.token} + + @property + def url_expected(self): + return '/calendar/{}/calendar.ics'.format(self.token) + + def setUp(self): + super().setUp() + + self.token = uuid.uuid4() + + self.t = Tirage.objects.create( + ouverture=self.now, + fermeture=self.now, + active=True, + ) + l = Salle.objects.create(name='Location') + self.s1 = self.t.spectacle_set.create( + price=1, slots=10, location=l, listing=True, + title='Spectacle 1', date=self.now + timedelta(days=1), + ) + self.s2 = self.t.spectacle_set.create( + price=2, slots=20, location=l, listing=True, + title='Spectacle 2', date=self.now + timedelta(days=2), + ) + self.s3 = self.t.spectacle_set.create( + price=3, slots=30, location=l, listing=True, + title='Spectacle 3', date=self.now + timedelta(days=3), + ) + + def test(self): + u = self.users['user'] + p = u.participant_set.create(tirage=self.t) + p.attribution_set.create(spectacle=self.s1) + + self.cs = CalendarSubscription.objects.create( + user=u, token=self.token, + subscribe_to_my_shows=True, subscribe_to_events=True, + ) + self.cs.other_shows.add(self.s2) + + r = self.client.get(self.url) + + def get_dt_from_ical(v): + return v.dt + + self.assertCalEqual(r.content.decode('utf-8'), [ + { + 'summary': 'Spectacle 1', + 'dtstart': (get_dt_from_ical, ( + (self.now + timedelta(days=1)).replace(microsecond=0) + )), + 'dtend': (get_dt_from_ical, ( + (self.now + timedelta(days=1, hours=2)).replace( + microsecond=0) + )), + 'location': 'Location', + 'uid': 'show-{}-{}@example.com'.format(self.s1.pk, self.t.pk), + }, + { + 'summary': 'Spectacle 2', + 'dtstart': (get_dt_from_ical, ( + (self.now + timedelta(days=2)).replace(microsecond=0) + )), + 'dtend': (get_dt_from_ical, ( + (self.now + timedelta(days=2, hours=2)).replace( + microsecond=0) + )), + 'location': 'Location', + 'uid': 'show-{}-{}@example.com'.format(self.s2.pk, self.t.pk), + }, + ]) From acf284862a57f1857a2049067d4bb081a797fffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Jan 2018 18:00:56 +0100 Subject: [PATCH 046/122] Users should be able to refuse to subscribe to shows and events --- gestioncof/forms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 2124b7c8..5a25b815 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -351,10 +351,12 @@ EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset) class CalendarForm(forms.ModelForm): subscribe_to_events = forms.BooleanField( initial=True, - label="Événements du COF") + label="Événements du COF", + required=False) subscribe_to_my_shows = forms.BooleanField( initial=True, - label="Les spectacles pour lesquels j'ai obtenu une place") + label="Les spectacles pour lesquels j'ai obtenu une place", + required=False) other_shows = forms.ModelMultipleChoiceField( label="Spectacles supplémentaires", queryset=Spectacle.objects.filter(tirage__active=True), From 38539a9d53607efd417bee8d4d1d930999e39d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Jan 2018 18:03:01 +0100 Subject: [PATCH 047/122] Name url to export calendar to ical --- gestioncof/templates/gestioncof/calendar_subscription.html | 2 +- gestioncof/tests/test_views.py | 2 +- gestioncof/urls.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gestioncof/templates/gestioncof/calendar_subscription.html b/gestioncof/templates/gestioncof/calendar_subscription.html index b13cb7f2..4b9e3cbb 100644 --- a/gestioncof/templates/gestioncof/calendar_subscription.html +++ b/gestioncof/templates/gestioncof/calendar_subscription.html @@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA. {% if token %}

    Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à -cette adresse.

    +cette adresse.

    • Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index e33fe2f2..06d6be4a 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -70,7 +70,7 @@ class CalendarViewTests(ViewTestCaseMixin, TestCase): class CalendarICSViewTests(ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.calendar_ics' + url_name = 'calendar.ics' auth_user = None auth_forbidden = [] diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 2be609b3..dde543a5 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -52,7 +52,8 @@ events_patterns = [ calendar_patterns = [ url(r'^subscription$', views.calendar, name='calendar'), - url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics) + url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics, + name='calendar.ics'), ] clubs_patterns = [ From 80ca35a4c0ff1f69916037ffba7713c4a57e5532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 14:49:02 +0100 Subject: [PATCH 048/122] Add helper to check HttpResponse containing csv --- shared/tests/testcases.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 15792383..cac036af 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -1,3 +1,4 @@ +import csv from unittest import mock from urllib.parse import parse_qs, urlparse @@ -92,6 +93,10 @@ class TestCaseMixin: else: self.assertEqual(actual, expected) + def load_from_csv_response(self, r): + decoded = r.content.decode('utf-8') + return list(csv.reader(decoded.split('\n')[:-1])) + class ViewTestCaseMixin(TestCaseMixin): """ From f371606cdbd98298383a038879b6f51d5242b447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 14:58:38 +0100 Subject: [PATCH 049/122] cof -- Add tests for export views --- gestioncof/tests/test_views.py | 159 +++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..8cd323cc --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,159 @@ +import csv + +from django.test import TestCase + +from gestioncof.models import Event +from gestioncof.tests.testcases import ViewTestCaseMixin + +from .utils import create_user + + +class ExportMembersViewTests(ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.export_members' + url_expected = '/export/members' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + u1, u2 = self.users['member'], self.users['staff'] + u1.first_name = 'first' + u1.last_name = 'last' + u1.email = 'user@mail.net' + u1.save() + u1.profile.phone = '0123456789' + u1.profile.departement = 'Dept' + u1.profile.save() + + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + data = list(csv.reader(r.content.decode('utf-8').split('\n')[:-1])) + self.assertListEqual(data, [ + [ + str(u1.pk), 'member', 'first', 'last', 'user@mail.net', + '0123456789', '1A', 'Dept', 'normalien', + ], + [str(u2.pk), 'staff', '', '', '', '', '1A', '', 'normalien'], + ]) + + +class MegaHelpers: + + def setUp(self): + super().setUp() + + u1 = create_user('u1') + u1.first_name = 'first' + u1.last_name = 'last' + u1.email = 'user@mail.net' + u1.save() + u1.profile.phone = '0123456789' + u1.profile.departement = 'Dept' + u1.profile.comments = 'profile.comments' + u1.profile.save() + + u2 = create_user('u2') + u2.profile.save() + + m = Event.objects.create(title='MEGA 2017') + + cf1 = m.commentfields.create(name='Commentaire') + cf2 = m.commentfields.create( + name='Comment Field 2', fieldtype='char', + ) + + option_type = m.options.create(name='Conscrit/Orga ?') + choice_orga = option_type.choices.create(value='Orga') + choice_conscrit = option_type.choices.create(value='Conscrit') + + mr1 = m.eventregistration_set.create(user=u1) + mr1.options.add(choice_orga) + mr1.comments.create(commentfield=cf1, content='Comment 1') + mr1.comments.create(commentfield=cf2, content='Comment 2') + + mr2 = m.eventregistration_set.create(user=u2) + mr2.options.add(choice_conscrit) + + self.u1 = u1 + self.u2 = u2 + self.m = m + self.choice_orga = choice_orga + self.choice_conscrit = choice_conscrit + + +class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.export_mega' + url_expected = '/export/mega' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertListEqual(self.load_from_csv_response(r), [ + [ + 'u1', 'first', 'last', 'user@mail.net', '0123456789', + str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2', + ], + ['u2', '', '', '', '', str(self.u2.pk), '', ''], + ]) + + +class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.export_mega_orgas' + url_expected = '/export/mega/orgas' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertListEqual(self.load_from_csv_response(r), [ + [ + 'u1', 'first', 'last', 'user@mail.net', '0123456789', + str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2', + ], + ]) + + +class ExportMegaParticipantsViewTests( + MegaHelpers, ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.export_mega_participants' + url_expected = '/export/mega/participants' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertListEqual(self.load_from_csv_response(r), [ + ['u2', '', '', '', '', str(self.u2.pk), '', ''], + ]) + + +class ExportMegaRemarksViewTests( + MegaHelpers, ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.export_mega_remarksonly' + url_expected = '/export/mega/avecremarques' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertListEqual(self.load_from_csv_response(r), [ + [ + 'u1', 'first', 'last', 'user@mail.net', '0123456789', + str(self.u1.pk), 'profile.comments', 'Comment 1', + ], + ]) From a813507ddd6218e51769f8a0b8e17ddc7f04115f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 14:59:57 +0100 Subject: [PATCH 050/122] Name urls of export views (cof members, mega) --- gestioncof/templates/gestioncof/utile_cof.html | 8 ++++---- gestioncof/tests/test_views.py | 10 +++++----- gestioncof/urls.py | 15 ++++++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/gestioncof/templates/gestioncof/utile_cof.html b/gestioncof/templates/gestioncof/utile_cof.html index ae024949..8cea33df 100644 --- a/gestioncof/templates/gestioncof/utile_cof.html +++ b/gestioncof/templates/gestioncof/utile_cof.html @@ -7,15 +7,15 @@

      Liens utiles du COF

      COF

      Mega

      Note : pour ouvrir les fichiers .csv avec Excel, il faut diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 8cd323cc..02288e2b 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -9,7 +9,7 @@ from .utils import create_user class ExportMembersViewTests(ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.export_members' + url_name = 'cof.membres_export' url_expected = '/export/members' auth_user = 'staff' @@ -83,7 +83,7 @@ class MegaHelpers: class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.export_mega' + url_name = 'cof.mega_export' url_expected = '/export/mega' auth_user = 'staff' @@ -103,7 +103,7 @@ class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.export_mega_orgas' + url_name = 'cof.mega_export_orgas' url_expected = '/export/mega/orgas' auth_user = 'staff' @@ -124,7 +124,7 @@ class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): class ExportMegaParticipantsViewTests( MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.export_mega_participants' + url_name = 'cof.mega_export_participants' url_expected = '/export/mega/participants' auth_user = 'staff' @@ -141,7 +141,7 @@ class ExportMegaParticipantsViewTests( class ExportMegaRemarksViewTests( MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.export_mega_remarksonly' + url_name = 'cof.mega_export_remarks' url_expected = '/export/mega/avecremarques' auth_user = 'staff' diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 2be609b3..ef3b4190 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -6,12 +6,17 @@ from gestioncof import views, petits_cours_views from gestioncof.decorators import buro_required export_patterns = [ - url(r'^members$', views.export_members), - url(r'^mega/avecremarques$', views.export_mega_remarksonly), - url(r'^mega/participants$', views.export_mega_participants), - url(r'^mega/orgas$', views.export_mega_orgas), + url(r'^members$', views.export_members, + name='cof.membres_export'), + url(r'^mega/avecremarques$', views.export_mega_remarksonly, + name='cof.mega_export_remarks'), + url(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.+)$', views.export_mega_bytype), - url(r'^mega$', views.export_mega), + url(r'^mega$', views.export_mega, + name='cof.mega_export'), ] petitcours_patterns = [ From bd89dce11d72f6ecf86501cfaf920dac646c6914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 21:38:01 +0100 Subject: [PATCH 051/122] Add testing helpers to create superuser --- gestioncof/tests/utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gestioncof/tests/utils.py b/gestioncof/tests/utils.py index 8d55680a..7ba361b7 100644 --- a/gestioncof/tests/utils.py +++ b/gestioncof/tests/utils.py @@ -9,7 +9,9 @@ def _create_user(username, is_cof=False, is_staff=False, attrs=None): password = attrs.pop('password', username) - user_keys = ['first_name', 'last_name', 'email', 'is_staff'] + user_keys = [ + 'first_name', 'last_name', 'email', 'is_staff', 'is_superuser', + ] user_attrs = {k: v for k, v in attrs.items() if k in user_keys} profile_keys = [ @@ -49,3 +51,11 @@ def create_member(username, attrs=None): def create_staff(username, attrs=None): return _create_user(username, is_cof=True, is_staff=True, attrs=attrs) + + +def create_root(username, attrs=None): + if attrs is None: + attrs = {} + attrs.setdefault('is_staff', True) + attrs.setdefault('is_superuser', True) + return _create_user(username, attrs=attrs) From 91162addb95fcafbf555288cc7fdeafa6f910b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 21:41:02 +0100 Subject: [PATCH 052/122] cof -- Add tests for some views --- gestioncof/tests/test_views.py | 131 +++++++++++++++++++++++++++++++++ gestioncof/views.py | 1 + 2 files changed, 132 insertions(+) create mode 100644 gestioncof/tests/test_views.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..9f13fb36 --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -0,0 +1,131 @@ +from django.contrib import messages +from django.contrib.messages.api import get_messages +from django.contrib.messages.storage.base import Message +from django.test import TestCase +from django.urls import reverse + +from gestioncof.tests.testcases import ViewTestCaseMixin + +from .utils import create_member, create_root, create_user + + +class HomeViewTests(ViewTestCaseMixin, TestCase): + url_name = 'home' + url_expected = '/' + + auth_user = 'user' + auth_forbidden = [None] + + def test(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class ProfileViewTests(ViewTestCaseMixin, TestCase): + url_name = 'profile' + url_expected = '/profile' + + http_methods = ['GET', 'POST'] + + auth_user = 'member' + auth_forbidden = [None, 'user'] + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post(self): + u = self.users['member'] + + r = self.client.post(self.url, { + 'first_name': 'First', + 'last_name': 'Last', + 'phone': '', + # 'mailing_cof': '1', + # 'mailing_bda': '1', + # 'mailing_bda_revente': '1', + }) + + self.assertEqual(r.status_code, 200) + expected_message = Message(messages.SUCCESS, ( + "Votre profil a été mis à jour avec succès !" + )) + self.assertIn(expected_message, get_messages(r.wsgi_request)) + u.refresh_from_db() + self.assertEqual(u.first_name, 'First') + self.assertEqual(u.last_name, 'Last') + self.assertFalse(u.profile.mailing_cof) + self.assertFalse(u.profile.mailing_bda) + self.assertFalse(u.profile.mailing_bda_revente) + + +class UtilsViewTests(ViewTestCaseMixin, TestCase): + url_name = 'utile_cof' + url_expected = '/utile_cof' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class MailingListDiffCof(ViewTestCaseMixin, TestCase): + url_name = 'gestioncof.views.liste_diffcof' + url_expected = '/utile_cof/diff_cof' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def setUp(self): + super().setUp() + + self.u1 = create_member('u1', attrs={'mailing_cof': True}) + self.u2 = create_member('u2', attrs={'mailing_cof': False}) + self.u3 = create_user('u3', attrs={'mailing_cof': True}) + + def test(self): + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertEqual(r.context['personnes'].get(), self.u1.profile) + + +class ConfigUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'config.edit' + url_expected = '/config' + + http_methods = ['GET', 'POST'] + + auth_user = 'root' + auth_forbidden = [None, 'user', 'member', 'staff'] + + def get_users_extra(self): + return { + 'root': create_root('root'), + } + + def test_get(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post(self): + r = self.client.post(self.url, { + 'gestion_banner': 'Announcement !', + }) + + self.assertRedirects(r, reverse('home')) + + +class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase): + url_name = 'cof-user-autocomplete' + url_expected = '/user/autocomplete' + + auth_user = 'staff' + auth_forbidden = [None, 'user', 'member'] + + def test(self): + r = self.client.get(self.url, {'q': 'user'}) + + self.assertEqual(r.status_code, 200) diff --git a/gestioncof/views.py b/gestioncof/views.py index 5dfee83f..2039bb65 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -9,6 +9,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden from django.contrib.auth.decorators import login_required from django.contrib.auth.views import ( login as django_login_view, logout as django_logout_view, + redirect_to_login, ) from django.contrib.auth.models import User from django.contrib.sites.models import Site From 0876a004e55a04cf638903a2aa423e608a3654bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 14:59:57 +0100 Subject: [PATCH 053/122] Name urls of export views (cof members, mega) --- gestioncof/templates/gestioncof/utile_cof.html | 8 ++++---- gestioncof/urls.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/gestioncof/templates/gestioncof/utile_cof.html b/gestioncof/templates/gestioncof/utile_cof.html index ae024949..8cea33df 100644 --- a/gestioncof/templates/gestioncof/utile_cof.html +++ b/gestioncof/templates/gestioncof/utile_cof.html @@ -7,15 +7,15 @@

      Liens utiles du COF

      COF

      Mega

      Note : pour ouvrir les fichiers .csv avec Excel, il faut diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 2be609b3..ef3b4190 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -6,12 +6,17 @@ from gestioncof import views, petits_cours_views from gestioncof.decorators import buro_required export_patterns = [ - url(r'^members$', views.export_members), - url(r'^mega/avecremarques$', views.export_mega_remarksonly), - url(r'^mega/participants$', views.export_mega_participants), - url(r'^mega/orgas$', views.export_mega_orgas), + url(r'^members$', views.export_members, + name='cof.membres_export'), + url(r'^mega/avecremarques$', views.export_mega_remarksonly, + name='cof.mega_export_remarks'), + url(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.+)$', views.export_mega_bytype), - url(r'^mega$', views.export_mega), + url(r'^mega$', views.export_mega, + name='cof.mega_export'), ] petitcours_patterns = [ From f8361b9114700facc4ebfcaa79d024be49496193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 21:53:19 +0100 Subject: [PATCH 054/122] Add & fix urls naming --- cof/urls.py | 6 ++++-- gestioncof/templates/gestioncof/profile.html | 2 +- gestioncof/templates/gestioncof/utile_cof.html | 2 +- gestioncof/tests/test_views.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index e6e5d313..33d4fbc6 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -86,13 +86,15 @@ urlpatterns = [ url(r'^utile_bda$', gestioncof_views.utile_bda, name='utile_bda'), url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff), - url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), + url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof, + name='ml_diffcof'), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^k-fet/', include('kfet.urls')), url(r'^cms/', include(wagtailadmin_urls)), url(r'^documents/', include(wagtaildocs_urls)), # djconfig - url(r"^config", gestioncof_views.ConfigUpdate.as_view()), + url(r"^config", gestioncof_views.ConfigUpdate.as_view(), + name='config.edit'), ] if 'debug_toolbar' in settings.INSTALLED_APPS: diff --git a/gestioncof/templates/gestioncof/profile.html b/gestioncof/templates/gestioncof/profile.html index 59358239..5decdfb3 100644 --- a/gestioncof/templates/gestioncof/profile.html +++ b/gestioncof/templates/gestioncof/profile.html @@ -5,7 +5,7 @@ {% block realcontent %}

      Modifier mon profil

      - +
      {% csrf_token %} diff --git a/gestioncof/templates/gestioncof/utile_cof.html b/gestioncof/templates/gestioncof/utile_cof.html index 8cea33df..637055c5 100644 --- a/gestioncof/templates/gestioncof/utile_cof.html +++ b/gestioncof/templates/gestioncof/utile_cof.html @@ -8,7 +8,7 @@

      COF

      Mega

      diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 9f13fb36..6f8e95c1 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -72,7 +72,7 @@ class UtilsViewTests(ViewTestCaseMixin, TestCase): class MailingListDiffCof(ViewTestCaseMixin, TestCase): - url_name = 'gestioncof.views.liste_diffcof' + url_name = 'ml_diffcof' url_expected = '/utile_cof/diff_cof' auth_user = 'staff' From 0235c4f7e8e4d04961419e17fd745384294629d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 21:54:41 +0100 Subject: [PATCH 055/122] Fix profile edition view - Fix a typo. - Bump version of django-bootstrap-form to be comaptible with Django 1.11. --- gestioncof/views.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index 2039bb65..6a2582e2 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -339,7 +339,7 @@ def profile(request): if form.is_valid(): form.save() messages.success(request, - "Votre profil a été mis à jour avec succès !") + "Votre profil a été mis à jour avec succès !") else: form = UserProfileForm(instance=request.user.profile) return render(request, "gestioncof/profile.html", {"form": form}) diff --git a/requirements.txt b/requirements.txt index d1046042..b30660ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ psycopg2 Pillow six unicodecsv -django-bootstrap-form==3.2.1 +django-bootstrap-form==3.3 asgiref==1.1.1 daphne==1.3.0 asgi-redis==1.3.0 From afa6972280fa560402e884f165193fda955f12f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 22 Jan 2018 21:59:07 +0100 Subject: [PATCH 056/122] Better handling of non-authorized users in config edition view --- gestioncof/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index 6a2582e2..2f6fd518 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -783,7 +783,7 @@ class ConfigUpdate(FormView): def dispatch(self, request, *args, **kwargs): if request.user is None or not request.user.is_superuser: - raise Http404 + return redirect_to_login(request.get_full_path()) return super().dispatch(request, *args, **kwargs) def form_valid(self, form): From d57c75d2a0b8ac2830e58f9d3194ae1967c6d788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 5 Feb 2018 22:35:35 +0100 Subject: [PATCH 057/122] Minor simplificatons after code review --- bda/tests/test_views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 3ee46792..b0846fd7 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -50,10 +50,12 @@ class BdATestHelpers: ] def require_custommails(self): - from gestioncof.management.commands import syncmails - syncmails.load_from_file() + from django.core.management import call_command + call_command("syncmails") - def check_restricted_access(self, url, validate_user=user_is_cof, redirect_url=None): + 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 @@ -73,14 +75,14 @@ class BdATestHelpers: class TestBdAViews(BdATestHelpers, TestCase): def setUp(self): - # Signals handlers on login/logout send messages. + # 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 - BdATestHelpers.setUp(self) + super().setUp() # Some BdA stuff self.tirage = Tirage.objects.create( title="Test tirage", From ac1a57d96993466d38a2e87edcbcd1e5501b8c54 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sun, 11 Feb 2018 17:01:26 +0100 Subject: [PATCH 058/122] Make provisioning script stop immediately on errors By default, bash will ignore any failing commands and happily proceed to execute the next ones. This is usually not the behavior the we want in provisioning script (or ever in scripts, actually): if one step of the provisioning fails, it doesn't make much sense to proceed with the following ones. This simple patch uses `set -e` to ask bash to abort the whole script if any command within it fails, leading to outputs that are easier to parse since the commands following a failing one will usually fail also, hiding the root cause. --- provisioning/bootstrap.sh | 3 +++ provisioning/prepare_django.sh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 69bbcf4c..cb6917a7 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -1,5 +1,8 @@ #!/bin/sh +# Stop if an error is encountered +set -e + # Configuration de la base de données. Le mot de passe est constant car c'est # pour une installation de dév locale qui ne sera accessible que depuis la # machine virtuelle. diff --git a/provisioning/prepare_django.sh b/provisioning/prepare_django.sh index 1818a0cd..891108e8 100644 --- a/provisioning/prepare_django.sh +++ b/provisioning/prepare_django.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Stop if an error is encountered. +set -e + python manage.py migrate python manage.py loaddata gestion sites articles python manage.py loaddevdata From 3314670cab2917f3d57e3340c56a7a5daddd92c8 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sun, 11 Feb 2018 19:09:07 +0100 Subject: [PATCH 059/122] Various fixes for Django 1.11 - The {% cycle %} command was used non-quoted arguments separated by commas, while it is supposed to use quoted arguments separated by spaces (I'm actually not sure how that ever worked :) - django-bootstrap-form was at version 3.2.1 which is not compatible with Django 1.11 (but also required by GestioCOF). I upgraded it to version 3.3. --- bda/templates/bda/inscription-formset.html | 2 +- gestioncof/templates/inscription-petit-cours-formset.html | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bda/templates/bda/inscription-formset.html b/bda/templates/bda/inscription-formset.html index 65ef389b..88b65600 100644 --- a/bda/templates/bda/inscription-formset.html +++ b/bda/templates/bda/inscription-formset.html @@ -14,7 +14,7 @@
    {% endif %} - + {% for field in form.visible_fields %} {% if field.name != "DELETE" and field.name != "priority" %} {% endif %} - + {% for field in form.visible_fields %} {% if field.name != "DELETE" and field.name != "priority" %}
    diff --git a/gestioncof/templates/inscription-petit-cours-formset.html b/gestioncof/templates/inscription-petit-cours-formset.html index ec8979f5..40311772 100644 --- a/gestioncof/templates/inscription-petit-cours-formset.html +++ b/gestioncof/templates/inscription-petit-cours-formset.html @@ -16,7 +16,7 @@
    diff --git a/requirements.txt b/requirements.txt index d1046042..b30660ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ psycopg2 Pillow six unicodecsv -django-bootstrap-form==3.2.1 +django-bootstrap-form==3.3 asgiref==1.1.1 daphne==1.3.0 asgi-redis==1.3.0 From 6ecc9a54b36d153dea502057531dd5b6bb7ea8cc Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Sun, 11 Feb 2018 19:24:01 +0100 Subject: [PATCH 060/122] Properly propagate the default number of places in tirage Fixes #182. --- bda/templates/bda/inscription-tirage.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bda/templates/bda/inscription-tirage.html b/bda/templates/bda/inscription-tirage.html index d56b4229..3fd81378 100644 --- a/bda/templates/bda/inscription-tirage.html +++ b/bda/templates/bda/inscription-tirage.html @@ -27,6 +27,14 @@ var django = { var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-'); $(this).attr('for', newFor); }); + // Cloning @@ -60,6 +64,44 @@
    + +

    Articles non vendus

    +

    Article{{ not_sold_articles|length|pluralize }} non vendu{{ nots_sold_article|length|pluralize }}

    +
    + + + + + + + + + + + + + {% for article in not_sold_articles %} + {% ifchanged article.category %} + + + + {% endifchanged %} + + + + + + + + + {% endfor %} + +
    NomPrixStockEn venteAffichéDernier inventaire
    {{ article.category.name }}
    + + {{ article.name }} + + {{ article.price }}€{{ article.stock }}{{ article.is_sold | yesno:"En vente,Non vendu"}}{{ article.hidden | yesno:"Caché,Affiché" }}{{ article.inventory.0.at }}
    +
    {% endblock %} diff --git a/kfet/views.py b/kfet/views.py index 2b69684d..63bd280c 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -706,6 +706,14 @@ class ArticleList(ListView): ) template_name = 'kfet/article.html' context_object_name = 'articles' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + articles = context[self.context_object_name] + context['nb_articles'] = len(articles) + context[self.context_object_name] = articles.filter(is_sold=True) + context['not_sold_articles'] = articles.filter(is_sold=False) + return context # Article - Create From 35e17a81a6e2e91f22facc5ca9df6a0a56a8f67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 5 Apr 2018 23:48:53 +0200 Subject: [PATCH 062/122] New year -> new promo -> migration in k-fet --- kfet/migrations/0063_promo.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 kfet/migrations/0063_promo.py diff --git a/kfet/migrations/0063_promo.py b/kfet/migrations/0063_promo.py new file mode 100644 index 00000000..3fac5a8a --- /dev/null +++ b/kfet/migrations/0063_promo.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-05 21:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0062_delete_globalpermissions'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='promo', + field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018)], default=2017, null=True), + ), + ] From 623047dca2dabb3e8eec9e5b97ef30f8c64caf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 6 Apr 2018 11:11:02 +0200 Subject: [PATCH 063/122] Fix old-style reversal of calendar urls --- bda/templates/bda/resume_places.html | 2 +- gestioncof/templates/gestioncof/calendar_subscription.html | 2 +- gestioncof/urls.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bda/templates/bda/resume_places.html b/bda/templates/bda/resume_places.html index 3785169b..7cbd06ea 100644 --- a/bda/templates/bda/resume_places.html +++ b/bda/templates/bda/resume_places.html @@ -16,7 +16,7 @@

    Total à payer : {{ total|floatformat }}€


    Ne manque pas un spectacle avec le - calendrier + calendrier automatique !

    {% else %}

    Vous n'avez aucune place :(

    diff --git a/gestioncof/templates/gestioncof/calendar_subscription.html b/gestioncof/templates/gestioncof/calendar_subscription.html index b13cb7f2..345312e3 100644 --- a/gestioncof/templates/gestioncof/calendar_subscription.html +++ b/gestioncof/templates/gestioncof/calendar_subscription.html @@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA. {% if token %}

    Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à -cette adresse.

    +cette adresse.

    • Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 2be609b3..1a66dd57 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -52,7 +52,9 @@ events_patterns = [ calendar_patterns = [ url(r'^subscription$', views.calendar, name='calendar'), - url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics) + url(r'^(?P[a-z0-9-]+)/calendar.ics$', + views.calendar_ics, + name="calendar_ics") ] clubs_patterns = [ From 6328cdaa1965a6ebf0e5381b157a30f5354dfbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 7 Apr 2018 12:05:16 +0200 Subject: [PATCH 064/122] Tests: the order of our csv files is not relevant --- gestioncof/tests/test_views.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 1425353e..3b4a832b 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -38,13 +38,18 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) data = list(csv.reader(r.content.decode('utf-8').split('\n')[:-1])) - self.assertListEqual(data, [ + expected = [ [ str(u1.pk), 'member', 'first', 'last', 'user@mail.net', '0123456789', '1A', 'Dept', 'normalien', ], [str(u2.pk), 'staff', '', '', '', '', '1A', '', 'normalien'], - ]) + ] + # Sort before checking equality, the order of the output of csv.reader + # does not seem deterministic + expected.sort(key=lambda row: int(row[0])) + data.sort(key=lambda row: int(row[0])) + self.assertListEqual(data, expected) class MegaHelpers: From 3463017d597f01df11df81e2c2fc653cf1a8e26d Mon Sep 17 00:00:00 2001 From: Martin Pepin Date: Sat, 7 Apr 2018 12:54:50 +0200 Subject: [PATCH 065/122] build status in README.me --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 01f4ead2..b9d736ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # GestioCOF +![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg) + ## Installation ### Vagrant From 71b4e6253da3abc8126b433f86ca54fe47081693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 7 Apr 2018 13:42:19 +0200 Subject: [PATCH 066/122] Merge branch 'master' into aureplop/cof-tests_calendar --- kfet/templates/kfet/account.html | 2 +- kfet/templates/kfet/account_negative.html | 2 +- kfet/templates/kfet/article.html | 2 +- kfet/templates/kfet/article_inventories_snippet.html | 2 +- kfet/templates/kfet/article_suppliers_snippet.html | 2 +- kfet/templates/kfet/category.html | 2 +- kfet/templates/kfet/checkout.html | 2 +- kfet/templates/kfet/checkout_read.html | 2 +- kfet/templates/kfet/inventory.html | 2 +- kfet/templates/kfet/inventory_read.html | 2 +- kfet/templates/kfet/order.html | 2 +- kfet/templates/kfet/order_create.html | 2 +- kfet/templates/kfet/order_read.html | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/kfet/templates/kfet/account.html b/kfet/templates/kfet/account.html index a1a1f6bb..9b63c1da 100644 --- a/kfet/templates/kfet/account.html +++ b/kfet/templates/kfet/account.html @@ -39,7 +39,7 @@
      diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index b67b94fc..fa8b508d 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -37,7 +37,7 @@
      diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index caf70cfa..87a8b76a 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -28,7 +28,7 @@
      diff --git a/kfet/templates/kfet/article_inventories_snippet.html b/kfet/templates/kfet/article_inventories_snippet.html index df5c8dea..6a368ab1 100644 --- a/kfet/templates/kfet/article_inventories_snippet.html +++ b/kfet/templates/kfet/article_inventories_snippet.html @@ -1,6 +1,6 @@
      diff --git a/kfet/templates/kfet/article_suppliers_snippet.html b/kfet/templates/kfet/article_suppliers_snippet.html index 84922035..f82a313d 100644 --- a/kfet/templates/kfet/article_suppliers_snippet.html +++ b/kfet/templates/kfet/article_suppliers_snippet.html @@ -1,6 +1,6 @@
      diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html index a31cc3cf..0a8b58be 100644 --- a/kfet/templates/kfet/category.html +++ b/kfet/templates/kfet/category.html @@ -19,7 +19,7 @@
      diff --git a/kfet/templates/kfet/checkout.html b/kfet/templates/kfet/checkout.html index c2c5e4bc..96ce0577 100644 --- a/kfet/templates/kfet/checkout.html +++ b/kfet/templates/kfet/checkout.html @@ -26,7 +26,7 @@
      diff --git a/kfet/templates/kfet/checkout_read.html b/kfet/templates/kfet/checkout_read.html index 37a6e173..acfd4462 100644 --- a/kfet/templates/kfet/checkout_read.html +++ b/kfet/templates/kfet/checkout_read.html @@ -16,7 +16,7 @@ {% else %}
      diff --git a/kfet/templates/kfet/inventory.html b/kfet/templates/kfet/inventory.html index f05dc32a..bee373bc 100644 --- a/kfet/templates/kfet/inventory.html +++ b/kfet/templates/kfet/inventory.html @@ -19,7 +19,7 @@
      diff --git a/kfet/templates/kfet/inventory_read.html b/kfet/templates/kfet/inventory_read.html index 1edc21e0..964e81b0 100644 --- a/kfet/templates/kfet/inventory_read.html +++ b/kfet/templates/kfet/inventory_read.html @@ -29,7 +29,7 @@
      diff --git a/kfet/templates/kfet/order.html b/kfet/templates/kfet/order.html index 0e4ed868..37391b87 100644 --- a/kfet/templates/kfet/order.html +++ b/kfet/templates/kfet/order.html @@ -57,7 +57,7 @@
      diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index e2e7c4cd..7cb4d1cb 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -13,7 +13,7 @@
      diff --git a/kfet/templates/kfet/order_read.html b/kfet/templates/kfet/order_read.html index 9241c394..41abc381 100644 --- a/kfet/templates/kfet/order_read.html +++ b/kfet/templates/kfet/order_read.html @@ -44,7 +44,7 @@
      From 158b19778b19daeba35413027ba30b518d1110a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 7 Apr 2018 14:20:41 +0200 Subject: [PATCH 067/122] also sort the unsold table --- kfet/templates/kfet/article.html | 61 +++++++++++++++++++------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index 002abadd..b1f173c6 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -79,38 +79,49 @@

      Articles non vendus

      Article{{ not_sold_articles|length|pluralize }} non vendu{{ nots_sold_article|length|pluralize }}

      -
      +
      - - - + + + - - {% for article in not_sold_articles %} - {% ifchanged article.category %} - - - - {% endifchanged %} - - - - - - - - - {% endfor %} - + {% regroup not_sold_articles by category as not_sold_category_list %} + + {% for category in not_sold_category_list %} + + + + + + + {% for article in category.list %} + + + + + + + {% with last_inventory=article.inventory.0 %} + + {% endwith %} + + {% endfor %} + + {% endfor %}
      Nom Prix StockEn venteAffichéDernier inventaireEn venteAffichéDernier inventaire
      {{ article.category.name }}
      - - {{ article.name }} - - {{ article.price }}€{{ article.stock }}{{ article.is_sold | yesno:"En vente,Non vendu"}}{{ article.hidden | yesno:"Caché,Affiché" }}{{ article.inventory.0.at }}
      {{ category.grouper }}
      + + {{ article.name }} + + {{ article.price }}€{{ article.stock }}{{ article.is_sold | yesno:"En vente,Non vendu"}}{{ article.hidden | yesno:"Caché,Affiché" }} + {{ last_inventory.at|date:'d/m/Y H:i' }} +
      From 53a4c78903d6b09611aaa9eb10ee9d36a05d8cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 7 Apr 2018 14:21:07 +0200 Subject: [PATCH 068/122] Remove duplicate line --- kfet/templates/kfet/article.html | 1 - 1 file changed, 1 deletion(-) diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index b1f173c6..6b48ddbb 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -76,7 +76,6 @@
    -

    Articles non vendus

    Article{{ not_sold_articles|length|pluralize }} non vendu{{ nots_sold_article|length|pluralize }}

    Date: Sun, 8 Apr 2018 22:32:59 +0200 Subject: [PATCH 069/122] Bump django-cors-headers --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 36370bdb..5ad482a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ channels==1.1.5 python-dateutil wagtail==1.10.* wagtailmenus==2.2.* -django-cors-headers==2.1.0 +django-cors-headers==2.2.0 # Production tools wheel From a7cd1e04cd3f93d6d421762b114cfef3dceb4244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 8 Apr 2018 22:33:19 +0200 Subject: [PATCH 070/122] prefer CORS_ORIGIN_WHITELIST to CORS_ORIGIN_REGEX_WHITELIST --- cof/settings/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index 00e03869..8ec003ad 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -210,9 +210,11 @@ AUTHENTICATION_BACKENDS = ( RECAPTCHA_USE_SSL = True -CORS_ORIGIN_REGEX_WHITELIST = ( +CORS_ORIGIN_WHITELIST = ( 'bda.ens.fr', + 'www.bda.ens.fr' 'cof.ens.fr', + 'www.cof.ens.fr', ) # Cache settings From 6168045c3aaed467d5135fa158ca4e1f311e7049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 9 Apr 2018 22:43:25 +0200 Subject: [PATCH 071/122] bda: swap double/autoquit descriptions --- bda/migrations/0012_swap_double_choice.py | 53 +++++++++++++++++++++++ bda/models.py | 4 +- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 bda/migrations/0012_swap_double_choice.py diff --git a/bda/migrations/0012_swap_double_choice.py b/bda/migrations/0012_swap_double_choice.py new file mode 100644 index 00000000..56f3e739 --- /dev/null +++ b/bda/migrations/0012_swap_double_choice.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def swap_double_choice(apps, schema_editor): + choices = apps.get_model("bda", "ChoixSpectacle").objects + + choices.filter(double_choice="double").update(double_choice="tmp") + choices.filter(double_choice="autoquit").update(double_choice="double") + choices.filter(double_choice="tmp").update(double_choice="autoquit") + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0011_tirage_appear_catalogue'), + ] + + operations = [ + # Temporarily allow an extra "tmp" value for the `double_choice` field + migrations.AlterField( + model_name='choixspectacle', + name='double_choice', + field=models.CharField( + verbose_name='Nombre de places', + max_length=10, + default='1', + choices=[ + ('tmp', 'tmp'), + ('1', '1 place'), + ('double', '2 places si possible, 1 sinon'), + ('autoquit', '2 places sinon rien') + ] + ), + ), + migrations.RunPython(swap_double_choice, migrations.RunPython.noop), + migrations.AlterField( + model_name='choixspectacle', + name='double_choice', + field=models.CharField( + verbose_name='Nombre de places', + max_length=10, + default='1', + choices=[ + ('1', '1 place'), + ('double', '2 places si possible, 1 sinon'), + ('autoquit', '2 places sinon rien') + ] + ), + ), + ] diff --git a/bda/models.py b/bda/models.py index 41462d70..63e08221 100644 --- a/bda/models.py +++ b/bda/models.py @@ -170,8 +170,8 @@ class Participant(models.Model): DOUBLE_CHOICES = ( ("1", "1 place"), - ("autoquit", "2 places si possible, 1 sinon"), - ("double", "2 places sinon rien"), + ("double", "2 places si possible, 1 sinon"), + ("autoquit", "2 places sinon rien"), ) From fc37c5a8a08d9ff430de2db1bbf6dd689c333d71 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Apr 2018 10:54:45 +0200 Subject: [PATCH 072/122] Annulation des reventes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - On peut annuler des reventes à tout point du processus - Le formulaire d'annulation donne plus d'informations --- bda/forms.py | 8 +++-- bda/templates/bda/revente/manage.html | 46 +++++++++++---------------- bda/views.py | 10 +----- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 90b0359f..59730e19 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -55,12 +55,16 @@ class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): label = "{show}{suffix}" suffix = "" 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: suffix = " -- Vendue à {firstname} {lastname}".format( firstname=obj.soldTo.user.first_name, lastname=obj.soldTo.user.last_name, ) + elif obj.shotgun: + suffix = " -- Tirage infructueux" + elif obj.notif_sent: + suffix = " -- Inscriptions au tirage en cours" else: # Ce n'est pas à nous : on ne voit jamais l'acheteur suffix = " -- Vendue par {firstname} {lastname}".format( @@ -103,10 +107,10 @@ class AnnulForm(forms.Form): self.fields['reventes'].queryset = ( participant.original_shows .filter(attribution__spectacle__date__gte=timezone.now(), - notif_sent=False, soldTo__isnull=True) .select_related('attribution__spectacle', 'attribution__spectacle__location') + .order_by('-date') ) diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index cf0ba80e..5147ff16 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -6,7 +6,7 @@

    Gestion des places que je revends

    {% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %} -{% if resellform.attributions %} +{% if resell_attributions %}

    Places non revendues

    @@ -29,34 +29,24 @@
    {% endif %} -{% if annul_reventes or overdue %} +{% if annul_reventes %}

    Places en cours de revente

    - {% if annul_reventes %} -
    - - Vous pouvez annuler les places mises en vente il y a moins d'une heure. -
    - {% endif %} - {% csrf_token %} -
    -
    -
      - {% for revente in annul_reventes %} -
    • {{ revente.tag }} {{ revente.choice_label }}
    • - {% endfor %} - {% for attrib in overdue %} -
    • - - {{ attrib.spectacle }} -
    • - {% endfor %} -
    -
    -
    - {% if annul_reventes %} - - {% endif %} +
    + + Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se. +
    + {% csrf_token %} +
    +
    +
      + {% for revente in annul_reventes %} +
    • {{ revente.tag }} {{ revente.choice_label }}
    • + {% endfor %} +
    +
    +
    +
    @@ -82,7 +72,7 @@ {% 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 %}

    Plus de reventes possibles !

    {% endif %} diff --git a/bda/views.py b/bda/views.py index e88e5955..b92f4aee 100644 --- a/bda/views.py +++ b/bda/views.py @@ -430,16 +430,8 @@ def revente_manage(request, tirage_id): - SpectacleRevente.remorse_time) revente.reset(new_date=new_date) - overdue = participant.attribution_set.filter( - spectacle__date__gte=timezone.now(), - revente__isnull=False, - revente__seller=participant, - revente__notif_sent=True)\ - .filter( - Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) - return render(request, "bda/revente/manage.html", - {'tirage': tirage, 'overdue': overdue, "soldform": soldform, + {'tirage': tirage, "soldform": soldform, "annulform": annulform, "resellform": resellform}) From dd9a81d891df9503fc82a32188ba35c2da00a71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 14 Apr 2018 14:00:37 +0200 Subject: [PATCH 073/122] Update install instructions --- README.md | 106 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index b9d736ae..939d8ac7 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,69 @@ ## Installation +Il est possible d'installer vargant sur votre machine de deux façons différentes : + +- L'installation manuelle (**recommandée** sous linux et OSX), plus légère +- L'installation via vagrant qui fonctionne aussi sous windows mais un peu plus lourde + +### Installation manuelle + +Il est fortement conseillé d'utiliser un environnement virtuel pour Python. + +Il vous faudra installer pip, les librairies de développement de python ainsi +que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous +Debian et dérivées (Ubuntu, ...) : + + sudo apt-get install python3-pip python3-dev python3-venv sqlite3 + +Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; +fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF +(le dossier où se trouve ce README), et créez-le maintenant : + + virtualenv -p python3 venv + +Pour l'activer, il faut taper + + . venv/bin/activate + +depuis le même dossier. + +Vous pouvez maintenant installer les dépendances Python depuis le fichier +`requirements-devel.txt` : + + pip install -U pip # parfois nécessaire la première fois + pip install -r requirements-devel.txt + +Pour terminer, copier le fichier `cof/settings/secret_example.py` vers +`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique +pour profiter de façon transparente des mises à jour du fichier: + + ln -s secret_example.py cof/settings/secret.py + + +#### Fin d'installation + +Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base +de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des +données bidons bien pratiques pour développer avec la commande suivante : + + bash provisioning/prepare_django.sh + +Vous êtes prêts à développer ! Lancer GestioCOF en faisant + + python manage.py runserver + + ### Vagrant -La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser +Une autre façon d'installer GestioCOF sur votre machine est d'utiliser [Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout le monde à la même configuration de développement (même sous Windows !), et l'installation se fait en une commande. Pour utiliser Vagrant, il faut le -[télécharger](https://www.vagrantup.com/downloads.html) et l'installer. +[télécharger](https://www.vagrantup.com/downloads.html) et l'installer. Si vous êtes sous Linux, votre distribution propose probablement des paquets Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui @@ -83,55 +136,6 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url code change, il faut relancer le worker avec `sudo systemctl restart worker.service` pour visualiser la dernière version du code. - -### Installation manuelle - -Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant, -il est fortement conseillé d'utiliser un environnement virtuel pour Python. - -Il vous faudra installer pip, les librairies de développement de python ainsi -que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous -Debian et dérivées (Ubuntu, ...) : - - sudo apt-get install python3-pip python3-dev sqlite3 - -Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; -fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF -(le dossier où se trouve ce README), et créez-le maintenant : - - python3 -m venv venv - -Pour l'activer, il faut faire - - . venv/bin/activate - -dans le même dossier. - -Vous pouvez maintenant installer les dépendances Python depuis le fichier -`requirements-devel.txt` : - - pip install -U pip - pip install -r requirements-devel.txt - -Pour terminer, copier le fichier `cof/settings/secret_example.py` vers -`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique -pour profiter de façon transparente des mises à jour du fichier: - - ln -s secret_example.py cof/settings/secret.py - - -#### Fin d'installation - -Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base -de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des -données bidons bien pratiques pour développer avec la commande suivante : - - bash provisioning/prepare_django.sh - -Vous êtes prêts à développer ! Lancer GestioCOF en faisant - - python manage.py runserver - ### Mise à jour Pour mettre à jour les paquets Python, utiliser la commande suivante : From 769f4fc7b811994aa20fed6a165372127d4e6558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 14 Apr 2018 14:15:29 +0200 Subject: [PATCH 074/122] README: mention the test database and unit tests --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 939d8ac7..a97e623e 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,32 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire : python manage.py migrate +## Outils pour développer + +### Base de donnée + +Quelle que soit la méthode d'installation choisie, la base de donnée locale est + peuplée avec des données artificielles pour faciliter le développement. + +- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez + vous sur ce compte pour accéder à tout GestioCOF. +- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques + spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA. +- À chaque compte est associé un trigramme K-Fêt +- Un certain nombre d'articles K-Fêt sont renseignés. + +### Tests unitaires + +On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab +à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante. + +Pour lancer les tests : + +``` +python manage.py test +``` + + ## Documentation utilisateur Une brève documentation utilisateur est accessible sur le From 2faa8d6b65ff2d919fa26ebc3c83036c9db339cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 14 Apr 2018 23:11:14 +0200 Subject: [PATCH 075/122] =?UTF-8?q?README:=20typo,=20some=20links,=20?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a97e623e..a0dc5bc1 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ ## Installation -Il est possible d'installer vargant sur votre machine de deux façons différentes : +Il est possible d'installer GestioCOF sur votre machine de deux façons différentes : -- L'installation manuelle (**recommandée** sous linux et OSX), plus légère -- L'installation via vagrant qui fonctionne aussi sous windows mais un peu plus lourde +- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère +- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde ### Installation manuelle @@ -23,7 +23,7 @@ Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF (le dossier où se trouve ce README), et créez-le maintenant : - virtualenv -p python3 venv + python3 -m venv venv Pour l'activer, il faut taper @@ -52,6 +52,9 @@ données bidons bien pratiques pour développer avec la commande suivante : bash provisioning/prepare_django.sh +Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas +pour plus de détails. + Vous êtes prêts à développer ! Lancer GestioCOF en faisant python manage.py runserver From e21666a1129a46a0fd621cc753bfe3522d1075b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 16 Apr 2018 16:34:34 +0200 Subject: [PATCH 076/122] Fix old-style urls (registration) --- cof/urls.py | 3 ++- gestioncof/templates/gestioncof/registration_form.html | 2 +- gestioncof/templates/registration.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index 8aaa2bee..5e4e806c 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -68,7 +68,8 @@ urlpatterns = [ url(r'^registration/empty$', gestioncof_views.registration_form2, name="empty-registration"), # Autocompletion - url(r'^autocomplete/registration$', autocomplete), + url(r'^autocomplete/registration$', autocomplete, + name="cof.registration.autocomplete"), url(r'^user/autocomplete$', gestioncof_views.user_autocomplete, name='cof-user-autocomplete'), # Interface admin diff --git a/gestioncof/templates/gestioncof/registration_form.html b/gestioncof/templates/gestioncof/registration_form.html index 8668152b..37f24cff 100644 --- a/gestioncof/templates/gestioncof/registration_form.html +++ b/gestioncof/templates/gestioncof/registration_form.html @@ -7,7 +7,7 @@ {% else %}

    Inscription d'un nouveau compte (extérieur ?)

    {% endif %} -
    + {% csrf_token %}
    {{ user_form | bootstrap }} diff --git a/gestioncof/templates/registration.html b/gestioncof/templates/registration.html index 99ab3e73..6e6421cc 100644 --- a/gestioncof/templates/registration.html +++ b/gestioncof/templates/registration.html @@ -18,7 +18,7 @@ // On attend que la page soit prête pour executer le code $(document).ready(function() { $('input#search_autocomplete').yourlabsAutocomplete({ - url: '{% url 'gestioncof.autocomplete.autocomplete' %}', + url: '{% url 'cof.registration.autocomplete' %}', minimumCharacters: 3, id: 'search_autocomplete', choiceSelector: 'li:has(a)', From ece9a54df395f27efcf9ad988cfe9311f04af4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 14 May 2018 13:22:59 +0200 Subject: [PATCH 077/122] Upgrade to reCAPTCHA v2 reCAPTCHA v1 has been shut down since March 2018. We now uses reCAPTCHA v2: - user must check a simple checkbox (No CAPTCHA), - eventually he must validate a challenge. Moving keys settings allows to use the captcha for development. Fixes #192. --- cof/settings/common.py | 11 ++++++++--- cof/settings/prod.py | 6 +++++- gestioncof/templates/demande-petit-cours-raw.html | 2 +- requirements.txt | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index f53a46b8..02c796ad 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -42,9 +42,6 @@ REDIS_DB = import_secret("REDIS_DB") REDIS_HOST = import_secret("REDIS_HOST") REDIS_PORT = import_secret("REDIS_PORT") -RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") -RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") - KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") @@ -207,6 +204,14 @@ AUTHENTICATION_BACKENDS = ( 'kfet.auth.backends.GenericBackend', ) + +# reCAPTCHA settings +# https://github.com/praekelt/django-recaptcha +# +# Default settings authorize reCAPTCHA usage for local developement. +# Public and private keys are appended in the 'prod' module settings. + +NOCAPTCHA = True RECAPTCHA_USE_SSL = True CORS_ORIGIN_WHITELIST = ( diff --git a/cof/settings/prod.py b/cof/settings/prod.py index 2ffdf02f..fcdb3fdb 100644 --- a/cof/settings/prod.py +++ b/cof/settings/prod.py @@ -6,7 +6,7 @@ The settings that are not listed here are imported from .common import os from .common import * # NOQA -from .common import BASE_DIR +from .common import BASE_DIR, import_secret DEBUG = False @@ -28,3 +28,7 @@ STATIC_ROOT = os.path.join( STATIC_URL = "/gestion/static/" MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") MEDIA_URL = "/gestion/media/" + + +RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") +RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") diff --git a/gestioncof/templates/demande-petit-cours-raw.html b/gestioncof/templates/demande-petit-cours-raw.html index f9bafd8e..a218a0e0 100644 --- a/gestioncof/templates/demande-petit-cours-raw.html +++ b/gestioncof/templates/demande-petit-cours-raw.html @@ -7,7 +7,7 @@ {% if success %}

    Votre demande a été enregistrée avec succès !

    {% else %} - + {% csrf_token %}
    {{ form | bootstrap }} diff --git a/requirements.txt b/requirements.txt index 8ddf0c64..19b185c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ django-autocomplete-light==3.1.3 django-autoslug==1.9.3 django-cas-ng==3.5.7 django-djconfig==0.5.3 -django-recaptcha==1.2.1 +django-recaptcha==1.4.0 django-redis-cache==1.7.1 icalendar psycopg2 From 2a9125ffaaad0dc5ae48856426f8011d3b35a9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 24 May 2018 21:23:46 +0200 Subject: [PATCH 078/122] resolve migration conflict --- bda/migrations/0013_merge_20180524_2123.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 bda/migrations/0013_merge_20180524_2123.py diff --git a/bda/migrations/0013_merge_20180524_2123.py b/bda/migrations/0013_merge_20180524_2123.py new file mode 100644 index 00000000..ae8b0630 --- /dev/null +++ b/bda/migrations/0013_merge_20180524_2123.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-05-24 19:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0012_notif_time'), + ('bda', '0012_swap_double_choice'), + ] + + operations = [ + ] From b0301f0304b4665d0ef21cd691c465260962dbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 28 May 2018 00:07:34 +0200 Subject: [PATCH 079/122] fix slugurl name error --- kfet/templates/kfet/base_nav.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index f4c07e05..1cded20b 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,7 +1,7 @@ {% load i18n static %} {% load wagtailcore_tags %} -{% slugurl "kfet" as kfet_home_url %} +{% slugurl "k-fet" as kfet_home_url %}
    {{ form.as_table }} From 837b48af368115c813c0d2c6d720b81a8b917b1e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 29 May 2018 14:39:29 +0200 Subject: [PATCH 081/122] Fix js path for autocomplete --- kfet/templates/kfet/account_create_special.html | 2 +- kfet/templates/kfet/kpsul.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/templates/kfet/account_create_special.html b/kfet/templates/kfet/account_create_special.html index ecd6ac22..cd5ce7ba 100644 --- a/kfet/templates/kfet/account_create_special.html +++ b/kfet/templates/kfet/account_create_special.html @@ -5,7 +5,7 @@ {% block header-title %}Création d'un compte{% endblock %} {% block extra_head %} - + {% endblock %} {% block main-class %}content-form{% endblock %} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index a6a01d84..08b060bf 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -3,7 +3,7 @@ {% block extra_head %} - + {% endblock %} From 12ae10f2c4e2d98daa23bc4412ab9f6d775623df Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 30 May 2018 10:13:37 +0200 Subject: [PATCH 082/122] Fix le transfert des reventes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il y avait une typo qui causait une erreur quand on voulait transférer une revente. --- bda/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/views.py b/bda/views.py index 9593404e..b49eb030 100644 --- a/bda/views.py +++ b/bda/views.py @@ -410,7 +410,7 @@ def revente_manage(request, tirage_id): soldform = SoldForm(participant, request.POST, prefix='sold') if soldform.is_valid(): reventes = soldform.cleaned_data['reventes'] - for reventes in reventes: + for revente in reventes: revente.attribution.participant = revente.soldTo revente.attribution.save() From 68e71317cbf42f52dad5789d5e349e8f08309be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 1 Jun 2018 17:08:24 +0200 Subject: [PATCH 083/122] Hotfix: broken urls for mailing lists --- cof/urls.py | 6 ++++-- gestioncof/templates/utile_bda.html | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index 5e4e806c..a1d0c9bf 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -84,10 +84,12 @@ urlpatterns = [ name='utile_cof'), url(r'^utile_bda$', gestioncof_views.utile_bda, name='utile_bda'), - url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff), + url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff, + name="ml_diffbda"), url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof, name='ml_diffcof'), - url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), + url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente, + name="ml_bda_revente"), url(r'^k-fet/', include('kfet.urls')), url(r'^cms/', include(wagtailadmin_urls)), url(r'^documents/', include(wagtaildocs_urls)), diff --git a/gestioncof/templates/utile_bda.html b/gestioncof/templates/utile_bda.html index 8948de97..11e96f72 100644 --- a/gestioncof/templates/utile_bda.html +++ b/gestioncof/templates/utile_bda.html @@ -7,7 +7,7 @@

    Liens utiles du BdA

    Listes mail

    {% endblock %} From 645c01ebd19f883e35e24cf149d41d685669f50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 16 Jun 2018 12:33:51 +0200 Subject: [PATCH 084/122] =?UTF-8?q?kfet=20--=20As=20a=20kfet=20staff,=20re?= =?UTF-8?q?move=20the=20requirement=20to=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provide a password in the edit view of an account. --- kfet/forms.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 03fcce3a..522f20de 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -75,14 +75,19 @@ class AccountRestrictForm(AccountForm): class Meta(AccountForm.Meta): fields = ['is_frozen'] + class AccountPwdForm(forms.Form): pwd1 = forms.CharField( - label="Mot de passe K-Fêt", - help_text="Le mot de passe doit contenir au moins huit caractères", - widget=forms.PasswordInput) + label="Mot de passe K-Fêt", + required=False, + help_text="Le mot de passe doit contenir au moins huit caractères", + widget=forms.PasswordInput, + ) pwd2 = forms.CharField( - label="Confirmer le mot de passe", - widget=forms.PasswordInput) + label="Confirmer le mot de passe", + required=False, + widget=forms.PasswordInput, + ) def clean(self): pwd1 = self.cleaned_data.get('pwd1', '') @@ -93,6 +98,7 @@ class AccountPwdForm(forms.Form): raise ValidationError("Les mots de passes sont différents") super().clean() + class CofForm(forms.ModelForm): def clean_is_cof(self): instance = getattr(self, 'instance', None) From 898a354c2d419de3a9887a8eb2ed9b438f296b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 5 Aug 2018 18:11:10 +0200 Subject: [PATCH 085/122] Members can change their registration email --- gestioncof/forms.py | 22 ++++------- gestioncof/templates/gestioncof/profile.html | 41 +++++++++----------- gestioncof/tests/test_views.py | 6 +-- gestioncof/views.py | 24 +++++++----- 4 files changed, 44 insertions(+), 49 deletions(-) diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 4a9087fd..1fcc6ea6 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -170,25 +170,16 @@ class EventStatusFilterForm(forms.Form): yield ("has_paid", None, value) -class UserProfileForm(forms.ModelForm): - first_name = forms.CharField(label=_('Prénom'), max_length=30) - last_name = forms.CharField(label=_('Nom'), max_length=30) +class UserForm(forms.ModelForm): + class Meta: + model = User + fields = ["first_name", "last_name", "email"] - def __init__(self, *args, **kw): - super().__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: model = CofProfile - fields = ["first_name", "last_name", "phone", "mailing_cof", - "mailing_bda", "mailing_bda_revente"] + fields = ["phone", "mailing_cof", "mailing_bda", "mailing_bda_revente"] class RegistrationUserForm(forms.ModelForm): @@ -252,6 +243,7 @@ class RegistrationProfileForm(forms.ModelForm): "departement", "is_cof", "type_cotiz", "mailing_cof", "mailing_bda", "mailing_bda_revente", "comments") + STATUS_CHOICES = (('no', 'Non'), ('wait', 'Oui mais attente paiement'), ('paid', 'Oui payé'),) diff --git a/gestioncof/templates/gestioncof/profile.html b/gestioncof/templates/gestioncof/profile.html index 5decdfb3..9d418ef6 100644 --- a/gestioncof/templates/gestioncof/profile.html +++ b/gestioncof/templates/gestioncof/profile.html @@ -4,26 +4,23 @@ {% block page_size %}col-sm-8{%endblock%} {% block realcontent %} -

    Modifier mon profil

    - -
    - {% csrf_token %} - - {% for field in form %} - {{ field | bootstrap }} - {% endfor %} - -
    - {% if user.profile.comments %} -
    -

    Commentaires

    -

    - {{ user.profile.comments }} -

    -
    - {% endif %} -
    - -
    - +

    Modifier mon profil

    +
    +
    + {% csrf_token %} + {{ user_form | bootstrap }} + {{ profile_form | bootstrap }} +
    + + {% if user.profile.comments %} +
    +

    Commentaires

    +

    {{ user.profile.comments }}

    +
    + {% endif %} + +
    + +
    + {% endblock %} diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 47139327..1067e586 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -46,9 +46,9 @@ class ProfileViewTests(ViewTestCaseMixin, TestCase): u = self.users['member'] r = self.client.post(self.url, { - 'first_name': 'First', - 'last_name': 'Last', - 'phone': '', + 'u-first_name': 'First', + 'u-last_name': 'Last', + 'p-phone': '', # 'mailing_cof': '1', # 'mailing_bda': '1', # 'mailing_bda_revente': '1', diff --git a/gestioncof/views.py b/gestioncof/views.py index 6126eb10..64fe89a0 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -32,7 +32,8 @@ from gestioncof.models import EventCommentField, EventCommentValue, \ from gestioncof.models import CofProfile, Club from gestioncof.decorators import buro_required, cof_required from gestioncof.forms import ( - UserProfileForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm, + UserForm, ProfileForm, + EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm, EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm ) @@ -334,15 +335,20 @@ def survey_status(request, survey_id): @cof_required def profile(request): + user = request.user + data = request.POST if request.method == "POST" else None + user_form = UserForm(data=data, instance=user, prefix="u") + profile_form = ProfileForm(data=data, instance=user.profile, prefix="p") if request.method == "POST": - form = UserProfileForm(request.POST, instance=request.user.profile) - if form.is_valid(): - form.save() - messages.success(request, - "Votre profil a été mis à jour avec succès !") - else: - form = UserProfileForm(instance=request.user.profile) - return render(request, "gestioncof/profile.html", {"form": form}) + if user_form.is_valid() and profile_form.is_valid(): + user_form.save() + profile_form.save() + messages.success( + request, + _("Votre profil a été mis à jour avec succès !") + ) + context = {"user_form": user_form, "profile_form": profile_form} + return render(request, "gestioncof/profile.html", context) def registration_set_ro_fields(user_form, profile_form): From 66184fbee649a3eefc7ab6673f05b5814ee74721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 5 Aug 2018 18:34:05 +0200 Subject: [PATCH 086/122] CI: set python version to 3.5 --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19bcc736..e0ced08d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,5 @@ +image: "python:3.5" + services: - postgres:latest - redis:latest @@ -34,6 +36,7 @@ before_script: # 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 + - python --version test: stage: test From 91393dcea7caf4da586a66390c55ae88daed3272 Mon Sep 17 00:00:00 2001 From: Theo Delemazure Date: Sun, 2 Sep 2018 20:34:09 +0200 Subject: [PATCH 087/122] Update models.py --- gestioncof/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gestioncof/models.py b/gestioncof/models.py index 0d816155..8a5b6a53 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -72,6 +72,7 @@ class CofProfile(models.Model): TYPE_COTIZ_CHOICES)) mailing_cof = models.BooleanField("Recevoir les mails COF", default=False) mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False) + mailing_unernestaparis = models.BooleanField("Recevoir les mails unErnestAParis", default=False) mailing_bda_revente = models.BooleanField( "Recevoir les mails de revente de places BdA", default=False) comments = models.TextField( From 73cf39baa81ae39c4f70192e2a6ac160c4e31fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 2 Sep 2018 23:25:58 +0200 Subject: [PATCH 088/122] missing migration --- .../0014_cofprofile_mailing_unernestaparis.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py diff --git a/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py b/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py new file mode 100644 index 00000000..1d842329 --- /dev/null +++ b/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py @@ -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'), + ), + ] From 327ef210dbc0e74f2b6c1968cebcee2c08c1beb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 2 Sep 2018 23:26:18 +0200 Subject: [PATCH 089/122] make unernestaparis visible in forms --- gestioncof/forms.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 1fcc6ea6..4ad9b058 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -179,7 +179,13 @@ class UserForm(forms.ModelForm): class ProfileForm(forms.ModelForm): class Meta: model = CofProfile - fields = ["phone", "mailing_cof", "mailing_bda", "mailing_bda_revente"] + fields = [ + "phone", + "mailing_cof", + "mailing_bda", + "mailing_bda_revente", + "mailing_unernestaparis" + ] class RegistrationUserForm(forms.ModelForm): @@ -223,6 +229,7 @@ class RegistrationProfileForm(forms.ModelForm): self.fields['mailing_cof'].initial = True self.fields['mailing_bda'].initial = True self.fields['mailing_bda_revente'].initial = True + self.fields['mailing_unernestaparis'].initial = True self.fields.keyOrder = [ 'login_clipper', @@ -234,14 +241,16 @@ class RegistrationProfileForm(forms.ModelForm): 'mailing_cof', 'mailing_bda', 'mailing_bda_revente', + "mailing_unernestaparis", 'comments' - ] + ] class Meta: model = CofProfile fields = ("login_clipper", "phone", "occupation", "departement", "is_cof", "type_cotiz", "mailing_cof", - "mailing_bda", "mailing_bda_revente", "comments") + "mailing_bda", "mailing_bda_revente", + "mailing_unernestaparis", "comments") STATUS_CHOICES = (('no', 'Non'), From a750c62baf48cfab57d01f2d9b09372150fd06da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 2 Sep 2018 23:27:21 +0200 Subject: [PATCH 090/122] New year, new promotion: 2018 --- kfet/migrations/0064_promo_2018.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 kfet/migrations/0064_promo_2018.py diff --git a/kfet/migrations/0064_promo_2018.py b/kfet/migrations/0064_promo_2018.py new file mode 100644 index 00000000..c99d85b5 --- /dev/null +++ b/kfet/migrations/0064_promo_2018.py @@ -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 = [ + ('kfet', '0063_promo'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='promo', + field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018)], default=2018, null=True), + ), + ] From f297a1a0cf4b858a0d61b32be3b581fe9826df13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 9 Sep 2018 07:20:18 +0200 Subject: [PATCH 091/122] =?UTF-8?q?update=20hardcoded=20Mega=20views=20for?= =?UTF-8?q?=202018=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/tests/test_views.py | 6 ++--- gestioncof/views.py | 43 ++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 1067e586..f6dd7eb9 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -191,14 +191,14 @@ class MegaHelpers: u2 = create_user('u2') u2.profile.save() - m = Event.objects.create(title='MEGA 2017') + m = Event.objects.create(title='MEGA 2018') - cf1 = m.commentfields.create(name='Commentaire') + cf1 = m.commentfields.create(name='Commentaires') cf2 = m.commentfields.create( name='Comment Field 2', fieldtype='char', ) - option_type = m.options.create(name='Conscrit/Orga ?') + option_type = m.options.create(name='Orga ? Conscrit ?') choice_orga = option_type.choices.create(value='Orga') choice_conscrit = option_type.choices.create(value='Conscrit') diff --git a/gestioncof/views.py b/gestioncof/views.py index 64fe89a0..d77794bb 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -594,6 +594,19 @@ def export_members(request): return response +# ---------------------------------------- +# Début des exports Mega machins hardcodés +# ---------------------------------------- + + +MEGA_YEAR = 2018 +MEGA_EVENT_NAME = "MEGA 2018" +MEGA_COMMENTFIELD_NAME = "Commentaires" +MEGA_CONSCRITORGAFIELD_NAME = "Orga ? Conscrit ?" +MEGA_CONSCRIT = "Conscrit" +MEGA_ORGA = "Orga" + + def csv_export_mega(filename, qs): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename=' + filename @@ -615,13 +628,13 @@ def csv_export_mega(filename, qs): @buro_required def export_mega_remarksonly(request): - filename = 'remarques_mega_2017.csv' + filename = 'remarques_mega_{}.csv'.format(MEGA_YEAR) response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename=' + filename writer = unicodecsv.writer(response) - event = Event.objects.get(title="MEGA 2017") - commentfield = event.commentfields.get(name="Commentaire") + event = Event.objects.get(title=MEGA_EVENT_NAME) + commentfield = event.commentfields.get(name=MEGA_COMMENTFIELD_NAME) for val in commentfield.values.all(): reg = val.registration user = reg.user @@ -653,32 +666,36 @@ def export_mega_remarksonly(request): @buro_required def export_mega_orgas(request): - event = Event.objects.get(title="MEGA 2017") - type_option = event.options.get(name="Conscrit/Orga ?") - participant_type = type_option.choices.get(value="Orga").id + event = Event.objects.get(title=MEGA_EVENT_NAME) + type_option = event.options.get(name=MEGA_CONSCRITORGAFIELD_NAME) + participant_type = type_option.choices.get(value=MEGA_ORGA).id qs = EventRegistration.objects.filter(event=event).filter( options__id=participant_type ) - return csv_export_mega('orgas_mega_2017.csv', qs) + return csv_export_mega('orgas_mega_{}.csv'.format(MEGA_YEAR), qs) @buro_required def export_mega_participants(request): - event = Event.objects.get(title="MEGA 2017") - type_option = event.options.get(name="Conscrit/Orga ?") - participant_type = type_option.choices.get(value="Conscrit").id + event = Event.objects.get(title=MEGA_EVENT_NAME) + type_option = event.options.get(name=MEGA_CONSCRITORGAFIELD_NAME) + participant_type = type_option.choices.get(value=MEGA_CONSCRIT).id qs = EventRegistration.objects.filter(event=event).filter( options__id=participant_type ) - return csv_export_mega('participants_mega_2017.csv', qs) + return csv_export_mega('conscrits_mega_{}.csv'.format(MEGA_YEAR), qs) @buro_required def export_mega(request): - event = Event.objects.filter(title="MEGA 2017") + event = Event.objects.filter(title=MEGA_EVENT_NAME) qs = EventRegistration.objects.filter(event=event) \ .order_by("user__username") - return csv_export_mega('all_mega_2017.csv', qs) + return csv_export_mega('all_mega_{}.csv'.format(MEGA_YEAR), qs) + +# ------------------------------ +# Fin des exports Mega hardcodés +# ------------------------------ @buro_required From 44e5387f1572fe81af3339ad98a5a41384de4646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 30 Sep 2018 12:56:36 +0200 Subject: [PATCH 092/122] cof.tests -- Really check initial of built form --- gestioncof/tests/test_views.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 70d65bd6..865bb56e 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -217,9 +217,6 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): auth_user = 'staff' auth_forbidden = [None, 'user', 'member'] - def get_initial(self, form, name): - return form.get_initial_for_field(form.fields[name], name) - def test_empty(self): r = self.client.get(self.t_urls[0]) @@ -241,9 +238,9 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): events_form = r.context['event_formset'] clubs_form = r.context['clubs_form'] - self.assertEqual(self.get_initial(user_form, 'username'), 'user') - self.assertEqual(self.get_initial(user_form, 'first_name'), 'first') - self.assertEqual(self.get_initial(user_form, 'last_name'), 'last') + self.assertEqual(user_form['username'].initial, 'user') + self.assertEqual(user_form['first_name'].initial, 'first') + self.assertEqual(user_form['last_name'].initial, 'last') def test_clipper(self): r = self.client.get(self.t_urls[2]) @@ -253,13 +250,10 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): events_form = r.context['event_formset'] clubs_form = r.context['clubs_form'] - self.assertEqual(self.get_initial(user_form, 'first_name'), 'First') - self.assertEqual( - self.get_initial(user_form, 'last_name'), 'Last1 Last2') - self.assertEqual( - self.get_initial(user_form, 'email'), 'uid@clipper.ens.fr') - self.assertEqual( - self.get_initial(profile_form, 'login_clipper'), 'uid') + self.assertEqual(user_form['first_name'].initial, 'First') + self.assertEqual(user_form['last_name'].initial, 'Last1 Last2') + self.assertEqual(user_form['email'].initial, 'uid@clipper.ens.fr') + self.assertEqual(profile_form['login_clipper'].initial, 'uid') @override_settings(LDAP_SERVER_URL='ldap_url') From 064c23902bed0e118652f72ba96a92ae8c4a7e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 30 Sep 2018 12:56:58 +0200 Subject: [PATCH 093/122] cof.tests -- Address flake8 concerns --- gestioncof/tests/test_views.py | 40 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 865bb56e..08da029f 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -165,9 +165,9 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase): o2 = e.options.create(name='Option 2', multi_choices=True) oc1 = o1.choices.create(value='O1 - Choice 1') - oc2 = o1.choices.create(value='O1 - Choice 2') + o1.choices.create(value='O1 - Choice 2') oc3 = o2.choices.create(value='O2 - Choice 1') - oc4 = o2.choices.create(value='O2 - Choice 2') + o2.choices.create(value='O2 - Choice 2') self.client.post(self.url, dict(self._minimal_data, **{ 'username': 'user', @@ -220,10 +220,10 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): def test_empty(self): r = self.client.get(self.t_urls[0]) - user_form = r.context['user_form'] - profile_form = r.context['profile_form'] - events_form = r.context['event_formset'] - clubs_form = r.context['clubs_form'] + self.assertIn('user_form', r.context) + self.assertIn('profile_form', r.context) + self.assertIn('event_formset', r.context) + self.assertIn('clubs_form', r.context) def test_username(self): u = self.users['user'] @@ -233,11 +233,11 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.t_urls[1]) + self.assertIn('user_form', r.context) + self.assertIn('profile_form', r.context) + self.assertIn('event_formset', r.context) + self.assertIn('clubs_form', r.context) user_form = r.context['user_form'] - profile_form = r.context['profile_form'] - events_form = r.context['event_formset'] - clubs_form = r.context['clubs_form'] - self.assertEqual(user_form['username'].initial, 'user') self.assertEqual(user_form['first_name'].initial, 'first') self.assertEqual(user_form['last_name'].initial, 'last') @@ -245,11 +245,12 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): def test_clipper(self): r = self.client.get(self.t_urls[2]) + self.assertIn('user_form', r.context) + self.assertIn('profile_form', r.context) + self.assertIn('event_formset', r.context) + self.assertIn('clubs_form', r.context) user_form = r.context['user_form'] profile_form = r.context['profile_form'] - events_form = r.context['event_formset'] - clubs_form = r.context['clubs_form'] - self.assertEqual(user_form['first_name'].initial, 'First') self.assertEqual(user_form['last_name'].initial, 'Last1 Last2') self.assertEqual(user_form['email'].initial, 'uid@clipper.ens.fr') @@ -782,9 +783,10 @@ class CalendarViewTests(ViewTestCaseMixin, TestCase): fermeture=self.now, active=True, ) - l = Salle.objects.create() + location = Salle.objects.create() s = t.spectacle_set.create( - date=self.now, price=3.5, slots=20, location=l, listing=True) + date=self.now, price=3.5, slots=20, location=location, + listing=True) r = self.client.post(self.url, {'other_shows': [str(s.pk)]}) @@ -815,17 +817,17 @@ class CalendarICSViewTests(ViewTestCaseMixin, TestCase): fermeture=self.now, active=True, ) - l = Salle.objects.create(name='Location') + location = Salle.objects.create(name='Location') self.s1 = self.t.spectacle_set.create( - price=1, slots=10, location=l, listing=True, + price=1, slots=10, location=location, listing=True, title='Spectacle 1', date=self.now + timedelta(days=1), ) self.s2 = self.t.spectacle_set.create( - price=2, slots=20, location=l, listing=True, + price=2, slots=20, location=location, listing=True, title='Spectacle 2', date=self.now + timedelta(days=2), ) self.s3 = self.t.spectacle_set.create( - price=3, slots=30, location=l, listing=True, + price=3, slots=30, location=location, listing=True, title='Spectacle 3', date=self.now + timedelta(days=3), ) From 6858df02be7772b06d04199406f32eee60ad71c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 30 Sep 2018 13:22:22 +0200 Subject: [PATCH 094/122] bda.tests -- Use urllib urlencode --- bda/tests/test_views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index b0846fd7..39c11c79 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -2,9 +2,9 @@ import json from datetime import timedelta from unittest import mock +from urllib.parse import urlencode from django.contrib.auth.models import User -from django.template.defaultfilters import urlencode from django.test import TestCase, Client from django.utils import timezone @@ -61,7 +61,11 @@ class BdATestHelpers: return redirect_url elif user is None: # client is not logged in - return "/login?next={}".format(urlencode(urlencode(url))) + login_url = "/login" + if url: + login_url += "?{}".format(urlencode({"next": url}, + safe="/")) + return login_url else: return "/" From 79c26c9dd661115341895c2b4865db38cd5d8a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 1 Oct 2018 15:37:41 +0200 Subject: [PATCH 095/122] kfet -- Remove unused view --- kfet/urls.py | 2 -- kfet/views.py | 75 +-------------------------------------------------- 2 files changed, 1 insertion(+), 76 deletions(-) diff --git a/kfet/urls.py b/kfet/urls.py index 96fd4ddf..98d0bbf9 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -24,8 +24,6 @@ urlpatterns = [ # Account - Create url(r'^accounts/new$', views.account_create, name='kfet.account.create'), - url(r'^accounts/new_special$', views.account_create_special, - name='kfet.account.create_special'), url(r'^accounts/new/user/(?P.+)$', views.account_create_ajax, name='kfet.account.create.fromuser'), url(r'^accounts/new/clipper/(?P[\w-]+)/(?P.*)$', diff --git a/kfet/views.py b/kfet/views.py index 29f7411a..f3e70dde 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -78,79 +78,6 @@ def account_is_validandfree_ajax(request): data = Account.is_validandfree(trigramme) return JsonResponse(data) -# Account - Create - -@login_required -@teamkfet_required -def account_create_special(request): - - # Enregistrement - if request.method == "POST": - trigramme_form = AccountTriForm(request.POST, initial={'balance':0}) - balance_form = AccountBalanceForm(request.POST) - - # Peuplement des forms - username = request.POST.get('username') - login_clipper = request.POST.get('login_clipper') - - forms = get_account_create_forms( - request, username=username, login_clipper=login_clipper) - - account_form = forms['account_form'] - cof_form = forms['cof_form'] - user_form = forms['user_form'] - - if all((user_form.is_valid(), cof_form.is_valid(), - trigramme_form.is_valid(), account_form.is_valid(), - balance_form.is_valid())): - # Checking permission - if not request.user.has_perm('kfet.special_add_account'): - messages.error(request, 'Permission refusée') - else: - data = {} - # Fill data for Account.save() - put_cleaned_data_in_dict(data, user_form) - put_cleaned_data_in_dict(data, cof_form) - - try: - account = trigramme_form.save(data = data) - account_form = AccountNoTriForm(request.POST, instance=account) - account_form.save() - balance_form = AccountBalanceForm(request.POST, instance=account) - balance_form.save() - amount = balance_form.cleaned_data['balance'] - checkout = Checkout.objects.get(name='Initial') - is_cof = account.is_cof - opegroup = OperationGroup.objects.create( - on_acc=account, - checkout=checkout, - amount = amount, - is_cof = account.is_cof) - ope = Operation.objects.create( - group = opegroup, - type = Operation.INITIAL, - amount = amount) - messages.success(request, 'Compte créé : %s' % account.trigramme) - return redirect('kfet.account.create') - except Account.UserHasAccount as e: - messages.error(request, \ - "Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme) - else: - initial = { 'trigramme': request.GET.get('trigramme', '') } - trigramme_form = AccountTriForm(initial = initial) - balance_form = AccountBalanceForm(initial = {'balance': 0}) - account_form = None - cof_form = None - user_form = None - - return render(request, "kfet/account_create_special.html", { - 'trigramme_form': trigramme_form, - 'account_form': account_form, - 'cof_form': cof_form, - 'user_form': user_form, - 'balance_form': balance_form, - }) - # Account - Create @@ -704,7 +631,7 @@ class ArticleList(ListView): ) template_name = 'kfet/article.html' context_object_name = 'articles' - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) articles = context[self.context_object_name] From 93d3c124fdc98c79a42dcab58267ba47cca0271b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 1 Oct 2018 16:45:48 +0200 Subject: [PATCH 096/122] kfet -- Add fixme related to available checkouts in K-Psul --- kfet/forms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kfet/forms.py b/kfet/forms.py index 522f20de..5fcd30a6 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -273,6 +273,10 @@ class ArticleRestrictForm(ArticleForm): # ----- class KPsulOperationGroupForm(forms.ModelForm): + # FIXME(AD): Use timezone.now instead of timezone.now() to avoid using a + # fixed datetime (application boot here). + # One may even use: Checkout.objects.is_valid() if changing + # to now = timezone.now is ok in 'is_valid' definition. checkout = forms.ModelChoiceField( queryset = Checkout.objects.filter( is_protected=False, valid_from__lte=timezone.now(), From 0f688a8f1c6aaefee0c6a29c0be2dff0bb255e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 1 Oct 2018 23:03:45 +0200 Subject: [PATCH 097/122] kfet -- Stack errors of KPsulOperationForm Delete an error never raised, and avoid duplicate messages. --- kfet/forms.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 5fcd30a6..9d0fadd8 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -3,6 +3,7 @@ from decimal import Decimal from django import forms from django.core.exceptions import ValidationError +from django.core import validators from django.contrib.auth.models import User from django.forms import modelformset_factory from django.utils import timezone @@ -321,13 +322,18 @@ class KPsulOperationForm(forms.ModelForm): queryset=Article.objects.select_related('category').all(), required=False, widget = forms.HiddenInput()) + article_nb = forms.IntegerField( + required=False, + initial=None, + validators=[validators.MinValueValidator(1)], + widget=forms.HiddenInput(), + ) class Meta: model = Operation fields = ['type', 'amount', 'article', 'article_nb'] widgets = { 'type': forms.HiddenInput(), 'amount': forms.HiddenInput(), - 'article_nb': forms.HiddenInput(), } def clean(self): @@ -336,22 +342,26 @@ class KPsulOperationForm(forms.ModelForm): amount = self.cleaned_data.get('amount') article = self.cleaned_data.get('article') article_nb = self.cleaned_data.get('article_nb') + errors = [] if type_ope and type_ope == Operation.PURCHASE: - if not article or not article_nb: - raise ValidationError( - "Un achat nécessite un article et une quantité") - if article_nb < 1: - raise ValidationError("Impossible d'acheter moins de 1 article") + if not article or article_nb is None or article_nb < 1: + errors.append(ValidationError( + "Un achat nécessite un article et une quantité")) elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]: if not amount or article or article_nb: - raise ValidationError("Bad request") - if type_ope == Operation.DEPOSIT and amount <= 0: - raise ValidationError("Charge non positive") - if type_ope == Operation.WITHDRAW and amount >= 0: - raise ValidationError("Retrait non négatif") + errors.append(ValidationError("Bad request")) + else: + if type_ope == Operation.DEPOSIT and amount <= 0: + errors.append(ValidationError("Charge non positive")) + elif type_ope == Operation.WITHDRAW and amount >= 0: + errors.append(ValidationError("Retrait non négatif")) self.cleaned_data['article'] = None self.cleaned_data['article_nb'] = None + if errors: + raise ValidationError(errors) + + KPsulOperationFormSet = modelformset_factory( Operation, form = KPsulOperationForm, From 22011faba965a98768874e200d8b6a088f5084b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 2 Oct 2018 12:02:24 +0200 Subject: [PATCH 098/122] kfet -- Init KFetConfig, even without request, for easy testing --- kfet/config.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/kfet/config.py b/kfet/config.py index f248b370..452c2a09 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -1,25 +1,39 @@ from django.core.exceptions import ValidationError from django.db import models -from djconfig import config +import djconfig class KFetConfig(object): """kfet app configuration. Enhance isolation with backend used to store config. - Usable after DjConfig middleware was called. """ prefix = 'kfet_' + def __init__(self): + # Set this to False again to reload the config, e.g for testing + # purposes. + self._conf_init = False + + def _check_init(self): + # For initialization purposes, we call 'reload_maybe' directly + # (normaly done once per request in middleware). + # Note it should be called only once across requests, if you use + # kfet_config instance below. + if not self._conf_init: + djconfig.reload_maybe() + self._conf_init = True + def __getattr__(self, key): + self._check_init() if key == 'subvention_cof': # Allows accessing to the reduction as a subvention # Other reason: backward compatibility reduction_mult = 1 - self.reduction_cof/100 return (1/reduction_mult - 1) * 100 - return getattr(config, self._get_dj_key(key)) + return getattr(djconfig.config, self._get_dj_key(key)) def list(self): """Get list of kfet app configuration. @@ -30,7 +44,8 @@ class KFetConfig(object): """ # prevent circular imports from kfet.forms import KFetConfigForm - return [(field.label, getattr(config, name), ) + self._check_init() + return [(field.label, getattr(djconfig.config, name), ) for name, field in KFetConfigForm.base_fields.items()] def _get_dj_key(self, key): @@ -47,6 +62,7 @@ class KFetConfig(object): # prevent circular imports from kfet.forms import KFetConfigForm + self._check_init() # get old config new_cfg = KFetConfigForm().initial # update to new config From 507a59c9141c57fa0ab50f8050b6f1bc168c09c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 3 Oct 2018 18:01:19 +0200 Subject: [PATCH 099/122] kfet.tests -- Add tests for perform_operations view --- kfet/tests/test_views.py | 1434 +++++++++++++++++++++++++++++++++++++- kfet/tests/testcases.py | 11 +- 2 files changed, 1437 insertions(+), 8 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 40f895a1..28599937 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -10,12 +10,12 @@ from django.utils import timezone from ..config import kfet_config from ..models import ( - Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory, - InventoryArticle, Operation, OperationGroup, Order, OrderArticle, Supplier, - SupplierArticle, Transfer, TransferGroup, + Account, AccountNegative, Article, ArticleCategory, Checkout, + CheckoutStatement, Inventory, InventoryArticle, Operation, OperationGroup, + Order, OrderArticle, Supplier, SupplierArticle, Transfer, TransferGroup, ) from .testcases import ViewTestCaseMixin -from .utils import create_team, create_user, get_perms +from .utils import create_team, create_user, get_perms, user_add_perms class AccountListViewTests(ViewTestCaseMixin, TestCase): @@ -1452,6 +1452,42 @@ class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase): class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): + """ + Test cases for kpsul_perform_operations view. + + Below is the test ordering, try to keep this organized ;-) + + * OperationGroup: + - test_group... + - test_invalid_group... + * Operation: + - test_purchase... + - test_invalid_purchase... + - test_deposit... + - test_invalid_deposit... + - test_withdraw... + - test_invalid_withdraw... + - test_edit... + - test_invalid_edit... + * Addcost: + - test_addcost... + * Negative: + - test_negative... + - test_invalid_negative... + * More concrete examples: + - test_multi... + + To test valid requests, one should use '_assertResponseOk(response)' to get + hints about failure reasons, if any. + + At least one test per operation type should test the complete response and + behavior (HTTP, WebSocket, object updates, and object creations) + Other tests of the same operation type can only assert the specific + behavior differences. + + For invalid requests, response errors should be tested. + + """ url_name = 'kfet.kpsul.perform_operations' url_expected = '/k-fet/k-psul/perform_operations' @@ -1460,8 +1496,1394 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team' auth_forbidden = [None, 'user'] - def test_ok(self): - pass + with_liq = True + + def setUp(self): + super(KPsulPerformOperationsViewTests, self).setUp() + + # A Checkout, curently usable, balance=100 + self.checkout = Checkout.objects.create( + created_by=self.accounts["team"], + name="Checkout", + valid_from=timezone.now() - timedelta(days=7), + valid_to=timezone.now() + timedelta(days=7), + balance=Decimal("100.00"), + ) + # An Article, price=2.5, stock=20 + self.article = Article.objects.create( + category=ArticleCategory.objects.create(name="Category"), + name="Article", + price=Decimal("2.5"), + stock=20, + ) + # An Account, trigramme=000, balance=50 + # Do not assume user is cof, nor not cof. + self.account = self.accounts['user'] + self.account.balance = Decimal("50.00") + self.account.save() + + # Mock consumer of K-Psul websocket to catch what we're sending + kpsul_consumer_patcher = mock.patch("kfet.consumers.KPsul") + self.kpsul_consumer_mock = kpsul_consumer_patcher.start() + self.addCleanup(kpsul_consumer_patcher.stop) + + # Reset cache of kfet config + kfet_config._conf_init = False + + def _assertResponseOk(self, response): + """ + Asserts that status code of 'response' is 200, and returns the + deserialized content of the JSONResponse. + + In case status code is not 200, it prints the content of "errors" of + the response. + + """ + json_data = ( + json.loads(getattr(response, "content", b"{}").decode("utf-8")) + ) + try: + self.assertEqual(response.status_code, 200) + except AssertionError as exc: + msg = ( + "Expected response is 200, got {}. Errors: {}" + .format(response.status_code, json_data.get("errors")) + ) + raise AssertionError(msg) from exc + return json_data + + def get_base_post_data(self): + return { + # OperationGroup form + 'on_acc': str(self.account.pk), + 'checkout': str(self.checkout.pk), + # Operation formset + 'form-TOTAL_FORMS': '0', + 'form-INITIAL_FORMS': '0', + 'form-MIN_NUM_FORMS': '1', + 'form-MAX_NUM_FORMS': '1000', + } + + base_post_data = property(get_base_post_data) + + def test_invalid_group_on_acc(self): + data = dict(self.base_post_data, **{"on_acc": "GNR"}) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"]["operation_group"], ["on_acc"]) + + def test_group_on_acc_expects_comment(self): + user_add_perms( + self.users["team"], ["kfet.perform_commented_operations"] + ) + self.account.trigramme = "#13" + self.account.save() + self.assertTrue(self.account.need_comment) + + data = dict(self.base_post_data, **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + def test_invalid_group_on_acc_expects_comment(self): + user_add_perms( + self.users["team"], ["kfet.perform_commented_operations"] + ) + self.account.trigramme = "#13" + self.account.save() + self.assertTrue(self.account.need_comment) + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"]["need_comment"], True) + + def test_invalid_group_on_acc_needs_comment_requires_perm(self): + self.account.trigramme = "#13" + self.account.save() + self.assertTrue(self.account.need_comment) + + data = dict(self.base_post_data, **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["missing_perms"], + ["Enregistrer des commandes avec commentaires"], + ) + + def test_group_on_acc_frozen(self): + user_add_perms( + self.users["team"], ["kfet.override_frozen_protection"] + ) + self.account.is_frozen = True + self.account.save() + + data = dict(self.base_post_data, **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + def test_invalid_group_on_acc_frozen_requires_perm(self): + self.account.is_frozen = True + self.account.save() + + data = dict(self.base_post_data, **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["missing_perms"], + ["Forcer le gel d'un compte"], + ) + + def test_invalid_group_checkout(self): + self.checkout.valid_from -= timedelta(days=300) + self.checkout.valid_to -= timedelta(days=300) + self.checkout.save() + + data = dict(self.base_post_data) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"]["operation_group"], ["checkout"]) + + def test_invalid_group_expects_one_operation(self): + data = dict(self.base_post_data) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"]["operations"], []) + + def test_purchase_with_user_is_nof_cof(self): + self.account.cofprofile.is_cof = False + self.account.cofprofile.save() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + # Check response status + json_data = self._assertResponseOk(resp) + + # Check object creations + operation_group = OperationGroup.objects.get() + self.assertDictEqual(operation_group.__dict__, { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("-5.00"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": None, + }) + operation = Operation.objects.get() + self.assertDictEqual(operation.__dict__, { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-5.00"), + "article_id": self.article.pk, + "article_nb": 2, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "purchase", + }) + + # Check response content + self.assertDictEqual(json_data, { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }) + + # Check object updates + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("45.00")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("100.00")) + self.article.refresh_from_db() + self.assertEqual(self.article.stock, 18) + + # Check websocket data + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", + { + "opegroups": [ + { + "add": True, + "at": mock.ANY, + "amount": Decimal("-5.00"), + "checkout__name": "Checkout", + "comment": "", + "id": operation_group.pk, + "is_cof": False, + "on_acc__trigramme": "000", + "valid_by__trigramme": None, + "opes": [ + { + "id": operation.pk, + "addcost_amount": None, + "addcost_for__trigramme": None, + "amount": Decimal("-5.00"), + "article__name": "Article", + "article_nb": 2, + "canceled_at": None, + "canceled_by__trigramme": None, + "group_id": operation_group.pk, + "type": "purchase", + }, + ], + }, + ], + "checkouts": [ + { + "id": self.checkout.pk, + "balance": Decimal("100.00"), + }, + ], + "articles": [ + { + "id": self.article.pk, + "stock": 18, + }, + ], + }, + ) + + def test_purchase_with_user_is_cof(self): + kfet_config.set(kfet_reduction_cof=Decimal("20")) + self.account.cofprofile.is_cof = True + self.account.cofprofile.save() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-4.00")) + self.assertEqual(operation_group.is_cof, True) + operation = Operation.objects.get() + self.assertEqual(operation.amount, Decimal("-4.00")) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("46.00")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("100.00")) + self.article.refresh_from_db() + self.assertEqual(self.article.stock, 18) + + def test_purchase_with_cash(self): + data = dict(self.base_post_data, **{ + "on_acc": str(self.accounts["liq"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.on_acc, self.accounts["liq"]) + self.assertEqual(operation_group.is_cof, False) + + self.accounts["liq"].refresh_from_db() + self.assertEqual(self.accounts["liq"].balance, 0) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("105.00")) + self.article.refresh_from_db() + self.assertEqual(self.article.stock, 18) + + def test_invalid_purchase_expects_article(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": "", + "form-0-article_nb": "1", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], + [ + {"__all__": ["Un achat nécessite un article et une quantité"]}, + ], + ) + + def test_invalid_purchase_expects_article_nb(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], + [ + {"__all__": ["Un achat nécessite un article et une quantité"]}, + ], + ) + + def test_invalid_purchase_expects_article_nb_greater_than_1( + self + ): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "-1", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], + [ + { + "__all__": [ + "Un achat nécessite un article et une quantité", + ], + "article_nb": [ + "Assurez-vous que cette valeur est supérieure ou " + "égale à 1.", + ], + }, + ], + ) + + def test_invalid_operation_not_purchase_with_cash(self): + data = dict(self.base_post_data, **{ + "on_acc": str(self.accounts["liq"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10.00", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"]["account"], "LIQ") + + def test_deposit(self): + user_add_perms(self.users["team"], ["kfet.perform_deposit"]) + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + json_data = self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertDictEqual(operation_group.__dict__, { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("10.75"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": self.accounts["team"].pk, + }) + operation = Operation.objects.get() + self.assertDictEqual(operation.__dict__, { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("10.75"), + "article_id": None, + "article_nb": None, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "deposit", + }) + + self.assertDictEqual(json_data, { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("60.75")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("110.75")) + + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", + { + "opegroups": [ + { + "add": True, + "at": mock.ANY, + "amount": Decimal("10.75"), + "checkout__name": "Checkout", + "comment": "", + "id": operation_group.pk, + "is_cof": False, + "on_acc__trigramme": "000", + "valid_by__trigramme": "100", + "opes": [ + { + "id": operation.pk, + "addcost_amount": None, + "addcost_for__trigramme": None, + "amount": Decimal("10.75"), + "article__name": None, + "article_nb": None, + "canceled_at": None, + "canceled_by__trigramme": None, + "group_id": operation_group.pk, + "type": "deposit", + }, + ], + }, + ], + "checkouts": [ + { + "id": self.checkout.pk, + "balance": Decimal("110.75"), + }, + ], + "articles": [], + }, + ) + + def test_invalid_deposit_expects_amount(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + ) + + def test_invalid_deposit_too_many_params(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "3", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + ) + + def test_invalid_deposit_expects_positive_amount(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "-10", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], + [{"__all__": ["Charge non positive"]}] + ) + + def test_invalid_deposit_requires_perm(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["missing_perms"], ["Effectuer une charge"] + ) + + def test_withdraw(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "-10.75", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + json_data = self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertDictEqual(operation_group.__dict__, { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("-10.75"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": None, + }) + operation = Operation.objects.get() + self.assertDictEqual(operation.__dict__, { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-10.75"), + "article_id": None, + "article_nb": None, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "withdraw", + }) + + self.assertDictEqual(json_data, { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("39.25")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("89.25")) + + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", + { + "opegroups": [ + { + "add": True, + "at": mock.ANY, + "amount": Decimal("-10.75"), + "checkout__name": "Checkout", + "comment": "", + "id": operation_group.pk, + "is_cof": False, + "on_acc__trigramme": "000", + "valid_by__trigramme": None, + "opes": [ + { + "id": operation.pk, + "addcost_amount": None, + "addcost_for__trigramme": None, + "amount": Decimal("-10.75"), + "article__name": None, + "article_nb": None, + "canceled_at": None, + "canceled_by__trigramme": None, + "group_id": operation_group.pk, + "type": "withdraw", + }, + ], + }, + ], + "checkouts": [ + { + "id": self.checkout.pk, + "balance": Decimal("89.25"), + }, + ], + "articles": [], + }, + ) + + def test_invalid_withdraw_expects_amount(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + ) + + def test_invalid_withdraw_too_many_params(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "-10", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "3", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + ) + + def test_invalid_withdraw_expects_negative_amount(self): + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "10", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["operations"], + [{"__all__": ["Retrait non négatif"]}] + ) + + def test_edit(self): + user_add_perms(self.users["team"], ["kfet.edit_balance_account"]) + + data = dict(self.base_post_data, **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "edit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + json_data = self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertDictEqual(operation_group.__dict__, { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("10.75"), + "checkout_id": self.checkout.pk, + "comment": "A comment to explain it", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": self.accounts["team"].pk, + }) + operation = Operation.objects.get() + self.assertDictEqual(operation.__dict__, { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("10.75"), + "article_id": None, + "article_nb": None, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "edit", + }) + + self.assertDictEqual(json_data, { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("60.75")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("100.00")) + + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", + { + "opegroups": [ + { + "add": True, + "at": mock.ANY, + "amount": Decimal("10.75"), + "checkout__name": "Checkout", + "comment": "A comment to explain it", + "id": operation_group.pk, + "is_cof": False, + "on_acc__trigramme": "000", + "valid_by__trigramme": "100", + "opes": [ + { + "id": operation.pk, + "addcost_amount": None, + "addcost_for__trigramme": None, + "amount": Decimal("10.75"), + "article__name": None, + "article_nb": None, + "canceled_at": None, + "canceled_by__trigramme": None, + "group_id": operation_group.pk, + "type": "edit", + }, + ], + }, + ], + "checkouts": [ + { + "id": self.checkout.pk, + "balance": Decimal("100.00"), + }, + ], + "articles": [], + }, + ) + + def test_invalid_edit_requires_perm(self): + data = dict(self.base_post_data, **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "edit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"]["missing_perms"], + ["Modifier la balance d'un compte"], + ) + + def test_invalid_edit_expects_comment(self): + user_add_perms(self.users["team"], ["kfet.edit_balance_account"]) + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "edit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 400) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"]["need_comment"], True) + + def _setup_addcost(self): + self.register_user("addcost", create_user("addcost", "ADD")) + kfet_config.set( + addcost_amount=Decimal("0.50"), + addcost_for=self.accounts["addcost"], + ) + + def test_addcost_user_is_not_cof(self): + self.account.cofprofile.is_cof = False + self.account.cofprofile.save() + self._setup_addcost() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-6.00")) + operation = Operation.objects.get() + self.assertEqual(operation.addcost_for, self.accounts["addcost"]) + self.assertEqual(operation.addcost_amount, Decimal("1.00")) + self.assertEqual(operation.amount, Decimal("-6.00")) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("44.00")) + self.accounts["addcost"].refresh_from_db() + self.assertEqual(self.accounts["addcost"].balance, Decimal("1.00")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("100.00")) + + ws_data_ope = ( + self.kpsul_consumer_mock.group_send + .call_args[0][1]["opegroups"][0]["opes"][0] + ) + self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) + self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") + + def test_addcost_user_is_cof(self): + kfet_config.set(reduction_cof=Decimal("20")) + self.account.cofprofile.is_cof = True + self.account.cofprofile.save() + self._setup_addcost() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-4.80")) + operation = Operation.objects.get() + self.assertEqual(operation.addcost_for, self.accounts["addcost"]) + self.assertEqual(operation.addcost_amount, Decimal("0.80")) + self.assertEqual(operation.amount, Decimal("-4.80")) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("45.20")) + self.accounts["addcost"].refresh_from_db() + self.assertEqual(self.accounts["addcost"].balance, Decimal("0.80")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("100.00")) + + ws_data_ope = ( + self.kpsul_consumer_mock.group_send + .call_args[0][1]["opegroups"][0]["opes"][0] + ) + self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80")) + self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") + + def test_addcost_user_is_cash(self): + self.account.cofprofile.is_cof = True + self.account.cofprofile.save() + self._setup_addcost() + + data = dict(self.base_post_data, **{ + "on_acc": str(self.accounts["liq"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-6.00")) + operation = Operation.objects.get() + self.assertEqual(operation.addcost_for, self.accounts["addcost"]) + self.assertEqual(operation.addcost_amount, Decimal("1.00")) + self.assertEqual(operation.amount, Decimal("-6.00")) + + self.accounts["addcost"].refresh_from_db() + self.assertEqual(self.accounts["addcost"].balance, Decimal("1.00")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("106.00")) + + ws_data_ope = ( + self.kpsul_consumer_mock.group_send + .call_args[0][1]["opegroups"][0]["opes"][0] + ) + self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) + self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") + + def test_addcost_to_self(self): + self._setup_addcost() + self.accounts["addcost"].balance = Decimal("20.00") + self.accounts["addcost"].save() + + data = dict(self.base_post_data, **{ + "on_acc": str(self.accounts["addcost"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-5.00")) + operation = Operation.objects.get() + self.assertEqual(operation.addcost_for, None) + self.assertEqual(operation.addcost_amount, None) + self.assertEqual(operation.amount, Decimal("-5.00")) + + self.accounts["addcost"].refresh_from_db() + self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00")) + + ws_data_ope = ( + self.kpsul_consumer_mock.group_send + .call_args[0][1]["opegroups"][0]["opes"][0] + ) + self.assertEqual(ws_data_ope["addcost_amount"], None) + self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) + + def test_addcost_category_disabled(self): + self._setup_addcost() + self.article.category.has_addcost = False + self.article.category.save() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-5.00")) + operation = Operation.objects.get() + self.assertEqual(operation.addcost_for, None) + self.assertEqual(operation.addcost_amount, None) + self.assertEqual(operation.amount, Decimal("-5.00")) + + self.accounts["addcost"].refresh_from_db() + self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00")) + + ws_data_ope = ( + self.kpsul_consumer_mock.group_send + .call_args[0][1]["opegroups"][0]["opes"][0] + ) + self.assertEqual(ws_data_ope["addcost_amount"], None) + self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) + + def test_negative_new(self): + user_add_perms( + self.users["team"], ["kfet.perform_negative_operations"] + ) + self.account.balance = Decimal("1.00") + self.account.save() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("-4.00")) + + def test_negative_exists(self): + user_add_perms( + self.users["team"], ["kfet.perform_negative_operations"] + ) + self.account.balance = Decimal("-10.00") + self.account.save() + self.account.update_negative() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("-15.00")) + + def test_negative_exists_balance_higher_than_initial(self): + user_add_perms(self.users["team"], ["kfet.perform_deposit"]) + self.account.balance = Decimal("-10.00") + self.account.save() + self.account.update_negative() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "2", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "1", + "form-1-type": "deposit", + "form-1-amount": "5.00", + "form-1-article": "", + "form-1-article_nb": "", + }) + resp = self.client.post(self.url, data) + + self._assertResponseOk(resp) + + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("-7.50")) + + def test_invalid_negative_new_requires_perm(self): + self.account.balance = Decimal("1.00") + self.account.save() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual( + json_data["errors"], + { + "missing_perms": ["Enregistrer des commandes en négatif"], + }, + ) + + def test_invalid_negative_exceeds_allowed_duration_from_config(self): + user_add_perms( + self.users["team"], ["kfet.perform_negative_operations"] + ) + kfet_config.set(overdraft_duration=timedelta(days=5)) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.negative = AccountNegative.objects.create( + account=self.account, + start=timezone.now() - timedelta(days=5, minutes=1), + ) + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + + def test_invalid_negative_exceeds_allowed_duration_from_account(self): + user_add_perms( + self.users["team"], ["kfet.perform_negative_operations"] + ) + kfet_config.set(overdraft_duration=timedelta(days=5)) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.negative = AccountNegative.objects.create( + account=self.account, + start=timezone.now() - timedelta(days=3), + authz_overdraft_until=timezone.now() - timedelta(seconds=1), + ) + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + + def test_invalid_negative_exceeds_amount_allowed_from_config(self): + user_add_perms( + self.users["team"], ["kfet.perform_negative_operations"] + ) + kfet_config.set(overdraft_amount=Decimal("-1.00")) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.update_negative() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + + def test_invalid_negative_exceeds_amount_allowed_from_account(self): + user_add_perms( + self.users["team"], ["kfet.perform_negative_operations"] + ) + kfet_config.set(overdraft_amount=Decimal("10.00")) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.update_negative() + self.account.negative = AccountNegative.objects.create( + account=self.account, + start=timezone.now() - timedelta(days=3), + authz_overdraft_amount=Decimal("1.00"), + ) + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + }) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + + def test_multi_0(self): + article2 = Article.objects.create( + name="Article 2", + price=Decimal("4"), + stock=-5, + category=ArticleCategory.objects.first(), + ) + self.account.cofprofile.is_cof = False + self.account.cofprofile.save() + + data = dict(self.base_post_data, **{ + "form-TOTAL_FORMS": "2", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + "form-1-type": "purchase", + "form-1-amount": "", + "form-1-article": str(article2.pk), + "form-1-article_nb": "1", + }) + resp = self.client.post(self.url, data) + + # Check response status + json_data = self._assertResponseOk(resp) + + # Check object creations + operation_group = OperationGroup.objects.get() + self.assertDictEqual(operation_group.__dict__, { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("-9.00"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": None, + }) + operation_list = Operation.objects.all() + self.assertEqual(len(operation_list), 2) + self.assertDictEqual(operation_list[0].__dict__, { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-5.00"), + "article_id": self.article.pk, + "article_nb": 2, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "purchase", + }) + self.assertDictEqual(operation_list[1].__dict__, { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-4.00"), + "article_id": article2.pk, + "article_nb": 1, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "purchase", + }) + + # Check response content + self.assertDictEqual(json_data, { + "operationgroup": operation_group.pk, + "operations": [operation_list[0].pk, operation_list[1].pk], + "warnings": {}, + "errors": {}, + }) + + # Check object updates + self.account.refresh_from_db() + self.assertEqual(self.account.balance, Decimal("41.00")) + self.checkout.refresh_from_db() + self.assertEqual(self.checkout.balance, Decimal("100.00")) + self.article.refresh_from_db() + self.assertEqual(self.article.stock, 18) + article2.refresh_from_db() + self.assertEqual(article2.stock, -6) + + # Check websocket data + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", + { + "opegroups": [ + { + "add": True, + "at": mock.ANY, + "amount": Decimal("-9.00"), + "checkout__name": "Checkout", + "comment": "", + "id": operation_group.pk, + "is_cof": False, + "on_acc__trigramme": "000", + "valid_by__trigramme": None, + "opes": [ + { + "id": operation_list[0].pk, + "addcost_amount": None, + "addcost_for__trigramme": None, + "amount": Decimal("-5.00"), + "article__name": "Article", + "article_nb": 2, + "canceled_at": None, + "canceled_by__trigramme": None, + "group_id": operation_group.pk, + "type": "purchase", + }, + { + "id": operation_list[1].pk, + "addcost_amount": None, + "addcost_for__trigramme": None, + "amount": Decimal("-4.00"), + "article__name": "Article 2", + "article_nb": 1, + "canceled_at": None, + "canceled_by__trigramme": None, + "group_id": operation_group.pk, + "type": "purchase", + }, + ], + }, + ], + "checkouts": [ + { + "id": self.checkout.pk, + "balance": Decimal("100.00"), + }, + ], + "articles": [ + { + "id": self.article.pk, + "stock": 18, + }, + { + "id": article2.pk, + "stock": -6, + }, + ], + }, + ) class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index aa2fb1b6..3a69e9ca 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -178,7 +178,9 @@ class ViewTestCaseMixin(TestCaseMixin): During setup, three users are created with their kfet account: - 'user': a basic user without any permission, account trigramme: 000, - 'team': a user with kfet.is_team permission, account trigramme: 100, - - 'root': a superuser, account trigramme: 200. + - 'root': a superuser, account trigramme: 200, + - 'liq': if class attribute 'with_liq' is 'True', account + trigramme: LIQ. Their password is their username. One can create additionnal users with 'get_users_extra' method, or prevent @@ -221,6 +223,8 @@ class ViewTestCaseMixin(TestCaseMixin): auth_user = None auth_forbidden = [] + with_liq = False + def setUp(self): """ Warning: Do not forget to call super().setUp() in subclasses. @@ -262,7 +266,7 @@ class ViewTestCaseMixin(TestCaseMixin): """ # Format desc: username, password, trigramme - return { + users_base = { # user, user, 000 'user': create_user(), # team, team, 100 @@ -270,6 +274,9 @@ class ViewTestCaseMixin(TestCaseMixin): # root, root, 200 'root': create_root(), } + if self.with_liq: + users_base['liq'] = create_user('liq', 'LIQ') + return users_base @cached_property def users_base(self): From 7e55bf0cb1a6cfcd671c38b17576a58422921b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 1 Oct 2018 13:47:52 +0200 Subject: [PATCH 100/122] core -- Add code coverage to CI --- .gitignore | 1 + .gitlab-ci.yml | 5 ++++- README.md | 1 + setup.cfg | 16 ++++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index ab791b2e..2f3d166c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ venv/ media/ *.log *.sqlite3 +.coverage # PyCharm .idea diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0ced08d..e5efbf5b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,9 +36,12 @@ before_script: # 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 + - pip install coverage - python --version test: stage: test script: - - python manage.py test + - coverage run manage.py test + - coverage report + coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' diff --git a/README.md b/README.md index a0dc5bc1..803ef21f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # GestioCOF ![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg) +[![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 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..35eb701a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,16 @@ +[coverage:run] +source = + bda + cof + gestioncof + kfet + shared + utils +omit = + *migrations* + *test*.py +branch = true + +[coverage:report] +precision = 2 +show_missing = true From 6c5b7ed5cc53525ee7e27f3bcaeab5c3ba1f3ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 1 Oct 2018 13:56:07 +0200 Subject: [PATCH 101/122] core -- Update CI badge for current GitLab version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 803ef21f..524e558d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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 From cc4e3223b63de09f44b4ae003aa4aa21a4081b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 5 Oct 2018 23:16:06 +0200 Subject: [PATCH 102/122] core -- Disable coverage in GitLab CI --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e5efbf5b..1fd13307 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,4 +44,5 @@ test: script: - coverage run manage.py test - coverage report - coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' + # For GitLab, keep this commented. + # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' From b23810917d18a3351a0fa00d6ade665b0b0f59da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 03:09:01 +0200 Subject: [PATCH 103/122] core -- Remove not working cache of py installed packages... ... and use env var for pip install location. --- .gitlab-ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1fd13307..5386f031 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ variables: REDIS_PASSWD: "dummy" # Cached packages - PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" + PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip" # postgres service configuration POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" @@ -24,18 +24,16 @@ variables: cache: paths: - - vendor/python - - vendor/pip - - vendor/apt + - vendor/ before_script: - - mkdir -p vendor/{python,pip,apt} + - 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 --cache-dir vendor/pip -t vendor/python -r requirements.txt + - pip install --upgrade -r requirements.txt - pip install coverage - python --version From 104e71dcf620641ef20fed23579ca47d05ba1936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 03:14:49 +0200 Subject: [PATCH 104/122] core -- Add black,isort,flake8 to CI and pre-commit hook On CI: - black and isort in check mode must pass. - flake8 only prints errors WIP: make it also failed. On pre-commit: - black and isort will format staged files, if installed on path. - flake8 prints its output if necessary. --- .gitlab-ci.yml | 48 ++++++++++++------- .pre-commit.sh | 106 +++++++++++++++++++++++++++++++++++++++++ README.md | 10 ++++ pyproject.toml | 9 ++++ requirements-devel.txt | 5 ++ setup.cfg | 24 ++++++++++ 6 files changed, 186 insertions(+), 16 deletions(-) create mode 100755 .pre-commit.sh create mode 100644 pyproject.toml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5386f031..8fcd1144 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,25 +22,41 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD -cache: - paths: - - vendor/ - -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 - - pip install coverage - - python --version - 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: - coverage run manage.py test + after_script: - coverage report - # For GitLab, keep this commented. + cache: + key: test + paths: + - vendor/ + # For GitLab CI to get coverage from build. + # Keep this disabled for now, at 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/ diff --git a/.pre-commit.sh b/.pre-commit.sh new file mode 100755 index 00000000..e621b126 --- /dev/null +++ b/.pre-commit.sh @@ -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 + black --check $STAGED_PYTHON_FILES &>$BLACK_OUTPUT + printf "OK\n" + + if [ $? -ne 0 ]; then + black $STAGED_PYTHON_FILES &>$BLACK_OUTPUT + tail -1 $BLACK_OUTPUT + formatter_updated=1 + 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 + isort --check-only $STAGED_PYTHON_FILES &>$ISORT_OUTPUT + printf "OK\n" + + if [ $? -ne 0 ]; then + isort $STAGED_PYTHON_FILES &>$ISORT_OUTPUT + printf "Reformatted.\n" + formatter_updated=1 + 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 + flake8 $STAGED_PYTHON_FILES &>$FLAKE8_OUTPUT + + if [ $? -eq 0 ]; then + printf "OK\n" + else + printf "FAIL\n" + cat $FLAKE8_OUTPUT + checker_dirty=1 + 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 diff --git a/README.md b/README.md index 524e558d..2f08f3aa 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,16 @@ pour profiter de façon transparente des mises à jour du fichier: ln -s secret_example.py cof/settings/secret.py +Nous avons un git hook de pre-commit pour formatter et vérifier que votre code +vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez +encore l'installation *via* un lien symbolique: + + ln -s ../../.pre-commit.sh .git/hooks/pre-commit + +Pour plus d'informations à ce sujet, consulter la +[page](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/coding-style) +du wiki gestioCOF liée aux conventions. + #### Fin d'installation diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..93b26440 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.black] +# Automatically ignore files in .gitignore (opened at this time): +# https://github.com/ambv/black/issues/475 +exclude = ''' +/( + \.pyc + | venv +)/ +''' diff --git a/requirements-devel.txt b/requirements-devel.txt index 83053f76..6a5acdd7 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -2,3 +2,8 @@ django-debug-toolbar django-debug-panel ipython + +# Tools +# black # Uncomment when GC & most distros run with Python>=3.6 +flake8 +isort diff --git a/setup.cfg b/setup.cfg index 35eb701a..ec29c73c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,3 +14,27 @@ branch = true [coverage:report] precision = 2 show_missing = true + +[flake8] +exclude = migrations +max-line-length = 88 +ignore = + # whitespace before ':' (not PEP8-compliant for slicing) + E203, + # lambda expression + E731, + # line break before binary operator (not PEP8-compliant) + W503 + +[isort] +# For black compat: https://github.com/ambv/black#how-black-wraps-lines +combine_as_imports = true +default_section = THIRDPARTY +force_grid_wrap = 0 +include_trailing_comma = true +known_django = django +known_first_party = bda,cof,gestioncof,kfet,shared,utils +line_length = 88 +multi_line_output = 3 +not_skip = __init__.py +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER From fdd2b352897b0f47e9d3f827cf1f41abde073c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 12:35:49 +0200 Subject: [PATCH 105/122] core -- Apply black + isort to all files --- bda/__init__.py | 1 - bda/admin.py | 179 +- bda/algorithm.py | 27 +- bda/forms.py | 139 +- bda/management/commands/loadbdadevdata.py | 74 +- bda/management/commands/manage_reventes.py | 16 +- bda/management/commands/sendrappels.py | 19 +- bda/migrations/0001_initial.py | 212 +- bda/migrations/0002_add_tirage.py | 80 +- .../0003_update_tirage_and_spectacle.py | 14 +- bda/migrations/0004_mails-rappel.py | 20 +- bda/migrations/0005_encoding.py | 24 +- bda/migrations/0006_add_tirage_switch.py | 21 +- bda/migrations/0007_extends_spectacle.py | 117 +- bda/migrations/0008_py3.py | 121 +- bda/migrations/0009_revente.py | 106 +- .../0010_spectaclerevente_shotgun.py | 22 +- .../0011_tirage_appear_catalogue.py | 13 +- bda/migrations/0012_notif_time.py | 26 +- bda/migrations/0012_swap_double_choice.py | 38 +- bda/migrations/0013_merge_20180524_2123.py | 8 +- bda/models.py | 246 +- bda/tests/test_models.py | 46 +- bda/tests/test_revente.py | 74 +- bda/tests/test_views.py | 86 +- bda/urls.py | 103 +- bda/views.py | 659 ++-- cof/asgi.py | 1 + cof/locale/fr/formats.py | 2 +- cof/routing.py | 5 +- cof/settings/common.py | 242 +- cof/settings/dev.py | 25 +- cof/settings/local.py | 9 +- cof/settings/prod.py | 12 +- cof/settings/secret_example.py | 2 +- cof/urls.py | 161 +- gestioncof/__init__.py | 2 +- gestioncof/admin.py | 225 +- gestioncof/apps.py | 4 +- gestioncof/autocomplete.py | 73 +- gestioncof/csv_views.py | 15 +- gestioncof/decorators.py | 2 + gestioncof/forms.py | 285 +- gestioncof/management/base.py | 12 +- gestioncof/management/commands/loaddevdata.py | 47 +- gestioncof/management/commands/syncmails.py | 72 +- gestioncof/migrations/0001_initial.py | 955 +++-- .../0002_enable_unprocessed_demandes.py | 14 +- gestioncof/migrations/0003_event_image.py | 14 +- .../migrations/0004_registration_mail.py | 29 +- gestioncof/migrations/0005_encoding.py | 77 +- gestioncof/migrations/0006_add_calendar.py | 67 +- gestioncof/migrations/0007_alter_club.py | 49 +- gestioncof/migrations/0008_py3.py | 363 +- gestioncof/migrations/0009_delete_clipper.py | 10 +- .../migrations/0010_delete_custommail.py | 10 +- gestioncof/migrations/0011_longer_clippers.py | 14 +- .../migrations/0011_remove_cofprofile_num.py | 11 +- gestioncof/migrations/0012_merge.py | 7 +- gestioncof/migrations/0013_pei.py | 52 +- .../0014_cofprofile_mailing_unernestaparis.py | 14 +- gestioncof/models.py | 115 +- gestioncof/petits_cours_forms.py | 38 +- gestioncof/petits_cours_models.py | 112 +- gestioncof/petits_cours_views.py | 271 +- gestioncof/shared.py | 9 +- gestioncof/signals.py | 11 +- gestioncof/templatetags/utils.py | 14 +- gestioncof/tests/test_legacy.py | 10 +- gestioncof/tests/test_views.py | 1101 +++--- gestioncof/tests/testcases.py | 8 +- gestioncof/tests/utils.py | 33 +- gestioncof/urls.py | 104 +- gestioncof/views.py | 579 +-- gestioncof/widgets.py | 10 +- kfet/__init__.py | 2 +- kfet/apps.py | 3 +- kfet/auth/__init__.py | 6 +- kfet/auth/apps.py | 5 +- kfet/auth/backends.py | 7 +- kfet/auth/context_processors.py | 7 +- kfet/auth/fields.py | 7 +- kfet/auth/forms.py | 23 +- kfet/auth/middleware.py | 15 +- kfet/auth/migrations/0001_initial.py | 20 +- kfet/auth/models.py | 1 - kfet/auth/signals.py | 22 +- kfet/auth/tests.py | 161 +- kfet/auth/utils.py | 7 +- kfet/auth/views.py | 83 +- kfet/autocomplete.py | 95 +- kfet/cms/__init__.py | 2 +- kfet/cms/apps.py | 6 +- kfet/cms/context_processors.py | 14 +- kfet/cms/hooks.py | 6 +- .../management/commands/kfet_loadwagtail.py | 11 +- kfet/cms/migrations/0001_initial.py | 213 +- .../0002_alter_kfetpage_colcount.py | 17 +- kfet/cms/models.py | 120 +- kfet/config.py | 26 +- kfet/consumers.py | 4 +- kfet/context_processors.py | 2 +- kfet/decorators.py | 3 +- kfet/forms.py | 470 +-- kfet/management/commands/createopes.py | 124 +- kfet/management/commands/loadkfetdevdata.py | 80 +- kfet/migrations/0001_initial.py | 764 +++- kfet/migrations/0002_auto_20160802_2139.py | 19 +- kfet/migrations/0003_auto_20160802_2142.py | 13 +- kfet/migrations/0004_auto_20160802_2144.py | 12 +- kfet/migrations/0005_auto_20160802_2154.py | 27 +- kfet/migrations/0006_auto_20160804_0600.py | 17 +- kfet/migrations/0007_auto_20160804_0641.py | 12 +- kfet/migrations/0008_auto_20160804_1736.py | 19 +- kfet/migrations/0009_auto_20160805_0720.py | 14 +- kfet/migrations/0010_auto_20160806_2343.py | 27 +- kfet/migrations/0011_auto_20160807_1720.py | 16 +- kfet/migrations/0012_settings.py | 33 +- kfet/migrations/0013_auto_20160807_1840.py | 12 +- kfet/migrations/0014_auto_20160807_2314.py | 18 +- kfet/migrations/0015_auto_20160807_2324.py | 22 +- .../migrations/0016_settings_value_account.py | 20 +- kfet/migrations/0017_auto_20160808_0234.py | 16 +- kfet/migrations/0018_auto_20160808_0341.py | 23 +- kfet/migrations/0019_auto_20160808_0343.py | 23 +- kfet/migrations/0020_auto_20160808_0450.py | 17 +- kfet/migrations/0021_auto_20160808_0506.py | 12 +- kfet/migrations/0022_auto_20160808_0512.py | 22 +- kfet/migrations/0023_auto_20160808_0535.py | 16 +- .../0024_settings_value_duration.py | 12 +- kfet/migrations/0025_auto_20160809_0750.py | 24 +- kfet/migrations/0026_auto_20160809_0810.py | 12 +- kfet/migrations/0027_auto_20160811_0648.py | 60 +- kfet/migrations/0028_auto_20160820_0146.py | 36 +- kfet/migrations/0029_genericteamtoken.py | 22 +- kfet/migrations/0030_auto_20160821_0029.py | 25 +- kfet/migrations/0031_auto_20160822_0523.py | 26 +- kfet/migrations/0032_auto_20160822_2350.py | 70 +- .../0033_checkoutstatement_not_count.py | 12 +- kfet/migrations/0034_auto_20160823_0206.py | 12 +- kfet/migrations/0035_auto_20160823_1505.py | 30 +- kfet/migrations/0036_auto_20160823_1910.py | 31 +- kfet/migrations/0037_auto_20160826_2333.py | 51 +- kfet/migrations/0038_auto_20160828_0402.py | 42 +- kfet/migrations/0039_auto_20160828_0430.py | 14 +- kfet/migrations/0040_auto_20160829_2035.py | 38 +- kfet/migrations/0041_auto_20160830_1502.py | 35 +- kfet/migrations/0042_auto_20160831_0126.py | 36 +- kfet/migrations/0043_auto_20160901_0046.py | 57 +- kfet/migrations/0044_auto_20160901_1614.py | 40 +- kfet/migrations/0045_auto_20160905_0705.py | 56 +- kfet/migrations/0046_account_created_at.py | 12 +- kfet/migrations/0047_auto_20170104_1528.py | 56 +- kfet/migrations/0048_article_hidden.py | 15 +- kfet/migrations/0048_default_datetime.py | 14 +- kfet/migrations/0049_merge.py | 8 +- kfet/migrations/0050_remove_checkout.py | 34 +- kfet/migrations/0051_verbose_names.py | 335 +- kfet/migrations/0052_category_addcost.py | 20 +- kfet/migrations/0053_created_at.py | 12 +- kfet/migrations/0054_delete_settings.py | 36 +- kfet/migrations/0054_update_promos.py | 56 +- kfet/migrations/0055_move_permissions.py | 98 +- kfet/migrations/0056_change_account_meta.py | 26 +- kfet/migrations/0057_merge.py | 7 +- .../0058_delete_genericteamtoken.py | 10 +- kfet/migrations/0059_create_generic.py | 23 +- kfet/migrations/0060_amend_supplier.py | 41 +- kfet/migrations/0061_add_perms_config.py | 28 +- .../0062_delete_globalpermissions.py | 10 +- kfet/migrations/0063_promo.py | 57 +- kfet/migrations/0064_promo_2018.py | 57 +- kfet/models.py | 631 ++-- kfet/open/consumers.py | 5 +- kfet/open/open.py | 36 +- kfet/open/routing.py | 5 +- kfet/open/tests.py | 164 +- kfet/open/urls.py | 7 +- kfet/open/views.py | 13 +- kfet/routing.py | 5 +- kfet/statistic.py | 131 +- kfet/templatetags/dictionary_extras.py | 1 + kfet/templatetags/kfet_tags.py | 3 +- kfet/tests/test_config.py | 16 +- kfet/tests/test_forms.py | 19 +- kfet/tests/test_models.py | 12 +- kfet/tests/test_statistic.py | 27 +- kfet/tests/test_tests_utils.py | 69 +- kfet/tests/test_views.py | 3139 ++++++++--------- kfet/tests/testcases.py | 86 +- kfet/tests/utils.py | 68 +- kfet/urls.py | 357 +- kfet/utils.py | 28 +- kfet/views.py | 1953 +++++----- shared/tests/testcases.py | 92 +- utils/views/autocomplete.py | 5 +- 196 files changed, 10727 insertions(+), 8365 deletions(-) diff --git a/bda/__init__.py b/bda/__init__.py index 8b137891..e69de29b 100644 --- a/bda/__init__.py +++ b/bda/__init__.py @@ -1 +0,0 @@ - diff --git a/bda/admin.py b/bda/admin.py index 485471da..b32144f1 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,16 +1,24 @@ from datetime import timedelta -from custommail.shortcuts import send_mass_custom_mail +from custommail.shortcuts import send_mass_custom_mail +from dal.autocomplete import ModelSelect2 +from django import forms from django.contrib import admin -from django.db.models import Sum, Count +from django.db.models import Count, Sum from django.template.defaultfilters import pluralize from django.utils import timezone -from django import forms -from dal.autocomplete import ModelSelect2 - -from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ - Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente +from bda.models import ( + Attribution, + CategorieSpectacle, + ChoixSpectacle, + Participant, + Quote, + Salle, + Spectacle, + SpectacleRevente, + Tirage, +) class ReadOnlyMixin(object): @@ -27,8 +35,8 @@ class ReadOnlyMixin(object): class ChoixSpectacleAdminForm(forms.ModelForm): class Meta: widgets = { - 'participant': ModelSelect2(url='bda-participant-autocomplete'), - 'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'), + "participant": ModelSelect2(url="bda-participant-autocomplete"), + "spectacle": ModelSelect2(url="bda-spectacle-autocomplete"), } @@ -43,10 +51,10 @@ class AttributionTabularAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - spectacles = Spectacle.objects.select_related('location') + spectacles = Spectacle.objects.select_related("location") if self.listing is not None: spectacles = spectacles.filter(listing=self.listing) - self.fields['spectacle'].queryset = spectacles + self.fields["spectacle"].queryset = spectacles class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm): @@ -70,7 +78,7 @@ class AttributionInline(admin.TabularInline): class WithListingAttributionInline(AttributionInline): - exclude = ('given', ) + exclude = ("given",) form = WithListingAttributionTabularAdminForm listing = True @@ -81,12 +89,10 @@ class WithoutListingAttributionInline(AttributionInline): class ParticipantAdminForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['choicesrevente'].queryset = ( - Spectacle.objects - .select_related('location') + self.fields["choicesrevente"].queryset = Spectacle.objects.select_related( + "location" ) @@ -94,11 +100,13 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): inlines = [WithListingAttributionInline, WithoutListingAttributionInline] def get_queryset(self, request): - return Participant.objects.annotate(nb_places=Count('attributions'), - total=Sum('attributions__price')) + return Participant.objects.annotate( + nb_places=Count("attributions"), total=Sum("attributions__price") + ) def nb_places(self, obj): return obj.nb_places + nb_places.admin_order_field = "nb_places" nb_places.short_description = "Nombre de places" @@ -108,33 +116,32 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): return "%.02f €" % tot else: return "0 €" + total.admin_order_field = "total" total.short_description = "Total à payer" - list_display = ("user", "nb_places", "total", "paid", "paymenttype", - "tirage") + list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage") list_filter = ("paid", "tirage") - search_fields = ('user__username', 'user__first_name', 'user__last_name') - actions = ['send_attribs', ] + search_fields = ("user__username", "user__first_name", "user__last_name") + actions = ["send_attribs"] actions_on_bottom = True list_per_page = 400 readonly_fields = ("total",) - readonly_fields_update = ('user', 'tirage') + readonly_fields_update = ("user", "tirage") form = ParticipantAdminForm def send_attribs(self, request, queryset): datatuple = [] for member in queryset.all(): attribs = member.attributions.all() - context = {'member': member.user} + context = {"member": member.user} shortname = "" if len(attribs) == 0: shortname = "bda-attributions-decus" else: shortname = "bda-attributions" - context['places'] = attribs + context["places"] = attribs print(context) - datatuple.append((shortname, context, "bda@ens.fr", - [member.user.email])) + datatuple.append((shortname, context, "bda@ens.fr", [member.user.email])) send_mass_custom_mail(datatuple) count = len(queryset.all()) if count == 1: @@ -143,24 +150,23 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): else: message_bit = "%d membres ont" % count plural = "s" - self.message_user(request, "%s été informé%s avec succès." - % (message_bit, plural)) + self.message_user( + request, "%s été informé%s avec succès." % (message_bit, plural) + ) + send_attribs.short_description = "Envoyer les résultats par mail" class AttributionAdminForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if 'spectacle' in self.fields: - self.fields['spectacle'].queryset = ( - Spectacle.objects - .select_related('location') + if "spectacle" in self.fields: + self.fields["spectacle"].queryset = Spectacle.objects.select_related( + "location" ) - if 'participant' in self.fields: - self.fields['participant'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + if "participant" in self.fields: + self.fields["participant"].queryset = Participant.objects.select_related( + "user", "tirage" ) def clean(self): @@ -171,21 +177,26 @@ class AttributionAdminForm(forms.ModelForm): if participant.tirage != spectacle.tirage: raise forms.ValidationError( "Erreur : le participant et le spectacle n'appartiennent" - "pas au même tirage") + "pas au même tirage" + ) return cleaned_data class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): def paid(self, obj): return obj.participant.paid - paid.short_description = 'A payé' + + paid.short_description = "A payé" paid.boolean = True list_display = ("id", "spectacle", "participant", "given", "paid") - search_fields = ('spectacle__title', 'participant__user__username', - 'participant__user__first_name', - 'participant__user__last_name') + search_fields = ( + "spectacle__title", + "participant__user__username", + "participant__user__first_name", + "participant__user__last_name", + ) form = AttributionAdminForm - readonly_fields_update = ('spectacle', 'participant') + readonly_fields_update = ("spectacle", "participant") class ChoixSpectacleAdmin(admin.ModelAdmin): @@ -193,13 +204,15 @@ class ChoixSpectacleAdmin(admin.ModelAdmin): def tirage(self, obj): return obj.participant.tirage - list_display = ("participant", "tirage", "spectacle", "priority", - "double_choice") + + list_display = ("participant", "tirage", "spectacle", "priority", "double_choice") list_filter = ("double_choice", "participant__tirage") - search_fields = ('participant__user__username', - 'participant__user__first_name', - 'participant__user__last_name', - 'spectacle__title') + search_fields = ( + "participant__user__username", + "participant__user__first_name", + "participant__user__last_name", + "spectacle__title", + ) class QuoteInline(admin.TabularInline): @@ -209,42 +222,36 @@ class QuoteInline(admin.TabularInline): class SpectacleAdmin(admin.ModelAdmin): inlines = [QuoteInline] model = Spectacle - list_display = ("title", "date", "tirage", "location", "slots", "price", - "listing") - list_filter = ("location", "tirage",) + list_display = ("title", "date", "tirage", "location", "slots", "price", "listing") + list_filter = ("location", "tirage") search_fields = ("title", "location__name") - readonly_fields = ("rappel_sent", ) + readonly_fields = ("rappel_sent",) class TirageAdmin(admin.ModelAdmin): model = Tirage - list_display = ("title", "ouverture", "fermeture", "active", - "enable_do_tirage") - readonly_fields = ("tokens", ) - list_filter = ("active", ) - search_fields = ("title", ) + list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage") + readonly_fields = ("tokens",) + list_filter = ("active",) + search_fields = ("title",) class SalleAdmin(admin.ModelAdmin): model = Salle - search_fields = ('name', 'address') + search_fields = ("name", "address") class SpectacleReventeAdminForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['confirmed_entry'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + self.fields["confirmed_entry"].queryset = Participant.objects.select_related( + "user", "tirage" ) - self.fields['seller'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + self.fields["seller"].queryset = Participant.objects.select_related( + "user", "tirage" ) - self.fields['soldTo'].queryset = ( - Participant.objects - .select_related('user', 'tirage') + self.fields["soldTo"].queryset = Participant.objects.select_related( + "user", "tirage" ) @@ -252,6 +259,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin): """ Administration des reventes de spectacles """ + model = SpectacleRevente def spectacle(self, obj): @@ -263,12 +271,14 @@ class SpectacleReventeAdmin(admin.ModelAdmin): list_display = ("spectacle", "seller", "date", "soldTo") raw_id_fields = ("attribution",) readonly_fields = ("date_tirage",) - search_fields = ['attribution__spectacle__title', - 'seller__user__username', - 'seller__user__first_name', - 'seller__user__last_name'] + search_fields = [ + "attribution__spectacle__title", + "seller__user__username", + "seller__user__first_name", + "seller__user__last_name", + ] - actions = ['transfer', 'reinit'] + actions = ["transfer", "reinit"] actions_on_bottom = True form = SpectacleReventeAdminForm @@ -284,10 +294,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin): attrib.save() self.message_user( request, - "%d attribution%s %s été transférée%s avec succès." % ( - count, pluralize(count), - pluralize(count, "a,ont"), pluralize(count)) - ) + "%d attribution%s %s été transférée%s avec succès." + % (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)), + ) + transfer.short_description = "Transférer les reventes sélectionnées" def reinit(self, request, queryset): @@ -296,14 +306,15 @@ class SpectacleReventeAdmin(admin.ModelAdmin): """ count = queryset.count() 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)) self.message_user( request, - "%d attribution%s %s été réinitialisée%s avec succès." % ( - count, pluralize(count), - pluralize(count, "a,ont"), pluralize(count)) - ) + "%d attribution%s %s été réinitialisée%s avec succès." + % (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)), + ) + reinit.short_description = "Réinitialiser les reventes sélectionnées" diff --git a/bda/algorithm.py b/bda/algorithm.py index f0f48ad9..830ef119 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,7 +1,7 @@ -from django.db.models import Max - import random +from django.db.models import Max + class Algorithm(object): @@ -16,7 +16,7 @@ class Algorithm(object): show.requests - on crée des tables de demandes pour chaque personne, afin de pouvoir modifier les rankings""" - self.max_group = 2*max(choice.priority for choice in choices) + self.max_group = 2 * max(choice.priority for choice in choices) self.shows = [] showdict = {} for show in shows: @@ -54,16 +54,19 @@ class Algorithm(object): self.ranks[member][show] -= increment def appendResult(self, l, member, show): - l.append((member, - self.ranks[member][show], - self.origranks[member][show], - self.choices[member][show].double)) + l.append( + ( + member, + self.ranks[member][show], + self.origranks[member][show], + self.choices[member][show].double, + ) + ) def __call__(self, seed): random.seed(seed) results = [] - shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, - reverse=True) + shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True) for show in shows: # On regroupe tous les gens ayant le même rang groups = dict([(i, []) for i in range(1, self.max_group + 1)]) @@ -82,8 +85,10 @@ class Algorithm(object): if len(winners) + 1 < show.slots: self.appendResult(winners, member, show) self.appendResult(winners, member, show) - elif not self.choices[member][show].autoquit \ - and len(winners) < show.slots: + elif ( + not self.choices[member][show].autoquit + and len(winners) < show.slots + ): self.appendResult(winners, member, show) self.appendResult(losers, member, show) else: diff --git a/bda/forms.py b/bda/forms.py index 7e81587a..4560d3e5 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -6,7 +6,6 @@ from bda.models import Attribution, Spectacle, SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -16,9 +15,9 @@ class InscriptionInlineFormSet(BaseInlineFormSet): # set once for all "spectacle" field choices # - restrict choices to the spectacles of this tirage # - force_choices avoid many db requests - spectacles = tirage.spectacle_set.select_related('location') + spectacles = tirage.spectacle_set.select_related("location") choices = [(sp.pk, str(sp)) for sp in spectacles] - self.force_choices('spectacle', choices) + self.force_choices("spectacle", choices) def force_choices(self, name, choices): """Set choices of a field. @@ -30,7 +29,7 @@ class InscriptionInlineFormSet(BaseInlineFormSet): for form in self.forms: field = form.fields[name] if field.empty_label is not None: - field.choices = [('', field.empty_label)] + choices + field.choices = [("", field.empty_label)] + choices else: field.choices = choices @@ -56,125 +55,117 @@ class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): # C'est notre propre revente : pas besoin de spécifier le vendeur if obj.soldTo is not None: suffix = " -- Vendue à {firstname} {lastname}".format( - firstname=obj.soldTo.user.first_name, - lastname=obj.soldTo.user.last_name, - ) + firstname=obj.soldTo.user.first_name, + lastname=obj.soldTo.user.last_name, + ) else: # Ce n'est pas à nous : on ne voit jamais l'acheteur suffix = " -- Vendue par {firstname} {lastname}".format( - firstname=obj.seller.user.first_name, - lastname=obj.seller.user.last_name, - ) + firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name + ) - return label.format(show=str(obj.attribution.spectacle), - suffix=suffix) + return label.format(show=str(obj.attribution.spectacle), suffix=suffix) class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( - label='', - queryset=Attribution.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False) + label="", + queryset=Attribution.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['attributions'].queryset = ( - participant.attribution_set - .filter(spectacle__date__gte=timezone.now()) + self.fields["attributions"].queryset = ( + participant.attribution_set.filter(spectacle__date__gte=timezone.now()) .exclude(revente__seller=participant) - .select_related('spectacle', 'spectacle__location', - 'participant__user') + .select_related("spectacle", "spectacle__location", "participant__user") ) class AnnulForm(forms.Form): reventes = ReventeModelMultipleChoiceField( - own=True, - label='', - queryset=Attribution.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False) + own=True, + label="", + queryset=Attribution.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['reventes'].queryset = ( - participant.original_shows - .filter(attribution__spectacle__date__gte=timezone.now(), - notif_sent=False, - soldTo__isnull=True) - .select_related('attribution__spectacle', - 'attribution__spectacle__location') - ) + self.fields["reventes"].queryset = participant.original_shows.filter( + attribution__spectacle__date__gte=timezone.now(), + notif_sent=False, + soldTo__isnull=True, + ).select_related("attribution__spectacle", "attribution__spectacle__location") class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( - queryset=Spectacle.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False) + queryset=Spectacle.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, tirage, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['spectacles'].queryset = ( - tirage.spectacle_set - .select_related('location') - .filter(date__gte=timezone.now()) - ) + self.fields["spectacles"].queryset = tirage.spectacle_set.select_related( + "location" + ).filter(date__gte=timezone.now()) class ReventeTirageAnnulForm(forms.Form): reventes = ReventeModelMultipleChoiceField( - own=False, - label='', - queryset=SpectacleRevente.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False - ) + own=False, + label="", + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['reventes'].queryset = ( - participant.entered.filter(soldTo__isnull=True) - .select_related('attribution__spectacle', - 'seller__user') - ) + self.fields["reventes"].queryset = participant.entered.filter( + soldTo__isnull=True + ).select_related("attribution__spectacle", "seller__user") class ReventeTirageForm(forms.Form): reventes = ReventeModelMultipleChoiceField( - own=False, - label='', - queryset=SpectacleRevente.objects.none(), - widget=forms.CheckboxSelectMultiple, - required=False - ) + own=False, + label="", + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['reventes'].queryset = ( + self.fields["reventes"].queryset = ( SpectacleRevente.objects.filter( - notif_sent=True, - shotgun=False, - tirage_done=False - ).exclude(confirmed_entry=participant) - .select_related('attribution__spectacle') + notif_sent=True, shotgun=False, tirage_done=False + ) + .exclude(confirmed_entry=participant) + .select_related("attribution__spectacle") ) class SoldForm(forms.Form): reventes = ReventeModelMultipleChoiceField( - own=True, - label='', - queryset=Attribution.objects.none(), - widget=forms.CheckboxSelectMultiple) + own=True, + label="", + queryset=Attribution.objects.none(), + widget=forms.CheckboxSelectMultiple, + ) def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['reventes'].queryset = ( - participant.original_shows - .filter(soldTo__isnull=False) + self.fields["reventes"].queryset = ( + participant.original_shows.filter(soldTo__isnull=False) .exclude(soldTo=participant) - .select_related('attribution__spectacle', - 'attribution__spectacle__location') + .select_related( + "attribution__spectacle", "attribution__spectacle__location" + ) ) diff --git a/bda/management/commands/loadbdadevdata.py b/bda/management/commands/loadbdadevdata.py index a8e3f298..a608db6a 100644 --- a/bda/management/commands/loadbdadevdata.py +++ b/bda/management/commands/loadbdadevdata.py @@ -5,17 +5,15 @@ Crée deux tirages de test et y inscrit les utilisateurs import os import random -from django.utils import timezone from django.contrib.auth.models import User +from django.utils import timezone -from gestioncof.management.base import MyBaseCommand -from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle +from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage from bda.views import do_tirage - +from gestioncof.management.base import MyBaseCommand # Où sont stockés les fichiers json -DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), - 'data') +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") class Command(MyBaseCommand): @@ -27,27 +25,29 @@ class Command(MyBaseCommand): # --- Tirage.objects.all().delete() - Tirage.objects.bulk_create([ - Tirage( - title="Tirage de test 1", - ouverture=timezone.now()-timezone.timedelta(days=7), - fermeture=timezone.now(), - active=True - ), - Tirage( - title="Tirage de test 2", - ouverture=timezone.now(), - fermeture=timezone.now()+timezone.timedelta(days=60), - active=True - ) - ]) + Tirage.objects.bulk_create( + [ + Tirage( + title="Tirage de test 1", + ouverture=timezone.now() - timezone.timedelta(days=7), + fermeture=timezone.now(), + active=True, + ), + Tirage( + title="Tirage de test 2", + ouverture=timezone.now(), + fermeture=timezone.now() + timezone.timedelta(days=60), + active=True, + ), + ] + ) tirages = Tirage.objects.all() # --- # Salles # --- - locations = self.from_json('locations.json', DATA_DIR, Salle) + locations = self.from_json("locations.json", DATA_DIR, Salle) # --- # Spectacles @@ -60,15 +60,13 @@ class Command(MyBaseCommand): """ show.tirage = random.choice(tirages) show.listing = bool(random.randint(0, 1)) - show.date = ( - show.tirage.fermeture - + timezone.timedelta(days=random.randint(60, 90)) + show.date = show.tirage.fermeture + timezone.timedelta( + days=random.randint(60, 90) ) show.location = random.choice(locations) return show - shows = self.from_json( - 'shows.json', DATA_DIR, Spectacle, show_callback - ) + + shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback) # --- # Inscriptions @@ -79,23 +77,19 @@ class Command(MyBaseCommand): choices = [] for user in User.objects.filter(profile__is_cof=True): for tirage in tirages: - part, _ = Participant.objects.get_or_create( - user=user, - tirage=tirage - ) + part, _ = Participant.objects.get_or_create(user=user, tirage=tirage) shows = random.sample( - list(tirage.spectacle_set.all()), - tirage.spectacle_set.count() // 2 + list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2 ) for (rank, show) in enumerate(shows): - choices.append(ChoixSpectacle( - participant=part, - spectacle=show, - priority=rank + 1, - double_choice=random.choice( - ['1', 'double', 'autoquit'] + choices.append( + ChoixSpectacle( + participant=part, + spectacle=show, + priority=rank + 1, + double_choice=random.choice(["1", "double", "autoquit"]), ) - )) + ) ChoixSpectacle.objects.bulk_create(choices) self.stdout.write("- {:d} inscriptions générées".format(len(choices))) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 5a06d40b..52d25252 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -4,12 +4,12 @@ Gestion en ligne de commande des reventes. from django.core.management import BaseCommand from django.utils import timezone + from bda.models import SpectacleRevente class Command(BaseCommand): - help = "Envoie les mails de notification et effectue " \ - "les tirages au sort des reventes" + help = "Envoie les mails de notification et effectue " "les tirages au sort des reventes" leave_locale_alone = True def handle(self, *args, **options): @@ -28,22 +28,18 @@ class Command(BaseCommand): ) # 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)) revente.send_notif() self.stdout.write( - "Mails d'inscription à la revente [%s] envoyés" - % revente + "Mails d'inscription à la revente [%s] envoyés" % revente ) # 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)) winner = revente.tirage() - self.stdout.write( - "Tirage effectué pour la revente [%s]" - % revente - ) + self.stdout.write("Tirage effectué pour la revente [%s]" % revente) if winner: self.stdout.write("Gagnant : %s" % winner.user) diff --git a/bda/management/commands/sendrappels.py b/bda/management/commands/sendrappels.py index 82889f80..33f85330 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -3,27 +3,28 @@ Gestion en ligne de commande des mails de rappel. """ from datetime import timedelta + from django.core.management.base import BaseCommand from django.utils import timezone + from bda.models import Spectacle class Command(BaseCommand): - help = 'Envoie les mails de rappel des spectacles dont la date ' \ - 'approche.\nNe renvoie pas les mails déjà envoyés.' + help = "Envoie les mails de rappel des spectacles dont la date " "approche.\nNe renvoie pas les mails déjà envoyés." leave_locale_alone = True def handle(self, *args, **options): now = timezone.now() delay = timedelta(days=4) - shows = Spectacle.objects \ - .filter(date__range=(now, now+delay)) \ - .filter(tirage__active=True) \ - .filter(rappel_sent__isnull=True) \ + shows = ( + Spectacle.objects.filter(date__range=(now, now + delay)) + .filter(tirage__active=True) + .filter(rappel_sent__isnull=True) .all() + ) for show in shows: show.send_rappel() - self.stdout.write( - 'Mails de rappels pour %s envoyés avec succès.' % show) + self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show) if not shows: - self.stdout.write('Aucun mail à envoyer.') + self.stdout.write("Aucun mail à envoyer.") diff --git a/bda/migrations/0001_initial.py b/bda/migrations/0001_initial.py index c4494413..077ddd4e 100644 --- a/bda/migrations/0001_initial.py +++ b/bda/migrations/0001_initial.py @@ -1,108 +1,206 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='Attribution', + name="Attribution", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('given', models.BooleanField(default=False, verbose_name='Donn\xe9e')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("given", models.BooleanField(default=False, verbose_name="Donn\xe9e")), ], ), migrations.CreateModel( - name='ChoixSpectacle', + name="ChoixSpectacle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('priority', models.PositiveIntegerField(verbose_name=b'Priorit\xc3\xa9')), - ('double_choice', models.CharField(default=b'1', max_length=10, verbose_name=b'Nombre de places', choices=[(b'1', b'1 place'), (b'autoquit', b'2 places si possible, 1 sinon'), (b'double', b'2 places sinon rien')])), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "priority", + models.PositiveIntegerField(verbose_name=b"Priorit\xc3\xa9"), + ), + ( + "double_choice", + models.CharField( + default=b"1", + max_length=10, + verbose_name=b"Nombre de places", + choices=[ + (b"1", b"1 place"), + (b"autoquit", b"2 places si possible, 1 sinon"), + (b"double", b"2 places sinon rien"), + ], + ), + ), ], options={ - 'ordering': ('priority',), - 'verbose_name': 'voeu', - 'verbose_name_plural': 'voeux', + "ordering": ("priority",), + "verbose_name": "voeu", + "verbose_name_plural": "voeux", }, ), migrations.CreateModel( - name='Participant', + name="Participant", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('paid', models.BooleanField(default=False, verbose_name='A pay\xe9')), - ('paymenttype', models.CharField(blank=True, max_length=6, verbose_name='Moyen de paiement', choices=[(b'cash', 'Cash'), (b'cb', b'CB'), (b'cheque', 'Ch\xe8que'), (b'autre', 'Autre')])), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("paid", models.BooleanField(default=False, verbose_name="A pay\xe9")), + ( + "paymenttype", + models.CharField( + blank=True, + max_length=6, + verbose_name="Moyen de paiement", + choices=[ + (b"cash", "Cash"), + (b"cb", b"CB"), + (b"cheque", "Ch\xe8que"), + (b"autre", "Autre"), + ], + ), + ), ], ), migrations.CreateModel( - name='Salle', + name="Salle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=300, verbose_name=b'Nom')), - ('address', models.TextField(verbose_name=b'Adresse')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=300, verbose_name=b"Nom")), + ("address", models.TextField(verbose_name=b"Adresse")), ], ), migrations.CreateModel( - name='Spectacle', + name="Spectacle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=300, verbose_name=b'Titre')), - ('date', models.DateTimeField(verbose_name=b'Date & heure')), - ('description', models.TextField(verbose_name=b'Description', blank=True)), - ('slots_description', models.TextField(verbose_name=b'Description des places', blank=True)), - ('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)), - ('slots', models.IntegerField(verbose_name=b'Places')), - ('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')), - ('location', models.ForeignKey(to='bda.Salle', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=300, verbose_name=b"Titre")), + ("date", models.DateTimeField(verbose_name=b"Date & heure")), + ( + "description", + models.TextField(verbose_name=b"Description", blank=True), + ), + ( + "slots_description", + models.TextField( + verbose_name=b"Description des places", blank=True + ), + ), + ( + "price", + models.FloatField(verbose_name=b"Prix d'une place", blank=True), + ), + ("slots", models.IntegerField(verbose_name=b"Places")), + ( + "priority", + models.IntegerField(default=1000, verbose_name=b"Priorit\xc3\xa9"), + ), + ( + "location", + models.ForeignKey(to="bda.Salle", on_delete=models.CASCADE), + ), ], options={ - 'ordering': ('priority', 'date', 'title'), - 'verbose_name': 'Spectacle', + "ordering": ("priority", "date", "title"), + "verbose_name": "Spectacle", }, ), migrations.AddField( - model_name='participant', - name='attributions', - field=models.ManyToManyField(related_name='attributed_to', through='bda.Attribution', to='bda.Spectacle'), + model_name="participant", + name="attributions", + field=models.ManyToManyField( + related_name="attributed_to", + through="bda.Attribution", + to="bda.Spectacle", + ), ), migrations.AddField( - model_name='participant', - name='choices', - field=models.ManyToManyField(related_name='chosen_by', through='bda.ChoixSpectacle', to='bda.Spectacle'), + model_name="participant", + name="choices", + field=models.ManyToManyField( + related_name="chosen_by", + through="bda.ChoixSpectacle", + to="bda.Spectacle", + ), ), migrations.AddField( - model_name='participant', - name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="participant", + name="user", + field=models.OneToOneField( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='choixspectacle', - name='participant', - field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), + model_name="choixspectacle", + name="participant", + field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE), ), migrations.AddField( - model_name='choixspectacle', - name='spectacle', - field=models.ForeignKey(related_name='participants', to='bda.Spectacle', on_delete=models.CASCADE), + model_name="choixspectacle", + name="spectacle", + field=models.ForeignKey( + related_name="participants", + to="bda.Spectacle", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='attribution', - name='participant', - field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), + model_name="attribution", + name="participant", + field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE), ), migrations.AddField( - model_name='attribution', - name='spectacle', - field=models.ForeignKey(related_name='attribues', to='bda.Spectacle', on_delete=models.CASCADE), + model_name="attribution", + name="spectacle", + field=models.ForeignKey( + related_name="attribues", to="bda.Spectacle", on_delete=models.CASCADE + ), ), migrations.AlterUniqueTogether( - name='choixspectacle', - unique_together=set([('participant', 'spectacle')]), + name="choixspectacle", unique_together=set([("participant", "spectacle")]) ), ] diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py index 79f79a57..f4b01ed2 100644 --- a/bda/migrations/0002_add_tirage.py +++ b/bda/migrations/0002_add_tirage.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models from django.utils import timezone @@ -36,49 +36,77 @@ def fill_tirage_fields(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('bda', '0001_initial'), - ] + dependencies = [("bda", "0001_initial")] operations = [ migrations.CreateModel( - name='Tirage', + name="Tirage", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=300, verbose_name=b'Titre')), - ('ouverture', models.DateTimeField(verbose_name=b"Date et heure d'ouverture du tirage")), - ('fermeture', models.DateTimeField(verbose_name=b'Date et heure de fermerture du tirage')), - ('token', models.TextField(verbose_name=b'Graine du tirage', blank=True)), - ('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=300, verbose_name=b"Titre")), + ( + "ouverture", + models.DateTimeField( + verbose_name=b"Date et heure d'ouverture du tirage" + ), + ), + ( + "fermeture", + models.DateTimeField( + verbose_name=b"Date et heure de fermerture du tirage" + ), + ), + ( + "token", + models.TextField(verbose_name=b"Graine du tirage", blank=True), + ), + ( + "active", + models.BooleanField(default=True, verbose_name=b"Tirage actif"), + ), ], ), migrations.AlterField( - model_name='participant', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="participant", + name="user", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), # Create fields `spectacle` for `Participant` and `Spectacle` models. # These fields are not nullable, but we first create them as nullable # to give a default value for existing instances of these models. migrations.AddField( - model_name='participant', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), + model_name="participant", + name="tirage", + field=models.ForeignKey( + to="bda.Tirage", null=True, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='spectacle', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), + model_name="spectacle", + name="tirage", + field=models.ForeignKey( + to="bda.Tirage", null=True, on_delete=models.CASCADE + ), ), migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop), migrations.AlterField( - model_name='participant', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), + model_name="participant", + name="tirage", + field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE), ), migrations.AlterField( - model_name='spectacle', - name='tirage', - field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), + model_name="spectacle", + name="tirage", + field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0003_update_tirage_and_spectacle.py b/bda/migrations/0003_update_tirage_and_spectacle.py index f5ca671a..3548eb88 100644 --- a/bda/migrations/0003_update_tirage_and_spectacle.py +++ b/bda/migrations/0003_update_tirage_and_spectacle.py @@ -6,19 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0002_add_tirage'), - ] + dependencies = [("bda", "0002_add_tirage")] operations = [ migrations.AlterField( - model_name='spectacle', - name='price', + model_name="spectacle", + name="price", field=models.FloatField(verbose_name=b"Prix d'une place"), ), migrations.AlterField( - model_name='tirage', - name='active', - field=models.BooleanField(default=False, verbose_name=b'Tirage actif'), + model_name="tirage", + name="active", + field=models.BooleanField(default=False, verbose_name=b"Tirage actif"), ), ] diff --git a/bda/migrations/0004_mails-rappel.py b/bda/migrations/0004_mails-rappel.py index f17b711f..d331568a 100644 --- a/bda/migrations/0004_mails-rappel.py +++ b/bda/migrations/0004_mails-rappel.py @@ -6,20 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0003_update_tirage_and_spectacle'), - ] + dependencies = [("bda", "0003_update_tirage_and_spectacle")] operations = [ migrations.AddField( - model_name='spectacle', - name='listing', - field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'), + model_name="spectacle", + name="listing", + field=models.BooleanField( + default=False, verbose_name=b"Les places sont sur listing" + ), preserve_default=False, ), migrations.AddField( - model_name='spectacle', - name='rappel_sent', - field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True), + model_name="spectacle", + name="rappel_sent", + field=models.DateTimeField( + null=True, verbose_name=b"Mail de rappel envoy\xc3\xa9", blank=True + ), ), ] diff --git a/bda/migrations/0005_encoding.py b/bda/migrations/0005_encoding.py index b36113c2..eedfcee4 100644 --- a/bda/migrations/0005_encoding.py +++ b/bda/migrations/0005_encoding.py @@ -6,24 +6,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0004_mails-rappel'), - ] + dependencies = [("bda", "0004_mails-rappel")] operations = [ migrations.AlterField( - model_name='choixspectacle', - name='priority', - field=models.PositiveIntegerField(verbose_name='Priorit\xe9'), + model_name="choixspectacle", + name="priority", + field=models.PositiveIntegerField(verbose_name="Priorit\xe9"), ), migrations.AlterField( - model_name='spectacle', - name='priority', - field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'), + model_name="spectacle", + name="priority", + field=models.IntegerField(default=1000, verbose_name="Priorit\xe9"), ), migrations.AlterField( - model_name='spectacle', - name='rappel_sent', - field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True), + model_name="spectacle", + name="rappel_sent", + field=models.DateTimeField( + null=True, verbose_name="Mail de rappel envoy\xe9", blank=True + ), ), ] diff --git a/bda/migrations/0006_add_tirage_switch.py b/bda/migrations/0006_add_tirage_switch.py index fc923c9a..ccfe7505 100644 --- a/bda/migrations/0006_add_tirage_switch.py +++ b/bda/migrations/0006_add_tirage_switch.py @@ -10,26 +10,25 @@ def forwards_func(apps, schema_editor): db_alias = schema_editor.connection.alias for tirage in Tirage.objects.using(db_alias).all(): if tirage.tokens: - tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % ( - timezone.now().strftime("%y-%m-%d %H:%M:%S"), - tirage.tokens) + tirage.tokens = 'Before %s\n"""%s"""\n' % ( + timezone.now().strftime("%y-%m-%d %H:%M:%S"), + tirage.tokens, + ) tirage.save() class Migration(migrations.Migration): - dependencies = [ - ('bda', '0005_encoding'), - ] + dependencies = [("bda", "0005_encoding")] operations = [ - migrations.RenameField('tirage', 'token', 'tokens'), + migrations.RenameField("tirage", "token", "tokens"), migrations.AddField( - model_name='tirage', - name='enable_do_tirage', + model_name="tirage", + name="enable_do_tirage", field=models.BooleanField( - default=False, - verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'), + default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9" + ), ), migrations.RunPython(forwards_func, migrations.RunPython.noop), ] diff --git a/bda/migrations/0007_extends_spectacle.py b/bda/migrations/0007_extends_spectacle.py index 6ea11dc0..87182ff7 100644 --- a/bda/migrations/0007_extends_spectacle.py +++ b/bda/migrations/0007_extends_spectacle.py @@ -1,91 +1,100 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0006_add_tirage_switch'), - ] + dependencies = [("bda", "0006_add_tirage_switch")] operations = [ migrations.CreateModel( - name='CategorieSpectacle', + name="CategorieSpectacle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, - auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=100, verbose_name='Nom', - unique=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField(max_length=100, verbose_name="Nom", unique=True), + ), ], - options={ - 'verbose_name': 'Cat\xe9gorie', - }, + options={"verbose_name": "Cat\xe9gorie"}, ), migrations.CreateModel( - name='Quote', + name="Quote", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, - auto_created=True, primary_key=True)), - ('text', models.TextField(verbose_name='Citation')), - ('author', models.CharField(max_length=200, - verbose_name='Auteur')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("text", models.TextField(verbose_name="Citation")), + ("author", models.CharField(max_length=200, verbose_name="Auteur")), ], ), migrations.AlterModelOptions( - name='spectacle', - options={'ordering': ('date', 'title'), - 'verbose_name': 'Spectacle'}, - ), - migrations.RemoveField( - model_name='spectacle', - name='priority', + name="spectacle", + options={"ordering": ("date", "title"), "verbose_name": "Spectacle"}, ), + migrations.RemoveField(model_name="spectacle", name="priority"), migrations.AddField( - model_name='spectacle', - name='ext_link', + model_name="spectacle", + name="ext_link", field=models.CharField( max_length=500, - verbose_name='Lien vers le site du spectacle', - blank=True), + verbose_name="Lien vers le site du spectacle", + blank=True, + ), ), migrations.AddField( - model_name='spectacle', - name='image', - field=models.ImageField(upload_to='imgs/shows/', null=True, - verbose_name='Image', blank=True), + model_name="spectacle", + name="image", + field=models.ImageField( + upload_to="imgs/shows/", null=True, verbose_name="Image", blank=True + ), ), migrations.AlterField( - model_name='tirage', - name='enable_do_tirage', + model_name="tirage", + name="enable_do_tirage", field=models.BooleanField( - default=False, - verbose_name='Le tirage peut \xeatre lanc\xe9'), + default=False, verbose_name="Le tirage peut \xeatre lanc\xe9" + ), ), migrations.AlterField( - model_name='tirage', - name='tokens', - field=models.TextField(verbose_name='Graine(s) du tirage', - blank=True), + model_name="tirage", + name="tokens", + field=models.TextField(verbose_name="Graine(s) du tirage", blank=True), ), migrations.AddField( - model_name='spectacle', - name='category', - field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle', - on_delete=models.CASCADE, - null=True), + model_name="spectacle", + name="category", + field=models.ForeignKey( + blank=True, + to="bda.CategorieSpectacle", + on_delete=models.CASCADE, + null=True, + ), ), migrations.AddField( - model_name='spectacle', - name='vips', - field=models.TextField(verbose_name='Personnalit\xe9s', - blank=True), + model_name="spectacle", + name="vips", + field=models.TextField(verbose_name="Personnalit\xe9s", blank=True), ), migrations.AddField( - model_name='quote', - name='spectacle', - field=models.ForeignKey(to='bda.Spectacle', - on_delete=models.CASCADE), + model_name="quote", + name="spectacle", + field=models.ForeignKey(to="bda.Spectacle", on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0008_py3.py b/bda/migrations/0008_py3.py index fe6a8eaf..6aa69abd 100644 --- a/bda/migrations/0008_py3.py +++ b/bda/migrations/0008_py3.py @@ -1,103 +1,110 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0007_extends_spectacle'), - ] + dependencies = [("bda", "0007_extends_spectacle")] operations = [ migrations.AlterField( - model_name='choixspectacle', - name='double_choice', + model_name="choixspectacle", + name="double_choice", field=models.CharField( - verbose_name='Nombre de places', - choices=[('1', '1 place'), - ('autoquit', '2 places si possible, 1 sinon'), - ('double', '2 places sinon rien')], - max_length=10, default='1'), + verbose_name="Nombre de places", + choices=[ + ("1", "1 place"), + ("autoquit", "2 places si possible, 1 sinon"), + ("double", "2 places sinon rien"), + ], + max_length=10, + default="1", + ), ), migrations.AlterField( - model_name='participant', - name='paymenttype', + model_name="participant", + name="paymenttype", field=models.CharField( blank=True, - choices=[('cash', 'Cash'), ('cb', 'CB'), - ('cheque', 'Chèque'), ('autre', 'Autre')], - max_length=6, verbose_name='Moyen de paiement'), + choices=[ + ("cash", "Cash"), + ("cb", "CB"), + ("cheque", "Chèque"), + ("autre", "Autre"), + ], + max_length=6, + verbose_name="Moyen de paiement", + ), ), migrations.AlterField( - model_name='salle', - name='address', - field=models.TextField(verbose_name='Adresse'), + model_name="salle", + name="address", + field=models.TextField(verbose_name="Adresse"), ), migrations.AlterField( - model_name='salle', - name='name', - field=models.CharField(verbose_name='Nom', max_length=300), + model_name="salle", + name="name", + field=models.CharField(verbose_name="Nom", max_length=300), ), migrations.AlterField( - model_name='spectacle', - name='date', - field=models.DateTimeField(verbose_name='Date & heure'), + model_name="spectacle", + name="date", + field=models.DateTimeField(verbose_name="Date & heure"), ), migrations.AlterField( - model_name='spectacle', - name='description', - field=models.TextField(verbose_name='Description', blank=True), + model_name="spectacle", + name="description", + field=models.TextField(verbose_name="Description", blank=True), ), migrations.AlterField( - model_name='spectacle', - name='listing', - field=models.BooleanField( - verbose_name='Les places sont sur listing'), + model_name="spectacle", + name="listing", + field=models.BooleanField(verbose_name="Les places sont sur listing"), ), migrations.AlterField( - model_name='spectacle', - name='price', + model_name="spectacle", + name="price", field=models.FloatField(verbose_name="Prix d'une place"), ), migrations.AlterField( - model_name='spectacle', - name='slots', - field=models.IntegerField(verbose_name='Places'), + model_name="spectacle", + name="slots", + field=models.IntegerField(verbose_name="Places"), ), migrations.AlterField( - model_name='spectacle', - name='slots_description', - field=models.TextField(verbose_name='Description des places', - blank=True), + model_name="spectacle", + name="slots_description", + field=models.TextField(verbose_name="Description des places", blank=True), ), migrations.AlterField( - model_name='spectacle', - name='title', - field=models.CharField(verbose_name='Titre', max_length=300), + model_name="spectacle", + name="title", + field=models.CharField(verbose_name="Titre", max_length=300), ), migrations.AlterField( - model_name='tirage', - name='active', - field=models.BooleanField(verbose_name='Tirage actif', - default=False), + model_name="tirage", + name="active", + field=models.BooleanField(verbose_name="Tirage actif", default=False), ), migrations.AlterField( - model_name='tirage', - name='fermeture', + model_name="tirage", + name="fermeture", field=models.DateTimeField( - verbose_name='Date et heure de fermerture du tirage'), + verbose_name="Date et heure de fermerture du tirage" + ), ), migrations.AlterField( - model_name='tirage', - name='ouverture', + model_name="tirage", + name="ouverture", field=models.DateTimeField( - verbose_name="Date et heure d'ouverture du tirage"), + verbose_name="Date et heure d'ouverture du tirage" + ), ), migrations.AlterField( - model_name='tirage', - name='title', - field=models.CharField(verbose_name='Titre', max_length=300), + model_name="tirage", + name="title", + field=models.CharField(verbose_name="Titre", max_length=300), ), ] diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py index 70d6f338..d888140f 100644 --- a/bda/migrations/0009_revente.py +++ b/bda/migrations/0009_revente.py @@ -1,69 +1,87 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0008_py3'), - ] + dependencies = [("bda", "0008_py3")] operations = [ migrations.CreateModel( - name='SpectacleRevente', + name="SpectacleRevente", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, - auto_created=True, verbose_name='ID')), - ('date', models.DateTimeField( - verbose_name='Date de mise en vente', - default=django.utils.timezone.now)), - ('notif_sent', models.BooleanField( - verbose_name='Notification envoyée', default=False)), - ('tirage_done', models.BooleanField( - verbose_name='Tirage effectué', default=False)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ( + "date", + models.DateTimeField( + verbose_name="Date de mise en vente", + default=django.utils.timezone.now, + ), + ), + ( + "notif_sent", + models.BooleanField( + verbose_name="Notification envoyée", default=False + ), + ), + ( + "tirage_done", + models.BooleanField(verbose_name="Tirage effectué", default=False), + ), ], - options={ - 'verbose_name': 'Revente', - }, + options={"verbose_name": "Revente"}, ), migrations.AddField( - model_name='participant', - name='choicesrevente', - field=models.ManyToManyField(to='bda.Spectacle', - related_name='subscribed', - blank=True), + model_name="participant", + name="choicesrevente", + field=models.ManyToManyField( + to="bda.Spectacle", related_name="subscribed", blank=True + ), ), migrations.AddField( - model_name='spectaclerevente', - name='answered_mail', - field=models.ManyToManyField(to='bda.Participant', - related_name='wanted', - blank=True), + model_name="spectaclerevente", + name="answered_mail", + field=models.ManyToManyField( + to="bda.Participant", related_name="wanted", blank=True + ), ), migrations.AddField( - model_name='spectaclerevente', - name='attribution', - field=models.OneToOneField(to='bda.Attribution', - on_delete=models.CASCADE, - related_name='revente'), + model_name="spectaclerevente", + name="attribution", + field=models.OneToOneField( + to="bda.Attribution", on_delete=models.CASCADE, related_name="revente" + ), ), migrations.AddField( - model_name='spectaclerevente', - name='seller', - field=models.ForeignKey(to='bda.Participant', - on_delete=models.CASCADE, - verbose_name='Vendeur', - related_name='original_shows'), + model_name="spectaclerevente", + name="seller", + field=models.ForeignKey( + to="bda.Participant", + on_delete=models.CASCADE, + verbose_name="Vendeur", + related_name="original_shows", + ), ), migrations.AddField( - model_name='spectaclerevente', - name='soldTo', - field=models.ForeignKey(to='bda.Participant', - on_delete=models.CASCADE, - verbose_name='Vendue à', null=True, - blank=True), + model_name="spectaclerevente", + name="soldTo", + field=models.ForeignKey( + to="bda.Participant", + on_delete=models.CASCADE, + verbose_name="Vendue à", + null=True, + blank=True, + ), ), ] diff --git a/bda/migrations/0010_spectaclerevente_shotgun.py b/bda/migrations/0010_spectaclerevente_shotgun.py index 35b4da8a..da5c014c 100644 --- a/bda/migrations/0010_spectaclerevente_shotgun.py +++ b/bda/migrations/0010_spectaclerevente_shotgun.py @@ -1,33 +1,35 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations -from django.utils import timezone from datetime import timedelta +from django.db import migrations, models +from django.utils import timezone + def forwards_func(apps, schema_editor): SpectacleRevente = apps.get_model("bda", "SpectacleRevente") for revente in SpectacleRevente.objects.all(): is_expired = timezone.now() > revente.date_tirage() - is_direct = (revente.attribution.spectacle.date >= revente.date and - timezone.now() > revente.date + timedelta(minutes=15)) + is_direct = revente.attribution.spectacle.date >= revente.date and timezone.now() > revente.date + timedelta( + minutes=15 + ) revente.shotgun = is_expired or is_direct revente.save() class Migration(migrations.Migration): - dependencies = [ - ('bda', '0009_revente'), - ] + dependencies = [("bda", "0009_revente")] operations = [ migrations.AddField( - model_name='spectaclerevente', - name='shotgun', - field=models.BooleanField(default=False, verbose_name='Disponible imm\xe9diatement'), + model_name="spectaclerevente", + name="shotgun", + field=models.BooleanField( + default=False, verbose_name="Disponible imm\xe9diatement" + ), ), migrations.RunPython(forwards_func, migrations.RunPython.noop), ] diff --git a/bda/migrations/0011_tirage_appear_catalogue.py b/bda/migrations/0011_tirage_appear_catalogue.py index c2a2479d..446be392 100644 --- a/bda/migrations/0011_tirage_appear_catalogue.py +++ b/bda/migrations/0011_tirage_appear_catalogue.py @@ -6,17 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0010_spectaclerevente_shotgun'), - ] + dependencies = [("bda", "0010_spectaclerevente_shotgun")] operations = [ migrations.AddField( - model_name='tirage', - name='appear_catalogue', + model_name="tirage", + name="appear_catalogue", field=models.BooleanField( - default=False, - verbose_name='Tirage à afficher dans le catalogue' + default=False, verbose_name="Tirage à afficher dans le catalogue" ), - ), + ) ] diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py index ee777e35..96853a24 100644 --- a/bda/migrations/0012_notif_time.py +++ b/bda/migrations/0012_notif_time.py @@ -6,24 +6,26 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('bda', '0011_tirage_appear_catalogue'), - ] + dependencies = [("bda", "0011_tirage_appear_catalogue")] operations = [ migrations.RenameField( - model_name='spectaclerevente', - old_name='answered_mail', - new_name='confirmed_entry', + model_name="spectaclerevente", + old_name="answered_mail", + new_name="confirmed_entry", ), migrations.AlterField( - model_name='spectaclerevente', - name='confirmed_entry', - field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), + model_name="spectaclerevente", + name="confirmed_entry", + field=models.ManyToManyField( + blank=True, related_name="entered", to="bda.Participant" + ), ), migrations.AddField( - model_name='spectaclerevente', - name='notif_time', - field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True), + model_name="spectaclerevente", + name="notif_time", + field=models.DateTimeField( + blank=True, verbose_name="Moment d'envoi de la notification", null=True + ), ), ] diff --git a/bda/migrations/0012_swap_double_choice.py b/bda/migrations/0012_swap_double_choice.py index 56f3e739..e712f2ff 100644 --- a/bda/migrations/0012_swap_double_choice.py +++ b/bda/migrations/0012_swap_double_choice.py @@ -14,40 +14,38 @@ def swap_double_choice(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('bda', '0011_tirage_appear_catalogue'), - ] + dependencies = [("bda", "0011_tirage_appear_catalogue")] operations = [ # Temporarily allow an extra "tmp" value for the `double_choice` field migrations.AlterField( - model_name='choixspectacle', - name='double_choice', + model_name="choixspectacle", + name="double_choice", field=models.CharField( - verbose_name='Nombre de places', + verbose_name="Nombre de places", max_length=10, - default='1', + default="1", choices=[ - ('tmp', 'tmp'), - ('1', '1 place'), - ('double', '2 places si possible, 1 sinon'), - ('autoquit', '2 places sinon rien') - ] + ("tmp", "tmp"), + ("1", "1 place"), + ("double", "2 places si possible, 1 sinon"), + ("autoquit", "2 places sinon rien"), + ], ), ), migrations.RunPython(swap_double_choice, migrations.RunPython.noop), migrations.AlterField( - model_name='choixspectacle', - name='double_choice', + model_name="choixspectacle", + name="double_choice", field=models.CharField( - verbose_name='Nombre de places', + verbose_name="Nombre de places", max_length=10, - default='1', + default="1", choices=[ - ('1', '1 place'), - ('double', '2 places si possible, 1 sinon'), - ('autoquit', '2 places sinon rien') - ] + ("1", "1 place"), + ("double", "2 places si possible, 1 sinon"), + ("autoquit", "2 places sinon rien"), + ], ), ), ] diff --git a/bda/migrations/0013_merge_20180524_2123.py b/bda/migrations/0013_merge_20180524_2123.py index ae8b0630..8f78b6a9 100644 --- a/bda/migrations/0013_merge_20180524_2123.py +++ b/bda/migrations/0013_merge_20180524_2123.py @@ -7,10 +7,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('bda', '0012_notif_time'), - ('bda', '0012_swap_double_choice'), - ] + dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")] - operations = [ - ] + operations = [] diff --git a/bda/models.py b/bda/models.py index 63e01e31..bd4ea4cb 100644 --- a/bda/models.py +++ b/bda/models.py @@ -1,23 +1,22 @@ import calendar import random from datetime import timedelta -from custommail.shortcuts import send_mass_custom_mail +from custommail.models import CustomMail +from custommail.shortcuts import send_mass_custom_mail +from django.conf import settings +from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core import mail from django.db import models from django.db.models import Count -from django.contrib.auth.models import User -from django.conf import settings -from django.utils import timezone, formats - -from custommail.models import CustomMail +from django.utils import formats, timezone def get_generic_user(): generic, _ = User.objects.get_or_create( username="bda_generic", - defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"} + defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"}, ) return generic @@ -29,15 +28,15 @@ class Tirage(models.Model): tokens = models.TextField("Graine(s) du tirage", blank=True) active = models.BooleanField("Tirage actif", default=False) appear_catalogue = models.BooleanField( - "Tirage à afficher dans le catalogue", - default=False + "Tirage à afficher dans le catalogue", default=False ) - enable_do_tirage = models.BooleanField("Le tirage peut être lancé", - default=False) + enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False) def __str__(self): - return "%s - %s" % (self.title, formats.localize( - timezone.template_localtime(self.fermeture))) + return "%s - %s" % ( + self.title, + formats.localize(timezone.template_localtime(self.fermeture)), + ) class Salle(models.Model): @@ -49,7 +48,7 @@ class Salle(models.Model): class CategorieSpectacle(models.Model): - name = models.CharField('Nom', max_length=100, unique=True) + name = models.CharField("Nom", max_length=100, unique=True) def __str__(self): return self.name @@ -61,28 +60,26 @@ class CategorieSpectacle(models.Model): class Spectacle(models.Model): title = models.CharField("Titre", max_length=300) category = models.ForeignKey( - CategorieSpectacle, on_delete=models.CASCADE, - blank=True, null=True, + CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True ) date = models.DateTimeField("Date & heure") location = models.ForeignKey(Salle, on_delete=models.CASCADE) - vips = models.TextField('Personnalités', blank=True) + vips = models.TextField("Personnalités", blank=True) description = models.TextField("Description", blank=True) slots_description = models.TextField("Description des places", blank=True) - image = models.ImageField('Image', blank=True, null=True, - upload_to='imgs/shows/') - ext_link = models.CharField('Lien vers le site du spectacle', blank=True, - max_length=500) + image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/") + ext_link = models.CharField( + "Lien vers le site du spectacle", blank=True, max_length=500 + ) price = models.FloatField("Prix d'une place") slots = models.IntegerField("Places") tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) listing = models.BooleanField("Les places sont sur listing") - rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, - null=True) + rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True) class Meta: verbose_name = "Spectacle" - ordering = ("date", "title",) + ordering = ("date", "title") def timestamp(self): return "%d" % calendar.timegm(self.date.utctimetuple()) @@ -92,7 +89,7 @@ class Spectacle(models.Model): self.title, formats.localize(timezone.template_localtime(self.date)), self.location, - self.price + self.price, ) def getImgUrl(self): @@ -111,19 +108,21 @@ class Spectacle(models.Model): """ # On récupère la liste des participants + le BdA members = list( - User.objects - .filter(participant__attributions=self) - .annotate(nb_attr=Count("id")).order_by() + User.objects.filter(participant__attributions=self) + .annotate(nb_attr=Count("id")) + .order_by() ) bda_generic = get_generic_user() bda_generic.nb_attr = 1 members.append(bda_generic) # On écrit un mail personnalisé à chaque participant - datatuple = [( - 'bda-rappel', - {'member': member, "nb_attr": member.nb_attr, 'show': self}, - settings.MAIL_DATA['rappels']['FROM'], - [member.email]) + datatuple = [ + ( + "bda-rappel", + {"member": member, "nb_attr": member.nb_attr, "show": self}, + settings.MAIL_DATA["rappels"]["FROM"], + [member.email], + ) for member in members ] send_mass_custom_mail(datatuple) @@ -140,8 +139,8 @@ class Spectacle(models.Model): class Quote(models.Model): spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE) - text = models.TextField('Citation') - author = models.CharField('Auteur', max_length=200) + text = models.TextField("Citation") + author = models.CharField("Auteur", max_length=200) PAYMENT_TYPES = ( @@ -154,20 +153,20 @@ PAYMENT_TYPES = ( class Participant(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - choices = models.ManyToManyField(Spectacle, - through="ChoixSpectacle", - related_name="chosen_by") - attributions = models.ManyToManyField(Spectacle, - through="Attribution", - related_name="attributed_to") + choices = models.ManyToManyField( + Spectacle, through="ChoixSpectacle", related_name="chosen_by" + ) + attributions = models.ManyToManyField( + Spectacle, through="Attribution", related_name="attributed_to" + ) paid = models.BooleanField("A payé", default=False) - paymenttype = models.CharField("Moyen de paiement", - max_length=6, choices=PAYMENT_TYPES, - blank=True) + paymenttype = models.CharField( + "Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True + ) tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) - choicesrevente = models.ManyToManyField(Spectacle, - related_name="subscribed", - blank=True) + choicesrevente = models.ManyToManyField( + Spectacle, related_name="subscribed", blank=True + ) def __str__(self): return "%s - %s" % (self.user, self.tirage.title) @@ -183,30 +182,32 @@ DOUBLE_CHOICES = ( class ChoixSpectacle(models.Model): participant = models.ForeignKey(Participant, on_delete=models.CASCADE) spectacle = models.ForeignKey( - Spectacle, on_delete=models.CASCADE, - related_name="participants", + Spectacle, on_delete=models.CASCADE, related_name="participants" ) priority = models.PositiveIntegerField("Priorité") - double_choice = models.CharField("Nombre de places", - default="1", choices=DOUBLE_CHOICES, - max_length=10) + double_choice = models.CharField( + "Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10 + ) def get_double(self): return self.double_choice != "1" + double = property(get_double) def get_autoquit(self): return self.double_choice == "autoquit" + autoquit = property(get_autoquit) def __str__(self): return "Vœux de %s pour %s" % ( - self.participant.user.get_full_name(), - self.spectacle.title) + self.participant.user.get_full_name(), + self.spectacle.title, + ) class Meta: ordering = ("priority",) - unique_together = (("participant", "spectacle",),) + unique_together = (("participant", "spectacle"),) verbose_name = "voeu" verbose_name_plural = "voeux" @@ -214,48 +215,49 @@ class ChoixSpectacle(models.Model): class Attribution(models.Model): participant = models.ForeignKey(Participant, on_delete=models.CASCADE) spectacle = models.ForeignKey( - Spectacle, on_delete=models.CASCADE, - related_name="attribues", + Spectacle, on_delete=models.CASCADE, related_name="attribues" ) given = models.BooleanField("Donnée", default=False) def __str__(self): - return "%s -- %s, %s" % (self.participant.user, self.spectacle.title, - self.spectacle.date) + return "%s -- %s, %s" % ( + self.participant.user, + self.spectacle.title, + self.spectacle.date, + ) class SpectacleRevente(models.Model): attribution = models.OneToOneField( - Attribution, on_delete=models.CASCADE, - related_name="revente", + Attribution, on_delete=models.CASCADE, related_name="revente" + ) + date = models.DateTimeField("Date de mise en vente", default=timezone.now) + confirmed_entry = models.ManyToManyField( + Participant, related_name="entered", blank=True ) - date = models.DateTimeField("Date de mise en vente", - default=timezone.now) - confirmed_entry = models.ManyToManyField(Participant, - related_name="entered", - blank=True) seller = models.ForeignKey( - Participant, on_delete=models.CASCADE, + Participant, + on_delete=models.CASCADE, verbose_name="Vendeur", related_name="original_shows", ) soldTo = models.ForeignKey( - Participant, on_delete=models.CASCADE, + Participant, + on_delete=models.CASCADE, verbose_name="Vendue à", - blank=True, null=True, + blank=True, + null=True, ) - notif_sent = models.BooleanField("Notification envoyée", - default=False) + notif_sent = models.BooleanField("Notification envoyée", default=False) - notif_time = models.DateTimeField("Moment d'envoi de la notification", - blank=True, null=True) + notif_time = models.DateTimeField( + "Moment d'envoi de la notification", blank=True, null=True + ) - tirage_done = models.BooleanField("Tirage effectué", - default=False) + tirage_done = models.BooleanField("Tirage effectué", default=False) - shotgun = models.BooleanField("Disponible immédiatement", - default=False) + shotgun = models.BooleanField("Disponible immédiatement", default=False) #### # Some class attributes ### @@ -282,8 +284,9 @@ class SpectacleRevente(models.Model): def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - remaining_time = (self.attribution.spectacle.date - - self.real_notif_time - self.min_margin) + remaining_time = ( + self.attribution.spectacle.date - self.real_notif_time - self.min_margin + ) 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. """ spectacle_date = self.attribution.spectacle.date - return (spectacle_date <= timezone.now() + self.min_margin - + self.min_wait_time) + return spectacle_date <= timezone.now() + self.min_margin + self.min_wait_time @property def can_notif(self): - return (timezone.now() >= self.date + self.remorse_time) + return timezone.now() >= self.date + self.remorse_time def __str__(self): - return "%s -- %s" % (self.seller, - self.attribution.spectacle.title) + return "%s -- %s" % (self.seller, self.attribution.spectacle.title) class Meta: verbose_name = "Revente" @@ -327,17 +328,19 @@ class SpectacleRevente(models.Model): Envoie une notification pour indiquer la mise en vente d'une place sur BdA-Revente à tous les intéressés. """ - inscrits = self.attribution.spectacle.subscribed.select_related('user') - datatuple = [( - 'bda-revente', - { - 'member': participant.user, - 'show': self.attribution.spectacle, - 'revente': self, - 'site': Site.objects.get_current() - }, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email]) + inscrits = self.attribution.spectacle.subscribed.select_related("user") + datatuple = [ + ( + "bda-revente", + { + "member": participant.user, + "show": self.attribution.spectacle, + "revente": self, + "site": Site.objects.get_current(), + }, + settings.MAIL_DATA["revente"]["FROM"], + [participant.user.email], + ) for participant in inscrits ] send_mass_custom_mail(datatuple) @@ -350,16 +353,18 @@ class SpectacleRevente(models.Model): Envoie un mail à toutes les personnes intéréssées par le spectacle pour leur indiquer qu'il est désormais disponible au shotgun. """ - inscrits = self.attribution.spectacle.subscribed.select_related('user') - datatuple = [( - 'bda-shotgun', - { - 'member': participant.user, - 'show': self.attribution.spectacle, - 'site': Site.objects.get_current(), - }, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email]) + inscrits = self.attribution.spectacle.subscribed.select_related("user") + datatuple = [ + ( + "bda-shotgun", + { + "member": participant.user, + "show": self.attribution.spectacle, + "site": Site.objects.get_current(), + }, + settings.MAIL_DATA["revente"]["FROM"], + [participant.user.email], + ) for participant in inscrits ] send_mass_custom_mail(datatuple) @@ -389,30 +394,33 @@ class SpectacleRevente(models.Model): mails = [] context = { - 'acheteur': winner.user, - 'vendeur': seller.user, - 'show': spectacle, + "acheteur": winner.user, + "vendeur": seller.user, + "show": spectacle, } - c_mails_qs = CustomMail.objects.filter(shortname__in=[ - 'bda-revente-winner', 'bda-revente-loser', - 'bda-revente-seller', - ]) + c_mails_qs = CustomMail.objects.filter( + shortname__in=[ + "bda-revente-winner", + "bda-revente-loser", + "bda-revente-seller", + ] + ) c_mails = {cm.shortname: cm for cm in c_mails_qs} mails.append( - c_mails['bda-revente-winner'].get_message( + c_mails["bda-revente-winner"].get_message( context, - from_email=settings.MAIL_DATA['revente']['FROM'], + from_email=settings.MAIL_DATA["revente"]["FROM"], to=[winner.user.email], ) ) mails.append( - c_mails['bda-revente-seller'].get_message( + c_mails["bda-revente-seller"].get_message( context, - from_email=settings.MAIL_DATA['revente']['FROM'], + from_email=settings.MAIL_DATA["revente"]["FROM"], to=[seller.user.email], reply_to=[winner.user.email], ) @@ -422,12 +430,12 @@ class SpectacleRevente(models.Model): for inscrit in inscrits: if inscrit != winner: new_context = dict(context) - new_context['acheteur'] = inscrit.user + new_context["acheteur"] = inscrit.user mails.append( - c_mails['bda-revente-loser'].get_message( + c_mails["bda-revente-loser"].get_message( new_context, - from_email=settings.MAIL_DATA['revente']['FROM'], + from_email=settings.MAIL_DATA["revente"]["FROM"], to=[inscrit.user.email], ) ) diff --git a/bda/tests/test_models.py b/bda/tests/test_models.py index 95ce8646..b8a23ba7 100644 --- a/bda/tests/test_models.py +++ b/bda/tests/test_models.py @@ -7,28 +7,33 @@ from django.test import TestCase from django.utils import timezone from bda.models import ( - Attribution, Participant, Salle, Spectacle, SpectacleRevente, Tirage, + Attribution, + Participant, + Salle, + Spectacle, + SpectacleRevente, + Tirage, ) User = get_user_model() class SpectacleReventeTests(TestCase): - fixtures = ['gestioncof/management/data/custommail.json'] + fixtures = ["gestioncof/management/data/custommail.json"] def setUp(self): now = timezone.now() self.t = Tirage.objects.create( - title='Tirage', + title="Tirage", ouverture=now - timedelta(days=7), fermeture=now - timedelta(days=3), active=True, ) self.s = Spectacle.objects.create( - title='Spectacle', + title="Spectacle", date=now + timedelta(days=20), - location=Salle.objects.create(name='Salle', address='Address'), + location=Salle.objects.create(name="Salle", address="Address"), price=10.5, slots=5, tirage=self.t, @@ -36,31 +41,28 @@ class SpectacleReventeTests(TestCase): ) self.seller = Participant.objects.create( - user=User.objects.create( - username='seller', email='seller@mail.net'), + user=User.objects.create(username="seller", email="seller@mail.net"), tirage=self.t, ) self.p1 = Participant.objects.create( - user=User.objects.create(username='part1', email='part1@mail.net'), + user=User.objects.create(username="part1", email="part1@mail.net"), tirage=self.t, ) self.p2 = Participant.objects.create( - user=User.objects.create(username='part2', email='part2@mail.net'), + user=User.objects.create(username="part2", email="part2@mail.net"), tirage=self.t, ) self.p3 = Participant.objects.create( - user=User.objects.create(username='part3', email='part3@mail.net'), + user=User.objects.create(username="part3", email="part3@mail.net"), tirage=self.t, ) self.attr = Attribution.objects.create( - participant=self.seller, - spectacle=self.s, + participant=self.seller, spectacle=self.s ) self.rev = SpectacleRevente.objects.create( - attribution=self.attr, - seller=self.seller, + attribution=self.attr, seller=self.seller ) def test_tirage(self): @@ -69,7 +71,7 @@ class SpectacleReventeTests(TestCase): wanted_by = [self.p1, self.p2, self.p3] revente.confirmed_entry = wanted_by - with mock.patch('bda.models.random.choice') as mc: + with mock.patch("bda.models.random.choice") as mc: # Set winner to self.p1. mc.return_value = self.p1 @@ -87,14 +89,14 @@ class SpectacleReventeTests(TestCase): self.assertEqual(len(mails), 4) - m_seller = mails['seller@mail.net'] - self.assertListEqual(m_seller.to, ['seller@mail.net']) - self.assertListEqual(m_seller.reply_to, ['part1@mail.net']) + m_seller = mails["seller@mail.net"] + self.assertListEqual(m_seller.to, ["seller@mail.net"]) + self.assertListEqual(m_seller.reply_to, ["part1@mail.net"]) - m_winner = mails['part1@mail.net'] - self.assertListEqual(m_winner.to, ['part1@mail.net']) + m_winner = mails["part1@mail.net"] + self.assertListEqual(m_winner.to, ["part1@mail.net"]) self.assertCountEqual( - [mails['part2@mail.net'].to, mails['part3@mail.net'].to], - [['part2@mail.net'], ['part3@mail.net']], + [mails["part2@mail.net"].to, mails["part3@mail.net"].to], + [["part2@mail.net"], ["part3@mail.net"]], ) diff --git a/bda/tests/test_revente.py b/bda/tests/test_revente.py index 8ef7be19..b0d69dc7 100644 --- a/bda/tests/test_revente.py +++ b/bda/tests/test_revente.py @@ -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 bda.models import (Tirage, Spectacle, Salle, CategorieSpectacle, - SpectacleRevente, Attribution, Participant) +from django.contrib.auth.models import User +from django.test import Client, TestCase +from django.utils import timezone + +from bda.models import ( + Attribution, + CategorieSpectacle, + Participant, + Salle, + Spectacle, + SpectacleRevente, + Tirage, +) class TestModels(TestCase): def setUp(self): self.tirage = Tirage.objects.create( - title="Tirage test", - appear_catalogue=True, - ouverture=timezone.now(), - fermeture=timezone.now() + title="Tirage test", + appear_catalogue=True, + ouverture=timezone.now(), + fermeture=timezone.now(), ) self.category = CategorieSpectacle.objects.create(name="Category") self.location = Salle.objects.create(name="here") self.spectacle_soon = Spectacle.objects.create( - title="foo", date=timezone.now()+timedelta(days=1), - location=self.location, price=0, slots=42, - tirage=self.tirage, listing=False, category=self.category + title="foo", + date=timezone.now() + timedelta(days=1), + location=self.location, + price=0, + slots=42, + tirage=self.tirage, + listing=False, + category=self.category, ) self.spectacle_later = Spectacle.objects.create( - title="bar", date=timezone.now()+timedelta(days=30), - location=self.location, price=0, slots=42, - tirage=self.tirage, listing=False, category=self.category + title="bar", + date=timezone.now() + timedelta(days=30), + location=self.location, + price=0, + slots=42, + tirage=self.tirage, + listing=False, + category=self.category, ) user_buyer = User.objects.create_user( - username="bda_buyer", password="testbuyer" + username="bda_buyer", password="testbuyer" ) user_seller = User.objects.create_user( - username="bda_seller", password="testseller" - ) - self.buyer = Participant.objects.create( - user=user_buyer, tirage=self.tirage - ) - self.seller = Participant.objects.create( - user=user_seller, tirage=self.tirage + username="bda_seller", password="testseller" ) + self.buyer = Participant.objects.create(user=user_buyer, tirage=self.tirage) + self.seller = Participant.objects.create(user=user_seller, tirage=self.tirage) self.attr_soon = Attribution.objects.create( - participant=self.seller, spectacle=self.spectacle_soon + participant=self.seller, spectacle=self.spectacle_soon ) 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( - seller=self.seller, - attribution=self.attr_soon + seller=self.seller, attribution=self.attr_soon ) self.revente_later = SpectacleRevente.objects.create( - seller=self.seller, - attribution=self.attr_later + seller=self.seller, attribution=self.attr_later ) def test_urgent(self): @@ -64,6 +75,5 @@ class TestModels(TestCase): def test_tirage(self): self.revente_soon.confirmed_entry.add(self.buyer) - self.assertEqual(self.revente_soon.tirage(send_mails=False), - self.buyer) + self.assertEqual(self.revente_soon.tirage(send_mails=False), self.buyer) self.assertIsNone(self.revente_later.tirage(send_mails=False)) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 39c11c79..8bd5b462 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -1,14 +1,13 @@ import json - from datetime import timedelta from unittest import mock from urllib.parse import urlencode 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 ..models import Tirage, Spectacle, Salle, CategorieSpectacle +from ..models import CategorieSpectacle, Salle, Spectacle, Tirage def create_user(username, is_cof=False, is_buro=False): @@ -46,16 +45,17 @@ class BdATestHelpers: (staff, staff_c), (member, member_c), (other, other_c), - (None, Client()) + (None, Client()), ] def require_custommails(self): from django.core.management import call_command + call_command("syncmails") - def check_restricted_access(self, url, - validate_user=user_is_cof, - redirect_url=None): + 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 @@ -63,8 +63,7 @@ class BdATestHelpers: # client is not logged in login_url = "/login" if url: - login_url += "?{}".format(urlencode({"next": url}, - safe="/")) + login_url += "?{}".format(urlencode({"next": url}, safe="/")) return login_url else: return "/" @@ -82,7 +81,7 @@ class TestBdAViews(BdATestHelpers, TestCase): # 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 = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) # Set up the helpers @@ -96,23 +95,40 @@ class TestBdAViews(BdATestHelpers, TestCase): ) self.category = CategorieSpectacle.objects.create(name="Category") self.location = Salle.objects.create(name="here") - Spectacle.objects.bulk_create([ - Spectacle( - title="foo", date=timezone.now(), location=self.location, - price=0, slots=42, tirage=self.tirage, listing=False, - category=self.category - ), - Spectacle( - title="bar", date=timezone.now(), location=self.location, - price=1, slots=142, tirage=self.tirage, listing=False, - category=self.category - ), - Spectacle( - title="baz", date=timezone.now(), location=self.location, - price=2, slots=242, tirage=self.tirage, listing=False, - category=self.category - ), - ]) + Spectacle.objects.bulk_create( + [ + Spectacle( + title="foo", + date=timezone.now(), + location=self.location, + price=0, + slots=42, + tirage=self.tirage, + listing=False, + category=self.category, + ), + Spectacle( + title="bar", + date=timezone.now(), + location=self.location, + price=1, + slots=142, + tirage=self.tirage, + listing=False, + category=self.category, + ), + Spectacle( + title="baz", + date=timezone.now(), + location=self.location, + price=2, + slots=242, + tirage=self.tirage, + listing=False, + category=self.category, + ), + ] + ) def test_bda_inscriptions(self): # TODO: test the form @@ -195,7 +211,7 @@ class TestBdAViews(BdATestHelpers, TestCase): resp = client.get(url_list) self.assertJSONEqual( resp.content.decode("utf-8"), - [{"id": self.tirage.id, "title": self.tirage.title}] + [{"id": self.tirage.id, "title": self.tirage.title}], ) # Details @@ -203,15 +219,9 @@ class TestBdAViews(BdATestHelpers, TestCase): self.assertJSONEqual( resp.content.decode("utf-8"), { - "categories": [{ - "id": self.category.id, - "name": self.category.name - }], - "locations": [{ - "id": self.location.id, - "name": self.location.name - }], - } + "categories": [{"id": self.category.id, "name": self.category.name}], + "locations": [{"id": self.location.id, "name": self.location.name}], + }, ) # Descriptions @@ -224,7 +234,7 @@ class TestBdAViews(BdATestHelpers, TestCase): self.assertEqual(len(results), 3) self.assertEqual( {(s["title"], s["price"], s["slots"]) for s in results}, - {("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)} + {("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)}, ) diff --git a/bda/urls.py b/bda/urls.py index 7264d7b3..7ceccfe0 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -1,62 +1,75 @@ from django.conf.urls import url -from gestioncof.decorators import buro_required -from bda.views import SpectacleListView + from bda import views +from bda.views import SpectacleListView +from gestioncof.decorators import buro_required urlpatterns = [ - url(r'^inscription/(?P\d+)$', + url( + r"^inscription/(?P\d+)$", views.inscription, - name='bda-tirage-inscription'), - url(r'^places/(?P\d+)$', - views.places, - name="bda-places-attribuees"), - url(r'^etat-places/(?P\d+)$', - views.etat_places, - name='bda-etat-places'), - url(r'^tirage/(?P\d+)$', views.tirage), - url(r'^spectacles/(?P\d+)$', + name="bda-tirage-inscription", + ), + url(r"^places/(?P\d+)$", views.places, name="bda-places-attribuees"), + url(r"^etat-places/(?P\d+)$", views.etat_places, name="bda-etat-places"), + url(r"^tirage/(?P\d+)$", views.tirage), + url( + r"^spectacles/(?P\d+)$", buro_required(SpectacleListView.as_view()), - name="bda-liste-spectacles"), - url(r'^spectacles/(?P\d+)/(?P\d+)$', + name="bda-liste-spectacles", + ), + url( + r"^spectacles/(?P\d+)/(?P\d+)$", views.spectacle, - name="bda-spectacle"), - url(r'^spectacles/unpaid/(?P\d+)$', - views.unpaid, - name="bda-unpaid"), - url(r'^spectacles/autocomplete$', + name="bda-spectacle", + ), + url(r"^spectacles/unpaid/(?P\d+)$", views.unpaid, name="bda-unpaid"), + url( + r"^spectacles/autocomplete$", views.spectacle_autocomplete, - name="bda-spectacle-autocomplete"), - url(r'^participants/autocomplete$', + name="bda-spectacle-autocomplete", + ), + url( + r"^participants/autocomplete$", views.participant_autocomplete, - name="bda-participant-autocomplete"), - + name="bda-participant-autocomplete", + ), # Urls BdA-Revente - - url(r'^revente/(?P\d+)/manage$', + url( + r"^revente/(?P\d+)/manage$", views.revente_manage, - name='bda-revente-manage'), - url(r'^revente/(?P\d+)/subscribe$', + name="bda-revente-manage", + ), + url( + r"^revente/(?P\d+)/subscribe$", views.revente_subscribe, - name="bda-revente-subscribe"), - url(r'^revente/(?P\d+)/tirages$', + name="bda-revente-subscribe", + ), + url( + r"^revente/(?P\d+)/tirages$", views.revente_tirages, - name="bda-revente-tirages"), - url(r'^revente/(?P\d+)/buy$', + name="bda-revente-tirages", + ), + url( + r"^revente/(?P\d+)/buy$", views.revente_buy, - name="bda-revente-buy"), - url(r'^revente/(?P\d+)/confirm$', + name="bda-revente-buy", + ), + url( + r"^revente/(?P\d+)/confirm$", views.revente_confirm, - name='bda-revente-confirm'), - url(r'^revente/(?P\d+)/shotgun$', + name="bda-revente-confirm", + ), + url( + r"^revente/(?P\d+)/shotgun$", views.revente_shotgun, - name="bda-revente-shotgun"), - - url(r'^mails-rappel/(?P\d+)$', - views.send_rappel, - name="bda-rappels" - ), - url(r'^descriptions/(?P\d+)$', views.descriptions_spectacles, - name='bda-descriptions'), - url(r'^catalogue/(?P[a-z]+)$', views.catalogue, - name='bda-catalogue'), + name="bda-revente-shotgun", + ), + url(r"^mails-rappel/(?P\d+)$", views.send_rappel, name="bda-rappels"), + url( + r"^descriptions/(?P\d+)$", + views.descriptions_spectacles, + name="bda-descriptions", + ), + url(r"^catalogue/(?P[a-z]+)$", views.catalogue, name="bda-catalogue"), ] diff --git a/bda/views.py b/bda/views.py index b49eb030..cbad0b1b 100644 --- a/bda/views.py +++ b/bda/views.py @@ -1,36 +1,47 @@ -from collections import defaultdict -import random import hashlib -import time import json -from custommail.shortcuts import send_mass_custom_mail, send_custom_mail +import random +import time +from collections import defaultdict + from custommail.models import CustomMail -from django.shortcuts import render, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.db import transaction -from django.core import serializers -from django.db.models import Count, Q, Prefetch -from django.template.defaultfilters import pluralize -from django.forms.models import inlineformset_factory -from django.http import ( - HttpResponseBadRequest, HttpResponseRedirect, JsonResponse -) -from django.core.urlresolvers import reverse +from custommail.shortcuts import send_custom_mail, send_mass_custom_mail from django.conf import settings -from django.utils import timezone, formats +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core import serializers +from django.core.urlresolvers import reverse +from django.db import transaction +from django.db.models import Count, Prefetch, Q +from django.forms.models import inlineformset_factory +from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse +from django.shortcuts import get_object_or_404, render +from django.template.defaultfilters import pluralize +from django.utils import formats, timezone from django.views.generic.list import ListView -from gestioncof.decorators import cof_required, buro_required -from bda.models import ( - Spectacle, Participant, ChoixSpectacle, Attribution, Tirage, - SpectacleRevente, Salle, CategorieSpectacle -) + from bda.algorithm import Algorithm from bda.forms import ( - TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, - InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm + AnnulForm, + InscriptionInlineFormSet, + InscriptionReventeForm, + ResellForm, + ReventeTirageAnnulForm, + ReventeTirageForm, + SoldForm, + TokenForm, ) - +from bda.models import ( + Attribution, + CategorieSpectacle, + ChoixSpectacle, + Participant, + Salle, + Spectacle, + SpectacleRevente, + Tirage, +) +from gestioncof.decorators import buro_required, cof_required from utils.views.autocomplete import Select2QuerySetView @@ -45,7 +56,7 @@ def etat_places(request, tirage_id): """ tirage = get_object_or_404(Tirage, id=tirage_id) - spectacles = tirage.spectacle_set.select_related('location') + spectacles = tirage.spectacle_set.select_related("location") spectacles_dict = {} # index of spectacle by id for spectacle in spectacles: @@ -53,10 +64,9 @@ def etat_places(request, tirage_id): spectacles_dict[spectacle.id] = spectacle choices = ( - ChoixSpectacle.objects - .filter(spectacle__in=spectacles) - .values('spectacle') - .annotate(total=Count('spectacle')) + ChoixSpectacle.objects.filter(spectacle__in=spectacles) + .values("spectacle") + .annotate(total=Count("spectacle")) ) # choices *by spectacles* whose only 1 place is requested @@ -65,11 +75,11 @@ def etat_places(request, tirage_id): choices2 = choices.exclude(double_choice="1") for spectacle in choices1: - pk = spectacle['spectacle'] - spectacles_dict[pk].total += spectacle['total'] + pk = spectacle["spectacle"] + spectacles_dict[pk].total += spectacle["total"] for spectacle in choices2: - pk = spectacle['spectacle'] - spectacles_dict[pk].total += 2*spectacle['total'] + pk = spectacle["spectacle"] + spectacles_dict[pk].total += 2 * spectacle["total"] # here, each spectacle.total contains the number of requests @@ -84,13 +94,13 @@ def etat_places(request, tirage_id): "proposed": slots, "spectacles": spectacles, "total": total, - 'tirage': tirage + "tirage": tirage, } return render(request, "bda/etat-places.html", context) def _hash_queryset(queryset): - data = serializers.serialize("json", queryset).encode('utf-8') + data = serializers.serialize("json", queryset).encode("utf-8") hasher = hashlib.sha256() hasher.update(data) return hasher.hexdigest() @@ -99,15 +109,10 @@ def _hash_queryset(queryset): @cof_required def places(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) - participant, _ = ( - Participant.objects - .get_or_create(user=request.user, tirage=tirage) - ) - places = ( - participant.attribution_set - .order_by("spectacle__date", "spectacle") - .select_related("spectacle", "spectacle__location") - ) + participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage) + places = participant.attribution_set.order_by( + "spectacle__date", "spectacle" + ).select_related("spectacle", "spectacle__location") total = sum(place.spectacle.price for place in places) filtered_places = [] places_dict = {} @@ -129,13 +134,21 @@ def places(request, tirage_id): dates.append(date) # On prévient l'utilisateur s'il a deux places à la même date if warning: - messages.warning(request, "Attention, vous avez reçu des places pour " - "des spectacles différents à la même date.") - return render(request, "bda/resume_places.html", - {"participant": participant, - "places": filtered_places, - "tirage": tirage, - "total": total}) + messages.warning( + request, + "Attention, vous avez reçu des places pour " + "des spectacles différents à la même date.", + ) + return render( + request, + "bda/resume_places.html", + { + "participant": participant, + "places": filtered_places, + "tirage": tirage, + "total": total, + }, + ) @cof_required @@ -151,24 +164,24 @@ def inscription(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) if timezone.now() < tirage.ouverture: # Le tirage n'est pas encore ouvert. - opening = formats.localize( - timezone.template_localtime(tirage.ouverture)) - messages.error(request, "Le tirage n'est pas encore ouvert : " - "ouverture le {:s}".format(opening)) - return render(request, 'bda/resume-inscription-tirage.html', {}) + opening = formats.localize(timezone.template_localtime(tirage.ouverture)) + messages.error( + request, + "Le tirage n'est pas encore ouvert : " "ouverture le {:s}".format(opening), + ) + return render(request, "bda/resume-inscription-tirage.html", {}) - participant, _ = ( - Participant.objects.select_related('tirage') - .get_or_create(user=request.user, tirage=tirage) + participant, _ = Participant.objects.select_related("tirage").get_or_create( + user=request.user, tirage=tirage ) if timezone.now() > tirage.fermeture: # Le tirage est fermé. choices = participant.choixspectacle_set.order_by("priority") - messages.error(request, - " C'est fini : tirage au sort dans la journée !") - return render(request, "bda/resume-inscription-tirage.html", - {"choices": choices}) + messages.error(request, " C'est fini : tirage au sort dans la journée !") + return render( + request, "bda/resume-inscription-tirage.html", {"choices": choices} + ) BdaFormSet = inlineformset_factory( Participant, @@ -196,27 +209,33 @@ def inscription(request, tirage_id): # use *this* queryset dbstate = _hash_queryset(participant.choixspectacle_set.all()) total_price = 0 - choices = ( - participant.choixspectacle_set - .select_related('spectacle') - ) + choices = participant.choixspectacle_set.select_related("spectacle") for choice in choices: total_price += choice.spectacle.price if choice.double: total_price += choice.spectacle.price # Messages if success: - messages.success(request, "Votre inscription a été mise à jour avec " - "succès !") + messages.success( + request, "Votre inscription a été mise à jour avec " "succès !" + ) if stateerror: - messages.error(request, "Impossible d'enregistrer vos modifications " - ": vous avez apporté d'autres modifications " - "entre temps.") - return render(request, "bda/inscription-tirage.html", - {"formset": formset, - "total_price": total_price, - "dbstate": dbstate, - 'tirage': tirage}) + messages.error( + request, + "Impossible d'enregistrer vos modifications " + ": vous avez apporté d'autres modifications " + "entre temps.", + ) + return render( + request, + "bda/inscription-tirage.html", + { + "formset": formset, + "total_price": total_price, + "dbstate": dbstate, + "tirage": tirage, + }, + ) def do_tirage(tirage_elt, token): @@ -229,40 +248,39 @@ def do_tirage(tirage_elt, token): # Initialisation du dictionnaire data qui va contenir les résultats start = time.time() data = { - 'shows': tirage_elt.spectacle_set.select_related('location'), - 'token': token, - 'members': tirage_elt.participant_set.select_related('user'), - 'total_slots': 0, - 'total_losers': 0, - 'total_sold': 0, - 'total_deficit': 0, - 'opera_deficit': 0, + "shows": tirage_elt.spectacle_set.select_related("location"), + "token": token, + "members": tirage_elt.participant_set.select_related("user"), + "total_slots": 0, + "total_losers": 0, + "total_sold": 0, + "total_deficit": 0, + "opera_deficit": 0, } # On lance le tirage choices = ( - ChoixSpectacle.objects - .filter(spectacle__tirage=tirage_elt) - .order_by('participant', 'priority') - .select_related('participant', 'participant__user', 'spectacle') + ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt) + .order_by("participant", "priority") + .select_related("participant", "participant__user", "spectacle") ) - results = Algorithm(data['shows'], data['members'], choices)(token) + results = Algorithm(data["shows"], data["members"], choices)(token) # On compte les places attribuées et les déçus for (_, members, losers) in results: - data['total_slots'] += len(members) - data['total_losers'] += len(losers) + data["total_slots"] += len(members) + data["total_losers"] += len(losers) # On calcule le déficit et les bénéfices pour le BdA # FIXME: le traitement de l'opéra est sale for (show, members, _) in results: deficit = (show.slots - len(members)) * show.price - data['total_sold'] += show.slots * show.price + data["total_sold"] += show.slots * show.price if deficit >= 0: if "Opéra" in show.location.name: - data['opera_deficit'] += deficit - data['total_deficit'] += deficit - data["total_sold"] -= data['total_deficit'] + data["opera_deficit"] += deficit + data["total_deficit"] += deficit + data["total_sold"] -= data["total_deficit"] # Participant objects are not shared accross spectacle results, # so assign a single object for each Participant id @@ -288,32 +306,30 @@ def do_tirage(tirage_elt, token): # désactive le tirage Attribution.objects.filter(spectacle__tirage=tirage_elt).delete() tirage_elt.tokens += '{:s}\n"""{:s}"""\n'.format( - timezone.now().strftime("%y-%m-%d %H:%M:%S"), - token) + timezone.now().strftime("%y-%m-%d %H:%M:%S"), token + ) tirage_elt.enable_do_tirage = False tirage_elt.save() # On enregistre les nouvelles attributions - Attribution.objects.bulk_create([ - Attribution(spectacle=show, participant=member) - for show, members, _ in results - for member, _, _, _ in members - ]) + Attribution.objects.bulk_create( + [ + Attribution(spectacle=show, participant=member) + for show, members, _ in results + for member, _, _, _ in members + ] + ) # On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues ChoixRevente = Participant.choicesrevente.through # Suppression des reventes demandées/enregistrées # (si le tirage est relancé) + (ChoixRevente.objects.filter(spectacle__tirage=tirage_elt).delete()) ( - ChoixRevente.objects - .filter(spectacle__tirage=tirage_elt) - .delete() - ) - ( - SpectacleRevente.objects - .filter(attribution__spectacle__tirage=tirage_elt) - .delete() + SpectacleRevente.objects.filter( + attribution__spectacle__tirage=tirage_elt + ).delete() ) lost_by = defaultdict(set) @@ -335,13 +351,12 @@ def do_tirage(tirage_elt, token): @buro_required def tirage(request, tirage_id): tirage_elt = get_object_or_404(Tirage, id=tirage_id) - if not (tirage_elt.enable_do_tirage - and tirage_elt.fermeture < timezone.now()): - return render(request, "tirage-failed.html", {'tirage': tirage_elt}) + if not (tirage_elt.enable_do_tirage and tirage_elt.fermeture < timezone.now()): + return render(request, "tirage-failed.html", {"tirage": tirage_elt}) if request.POST: form = TokenForm(request.POST) if form.is_valid(): - results = do_tirage(tirage_elt, form.cleaned_data['token']) + results = do_tirage(tirage_elt, form.cleaned_data["token"]) return render(request, "bda-attrib-extra.html", results) else: form = TokenForm() @@ -360,56 +375,59 @@ def revente_manage(request, tirage_id): """ tirage = get_object_or_404(Tirage, id=tirage_id) participant, created = Participant.objects.get_or_create( - user=request.user, tirage=tirage) + user=request.user, tirage=tirage + ) if not participant.paid: return render(request, "bda/revente/notpaid.html", {}) - resellform = ResellForm(participant, prefix='resell') - annulform = AnnulForm(participant, prefix='annul') - soldform = SoldForm(participant, prefix='sold') + resellform = ResellForm(participant, prefix="resell") + annulform = AnnulForm(participant, prefix="annul") + soldform = SoldForm(participant, prefix="sold") - if request.method == 'POST': + if request.method == "POST": # On met en vente une place - if 'resell' in request.POST: - resellform = ResellForm(participant, request.POST, prefix='resell') + if "resell" in request.POST: + resellform = ResellForm(participant, request.POST, prefix="resell") if resellform.is_valid(): datatuple = [] attributions = resellform.cleaned_data["attributions"] with transaction.atomic(): for attribution in attributions: - revente, created = \ - SpectacleRevente.objects.get_or_create( - attribution=attribution, - defaults={'seller': participant}) + revente, created = SpectacleRevente.objects.get_or_create( + attribution=attribution, defaults={"seller": participant} + ) if not created: revente.reset() context = { - 'vendeur': participant.user, - 'show': attribution.spectacle, - 'revente': revente + "vendeur": participant.user, + "show": attribution.spectacle, + "revente": revente, } - datatuple.append(( - 'bda-revente-new', context, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email] - )) + datatuple.append( + ( + "bda-revente-new", + context, + settings.MAIL_DATA["revente"]["FROM"], + [participant.user.email], + ) + ) revente.save() send_mass_custom_mail(datatuple) # On annule une revente - elif 'annul' in request.POST: - annulform = AnnulForm(participant, request.POST, prefix='annul') + elif "annul" in request.POST: + annulform = AnnulForm(participant, request.POST, prefix="annul") if annulform.is_valid(): reventes = annulform.cleaned_data["reventes"] for revente in reventes: revente.delete() # On confirme une vente en transférant la place à la personne qui a # gagné le tirage - elif 'transfer' in request.POST: - soldform = SoldForm(participant, request.POST, prefix='sold') + elif "transfer" in request.POST: + soldform = SoldForm(participant, request.POST, prefix="sold") if soldform.is_valid(): - reventes = soldform.cleaned_data['reventes'] + reventes = soldform.cleaned_data["reventes"] for revente in reventes: revente.attribution.participant = revente.soldTo revente.attribution.save() @@ -417,28 +435,34 @@ def revente_manage(request, tirage_id): # On annule la revente après le tirage au sort (par exemple si # la personne qui a gagné le tirage ne se manifeste pas). La place est # alors remise en vente - elif 'reinit' in request.POST: - soldform = SoldForm(participant, request.POST, prefix='sold') + elif "reinit" in request.POST: + soldform = SoldForm(participant, request.POST, prefix="sold") if soldform.is_valid(): - reventes = soldform.cleaned_data['reventes'] + reventes = soldform.cleaned_data["reventes"] for revente in reventes: if revente.attribution.spectacle.date > timezone.now(): # On antidate pour envoyer le mail plus vite - new_date = (timezone.now() - - SpectacleRevente.remorse_time) + new_date = timezone.now() - SpectacleRevente.remorse_time revente.reset(new_date=new_date) overdue = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), revente__isnull=False, revente__seller=participant, - revente__notif_sent=True)\ - .filter( - Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) + revente__notif_sent=True, + ).filter(Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) - return render(request, "bda/revente/manage.html", - {'tirage': tirage, 'overdue': overdue, "soldform": soldform, - "annulform": annulform, "resellform": resellform}) + return render( + request, + "bda/revente/manage.html", + { + "tirage": tirage, + "overdue": overdue, + "soldform": soldform, + "annulform": annulform, + "resellform": resellform, + }, + ) @login_required @@ -448,58 +472,64 @@ def revente_tirages(request, tirage_id): tirage donné) et lui permet de s'inscrire et se désinscrire à ces reventes. """ tirage = get_object_or_404(Tirage, id=tirage_id) - participant, _ = Participant.objects.get_or_create( - user=request.user, tirage=tirage) + participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage) subform = ReventeTirageForm(participant, prefix="subscribe") annulform = ReventeTirageAnnulForm(participant, prefix="annul") - if request.method == 'POST': + if request.method == "POST": if "subscribe" in request.POST: - subform = ReventeTirageForm(participant, request.POST, - prefix="subscribe") + subform = ReventeTirageForm(participant, request.POST, prefix="subscribe") if subform.is_valid(): - reventes = subform.cleaned_data['reventes'] + reventes = subform.cleaned_data["reventes"] count = reventes.count() for revente in reventes: revente.confirmed_entry.add(participant) if count > 0: messages.success( request, - "Tu as bien été inscrit à {} revente{}" - .format(count, pluralize(count)) + "Tu as bien été inscrit à {} revente{}".format( + count, pluralize(count) + ), ) elif "annul" in request.POST: - annulform = ReventeTirageAnnulForm(participant, request.POST, - prefix="annul") + annulform = ReventeTirageAnnulForm( + participant, request.POST, prefix="annul" + ) if annulform.is_valid(): - reventes = annulform.cleaned_data['reventes'] + reventes = annulform.cleaned_data["reventes"] count = reventes.count() for revente in reventes: revente.confirmed_entry.remove(participant) if count > 0: messages.success( request, - "Tu as bien été désinscrit de {} revente{}" - .format(count, pluralize(count)) + "Tu as bien été désinscrit de {} revente{}".format( + count, pluralize(count) + ), ) - return render(request, "bda/revente/tirages.html", - {"annulform": annulform, "subform": subform}) + return render( + request, + "bda/revente/tirages.html", + {"annulform": annulform, "subform": subform}, + ) @login_required def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = Participant.objects.get_or_create( - user=request.user, tirage=revente.attribution.spectacle.tirage) + user=request.user, tirage=revente.attribution.spectacle.tirage + ) if not revente.notif_sent or revente.shotgun: - return render(request, "bda/revente/wrongtime.html", - {"revente": revente}) + return render(request, "bda/revente/wrongtime.html", {"revente": revente}) revente.confirmed_entry.add(participant) - return render(request, "bda/revente/confirmed.html", - {"spectacle": revente.attribution.spectacle, - "date": revente.date_tirage}) + return render( + request, + "bda/revente/confirmed.html", + {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}, + ) @login_required @@ -511,20 +541,18 @@ def revente_subscribe(request, tirage_id): spectacle à la liste des spectacles qui l'intéressent. """ tirage = get_object_or_404(Tirage, id=tirage_id) - participant, _ = Participant.objects.get_or_create( - user=request.user, tirage=tirage) + participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage) deja_revente = False success = False inscrit_revente = [] - if request.method == 'POST': + if request.method == "POST": form = InscriptionReventeForm(tirage, request.POST) if form.is_valid(): - choices = form.cleaned_data['spectacles'] + choices = form.cleaned_data["spectacles"] participant.choicesrevente = choices participant.save() for spectacle in choices: - qset = SpectacleRevente.objects.filter( - attribution__spectacle=spectacle) + qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle) if qset.filter(shotgun=True, soldTo__isnull=True).exists(): # Une place est disponible au shotgun, on suggère à # l'utilisateur d'aller la récupérer @@ -535,8 +563,8 @@ def revente_subscribe(request, tirage_id): # la revente ayant le moins d'inscrits min_resell = ( qset.filter(shotgun=False) - .annotate(nb_subscribers=Count('confirmed_entry')) - .order_by('nb_subscribers') + .annotate(nb_subscribers=Count("confirmed_entry")) + .order_by("nb_subscribers") .first() ) if min_resell is not None: @@ -545,21 +573,23 @@ def revente_subscribe(request, tirage_id): success = True else: form = InscriptionReventeForm( - tirage, - initial={'spectacles': participant.choicesrevente.all()} + tirage, initial={"spectacles": participant.choicesrevente.all()} ) # Messages if success: messages.success(request, "Ton inscription a bien été prise en compte") if deja_revente: - messages.info(request, "Des reventes existent déjà pour certains de " - "ces spectacles, vérifie les places " - "disponibles sans tirage !") + messages.info( + request, + "Des reventes existent déjà pour certains de " + "ces spectacles, vérifie les places " + "disponibles sans tirage !", + ) if inscrit_revente: shows = map("
  • {!s}
  • ".format, inscrit_revente) msg = ( "Tu as été inscrit à des reventes en cours pour les spectacles " - "
      {:s}
    ".format('\n'.join(shows)) + "
      {:s}
    ".format("\n".join(shows)) ) messages.info(request, msg, extra_tags="safe") @@ -570,19 +600,17 @@ def revente_subscribe(request, tirage_id): def revente_buy(request, spectacle_id): spectacle = get_object_or_404(Spectacle, id=spectacle_id) tirage = spectacle.tirage - participant, _ = Participant.objects.get_or_create( - user=request.user, tirage=tirage) + participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage) reventes = SpectacleRevente.objects.filter( - attribution__spectacle=spectacle, - soldTo__isnull=True) + attribution__spectacle=spectacle, soldTo__isnull=True + ) # Si l'utilisateur veut racheter une place qu'il est en train de revendre, # on supprime la revente en question. own_reventes = reventes.filter(seller=participant) if len(own_reventes) > 0: own_reventes[0].delete() - return HttpResponseRedirect(reverse("bda-revente-shotgun", - args=[tirage.id])) + return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id])) reventes_shotgun = reventes.filter(shotgun=True) @@ -594,94 +622,98 @@ def revente_buy(request, spectacle_id): revente.soldTo = participant revente.save() context = { - 'show': spectacle, - 'acheteur': request.user, - 'vendeur': revente.seller.user + "show": spectacle, + "acheteur": request.user, + "vendeur": revente.seller.user, } send_custom_mail( - 'bda-buy-shotgun', - 'bda@ens.fr', + "bda-buy-shotgun", + "bda@ens.fr", [revente.seller.user.email], context=context, ) - return render(request, "bda/revente/mail-success.html", - {"seller": revente.attribution.participant.user, - "spectacle": spectacle}) + return render( + request, + "bda/revente/mail-success.html", + {"seller": revente.attribution.participant.user, "spectacle": spectacle}, + ) - return render(request, "bda/revente/confirm-shotgun.html", - {"spectacle": spectacle, - "user": request.user}) + return render( + request, + "bda/revente/confirm-shotgun.html", + {"spectacle": spectacle, "user": request.user}, + ) @login_required def revente_shotgun(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) spectacles = ( - tirage.spectacle_set - .filter(date__gte=timezone.now()) - .select_related('location') - .prefetch_related(Prefetch( - 'attribues', - queryset=( - Attribution.objects - .filter(revente__shotgun=True, - revente__soldTo__isnull=True) - ), - to_attr='shotguns', - )) + tirage.spectacle_set.filter(date__gte=timezone.now()) + .select_related("location") + .prefetch_related( + Prefetch( + "attribues", + queryset=( + Attribution.objects.filter( + revente__shotgun=True, revente__soldTo__isnull=True + ) + ), + to_attr="shotguns", + ) + ) ) shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0] - return render(request, "bda/revente/shotgun.html", - {"shotgun": shotgun}) + return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun}) @buro_required def spectacle(request, tirage_id, spectacle_id): tirage = get_object_or_404(Tirage, id=tirage_id) spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage) - attributions = ( - spectacle.attribues - .select_related('participant', 'participant__user') + attributions = spectacle.attribues.select_related( + "participant", "participant__user" ) participants = {} for attrib in attributions: participant = attrib.participant - participant_info = {'lastname': participant.user.last_name, - 'name': participant.user.get_full_name, - 'username': participant.user.username, - 'email': participant.user.email, - 'given': int(attrib.given), - 'paid': participant.paid, - 'nb_places': 1} + participant_info = { + "lastname": participant.user.last_name, + "name": participant.user.get_full_name, + "username": participant.user.username, + "email": participant.user.email, + "given": int(attrib.given), + "paid": participant.paid, + "nb_places": 1, + } if participant.id in participants: - participants[participant.id]['nb_places'] += 1 - participants[participant.id]['given'] += attrib.given + participants[participant.id]["nb_places"] += 1 + participants[participant.id]["given"] += attrib.given else: participants[participant.id] = participant_info - participants_info = sorted(participants.values(), - key=lambda part: part['lastname']) - return render(request, "bda/participants.html", - {"spectacle": spectacle, "participants": participants_info}) + participants_info = sorted(participants.values(), key=lambda part: part["lastname"]) + return render( + request, + "bda/participants.html", + {"spectacle": spectacle, "participants": participants_info}, + ) class SpectacleListView(ListView): model = Spectacle - template_name = 'spectacle_list.html' + template_name = "spectacle_list.html" def get_queryset(self): - self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id']) - categories = ( - self.tirage.spectacle_set - .select_related('location') - ) + self.tirage = get_object_or_404(Tirage, id=self.kwargs["tirage_id"]) + categories = self.tirage.spectacle_set.select_related("location") return categories def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['tirage_id'] = self.tirage.id - context['tirage_name'] = self.tirage.title + context["tirage_id"] = self.tirage.id + context["tirage_name"] = self.tirage.title return context @@ -689,10 +721,9 @@ class SpectacleListView(ListView): def unpaid(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) unpaid = ( - tirage.participant_set - .annotate(nb_attributions=Count('attribution')) + tirage.participant_set.annotate(nb_attributions=Count("attribution")) .filter(paid=False, nb_attributions__gt=0) - .select_related('user') + .select_related("user") ) return render(request, "bda-unpaid.html", {"unpaid": unpaid}) @@ -702,51 +733,45 @@ def send_rappel(request, spectacle_id): show = get_object_or_404(Spectacle, id=spectacle_id) # Mails d'exemples custommail = CustomMail.objects.get(shortname="bda-rappel") - exemple_mail_1place = custommail.render({ - 'member': request.user, - 'show': show, - 'nb_attr': 1 - }) - exemple_mail_2places = custommail.render({ - 'member': request.user, - 'show': show, - 'nb_attr': 2 - }) + exemple_mail_1place = custommail.render( + {"member": request.user, "show": show, "nb_attr": 1} + ) + exemple_mail_2places = custommail.render( + {"member": request.user, "show": show, "nb_attr": 2} + ) # Contexte ctxt = { - 'show': show, - 'exemple_mail_1place': exemple_mail_1place, - 'exemple_mail_2places': exemple_mail_2places, - 'custommail': custommail, + "show": show, + "exemple_mail_1place": exemple_mail_1place, + "exemple_mail_2places": exemple_mail_2places, + "custommail": custommail, } # Envoi confirmé - if request.method == 'POST': + if request.method == "POST": members = show.send_rappel() - ctxt['sent'] = True - ctxt['members'] = members + ctxt["sent"] = True + ctxt["members"] = members # Demande de confirmation else: - ctxt['sent'] = False + ctxt["sent"] = False if show.rappel_sent: messages.warning( request, "Attention, un mail de rappel pour ce spectale a déjà été " - "envoyé le {}".format(formats.localize( - timezone.template_localtime(show.rappel_sent) - )) + "envoyé le {}".format( + formats.localize(timezone.template_localtime(show.rappel_sent)) + ), ) return render(request, "bda/mails-rappel.html", ctxt) def descriptions_spectacles(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) - shows_qs = ( - tirage.spectacle_set - .select_related('location') - .prefetch_related('quote_set') + shows_qs = tirage.spectacle_set.select_related("location").prefetch_related( + "quote_set" ) - category_name = request.GET.get('category', '') - location_id = request.GET.get('location', '') + category_name = request.GET.get("category", "") + location_id = request.GET.get("location", "") if category_name: shows_qs = shows_qs.filter(category__name=category_name) if location_id: @@ -754,8 +779,9 @@ def descriptions_spectacles(request, tirage_id): shows_qs = shows_qs.filter(location__id=int(location_id)) except ValueError: return HttpResponseBadRequest( - "La variable GET 'location' doit contenir un entier") - return render(request, 'descriptions.html', {'shows': shows_qs}) + "La variable GET 'location' doit contenir un entier" + ) + return render(request, "descriptions.html", {"shows": shows_qs}) def catalogue(request, request_type): @@ -768,44 +794,36 @@ def catalogue(request, request_type): if request_type == "list": # Dans ce cas on retourne la liste des tirages et de leur id en JSON data_return = list( - Tirage.objects.filter(appear_catalogue=True).values('id', 'title') + Tirage.objects.filter(appear_catalogue=True).values("id", "title") ) return JsonResponse(data_return, safe=False) if request_type == "details": # Dans ce cas on retourne une liste des catégories et des salles - tirage_id = request.GET.get('id', None) + tirage_id = request.GET.get("id", None) if tirage_id is None: - return HttpResponseBadRequest( - "Missing GET parameter: id " - ) + return HttpResponseBadRequest("Missing GET parameter: id ") try: tirage = get_object_or_404(Tirage, id=int(tirage_id)) except ValueError: - return HttpResponseBadRequest( - "Bad format: int expected for `id`" - ) + return HttpResponseBadRequest("Bad format: int expected for `id`") shows = tirage.spectacle_set.values_list("id", flat=True) categories = list( - CategorieSpectacle.objects - .filter(spectacle__in=shows) + CategorieSpectacle.objects.filter(spectacle__in=shows) .distinct() - .values('id', 'name') + .values("id", "name") ) locations = list( - Salle.objects - .filter(spectacle__in=shows) - .distinct() - .values('id', 'name') + Salle.objects.filter(spectacle__in=shows).distinct().values("id", "name") ) - data_return = {'categories': categories, 'locations': locations} + data_return = {"categories": categories, "locations": locations} return JsonResponse(data_return, safe=False) if request_type == "descriptions": # Ici on retourne les descriptions correspondant à la catégorie et # à la salle spécifiées - tirage_id = request.GET.get('id', '') - categories = request.GET.get('category', '[]') - locations = request.GET.get('location', '[]') + tirage_id = request.GET.get("id", "") + categories = request.GET.get("category", "[]") + locations = request.GET.get("location", "[]") try: tirage_id = int(tirage_id) categories_id = json.loads(categories) @@ -821,17 +839,16 @@ def catalogue(request, request_type): "following types:\n" "id: int, category: [int], location: [int]\n" "Data received:\n" - "id = {}, category = {}, locations = {}" - .format(request.GET.get('id', ''), - request.GET.get('category', '[]'), - request.GET.get('location', '[]')) + "id = {}, category = {}, locations = {}".format( + request.GET.get("id", ""), + request.GET.get("category", "[]"), + request.GET.get("location", "[]"), + ) ) tirage = get_object_or_404(Tirage, id=tirage_id) - shows_qs = ( - tirage.spectacle_set - .select_related('location') - .prefetch_related('quote_set') + shows_qs = tirage.spectacle_set.select_related("location").prefetch_related( + "quote_set" ) if categories_id and 0 not in categories_id: shows_qs = shows_qs.filter(category__id__in=categories_id) @@ -841,23 +858,27 @@ def catalogue(request, request_type): # On convertit les descriptions à envoyer en une liste facilement # JSONifiable (il devrait y avoir un moyen plus efficace en # redéfinissant le serializer de JSON) - data_return = [{ - 'title': spectacle.title, - 'category': str(spectacle.category), - 'date': str(formats.date_format( - timezone.localtime(spectacle.date), - "SHORT_DATETIME_FORMAT")), - 'location': str(spectacle.location), - 'vips': spectacle.vips, - 'description': spectacle.description, - 'slots_description': spectacle.slots_description, - 'quotes': [dict(author=quote.author, - text=quote.text) - for quote in spectacle.quote_set.all()], - 'image': spectacle.getImgUrl(), - 'ext_link': spectacle.ext_link, - 'price': spectacle.price, - 'slots': spectacle.slots + data_return = [ + { + "title": spectacle.title, + "category": str(spectacle.category), + "date": str( + formats.date_format( + timezone.localtime(spectacle.date), "SHORT_DATETIME_FORMAT" + ) + ), + "location": str(spectacle.location), + "vips": spectacle.vips, + "description": spectacle.description, + "slots_description": spectacle.slots_description, + "quotes": [ + dict(author=quote.author, text=quote.text) + for quote in spectacle.quote_set.all() + ], + "image": spectacle.getImgUrl(), + "ext_link": spectacle.ext_link, + "price": spectacle.price, + "slots": spectacle.slots, } for spectacle in shows_qs ] @@ -875,7 +896,7 @@ def catalogue(request, request_type): class ParticipantAutocomplete(Select2QuerySetView): model = Participant - search_fields = ('user__username', 'user__first_name', 'user__last_name') + search_fields = ("user__username", "user__first_name", "user__last_name") participant_autocomplete = buro_required(ParticipantAutocomplete.as_view()) @@ -883,7 +904,7 @@ participant_autocomplete = buro_required(ParticipantAutocomplete.as_view()) class SpectacleAutocomplete(Select2QuerySetView): model = Spectacle - search_fields = ('title',) + search_fields = ("title",) spectacle_autocomplete = buro_required(SpectacleAutocomplete.as_view()) diff --git a/cof/asgi.py b/cof/asgi.py index a34621c7..ab4ce291 100644 --- a/cof/asgi.py +++ b/cof/asgi.py @@ -1,4 +1,5 @@ import os + from channels.asgi import get_channel_layer if "DJANGO_SETTINGS_MODULE" not in os.environ: diff --git a/cof/locale/fr/formats.py b/cof/locale/fr/formats.py index 4b47ce3d..3120d5ce 100644 --- a/cof/locale/fr/formats.py +++ b/cof/locale/fr/formats.py @@ -2,4 +2,4 @@ Formats français. """ -DATETIME_FORMAT = r'l j F Y \à H:i' +DATETIME_FORMAT = r"l j F Y \à H:i" diff --git a/cof/routing.py b/cof/routing.py index c604adf4..3c2e5718 100644 --- a/cof/routing.py +++ b/cof/routing.py @@ -1,6 +1,3 @@ from channels.routing import include - -routing = [ - include('kfet.routing.routing', path=r'^/ws/k-fet'), -] +routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")] diff --git a/cof/settings/common.py b/cof/settings/common.py index 02c796ad..ebc7fb2a 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -46,112 +46,106 @@ KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") -BASE_DIR = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -TESTING = sys.argv[1] == 'test' +TESTING = sys.argv[1] == "test" # Application definition INSTALLED_APPS = [ - 'shared', - - 'gestioncof', - + "shared", + "gestioncof", # Must be before 'django.contrib.admin'. # https://django-autocomplete-light.readthedocs.io/en/master/install.html - 'dal', - 'dal_select2', - - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin', - 'django.contrib.admindocs', - - 'bda', - 'captcha', - 'django_cas_ng', - 'bootstrapform', - 'kfet', - 'kfet.open', - 'channels', - 'widget_tweaks', - 'custommail', - 'djconfig', - 'wagtail.wagtailforms', - 'wagtail.wagtailredirects', - 'wagtail.wagtailembeds', - 'wagtail.wagtailsites', - 'wagtail.wagtailusers', - 'wagtail.wagtailsnippets', - 'wagtail.wagtaildocs', - 'wagtail.wagtailimages', - 'wagtail.wagtailsearch', - 'wagtail.wagtailadmin', - 'wagtail.wagtailcore', - 'wagtail.contrib.modeladmin', - 'wagtailmenus', - 'modelcluster', - 'taggit', - 'kfet.auth', - 'kfet.cms', - 'corsheaders', + "dal", + "dal_select2", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.admin", + "django.contrib.admindocs", + "bda", + "captcha", + "django_cas_ng", + "bootstrapform", + "kfet", + "kfet.open", + "channels", + "widget_tweaks", + "custommail", + "djconfig", + "wagtail.wagtailforms", + "wagtail.wagtailredirects", + "wagtail.wagtailembeds", + "wagtail.wagtailsites", + "wagtail.wagtailusers", + "wagtail.wagtailsnippets", + "wagtail.wagtaildocs", + "wagtail.wagtailimages", + "wagtail.wagtailsearch", + "wagtail.wagtailadmin", + "wagtail.wagtailcore", + "wagtail.contrib.modeladmin", + "wagtailmenus", + "modelcluster", + "taggit", + "kfet.auth", + "kfet.cms", + "corsheaders", ] MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'kfet.auth.middleware.TemporaryAuthMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'djconfig.middleware.DjConfigMiddleware', - 'wagtail.wagtailcore.middleware.SiteMiddleware', - 'wagtail.wagtailredirects.middleware.RedirectMiddleware', + "corsheaders.middleware.CorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", + "kfet.auth.middleware.TemporaryAuthMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "djconfig.middleware.DjConfigMiddleware", + "wagtail.wagtailcore.middleware.SiteMiddleware", + "wagtail.wagtailredirects.middleware.RedirectMiddleware", ] -ROOT_URLCONF = 'cof.urls' +ROOT_URLCONF = "cof.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'wagtailmenus.context_processors.wagtailmenus', - 'djconfig.context_processors.config', - 'gestioncof.shared.context_processor', - 'kfet.auth.context_processors.temporary_auth', - 'kfet.context_processors.config', - ], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "wagtailmenus.context_processors.wagtailmenus", + "djconfig.context_processors.config", + "gestioncof.shared.context_processor", + "kfet.auth.context_processors.temporary_auth", + "kfet.context_processors.config", + ] }, - }, + } ] DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': DBNAME, - 'USER': DBUSER, - 'PASSWORD': DBPASSWD, - 'HOST': os.environ.get('DBHOST', 'localhost'), + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": DBNAME, + "USER": DBUSER, + "PASSWORD": DBPASSWD, + "HOST": os.environ.get("DBHOST", "localhost"), } } @@ -159,9 +153,9 @@ DATABASES = { # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ -LANGUAGE_CODE = 'fr-fr' +LANGUAGE_CODE = "fr-fr" -TIME_ZONE = 'Europe/Paris' +TIME_ZONE = "Europe/Paris" USE_I18N = True @@ -173,35 +167,35 @@ USE_TZ = True SITE_ID = 1 GRAPPELLI_ADMIN_HEADLINE = "GestioCOF" -GRAPPELLI_ADMIN_TITLE = "GestioCOF" +GRAPPELLI_ADMIN_TITLE = 'GestioCOF' MAIL_DATA = { - 'petits_cours': { - 'FROM': "Le COF ", - 'BCC': "archivescof@gmail.com", - 'REPLYTO': "cof@ens.fr"}, - 'rappels': { - 'FROM': 'Le BdA ', - 'REPLYTO': 'Le BdA '}, - 'revente': { - 'FROM': 'BdA-Revente ', - 'REPLYTO': 'BdA-Revente '}, + "petits_cours": { + "FROM": "Le COF ", + "BCC": "archivescof@gmail.com", + "REPLYTO": "cof@ens.fr", + }, + "rappels": {"FROM": "Le BdA ", "REPLYTO": "Le BdA "}, + "revente": { + "FROM": "BdA-Revente ", + "REPLYTO": "BdA-Revente ", + }, } LOGIN_URL = "cof-login" LOGIN_REDIRECT_URL = "home" -CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' -CAS_VERSION = '3' +CAS_SERVER_URL = "https://cas.eleves.ens.fr/" +CAS_VERSION = "3" CAS_LOGIN_MSG = None CAS_IGNORE_REFERER = True -CAS_REDIRECT_URL = '/' +CAS_REDIRECT_URL = "/" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'gestioncof.shared.COFCASBackend', - 'kfet.auth.backends.GenericBackend', + "django.contrib.auth.backends.ModelBackend", + "gestioncof.shared.COFCASBackend", + "kfet.auth.backends.GenericBackend", ) @@ -214,21 +208,16 @@ AUTHENTICATION_BACKENDS = ( NOCAPTCHA = True RECAPTCHA_USE_SSL = True -CORS_ORIGIN_WHITELIST = ( - 'bda.ens.fr', - 'www.bda.ens.fr' - 'cof.ens.fr', - 'www.cof.ens.fr', -) +CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") # Cache settings CACHES = { - 'default': { - 'BACKEND': 'redis_cache.RedisCache', - 'LOCATION': 'redis://:{passwd}@{host}:{port}/db' - .format(passwd=REDIS_PASSWD, host=REDIS_HOST, - port=REDIS_PORT, db=REDIS_DB), + "default": { + "BACKEND": "redis_cache.RedisCache", + "LOCATION": "redis://:{passwd}@{host}:{port}/db".format( + passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB + ), } } @@ -239,20 +228,25 @@ CHANNEL_LAYERS = { "default": { "BACKEND": "asgi_redis.RedisChannelLayer", "CONFIG": { - "hosts": [( - "redis://:{passwd}@{host}:{port}/{db}" - .format(passwd=REDIS_PASSWD, host=REDIS_HOST, - port=REDIS_PORT, db=REDIS_DB) - )], + "hosts": [ + ( + "redis://:{passwd}@{host}:{port}/{db}".format( + passwd=REDIS_PASSWD, + host=REDIS_HOST, + port=REDIS_PORT, + db=REDIS_DB, + ) + ) + ] }, "ROUTING": "cof.routing.routing", } } -FORMAT_MODULE_PATH = 'cof.locale' +FORMAT_MODULE_PATH = "cof.locale" # Wagtail settings -WAGTAIL_SITE_NAME = 'GestioCOF' +WAGTAIL_SITE_NAME = "GestioCOF" WAGTAIL_ENABLE_UPDATE_CHECK = False TAGGIT_CASE_INSENSITIVE = True diff --git a/cof/settings/dev.py b/cof/settings/dev.py index 114f37da..0fd367cd 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -6,32 +6,30 @@ The settings that are not listed here are imported from .common from .common import * # NOQA from .common import INSTALLED_APPS, MIDDLEWARE, TESTING - -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" DEBUG = True if TESTING: - PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.MD5PasswordHasher', - ] + PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] # --- # Apache static/media config # --- -STATIC_URL = '/static/' -STATIC_ROOT = '/srv/gestiocof/static/' +STATIC_URL = "/static/" +STATIC_ROOT = "/srv/gestiocof/static/" -MEDIA_ROOT = '/srv/gestiocof/media/' -MEDIA_URL = '/media/' +MEDIA_ROOT = "/srv/gestiocof/media/" +MEDIA_URL = "/media/" # --- # Debug tool bar # --- + def show_toolbar(request): """ On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar @@ -41,13 +39,10 @@ def show_toolbar(request): """ return DEBUG + if not TESTING: INSTALLED_APPS += ["debug_toolbar", "debug_panel"] - MIDDLEWARE = [ - "debug_panel.middleware.DebugPanelMiddleware" - ] + MIDDLEWARE + MIDDLEWARE = ["debug_panel.middleware.DebugPanelMiddleware"] + MIDDLEWARE - DEBUG_TOOLBAR_CONFIG = { - 'SHOW_TOOLBAR_CALLBACK': show_toolbar, - } + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} diff --git a/cof/settings/local.py b/cof/settings/local.py index 6e1f0802..06cdf4a0 100644 --- a/cof/settings/local.py +++ b/cof/settings/local.py @@ -8,21 +8,16 @@ import os from .dev import * # NOQA from .dev import BASE_DIR - # Use sqlite for local development DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3") + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Use the default cache backend for local development -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache" - } -} +CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} # Use the default in memory asgi backend for local development CHANNEL_LAYERS = { diff --git a/cof/settings/prod.py b/cof/settings/prod.py index fcdb3fdb..9e355ccb 100644 --- a/cof/settings/prod.py +++ b/cof/settings/prod.py @@ -8,21 +8,13 @@ import os from .common import * # NOQA from .common import BASE_DIR, import_secret - DEBUG = False -ALLOWED_HOSTS = [ - "cof.ens.fr", - "www.cof.ens.fr", - "dev.cof.ens.fr" -] +ALLOWED_HOSTS = ["cof.ens.fr", "www.cof.ens.fr", "dev.cof.ens.fr"] STATIC_ROOT = os.path.join( - os.path.dirname(os.path.dirname(BASE_DIR)), - "public", - "gestion", - "static", + os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static" ) STATIC_URL = "/gestion/static/" diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py index e966565a..7921d467 100644 --- a/cof/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -1,4 +1,4 @@ -SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' +SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah" ADMINS = None SERVER_EMAIL = "root@vagrant" EMAIL_HOST = "localhost" diff --git a/cof/urls.py b/cof/urls.py index a1d0c9bf..7a0bee4c 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -6,111 +6,130 @@ from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib import admin -from django.views.generic.base import TemplateView from django.contrib.auth import views as django_views +from django.views.generic.base import TemplateView from django_cas_ng import views as django_cas_views - from wagtail.wagtailadmin import urls as wagtailadmin_urls from wagtail.wagtailcore import urls as wagtail_urls from wagtail.wagtaildocs import urls as wagtaildocs_urls -from gestioncof import views as gestioncof_views, csv_views -from gestioncof.urls import export_patterns, petitcours_patterns, \ - surveys_patterns, events_patterns, calendar_patterns, \ - clubs_patterns +from gestioncof import csv_views, views as gestioncof_views from gestioncof.autocomplete import autocomplete +from gestioncof.urls import ( + calendar_patterns, + clubs_patterns, + events_patterns, + export_patterns, + petitcours_patterns, + surveys_patterns, +) admin.autodiscover() urlpatterns = [ # Page d'accueil - url(r'^$', gestioncof_views.home, name='home'), + url(r"^$", gestioncof_views.home, name="home"), # Le BdA - url(r'^bda/', include('bda.urls')), + url(r"^bda/", include("bda.urls")), # Les exports - url(r'^export/', include(export_patterns)), + url(r"^export/", include(export_patterns)), # Les petits cours - url(r'^petitcours/', include(petitcours_patterns)), + url(r"^petitcours/", include(petitcours_patterns)), # Les sondages - url(r'^survey/', include(surveys_patterns)), + url(r"^survey/", include(surveys_patterns)), # Evenements - url(r'^event/', include(events_patterns)), + url(r"^event/", include(events_patterns)), # Calendrier - url(r'^calendar/', include(calendar_patterns)), + url(r"^calendar/", include(calendar_patterns)), # Clubs - url(r'^clubs/', include(clubs_patterns)), + url(r"^clubs/", include(clubs_patterns)), # Authentification - url(r'^cof/denied$', TemplateView.as_view(template_name='cof-denied.html'), - name="cof-denied"), - url(r'^cas/login$', django_cas_views.login, name="cas_login_view"), - url(r'^cas/logout$', django_cas_views.logout), - url(r'^outsider/login$', gestioncof_views.login_ext, - name="ext_login_view"), - url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}), - url(r'^login$', gestioncof_views.login, name="cof-login"), - url(r'^logout$', gestioncof_views.logout, name="cof-logout"), + url( + r"^cof/denied$", + TemplateView.as_view(template_name="cof-denied.html"), + name="cof-denied", + ), + url(r"^cas/login$", django_cas_views.login, name="cas_login_view"), + url(r"^cas/logout$", django_cas_views.logout), + url(r"^outsider/login$", gestioncof_views.login_ext, name="ext_login_view"), + url(r"^outsider/logout$", django_views.logout, {"next_page": "home"}), + url(r"^login$", gestioncof_views.login, name="cof-login"), + url(r"^logout$", gestioncof_views.logout, name="cof-logout"), # Infos persos - url(r'^profile$', gestioncof_views.profile, - name='profile'), - url(r'^outsider/password-change$', django_views.password_change, - name='password_change'), - url(r'^outsider/password-change-done$', + url(r"^profile$", gestioncof_views.profile, name="profile"), + url( + r"^outsider/password-change$", + django_views.password_change, + name="password_change", + ), + url( + r"^outsider/password-change-done$", django_views.password_change_done, - name='password_change_done'), + name="password_change_done", + ), # Inscription d'un nouveau membre - url(r'^registration$', gestioncof_views.registration, - name='registration'), - url(r'^registration/clipper/(?P[\w-]+)/' - r'(?P.*)$', - gestioncof_views.registration_form2, name="clipper-registration"), - url(r'^registration/user/(?P.+)$', - gestioncof_views.registration_form2, name="user-registration"), - url(r'^registration/empty$', gestioncof_views.registration_form2, - name="empty-registration"), + url(r"^registration$", gestioncof_views.registration, name="registration"), + url( + r"^registration/clipper/(?P[\w-]+)/" r"(?P.*)$", + gestioncof_views.registration_form2, + name="clipper-registration", + ), + url( + r"^registration/user/(?P.+)$", + gestioncof_views.registration_form2, + name="user-registration", + ), + url( + r"^registration/empty$", + gestioncof_views.registration_form2, + name="empty-registration", + ), # Autocompletion - url(r'^autocomplete/registration$', autocomplete, - name="cof.registration.autocomplete"), - url(r'^user/autocomplete$', gestioncof_views.user_autocomplete, - name='cof-user-autocomplete'), + url( + r"^autocomplete/registration$", + autocomplete, + name="cof.registration.autocomplete", + ), + url( + r"^user/autocomplete$", + gestioncof_views.user_autocomplete, + name="cof-user-autocomplete", + ), # Interface admin - url(r'^admin/logout/', gestioncof_views.logout), - url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^admin/(?P[\d\w]+)/(?P[\d\w]+)/csv/', + url(r"^admin/logout/", gestioncof_views.logout), + url(r"^admin/doc/", include("django.contrib.admindocs.urls")), + url( + r"^admin/(?P[\d\w]+)/(?P[\d\w]+)/csv/", csv_views.admin_list_export, - {'fields': ['username', ]}), - url(r'^admin/', include(admin.site.urls)), + {"fields": ["username"]}, + ), + url(r"^admin/", include(admin.site.urls)), # Liens utiles du COF et du BdA - url(r'^utile_cof$', gestioncof_views.utile_cof, - name='utile_cof'), - url(r'^utile_bda$', gestioncof_views.utile_bda, - name='utile_bda'), - url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff, - name="ml_diffbda"), - url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof, - name='ml_diffcof'), - url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente, - name="ml_bda_revente"), - url(r'^k-fet/', include('kfet.urls')), - url(r'^cms/', include(wagtailadmin_urls)), - url(r'^documents/', include(wagtaildocs_urls)), + url(r"^utile_cof$", gestioncof_views.utile_cof, name="utile_cof"), + url(r"^utile_bda$", gestioncof_views.utile_bda, name="utile_bda"), + url(r"^utile_bda/bda_diff$", gestioncof_views.liste_bdadiff, name="ml_diffbda"), + url(r"^utile_cof/diff_cof$", gestioncof_views.liste_diffcof, name="ml_diffcof"), + url( + r"^utile_bda/bda_revente$", + gestioncof_views.liste_bdarevente, + name="ml_bda_revente", + ), + url(r"^k-fet/", include("kfet.urls")), + url(r"^cms/", include(wagtailadmin_urls)), + url(r"^documents/", include(wagtaildocs_urls)), # djconfig - url(r"^config", gestioncof_views.ConfigUpdate.as_view(), - name='config.edit'), + url(r"^config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"), ] -if 'debug_toolbar' in settings.INSTALLED_APPS: +if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar - urlpatterns += [ - url(r'^__debug__/', include(debug_toolbar.urls)), - ] + + urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))] if settings.DEBUG: # Si on est en production, MEDIA_ROOT est servi par Apache. # Il faut dire à Django de servir MEDIA_ROOT lui-même en développement. - urlpatterns += static(settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Wagtail for uncatched -urlpatterns += [ - url(r'', include(wagtail_urls)), -] +urlpatterns += [url(r"", include(wagtail_urls))] diff --git a/gestioncof/__init__.py b/gestioncof/__init__.py index b77fdb94..3bb260b9 100644 --- a/gestioncof/__init__.py +++ b/gestioncof/__init__.py @@ -1 +1 @@ -default_app_config = 'gestioncof.apps.GestioncofConfig' +default_app_config = "gestioncof.apps.GestioncofConfig" diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 54a6a5a0..e89d4271 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -1,23 +1,35 @@ +from dal.autocomplete import ModelSelect2 from django import forms from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ -from gestioncof.models import SurveyQuestionAnswer, SurveyQuestion, \ - CofProfile, EventOption, EventOptionChoice, Event, Club, \ - Survey, EventCommentField, EventRegistration -from gestioncof.petits_cours_models import PetitCoursDemande, \ - PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \ - PetitCoursAttributionCounter -from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import Group, Permission, User from django.core.urlresolvers import reverse -from django.utils.safestring import mark_safe from django.db.models import Q +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ -from dal.autocomplete import ModelSelect2 +from gestioncof.models import ( + Club, + CofProfile, + Event, + EventCommentField, + EventOption, + EventOptionChoice, + EventRegistration, + Survey, + SurveyQuestion, + SurveyQuestionAnswer, +) +from gestioncof.petits_cours_models import ( + PetitCoursAbility, + PetitCoursAttribution, + PetitCoursAttributionCounter, + PetitCoursDemande, + PetitCoursSubject, +) -def add_link_field(target_model='', field='', link_text=str, - desc_text=str): +def add_link_field(target_model="", field="", link_text=str, desc_text=str): def add_link(cls): reverse_name = target_model or cls.model.__name__.lower() @@ -28,14 +40,14 @@ def add_link_field(target_model='', field='', link_text=str, if not link_obj.id: return "" url = reverse(reverse_path, args=(link_obj.id,)) - return mark_safe("%s" - % (url, link_text(link_obj))) + return mark_safe("%s" % (url, link_text(link_obj))) + link.allow_tags = True - link.short_description = desc_text(reverse_name + ' link') + link.short_description = desc_text(reverse_name + " link") cls.link = link - cls.readonly_fields =\ - list(getattr(cls, 'readonly_fields', [])) + ['link'] + cls.readonly_fields = list(getattr(cls, "readonly_fields", [])) + ["link"] return cls + return add_link @@ -43,32 +55,28 @@ class SurveyQuestionAnswerInline(admin.TabularInline): model = SurveyQuestionAnswer -@add_link_field(desc_text=lambda x: "Réponses", - link_text=lambda x: "Éditer les réponses") +@add_link_field( + desc_text=lambda x: "Réponses", link_text=lambda x: "Éditer les réponses" +) class SurveyQuestionInline(admin.TabularInline): model = SurveyQuestion class SurveyQuestionAdmin(admin.ModelAdmin): - search_fields = ('survey__title', 'answer') - inlines = [ - SurveyQuestionAnswerInline, - ] + search_fields = ("survey__title", "answer") + inlines = [SurveyQuestionAnswerInline] class SurveyAdmin(admin.ModelAdmin): - search_fields = ('title', 'details') - inlines = [ - SurveyQuestionInline, - ] + search_fields = ("title", "details") + inlines = [SurveyQuestionInline] class EventOptionChoiceInline(admin.TabularInline): model = EventOptionChoice -@add_link_field(desc_text=lambda x: "Choix", - link_text=lambda x: "Éditer les choix") +@add_link_field(desc_text=lambda x: "Choix", link_text=lambda x: "Éditer les choix") class EventOptionInline(admin.TabularInline): model = EventOption @@ -78,18 +86,13 @@ class EventCommentFieldInline(admin.TabularInline): class EventOptionAdmin(admin.ModelAdmin): - search_fields = ('event__title', 'name') - inlines = [ - EventOptionChoiceInline, - ] + search_fields = ("event__title", "name") + inlines = [EventOptionChoiceInline] class EventAdmin(admin.ModelAdmin): - search_fields = ('title', 'location', 'description') - inlines = [ - EventOptionInline, - EventCommentFieldInline, - ] + search_fields = ("title", "location", "description") + inlines = [EventOptionInline, EventCommentFieldInline] class CofProfileInline(admin.StackedInline): @@ -98,10 +101,9 @@ class CofProfileInline(admin.StackedInline): class FkeyLookup(object): - def __init__(self, fkeydecl, short_description=None, - admin_order_field=None): - self.fk, fkattrs = fkeydecl.split('__', 1) - self.fkattrs = fkattrs.split('__') + def __init__(self, fkeydecl, short_description=None, admin_order_field=None): + self.fk, fkattrs = fkeydecl.split("__", 1) + self.fkattrs = fkattrs.split("__") self.short_description = short_description or self.fkattrs[-1] self.admin_order_field = admin_order_field or fkeydecl @@ -126,19 +128,19 @@ def ProfileInfo(field, short_description, boolean=False): return getattr(self.profile, field) except CofProfile.DoesNotExist: return "" + getter.short_description = short_description getter.boolean = boolean return getter -User.profile_login_clipper = FkeyLookup("profile__login_clipper", - "Login clipper") + +User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper") User.profile_phone = ProfileInfo("phone", "Téléphone") User.profile_occupation = ProfileInfo("occupation", "Occupation") User.profile_departement = ProfileInfo("departement", "Departement") User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True) User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True) -User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente", - "ML BdA-R", True) +User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente", "ML BdA-R", True) class UserProfileAdmin(UserAdmin): @@ -147,7 +149,8 @@ class UserProfileAdmin(UserAdmin): return obj.profile.is_buro except CofProfile.DoesNotExist: return False - is_buro.short_description = 'Membre du Buro' + + is_buro.short_description = "Membre du Buro" is_buro.boolean = True def is_cof(self, obj): @@ -155,27 +158,33 @@ class UserProfileAdmin(UserAdmin): return obj.profile.is_cof except CofProfile.DoesNotExist: return False - is_cof.short_description = 'Membre du COF' + + is_cof.short_description = "Membre du COF" is_cof.boolean = True - list_display = ( - UserAdmin.list_display - + ('profile_login_clipper', 'profile_phone', 'profile_occupation', - 'profile_mailing_cof', 'profile_mailing_bda', - 'profile_mailing_bda_revente', 'is_cof', 'is_buro', ) + list_display = UserAdmin.list_display + ( + "profile_login_clipper", + "profile_phone", + "profile_occupation", + "profile_mailing_cof", + "profile_mailing_bda", + "profile_mailing_bda_revente", + "is_cof", + "is_buro", ) - list_display_links = ('username', 'email', 'first_name', 'last_name') - list_filter = UserAdmin.list_filter \ - + ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof', - 'profile__mailing_bda') - search_fields = UserAdmin.search_fields + ('profile__phone',) - inlines = [ - CofProfileInline, - ] + list_display_links = ("username", "email", "first_name", "last_name") + list_filter = UserAdmin.list_filter + ( + "profile__is_cof", + "profile__is_buro", + "profile__mailing_cof", + "profile__mailing_bda", + ) + search_fields = UserAdmin.search_fields + ("profile__phone",) + inlines = [CofProfileInline] staff_fieldsets = [ - (None, {'fields': ['username', 'password']}), - (_('Personal info'), {'fields': ['first_name', 'last_name', 'email']}), + (None, {"fields": ["username", "password"]}), + (_("Personal info"), {"fields": ["first_name", "last_name", "email"]}), ] def get_fieldsets(self, request, user=None): @@ -184,15 +193,15 @@ class UserProfileAdmin(UserAdmin): return super().get_fieldsets(request, user) def save_model(self, request, user, form, change): - cof_group, created = Group.objects.get_or_create(name='COF') + cof_group, created = Group.objects.get_or_create(name="COF") if created: # Si le groupe COF n'était pas déjà dans la bdd # On lui assigne les bonnes permissions perms = Permission.objects.filter( - Q(content_type__app_label='gestioncof') - | Q(content_type__app_label='bda') - | (Q(content_type__app_label='auth') - & Q(content_type__model='user'))) + Q(content_type__app_label="gestioncof") + | Q(content_type__app_label="bda") + | (Q(content_type__app_label="auth") & Q(content_type__model="user")) + ) cof_group.permissions = perms # On y associe les membres du Burô cof_group.user_set = User.objects.filter(profile__is_buro=True) @@ -214,72 +223,97 @@ def user_str(self): return "{} ({})".format(self.get_full_name(), self.username) else: return self.username + + User.__str__ = user_str class EventRegistrationAdminForm(forms.ModelForm): class Meta: - widgets = { - 'user': ModelSelect2(url='cof-user-autocomplete'), - } + widgets = {"user": ModelSelect2(url="cof-user-autocomplete")} class EventRegistrationAdmin(admin.ModelAdmin): form = EventRegistrationAdminForm - list_display = ('__str__', 'event', 'user', 'paid') - list_filter = ('paid',) - search_fields = ('user__username', 'user__first_name', 'user__last_name', - 'user__email', 'event__title') + list_display = ("__str__", "event", "user", "paid") + list_filter = ("paid",) + search_fields = ( + "user__username", + "user__first_name", + "user__last_name", + "user__email", + "event__title", + ) class PetitCoursAbilityAdmin(admin.ModelAdmin): - list_display = ('user', 'matiere', 'niveau', 'agrege') - search_fields = ('user__username', 'user__first_name', 'user__last_name', - 'user__email', 'matiere__name', 'niveau') - list_filter = ('matiere', 'niveau', 'agrege') + list_display = ("user", "matiere", "niveau", "agrege") + search_fields = ( + "user__username", + "user__first_name", + "user__last_name", + "user__email", + "matiere__name", + "niveau", + ) + list_filter = ("matiere", "niveau", "agrege") class PetitCoursAttributionAdmin(admin.ModelAdmin): - list_display = ('user', 'demande', 'matiere', 'rank', ) - search_fields = ('user__username', 'matiere__name') + list_display = ("user", "demande", "matiere", "rank") + search_fields = ("user__username", "matiere__name") class PetitCoursAttributionCounterAdmin(admin.ModelAdmin): - list_display = ('user', 'matiere', 'count', ) - list_filter = ('matiere',) - search_fields = ('user__username', 'user__first_name', 'user__last_name', - 'user__email', 'matiere__name') - actions = ['reset', ] + list_display = ("user", "matiere", "count") + list_filter = ("matiere",) + search_fields = ( + "user__username", + "user__first_name", + "user__last_name", + "user__email", + "matiere__name", + ) + actions = ["reset"] actions_on_bottom = True def reset(self, request, queryset): queryset.update(count=0) + reset.short_description = "Remise à zéro du compteur" class PetitCoursDemandeAdmin(admin.ModelAdmin): - list_display = ('name', 'email', 'agrege_requis', 'niveau', 'created', - 'traitee', 'processed') - list_filter = ('traitee', 'niveau') - search_fields = ('name', 'email', 'phone', 'lieu', 'remarques') + list_display = ( + "name", + "email", + "agrege_requis", + "niveau", + "created", + "traitee", + "processed", + ) + list_filter = ("traitee", "niveau") + search_fields = ("name", "email", "phone", "lieu", "remarques") class ClubAdminForm(forms.ModelForm): def clean(self): cleaned_data = super().clean() - respos = cleaned_data.get('respos') - members = cleaned_data.get('membres') + respos = cleaned_data.get("respos") + members = cleaned_data.get("membres") for respo in respos.all(): if respo not in members: raise forms.ValidationError( "Erreur : le respo %s n'est pas membre du club." - % respo.get_full_name()) + % respo.get_full_name() + ) return cleaned_data class ClubAdmin(admin.ModelAdmin): - list_display = ['name'] + list_display = ["name"] form = ClubAdminForm @@ -294,7 +328,6 @@ admin.site.register(Club, ClubAdmin) admin.site.register(PetitCoursSubject) admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin) admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin) -admin.site.register(PetitCoursAttributionCounter, - PetitCoursAttributionCounterAdmin) +admin.site.register(PetitCoursAttributionCounter, PetitCoursAttributionCounterAdmin) admin.site.register(PetitCoursDemande, PetitCoursDemandeAdmin) admin.site.register(EventRegistration, EventRegistrationAdmin) diff --git a/gestioncof/apps.py b/gestioncof/apps.py index 78120ef4..b132366a 100644 --- a/gestioncof/apps.py +++ b/gestioncof/apps.py @@ -2,14 +2,16 @@ from django.apps import AppConfig class GestioncofConfig(AppConfig): - name = 'gestioncof' + name = "gestioncof" verbose_name = "Gestion des adhérents du COF" def ready(self): from . import signals + self.register_config() def register_config(self): import djconfig from .forms import GestioncofConfigForm + djconfig.register(GestioncofConfigForm) diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index d6483869..0aa94ae9 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -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 django import shortcuts -from django.http import Http404 -from django.db.models import Q -from django.contrib.auth.models import User -from django.conf import settings - -from gestioncof.models import CofProfile from gestioncof.decorators import buro_required +from gestioncof.models import CofProfile class Clipper(object): @@ -20,74 +19,70 @@ class Clipper(object): self.fullname = fullname def __str__(self): - return '{} ({})'.format(self.clipper, self.fullname) + return "{} ({})".format(self.clipper, self.fullname) def __eq__(self, other): - return ( - self.clipper == other.clipper and self.fullname == other.fullname) + return self.clipper == other.clipper and self.fullname == other.fullname @buro_required def autocomplete(request): if "q" not in request.GET: raise Http404 - q = request.GET['q'] - data = { - 'q': q, - } + q = request.GET["q"] + data = {"q": q} queries = {} bits = q.split() # Fetching data from User and CofProfile tables - queries['members'] = CofProfile.objects.filter(is_cof=True) - queries['users'] = User.objects.filter(profile__is_cof=False) + queries["members"] = CofProfile.objects.filter(is_cof=True) + queries["users"] = User.objects.filter(profile__is_cof=False) for bit in bits: - queries['members'] = queries['members'].filter( + queries["members"] = queries["members"].filter( Q(user__first_name__icontains=bit) | Q(user__last_name__icontains=bit) | Q(user__username__icontains=bit) - | Q(login_clipper__icontains=bit)) - queries['users'] = queries['users'].filter( + | Q(login_clipper__icontains=bit) + ) + queries["users"] = queries["users"].filter( Q(first_name__icontains=bit) | Q(last_name__icontains=bit) - | Q(username__icontains=bit)) - queries['members'] = queries['members'].distinct() - queries['users'] = queries['users'].distinct() + | Q(username__icontains=bit) + ) + queries["members"] = queries["members"].distinct() + queries["users"] = queries["users"].distinct() # Clearing redundancies - usernames = ( - set(queries['members'].values_list('login_clipper', flat='True')) - | set(queries['users'].values_list('profile__login_clipper', - flat='True')) + usernames = set(queries["members"].values_list("login_clipper", flat="True")) | set( + queries["users"].values_list("profile__login_clipper", flat="True") ) # Fetching data from the SPI - if getattr(settings, 'LDAP_SERVER_URL', None): + if getattr(settings, "LDAP_SERVER_URL", None): # Fetching - ldap_query = '(&{:s})'.format(''.join( - '(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit) - for bit in bits if bit.isalnum() - )) + ldap_query = "(&{:s})".format( + "".join( + "(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=bit) + for bit in bits + if bit.isalnum() + ) + ) if ldap_query != "(&)": # If none of the bits were legal, we do not perform the query entries = None with Connection(settings.LDAP_SERVER_URL) as conn: - conn.search( - 'dc=spi,dc=ens,dc=fr', ldap_query, - attributes=['uid', 'cn'] - ) + conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"]) entries = conn.entries # Clearing redundancies - queries['clippers'] = [ + queries["clippers"] = [ Clipper(entry.uid.value, entry.cn.value) for entry in entries - if entry.uid.value - and entry.uid.value not in usernames + if entry.uid.value and entry.uid.value not in usernames ] # Resulting data data.update(queries) - data['options'] = sum(len(query) for query in queries) + data["options"] = sum(len(query) for query in queries) return shortcuts.render(request, "autocomplete_user.html", data) diff --git a/gestioncof/csv_views.py b/gestioncof/csv_views.py index 733768dc..e85c78b0 100644 --- a/gestioncof/csv_views.py +++ b/gestioncof/csv_views.py @@ -1,14 +1,16 @@ import csv + +from django.apps import apps from django.http import HttpResponse, HttpResponseForbidden from django.template.defaultfilters import slugify -from django.apps import apps def export(qs, fields=None): model = qs.model - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=%s.csv' \ - % slugify(model.__name__) + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = "attachment; filename=%s.csv" % slugify( + model.__name__ + ) writer = csv.writer(response) # Write headers to CSV file if fields: @@ -32,8 +34,9 @@ def export(qs, fields=None): return response -def admin_list_export(request, model_name, app_label, queryset=None, - fields=None, list_display=True): +def admin_list_export( + request, model_name, app_label, queryset=None, fields=None, list_display=True +): """ Put the following line in your urls.py BEFORE your admin include (r'^admin/(?P[\d\w]+)/(?P[\d\w]+)/csv/', diff --git a/gestioncof/decorators.py b/gestioncof/decorators.py index 3875b77d..fe5a7ccc 100644 --- a/gestioncof/decorators.py +++ b/gestioncof/decorators.py @@ -8,6 +8,7 @@ def is_cof(user): except: return False + cof_required = user_passes_test(is_cof) @@ -18,4 +19,5 @@ def is_buro(user): except: return False + buro_required = user_passes_test(is_buro) diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 4ad9b058..aec5ce24 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -1,16 +1,13 @@ from django import forms -from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User -from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.forms.formsets import BaseFormSet, formset_factory - +from django.forms.widgets import CheckboxSelectMultiple, RadioSelect +from django.utils.translation import ugettext_lazy as _ from djconfig.forms import ConfigForm -from gestioncof.models import CofProfile, EventCommentValue, \ - CalendarSubscription, Club -from gestioncof.widgets import TriStateCheckbox - from bda.models import Spectacle +from gestioncof.models import CalendarSubscription, Club, CofProfile, EventCommentValue +from gestioncof.widgets import TriStateCheckbox class EventForm(forms.Form): @@ -28,31 +25,33 @@ class EventForm(forms.Form): choices[choice.event_option.id].append(choice.id) all_choices = choices for option in event.options.all(): - choices = [(choice.id, choice.value) - for choice in option.choices.all()] + choices = [(choice.id, choice.value) for choice in option.choices.all()] if option.multi_choices: - initial = [] if option.id not in all_choices \ - else all_choices[option.id] + initial = [] if option.id not in all_choices else all_choices[option.id] field = forms.MultipleChoiceField( label=option.name, choices=choices, widget=CheckboxSelectMultiple, required=False, - initial=initial) + initial=initial, + ) else: - initial = None if option.id not in all_choices \ - else all_choices[option.id][0] - field = forms.ChoiceField(label=option.name, - choices=choices, - widget=RadioSelect, - required=False, - initial=initial) + initial = ( + None if option.id not in all_choices else all_choices[option.id][0] + ) + field = forms.ChoiceField( + label=option.name, + choices=choices, + widget=RadioSelect, + required=False, + initial=initial, + ) field.option_id = option.id self.fields["option_%d" % option.id] = field def choices(self): for name, value in self.cleaned_data.items(): - if name.startswith('option_'): + if name.startswith("option_"): yield (self.fields[name].option_id, value) @@ -69,31 +68,33 @@ class SurveyForm(forms.Form): else: answers[answer.survey_question.id].append(answer.id) for question in survey.questions.all(): - choices = [(answer.id, answer.answer) - for answer in question.answers.all()] + choices = [(answer.id, answer.answer) for answer in question.answers.all()] if question.multi_answers: - initial = [] if question.id not in answers\ - else answers[question.id] + initial = [] if question.id not in answers else answers[question.id] field = forms.MultipleChoiceField( label=question.question, choices=choices, widget=CheckboxSelectMultiple, required=False, - initial=initial) + initial=initial, + ) else: - initial = None if question.id not in answers\ - else answers[question.id][0] - field = forms.ChoiceField(label=question.question, - choices=choices, - widget=RadioSelect, - required=False, - initial=initial) + initial = ( + None if question.id not in answers else answers[question.id][0] + ) + field = forms.ChoiceField( + label=question.question, + choices=choices, + widget=RadioSelect, + required=False, + initial=initial, + ) field.question_id = question.id self.fields["question_%d" % question.id] = field def answers(self): for name, value in self.cleaned_data.items(): - if name.startswith('question_'): + if name.startswith("question_"): yield (self.fields[name].question_id, value) @@ -104,8 +105,7 @@ class SurveyStatusFilterForm(forms.Form): for question in survey.questions.all(): for answer in question.answers.all(): name = "question_%d_answer_%d" % (question.id, answer.id) - if self.is_bound \ - and self.data.get(self.add_prefix(name), None): + if self.is_bound and self.data.get(self.add_prefix(name), None): initial = self.data.get(self.add_prefix(name), None) else: initial = "none" @@ -114,16 +114,20 @@ class SurveyStatusFilterForm(forms.Form): choices=[("yes", "yes"), ("no", "no"), ("none", "none")], widget=TriStateCheckbox, required=False, - initial=initial) + initial=initial, + ) field.question_id = question.id field.answer_id = answer.id self.fields[name] = field def filters(self): for name, value in self.cleaned_data.items(): - if name.startswith('question_'): - yield (self.fields[name].question_id, - self.fields[name].answer_id, value) + if name.startswith("question_"): + yield ( + self.fields[name].question_id, + self.fields[name].answer_id, + value, + ) class EventStatusFilterForm(forms.Form): @@ -133,8 +137,7 @@ class EventStatusFilterForm(forms.Form): for option in event.options.all(): for choice in option.choices.all(): name = "option_%d_choice_%d" % (option.id, choice.id) - if self.is_bound \ - and self.data.get(self.add_prefix(name), None): + if self.is_bound and self.data.get(self.add_prefix(name), None): initial = self.data.get(self.add_prefix(name), None) else: initial = "none" @@ -143,7 +146,8 @@ class EventStatusFilterForm(forms.Form): choices=[("yes", "yes"), ("no", "no"), ("none", "none")], widget=TriStateCheckbox, required=False, - initial=initial) + initial=initial, + ) field.option_id = option.id field.choice_id = choice.id self.fields[name] = field @@ -153,19 +157,19 @@ class EventStatusFilterForm(forms.Form): initial = self.data.get(self.add_prefix(name), None) else: initial = "none" - field = forms.ChoiceField(label="Événement payé", - choices=[("yes", "yes"), ("no", "no"), - ("none", "none")], - widget=TriStateCheckbox, - required=False, - initial=initial) + field = forms.ChoiceField( + label="Événement payé", + choices=[("yes", "yes"), ("no", "no"), ("none", "none")], + widget=TriStateCheckbox, + required=False, + initial=initial, + ) self.fields[name] = field def filters(self): for name, value in self.cleaned_data.items(): - if name.startswith('option_'): - yield (self.fields[name].option_id, - self.fields[name].choice_id, value) + if name.startswith("option_"): + yield (self.fields[name].option_id, self.fields[name].choice_id, value) elif name == "event_has_paid": yield ("has_paid", None, value) @@ -184,14 +188,14 @@ class ProfileForm(forms.ModelForm): "mailing_cof", "mailing_bda", "mailing_bda_revente", - "mailing_unernestaparis" + "mailing_unernestaparis", ] class RegistrationUserForm(forms.ModelForm): def __init__(self, *args, **kw): super().__init__(*args, **kw) - self.fields['username'].help_text = "" + self.fields["username"].help_text = "" class Meta: model = User @@ -202,22 +206,23 @@ class RegistrationPassUserForm(RegistrationUserForm): """ Formulaire pour changer le mot de passe d'un utilisateur. """ - password1 = forms.CharField(label=_('Mot de passe'), - widget=forms.PasswordInput) - password2 = forms.CharField(label=_('Confirmation du mot de passe'), - widget=forms.PasswordInput) + + password1 = forms.CharField(label=_("Mot de passe"), widget=forms.PasswordInput) + password2 = forms.CharField( + label=_("Confirmation du mot de passe"), widget=forms.PasswordInput + ) def clean_password2(self): - pass1 = self.cleaned_data['password1'] - pass2 = self.cleaned_data['password2'] + pass1 = self.cleaned_data["password1"] + pass2 = self.cleaned_data["password2"] if pass1 and pass2: if pass1 != pass2: - raise forms.ValidationError(_('Mots de passe non identiques.')) + raise forms.ValidationError(_("Mots de passe non identiques.")) return pass2 def save(self, commit=True, *args, **kwargs): user = super().save(commit, *args, **kwargs) - user.set_password(self.cleaned_data['password2']) + user.set_password(self.cleaned_data["password2"]) if commit: user.save() return user @@ -226,48 +231,62 @@ class RegistrationPassUserForm(RegistrationUserForm): class RegistrationProfileForm(forms.ModelForm): def __init__(self, *args, **kw): super().__init__(*args, **kw) - self.fields['mailing_cof'].initial = True - self.fields['mailing_bda'].initial = True - self.fields['mailing_bda_revente'].initial = True - self.fields['mailing_unernestaparis'].initial = True + self.fields["mailing_cof"].initial = True + self.fields["mailing_bda"].initial = True + self.fields["mailing_bda_revente"].initial = True + self.fields["mailing_unernestaparis"].initial = True self.fields.keyOrder = [ - 'login_clipper', - 'phone', - 'occupation', - 'departement', - 'is_cof', - 'type_cotiz', - 'mailing_cof', - 'mailing_bda', - 'mailing_bda_revente', + "login_clipper", + "phone", + "occupation", + "departement", + "is_cof", + "type_cotiz", + "mailing_cof", + "mailing_bda", + "mailing_bda_revente", "mailing_unernestaparis", - 'comments' + "comments", ] class Meta: model = CofProfile - fields = ("login_clipper", "phone", "occupation", - "departement", "is_cof", "type_cotiz", "mailing_cof", - "mailing_bda", "mailing_bda_revente", - "mailing_unernestaparis", "comments") + fields = ( + "login_clipper", + "phone", + "occupation", + "departement", + "is_cof", + "type_cotiz", + "mailing_cof", + "mailing_bda", + "mailing_bda_revente", + "mailing_unernestaparis", + "comments", + ) -STATUS_CHOICES = (('no', 'Non'), - ('wait', 'Oui mais attente paiement'), - ('paid', 'Oui payé'),) +STATUS_CHOICES = ( + ("no", "Non"), + ("wait", "Oui mais attente paiement"), + ("paid", "Oui payé"), +) class AdminEventForm(forms.Form): - status = forms.ChoiceField(label="Inscription", initial="no", - choices=STATUS_CHOICES, widget=RadioSelect) + status = forms.ChoiceField( + label="Inscription", initial="no", choices=STATUS_CHOICES, widget=RadioSelect + ) def __init__(self, *args, **kwargs): self.event = kwargs.pop("event") registration = kwargs.pop("current_registration", None) - current_choices, paid = \ - (registration.options.all(), registration.paid) \ - if registration is not None else ([], None) + current_choices, paid = ( + (registration.options.all(), registration.paid) + if registration is not None + else ([], None) + ) if paid is True: kwargs["initial"] = {"status": "paid"} elif paid is False: @@ -283,66 +302,69 @@ class AdminEventForm(forms.Form): choices[choice.event_option.id].append(choice.id) all_choices = choices for option in self.event.options.all(): - choices = [(choice.id, choice.value) - for choice in option.choices.all()] + choices = [(choice.id, choice.value) for choice in option.choices.all()] if option.multi_choices: - initial = [] if option.id not in all_choices\ - else all_choices[option.id] + initial = [] if option.id not in all_choices else all_choices[option.id] field = forms.MultipleChoiceField( label=option.name, choices=choices, widget=CheckboxSelectMultiple, required=False, - initial=initial) + initial=initial, + ) else: - initial = None if option.id not in all_choices\ - else all_choices[option.id][0] - field = forms.ChoiceField(label=option.name, - choices=choices, - widget=RadioSelect, - required=False, - initial=initial) + initial = ( + None if option.id not in all_choices else all_choices[option.id][0] + ) + field = forms.ChoiceField( + label=option.name, + choices=choices, + widget=RadioSelect, + required=False, + initial=initial, + ) field.option_id = option.id self.fields["option_%d" % option.id] = field for commentfield in self.event.commentfields.all(): initial = commentfield.default if registration is not None: try: - initial = registration.comments \ - .get(commentfield=commentfield).content + initial = registration.comments.get( + commentfield=commentfield + ).content except EventCommentValue.DoesNotExist: pass - widget = forms.Textarea if commentfield.fieldtype == "text" \ - else forms.TextInput - field = forms.CharField(label=commentfield.name, - widget=widget, - required=False, - initial=initial) + widget = ( + forms.Textarea if commentfield.fieldtype == "text" else forms.TextInput + ) + field = forms.CharField( + label=commentfield.name, widget=widget, required=False, initial=initial + ) field.comment_id = commentfield.id self.fields["comment_%d" % commentfield.id] = field def choices(self): for name, value in self.cleaned_data.items(): - if name.startswith('option_'): + if name.startswith("option_"): yield (self.fields[name].option_id, value) def comments(self): for name, value in self.cleaned_data.items(): - if name.startswith('comment_'): + if name.startswith("comment_"): yield (self.fields[name].comment_id, value) class BaseEventRegistrationFormset(BaseFormSet): def __init__(self, *args, **kwargs): - self.events = kwargs.pop('events') - self.current_registrations = kwargs.pop('current_registrations', None) + self.events = kwargs.pop("events") + self.current_registrations = kwargs.pop("current_registrations", None) self.extra = len(self.events) super().__init__(*args, **kwargs) def _construct_form(self, index, **kwargs): - kwargs['event'] = self.events[index] + kwargs["event"] = self.events[index] if self.current_registrations is not None: - kwargs['current_registration'] = self.current_registrations[index] + kwargs["current_registration"] = self.current_registrations[index] return super()._construct_form(index, **kwargs) @@ -351,34 +373,36 @@ EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset) class CalendarForm(forms.ModelForm): subscribe_to_events = forms.BooleanField( - initial=True, - label="Événements du COF", - required=False) + initial=True, label="Événements du COF", required=False + ) subscribe_to_my_shows = forms.BooleanField( - initial=True, - label="Les spectacles pour lesquels j'ai obtenu une place", - required=False) + initial=True, + label="Les spectacles pour lesquels j'ai obtenu une place", + required=False, + ) other_shows = forms.ModelMultipleChoiceField( - label="Spectacles supplémentaires", - queryset=Spectacle.objects.filter(tirage__active=True), - widget=forms.CheckboxSelectMultiple, - required=False) + label="Spectacles supplémentaires", + queryset=Spectacle.objects.filter(tirage__active=True), + widget=forms.CheckboxSelectMultiple, + required=False, + ) class Meta: model = CalendarSubscription - fields = ['subscribe_to_events', 'subscribe_to_my_shows', - 'other_shows'] + fields = ["subscribe_to_events", "subscribe_to_my_shows", "other_shows"] class ClubsForm(forms.Form): """ Formulaire d'inscription d'un membre à plusieurs clubs du COF. """ + clubs = forms.ModelMultipleChoiceField( - label="Inscriptions aux clubs du COF", - queryset=Club.objects.all(), - widget=forms.CheckboxSelectMultiple, - required=False) + label="Inscriptions aux clubs du COF", + queryset=Club.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) # --- @@ -386,9 +410,10 @@ class ClubsForm(forms.Form): # TODO: move this to the `gestion` app once the supportBDS branch is merged # --- + class GestioncofConfigForm(ConfigForm): gestion_banner = forms.CharField( label=_("Announcements banner"), help_text=_("An empty banner disables annoucements"), - max_length=2048 + max_length=2048, ) diff --git a/gestioncof/management/base.py b/gestioncof/management/base.py index ab4d1a30..7d7bcc30 100644 --- a/gestioncof/management/base.py +++ b/gestioncof/management/base.py @@ -2,8 +2,8 @@ Un mixin à utiliser avec BaseCommand pour charger des objets depuis un json """ -import os import json +import os from django.core.management.base import BaseCommand @@ -13,15 +13,14 @@ class MyBaseCommand(BaseCommand): Ajoute une méthode ``from_json`` qui charge des objets à partir d'un json. """ - def from_json(self, filename, data_dir, klass, - callback=lambda obj: obj): + def from_json(self, filename, data_dir, klass, callback=lambda obj: obj): """ Charge les objets contenus dans le fichier json référencé par ``filename`` dans la base de donnée. La fonction callback est appelées sur chaque objet avant enregistrement. """ self.stdout.write("Chargement de {:s}".format(filename)) - with open(os.path.join(data_dir, filename), 'r') as file: + with open(os.path.join(data_dir, filename), "r") as file: descriptions = json.load(file) objects = [] nb_new = 0 @@ -36,6 +35,7 @@ class MyBaseCommand(BaseCommand): objects.append(obj) nb_new += 1 self.stdout.write("- {:d} objets créés".format(nb_new)) - self.stdout.write("- {:d} objets gardés en l'état" - .format(len(objects)-nb_new)) + self.stdout.write( + "- {:d} objets gardés en l'état".format(len(objects) - nb_new) + ) return objects diff --git a/gestioncof/management/commands/loaddevdata.py b/gestioncof/management/commands/loaddevdata.py index 7358c695..44d77065 100644 --- a/gestioncof/management/commands/loaddevdata.py +++ b/gestioncof/management/commands/loaddevdata.py @@ -15,13 +15,14 @@ from django.core.management import call_command from gestioncof.management.base import MyBaseCommand from gestioncof.petits_cours_models import ( - PetitCoursAbility, PetitCoursSubject, LEVELS_CHOICES, - PetitCoursAttributionCounter + LEVELS_CHOICES, + PetitCoursAbility, + PetitCoursAttributionCounter, + PetitCoursSubject, ) # Où sont stockés les fichiers json -DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), - 'data') +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") class Command(MyBaseCommand): @@ -32,11 +33,11 @@ class Command(MyBaseCommand): Permet de ne pas créer l'utilisateur "root". """ parser.add_argument( - '--no-root', - action='store_true', - dest='no-root', + "--no-root", + action="store_true", + dest="no-root", default=False, - help='Ne crée pas l\'utilisateur "root"' + help='Ne crée pas l\'utilisateur "root"', ) def handle(self, *args, **options): @@ -45,24 +46,25 @@ class Command(MyBaseCommand): # --- # Gaulois - gaulois = self.from_json('gaulois.json', DATA_DIR, User) + gaulois = self.from_json("gaulois.json", DATA_DIR, User) for user in gaulois: user.profile.is_cof = True user.profile.save() # Romains - self.from_json('romains.json', DATA_DIR, User) + self.from_json("romains.json", DATA_DIR, User) # Root - no_root = options.get('no-root', False) + no_root = options.get("no-root", False) if not no_root: self.stdout.write("Création de l'utilisateur root") root, _ = User.objects.get_or_create( - username='root', - first_name='super', - last_name='user', - email='root@localhost') - root.set_password('root') + username="root", + first_name="super", + last_name="user", + email="root@localhost", + ) + root.set_password("root") root.is_staff = True root.is_superuser = True root.profile.is_cof = True @@ -87,18 +89,17 @@ class Command(MyBaseCommand): # L'utilisateur est compétent dans une matière subject = random.choice(subjects) if not PetitCoursAbility.objects.filter( - user=user, - matiere=subject).exists(): + user=user, matiere=subject + ).exists(): PetitCoursAbility.objects.create( user=user, matiere=subject, niveau=random.choice(levels), - agrege=bool(random.randint(0, 1)) + agrege=bool(random.randint(0, 1)), ) # On initialise son compteur d'attributions PetitCoursAttributionCounter.objects.get_or_create( - user=user, - matiere=subject + user=user, matiere=subject ) self.stdout.write("- {:d} inscriptions".format(nb_of_teachers)) @@ -106,10 +107,10 @@ class Command(MyBaseCommand): # Le BdA # --- - call_command('loadbdadevdata') + call_command("loadbdadevdata") # --- # La K-Fêt # --- - call_command('loadkfetdevdata') + call_command("loadkfetdevdata") diff --git a/gestioncof/management/commands/syncmails.py b/gestioncof/management/commands/syncmails.py index 8f302186..0dd15d34 100644 --- a/gestioncof/management/commands/syncmails.py +++ b/gestioncof/management/commands/syncmails.py @@ -4,11 +4,10 @@ Import des mails de GestioCOF dans la base de donnée import json import os -from custommail.models import Type, CustomMail, Variable -from django.core.management.base import BaseCommand +from custommail.models import CustomMail, Type, Variable 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") @@ -19,15 +18,15 @@ def dummy_log(__): # 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: + 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} + assoc = {"types": {}, "mails": {}} + status = {"synced": 0, "unchanged": 0} for obj in mail_data: - fields = obj['fields'] + fields = obj["fields"] # Pour les trois types d'objets : # - On récupère les objets référencés par les clefs étrangères @@ -36,58 +35,55 @@ def load_from_file(log=dummy_log, verbosity=1): # 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']) + 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 + assoc["types"][obj["pk"]] = var_type # Custom mails - if obj['model'] == 'custommail.custommail': + if obj["model"] == "custommail.custommail": mail = None try: - mail = CustomMail.objects.get(shortname=fields['shortname']) - status['unchanged'] += 1 + mail = CustomMail.objects.get(shortname=fields["shortname"]) + status["unchanged"] += 1 except CustomMail.DoesNotExist: mail = CustomMail.objects.create(**fields) - status['synced'] += 1 + status["synced"] += 1 if verbosity: - log('SYNCED {:s}'.format(fields['shortname'])) - assoc['mails'][obj['pk']] = mail + 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']) + 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'] + 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) - ) + log("{synced:d} mails synchronized {unchanged:d} unchanged".format(**status)) class Command(BaseCommand): - help = ("Va chercher les données mails de GestioCOF stocké au format json " - "dans /gestioncof/management/data/custommails.json. Le format des " - "données est celui donné par la commande :" - " `python manage.py dumpdata custommail --natural-foreign` " - "La bonne façon de mettre à jour ce fichier est donc de le " - "charger à l'aide de syncmails, le faire les modifications à " - "l'aide de l'interface administration et/ou du shell puis de le " - "remplacer par le nouveau résultat de la commande précédente.") + help = ( + "Va chercher les données mails de GestioCOF stocké au format json " + "dans /gestioncof/management/data/custommails.json. Le format des " + "données est celui donné par la commande :" + " `python manage.py dumpdata custommail --natural-foreign` " + "La bonne façon de mettre à jour ce fichier est donc de le " + "charger à l'aide de syncmails, le faire les modifications à " + "l'aide de l'interface administration et/ou du shell puis de le " + "remplacer par le nouveau résultat de la commande précédente." + ) def handle(self, *args, **options): load_from_file(log=self.stdout.write) diff --git a/gestioncof/migrations/0001_initial.py b/gestioncof/migrations/0001_initial.py index c6bb6151..b3c10b90 100644 --- a/gestioncof/migrations/0001_initial.py +++ b/gestioncof/migrations/0001_initial.py @@ -1,333 +1,856 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='Clipper', + name="Clipper", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('username', models.CharField(max_length=20, verbose_name=b'Identifiant')), - ('fullname', models.CharField(max_length=200, verbose_name=b'Nom complet')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "username", + models.CharField(max_length=20, verbose_name=b"Identifiant"), + ), + ( + "fullname", + models.CharField(max_length=200, verbose_name=b"Nom complet"), + ), ], ), migrations.CreateModel( - name='Club', + name="Club", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=200, verbose_name=b'Nom')), - ('description', models.TextField(verbose_name=b'Description')), - ('membres', models.ManyToManyField(related_name='clubs', to=settings.AUTH_USER_MODEL)), - ('respos', models.ManyToManyField(related_name='clubs_geres', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=200, verbose_name=b"Nom")), + ("description", models.TextField(verbose_name=b"Description")), + ( + "membres", + models.ManyToManyField( + related_name="clubs", to=settings.AUTH_USER_MODEL + ), + ), + ( + "respos", + models.ManyToManyField( + related_name="clubs_geres", to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.CreateModel( - name='CofProfile', + name="CofProfile", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('login_clipper', models.CharField(max_length=8, verbose_name=b'Login clipper', blank=True)), - ('is_cof', models.BooleanField(default=False, verbose_name=b'Membre du COF')), - ('num', models.IntegerField(default=0, verbose_name=b"Num\xc3\xa9ro d'adh\xc3\xa9rent", blank=True)), - ('phone', models.CharField(max_length=20, verbose_name=b'T\xc3\xa9l\xc3\xa9phone', blank=True)), - ('occupation', models.CharField(default=b'1A', max_length=9, verbose_name='Occupation', choices=[(b'exterieur', 'Ext\xe9rieur'), (b'1A', '1A'), (b'2A', '2A'), (b'3A', '3A'), (b'4A', '4A'), (b'archicube', 'Archicube'), (b'doctorant', 'Doctorant'), (b'CST', 'CST')])), - ('departement', models.CharField(max_length=50, verbose_name='D\xe9partement', blank=True)), - ('type_cotiz', models.CharField(default=b'normalien', max_length=9, verbose_name='Type de cotisation', choices=[(b'etudiant', 'Normalien \xe9tudiant'), (b'normalien', 'Normalien \xe9l\xe8ve'), (b'exterieur', 'Ext\xe9rieur')])), - ('mailing_cof', models.BooleanField(default=False, verbose_name=b'Recevoir les mails COF')), - ('mailing_bda', models.BooleanField(default=False, verbose_name=b'Recevoir les mails BdA')), - ('mailing_bda_revente', models.BooleanField(default=False, verbose_name=b'Recevoir les mails de revente de places BdA')), - ('comments', models.TextField(verbose_name=b'Commentaires visibles uniquement par le Buro', blank=True)), - ('is_buro', models.BooleanField(default=False, verbose_name=b'Membre du Bur\xc3\xb4')), - ('petits_cours_accept', models.BooleanField(default=False, verbose_name=b'Recevoir des petits cours')), - ('petits_cours_remarques', models.TextField(default=b'', verbose_name='Remarques et pr\xe9cisions pour les petits cours', blank=True)), - ('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "login_clipper", + models.CharField( + max_length=8, verbose_name=b"Login clipper", blank=True + ), + ), + ( + "is_cof", + models.BooleanField(default=False, verbose_name=b"Membre du COF"), + ), + ( + "num", + models.IntegerField( + default=0, + verbose_name=b"Num\xc3\xa9ro d'adh\xc3\xa9rent", + blank=True, + ), + ), + ( + "phone", + models.CharField( + max_length=20, + verbose_name=b"T\xc3\xa9l\xc3\xa9phone", + blank=True, + ), + ), + ( + "occupation", + models.CharField( + default=b"1A", + max_length=9, + verbose_name="Occupation", + choices=[ + (b"exterieur", "Ext\xe9rieur"), + (b"1A", "1A"), + (b"2A", "2A"), + (b"3A", "3A"), + (b"4A", "4A"), + (b"archicube", "Archicube"), + (b"doctorant", "Doctorant"), + (b"CST", "CST"), + ], + ), + ), + ( + "departement", + models.CharField( + max_length=50, verbose_name="D\xe9partement", blank=True + ), + ), + ( + "type_cotiz", + models.CharField( + default=b"normalien", + max_length=9, + verbose_name="Type de cotisation", + choices=[ + (b"etudiant", "Normalien \xe9tudiant"), + (b"normalien", "Normalien \xe9l\xe8ve"), + (b"exterieur", "Ext\xe9rieur"), + ], + ), + ), + ( + "mailing_cof", + models.BooleanField( + default=False, verbose_name=b"Recevoir les mails COF" + ), + ), + ( + "mailing_bda", + models.BooleanField( + default=False, verbose_name=b"Recevoir les mails BdA" + ), + ), + ( + "mailing_bda_revente", + models.BooleanField( + default=False, + verbose_name=b"Recevoir les mails de revente de places BdA", + ), + ), + ( + "comments", + models.TextField( + verbose_name=b"Commentaires visibles uniquement par le Buro", + blank=True, + ), + ), + ( + "is_buro", + models.BooleanField( + default=False, verbose_name=b"Membre du Bur\xc3\xb4" + ), + ), + ( + "petits_cours_accept", + models.BooleanField( + default=False, verbose_name=b"Recevoir des petits cours" + ), + ), + ( + "petits_cours_remarques", + models.TextField( + default=b"", + verbose_name="Remarques et pr\xe9cisions pour les petits cours", + blank=True, + ), + ), + ( + "user", + models.OneToOneField( + related_name="profile", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name': 'Profil COF', - 'verbose_name_plural': 'Profils COF', + "verbose_name": "Profil COF", + "verbose_name_plural": "Profils COF", }, ), migrations.CreateModel( - name='CustomMail', + name="CustomMail", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('shortname', models.SlugField()), - ('title', models.CharField(max_length=200, verbose_name=b'Titre')), - ('content', models.TextField(verbose_name=b'Contenu')), - ('comments', models.TextField(verbose_name=b'Informations contextuelles sur le mail', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("shortname", models.SlugField()), + ("title", models.CharField(max_length=200, verbose_name=b"Titre")), + ("content", models.TextField(verbose_name=b"Contenu")), + ( + "comments", + models.TextField( + verbose_name=b"Informations contextuelles sur le mail", + blank=True, + ), + ), ], - options={ - 'verbose_name': 'Mails personnalisables', - }, + options={"verbose_name": "Mails personnalisables"}, ), migrations.CreateModel( - name='Event', + name="Event", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=200, verbose_name=b'Titre')), - ('location', models.CharField(max_length=200, verbose_name=b'Lieu')), - ('start_date', models.DateField(null=True, verbose_name=b'Date de d\xc3\xa9but', blank=True)), - ('end_date', models.DateField(null=True, verbose_name=b'Date de fin', blank=True)), - ('description', models.TextField(verbose_name=b'Description', blank=True)), - ('registration_open', models.BooleanField(default=True, verbose_name=b'Inscriptions ouvertes')), - ('old', models.BooleanField(default=False, verbose_name=b'Archiver (\xc3\xa9v\xc3\xa9nement fini)')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=200, verbose_name=b"Titre")), + ("location", models.CharField(max_length=200, verbose_name=b"Lieu")), + ( + "start_date", + models.DateField( + null=True, verbose_name=b"Date de d\xc3\xa9but", blank=True + ), + ), + ( + "end_date", + models.DateField( + null=True, verbose_name=b"Date de fin", blank=True + ), + ), + ( + "description", + models.TextField(verbose_name=b"Description", blank=True), + ), + ( + "registration_open", + models.BooleanField( + default=True, verbose_name=b"Inscriptions ouvertes" + ), + ), + ( + "old", + models.BooleanField( + default=False, + verbose_name=b"Archiver (\xc3\xa9v\xc3\xa9nement fini)", + ), + ), ], - options={ - 'verbose_name': '\xc9v\xe9nement', - }, + options={"verbose_name": "\xc9v\xe9nement"}, ), migrations.CreateModel( - name='EventCommentField', + name="EventCommentField", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=200, verbose_name=b'Champ')), - ('fieldtype', models.CharField(default=b'text', max_length=10, verbose_name=b'Type', choices=[(b'text', 'Texte long'), (b'char', 'Texte court')])), - ('default', models.TextField(verbose_name=b'Valeur par d\xc3\xa9faut', blank=True)), - ('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=200, verbose_name=b"Champ")), + ( + "fieldtype", + models.CharField( + default=b"text", + max_length=10, + verbose_name=b"Type", + choices=[(b"text", "Texte long"), (b"char", "Texte court")], + ), + ), + ( + "default", + models.TextField( + verbose_name=b"Valeur par d\xc3\xa9faut", blank=True + ), + ), + ( + "event", + models.ForeignKey( + related_name="commentfields", + to="gestioncof.Event", + on_delete=models.CASCADE, + ), + ), ], - options={ - 'verbose_name': 'Champ', - }, + options={"verbose_name": "Champ"}, ), migrations.CreateModel( - name='EventCommentValue', + name="EventCommentValue", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('content', models.TextField(null=True, verbose_name=b'Contenu', blank=True)), - ('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "content", + models.TextField(null=True, verbose_name=b"Contenu", blank=True), + ), + ( + "commentfield", + models.ForeignKey( + related_name="values", + to="gestioncof.EventCommentField", + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='EventOption', + name="EventOption", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=200, verbose_name=b'Option')), - ('multi_choices', models.BooleanField(default=False, verbose_name=b'Choix multiples')), - ('event', models.ForeignKey(related_name='options', to='gestioncof.Event', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=200, verbose_name=b"Option")), + ( + "multi_choices", + models.BooleanField(default=False, verbose_name=b"Choix multiples"), + ), + ( + "event", + models.ForeignKey( + related_name="options", + to="gestioncof.Event", + on_delete=models.CASCADE, + ), + ), + ], + options={"verbose_name": "Option"}, + ), + migrations.CreateModel( + name="EventOptionChoice", + fields=[ + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("value", models.CharField(max_length=200, verbose_name=b"Valeur")), + ( + "event_option", + models.ForeignKey( + related_name="choices", + to="gestioncof.EventOption", + on_delete=models.CASCADE, + ), + ), + ], + options={"verbose_name": "Choix"}, + ), + migrations.CreateModel( + name="EventRegistration", + fields=[ + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "paid", + models.BooleanField(default=False, verbose_name=b"A pay\xc3\xa9"), + ), + ( + "event", + models.ForeignKey(to="gestioncof.Event", on_delete=models.CASCADE), + ), + ( + "filledcomments", + models.ManyToManyField( + to="gestioncof.EventCommentField", + through="gestioncof.EventCommentValue", + ), + ), + ("options", models.ManyToManyField(to="gestioncof.EventOptionChoice")), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), + ], + options={"verbose_name": "Inscription"}, + ), + migrations.CreateModel( + name="PetitCoursAbility", + fields=[ + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "niveau", + models.CharField( + max_length=12, + verbose_name="Niveau", + choices=[ + (b"college", "Coll\xe8ge"), + (b"lycee", "Lyc\xe9e"), + (b"prepa1styear", "Pr\xe9pa 1\xe8re ann\xe9e / L1"), + (b"prepa2ndyear", "Pr\xe9pa 2\xe8me ann\xe9e / L2"), + (b"licence3", "Licence 3"), + (b"other", "Autre (pr\xe9ciser dans les commentaires)"), + ], + ), + ), + ( + "agrege", + models.BooleanField(default=False, verbose_name="Agr\xe9g\xe9"), + ), ], options={ - 'verbose_name': 'Option', + "verbose_name": "Comp\xe9tence petits cours", + "verbose_name_plural": "Comp\xe9tences des petits cours", }, ), migrations.CreateModel( - name='EventOptionChoice', + name="PetitCoursAttribution", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('value', models.CharField(max_length=200, verbose_name=b'Valeur')), - ('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "date", + models.DateTimeField( + auto_now_add=True, verbose_name="Date d'attribution" + ), + ), + ("rank", models.IntegerField(verbose_name=b"Rang dans l'email")), + ( + "selected", + models.BooleanField( + default=False, verbose_name="S\xe9lectionn\xe9 par le demandeur" + ), + ), ], options={ - 'verbose_name': 'Choix', + "verbose_name": "Attribution de petits cours", + "verbose_name_plural": "Attributions de petits cours", }, ), migrations.CreateModel( - name='EventRegistration', + name="PetitCoursAttributionCounter", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('paid', models.BooleanField(default=False, verbose_name=b'A pay\xc3\xa9')), - ('event', models.ForeignKey(to='gestioncof.Event', on_delete=models.CASCADE)), - ('filledcomments', models.ManyToManyField(to='gestioncof.EventCommentField', through='gestioncof.EventCommentValue')), - ('options', models.ManyToManyField(to='gestioncof.EventOptionChoice')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "count", + models.IntegerField(default=0, verbose_name=b"Nombre d'envois"), + ), ], options={ - 'verbose_name': 'Inscription', + "verbose_name": "Compteur d'attribution de petits cours", + "verbose_name_plural": "Compteurs d'attributions de petits cours", }, ), migrations.CreateModel( - name='PetitCoursAbility', + name="PetitCoursDemande", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('niveau', models.CharField(max_length=12, verbose_name='Niveau', choices=[(b'college', 'Coll\xe8ge'), (b'lycee', 'Lyc\xe9e'), (b'prepa1styear', 'Pr\xe9pa 1\xe8re ann\xe9e / L1'), (b'prepa2ndyear', 'Pr\xe9pa 2\xe8me ann\xe9e / L2'), (b'licence3', 'Licence 3'), (b'other', 'Autre (pr\xe9ciser dans les commentaires)')])), - ('agrege', models.BooleanField(default=False, verbose_name='Agr\xe9g\xe9')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField(max_length=200, verbose_name="Nom/pr\xe9nom"), + ), + ( + "email", + models.CharField(max_length=300, verbose_name="Adresse email"), + ), + ( + "phone", + models.CharField( + max_length=20, + verbose_name="T\xe9l\xe9phone (facultatif)", + blank=True, + ), + ), + ( + "quand", + models.CharField( + help_text="Indiquez ici la p\xe9riode d\xe9sir\xe9e pour les petits cours (vacances scolaires, semaine, week-end).", + max_length=300, + verbose_name="Quand ?", + blank=True, + ), + ), + ( + "freq", + models.CharField( + help_text="Indiquez ici la fr\xe9quence envisag\xe9e (hebdomadaire, 2 fois par semaine, ...)", + max_length=300, + verbose_name="Fr\xe9quence", + blank=True, + ), + ), + ( + "lieu", + models.CharField( + help_text="Si vous avez avez une pr\xe9f\xe9rence sur le lieu.", + max_length=300, + verbose_name="Lieu (si pr\xe9f\xe9rence)", + blank=True, + ), + ), + ( + "agrege_requis", + models.BooleanField( + default=False, verbose_name="Agr\xe9g\xe9 requis" + ), + ), + ( + "niveau", + models.CharField( + default=b"", + max_length=12, + verbose_name="Niveau", + choices=[ + (b"college", "Coll\xe8ge"), + (b"lycee", "Lyc\xe9e"), + (b"prepa1styear", "Pr\xe9pa 1\xe8re ann\xe9e / L1"), + (b"prepa2ndyear", "Pr\xe9pa 2\xe8me ann\xe9e / L2"), + (b"licence3", "Licence 3"), + (b"other", "Autre (pr\xe9ciser dans les commentaires)"), + ], + ), + ), + ( + "remarques", + models.TextField( + verbose_name="Remarques et pr\xe9cisions", blank=True + ), + ), + ( + "traitee", + models.BooleanField(default=False, verbose_name="Trait\xe9e"), + ), + ( + "processed", + models.DateTimeField(verbose_name="Date de traitement", blank=True), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, verbose_name="Date de cr\xe9ation" + ), + ), ], options={ - 'verbose_name': 'Comp\xe9tence petits cours', - 'verbose_name_plural': 'Comp\xe9tences des petits cours', + "verbose_name": "Demande de petits cours", + "verbose_name_plural": "Demandes de petits cours", }, ), migrations.CreateModel( - name='PetitCoursAttribution', + name="PetitCoursSubject", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('date', models.DateTimeField(auto_now_add=True, verbose_name="Date d'attribution")), - ('rank', models.IntegerField(verbose_name=b"Rang dans l'email")), - ('selected', models.BooleanField(default=False, verbose_name='S\xe9lectionn\xe9 par le demandeur')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=30, verbose_name="Mati\xe8re")), + ( + "users", + models.ManyToManyField( + related_name="petits_cours_matieres", + through="gestioncof.PetitCoursAbility", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Attribution de petits cours', - 'verbose_name_plural': 'Attributions de petits cours', + "verbose_name": "Mati\xe8re de petits cours", + "verbose_name_plural": "Mati\xe8res des petits cours", }, ), migrations.CreateModel( - name='PetitCoursAttributionCounter', + name="Survey", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('count', models.IntegerField(default=0, verbose_name=b"Nombre d'envois")), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=200, verbose_name=b"Titre")), + ( + "details", + models.TextField(verbose_name=b"D\xc3\xa9tails", blank=True), + ), + ( + "survey_open", + models.BooleanField(default=True, verbose_name=b"Sondage ouvert"), + ), + ( + "old", + models.BooleanField( + default=False, verbose_name=b"Archiver (sondage fini)" + ), + ), ], - options={ - 'verbose_name': "Compteur d'attribution de petits cours", - 'verbose_name_plural': "Compteurs d'attributions de petits cours", - }, + options={"verbose_name": "Sondage"}, ), migrations.CreateModel( - name='PetitCoursDemande', + name="SurveyAnswer", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=200, verbose_name='Nom/pr\xe9nom')), - ('email', models.CharField(max_length=300, verbose_name='Adresse email')), - ('phone', models.CharField(max_length=20, verbose_name='T\xe9l\xe9phone (facultatif)', blank=True)), - ('quand', models.CharField(help_text='Indiquez ici la p\xe9riode d\xe9sir\xe9e pour les petits cours (vacances scolaires, semaine, week-end).', max_length=300, verbose_name='Quand ?', blank=True)), - ('freq', models.CharField(help_text='Indiquez ici la fr\xe9quence envisag\xe9e (hebdomadaire, 2 fois par semaine, ...)', max_length=300, verbose_name='Fr\xe9quence', blank=True)), - ('lieu', models.CharField(help_text='Si vous avez avez une pr\xe9f\xe9rence sur le lieu.', max_length=300, verbose_name='Lieu (si pr\xe9f\xe9rence)', blank=True)), - ('agrege_requis', models.BooleanField(default=False, verbose_name='Agr\xe9g\xe9 requis')), - ('niveau', models.CharField(default=b'', max_length=12, verbose_name='Niveau', choices=[(b'college', 'Coll\xe8ge'), (b'lycee', 'Lyc\xe9e'), (b'prepa1styear', 'Pr\xe9pa 1\xe8re ann\xe9e / L1'), (b'prepa2ndyear', 'Pr\xe9pa 2\xe8me ann\xe9e / L2'), (b'licence3', 'Licence 3'), (b'other', 'Autre (pr\xe9ciser dans les commentaires)')])), - ('remarques', models.TextField(verbose_name='Remarques et pr\xe9cisions', blank=True)), - ('traitee', models.BooleanField(default=False, verbose_name='Trait\xe9e')), - ('processed', models.DateTimeField(verbose_name='Date de traitement', blank=True)), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Date de cr\xe9ation')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ) ], - options={ - 'verbose_name': 'Demande de petits cours', - 'verbose_name_plural': 'Demandes de petits cours', - }, + options={"verbose_name": "R\xe9ponses"}, ), migrations.CreateModel( - name='PetitCoursSubject', + name="SurveyQuestion", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=30, verbose_name='Mati\xe8re')), - ('users', models.ManyToManyField(related_name='petits_cours_matieres', through='gestioncof.PetitCoursAbility', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "question", + models.CharField(max_length=200, verbose_name=b"Question"), + ), + ( + "multi_answers", + models.BooleanField(default=False, verbose_name=b"Choix multiples"), + ), + ( + "survey", + models.ForeignKey( + related_name="questions", + to="gestioncof.Survey", + on_delete=models.CASCADE, + ), + ), ], - options={ - 'verbose_name': 'Mati\xe8re de petits cours', - 'verbose_name_plural': 'Mati\xe8res des petits cours', - }, + options={"verbose_name": "Question"}, ), migrations.CreateModel( - name='Survey', + name="SurveyQuestionAnswer", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=200, verbose_name=b'Titre')), - ('details', models.TextField(verbose_name=b'D\xc3\xa9tails', blank=True)), - ('survey_open', models.BooleanField(default=True, verbose_name=b'Sondage ouvert')), - ('old', models.BooleanField(default=False, verbose_name=b'Archiver (sondage fini)')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "answer", + models.CharField(max_length=200, verbose_name=b"R\xc3\xa9ponse"), + ), + ( + "survey_question", + models.ForeignKey( + related_name="answers", + to="gestioncof.SurveyQuestion", + on_delete=models.CASCADE, + ), + ), ], - options={ - 'verbose_name': 'Sondage', - }, - ), - migrations.CreateModel( - name='SurveyAnswer', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ], - options={ - 'verbose_name': 'R\xe9ponses', - }, - ), - migrations.CreateModel( - name='SurveyQuestion', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('question', models.CharField(max_length=200, verbose_name=b'Question')), - ('multi_answers', models.BooleanField(default=False, verbose_name=b'Choix multiples')), - ('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey', on_delete=models.CASCADE)), - ], - options={ - 'verbose_name': 'Question', - }, - ), - migrations.CreateModel( - name='SurveyQuestionAnswer', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('answer', models.CharField(max_length=200, verbose_name=b'R\xc3\xa9ponse')), - ('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion', on_delete=models.CASCADE)), - ], - options={ - 'verbose_name': 'R\xe9ponse', - }, + options={"verbose_name": "R\xe9ponse"}, ), migrations.AddField( - model_name='surveyanswer', - name='answers', - field=models.ManyToManyField(related_name='selected_by', to='gestioncof.SurveyQuestionAnswer'), + model_name="surveyanswer", + name="answers", + field=models.ManyToManyField( + related_name="selected_by", to="gestioncof.SurveyQuestionAnswer" + ), ), migrations.AddField( - model_name='surveyanswer', - name='survey', - field=models.ForeignKey(to='gestioncof.Survey', on_delete=models.CASCADE), + model_name="surveyanswer", + name="survey", + field=models.ForeignKey(to="gestioncof.Survey", on_delete=models.CASCADE), ), migrations.AddField( - model_name='surveyanswer', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="surveyanswer", + name="user", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='petitcoursdemande', - name='matieres', - field=models.ManyToManyField(related_name='demandes', verbose_name='Mati\xe8res', to='gestioncof.PetitCoursSubject'), + model_name="petitcoursdemande", + name="matieres", + field=models.ManyToManyField( + related_name="demandes", + verbose_name="Mati\xe8res", + to="gestioncof.PetitCoursSubject", + ), ), migrations.AddField( - model_name='petitcoursdemande', - name='traitee_par', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), + model_name="petitcoursdemande", + name="traitee_par", + field=models.ForeignKey( + blank=True, + to=settings.AUTH_USER_MODEL, + null=True, + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='petitcoursattributioncounter', - name='matiere', - field=models.ForeignKey(verbose_name='Matiere', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE), + model_name="petitcoursattributioncounter", + name="matiere", + field=models.ForeignKey( + verbose_name="Matiere", + to="gestioncof.PetitCoursSubject", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='petitcoursattributioncounter', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="petitcoursattributioncounter", + name="user", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='petitcoursattribution', - name='demande', - field=models.ForeignKey(verbose_name='Demande', to='gestioncof.PetitCoursDemande', on_delete=models.CASCADE), + model_name="petitcoursattribution", + name="demande", + field=models.ForeignKey( + verbose_name="Demande", + to="gestioncof.PetitCoursDemande", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='petitcoursattribution', - name='matiere', - field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE), + model_name="petitcoursattribution", + name="matiere", + field=models.ForeignKey( + verbose_name="Mati\xe8re", + to="gestioncof.PetitCoursSubject", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='petitcoursattribution', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="petitcoursattribution", + name="user", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='petitcoursability', - name='matiere', - field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE), + model_name="petitcoursability", + name="matiere", + field=models.ForeignKey( + verbose_name="Mati\xe8re", + to="gestioncof.PetitCoursSubject", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='petitcoursability', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="petitcoursability", + name="user", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AddField( - model_name='eventcommentvalue', - name='registration', - field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration', on_delete=models.CASCADE), + model_name="eventcommentvalue", + name="registration", + field=models.ForeignKey( + related_name="comments", + to="gestioncof.EventRegistration", + on_delete=models.CASCADE, + ), ), migrations.AlterUniqueTogether( - name='surveyanswer', - unique_together=set([('user', 'survey')]), + name="surveyanswer", unique_together=set([("user", "survey")]) ), migrations.AlterUniqueTogether( - name='eventregistration', - unique_together=set([('user', 'event')]), + name="eventregistration", unique_together=set([("user", "event")]) ), ] diff --git a/gestioncof/migrations/0002_enable_unprocessed_demandes.py b/gestioncof/migrations/0002_enable_unprocessed_demandes.py index 18006588..d8514036 100644 --- a/gestioncof/migrations/0002_enable_unprocessed_demandes.py +++ b/gestioncof/migrations/0002_enable_unprocessed_demandes.py @@ -6,14 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0001_initial'), - ] + dependencies = [("gestioncof", "0001_initial")] operations = [ migrations.AlterField( - model_name='petitcoursdemande', - name='processed', - field=models.DateTimeField(null=True, verbose_name='Date de traitement', blank=True), - ), + model_name="petitcoursdemande", + name="processed", + field=models.DateTimeField( + null=True, verbose_name="Date de traitement", blank=True + ), + ) ] diff --git a/gestioncof/migrations/0003_event_image.py b/gestioncof/migrations/0003_event_image.py index 6d65b1a6..ac5d753b 100644 --- a/gestioncof/migrations/0003_event_image.py +++ b/gestioncof/migrations/0003_event_image.py @@ -6,14 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0002_enable_unprocessed_demandes'), - ] + dependencies = [("gestioncof", "0002_enable_unprocessed_demandes")] operations = [ migrations.AddField( - model_name='event', - name='image', - field=models.ImageField(upload_to=b'imgs/events/', null=True, verbose_name=b'Image', blank=True), - ), + model_name="event", + name="image", + field=models.ImageField( + upload_to=b"imgs/events/", null=True, verbose_name=b"Image", blank=True + ), + ) ] diff --git a/gestioncof/migrations/0004_registration_mail.py b/gestioncof/migrations/0004_registration_mail.py index d72900bf..0ceba245 100644 --- a/gestioncof/migrations/0004_registration_mail.py +++ b/gestioncof/migrations/0004_registration_mail.py @@ -8,27 +8,28 @@ def create_mail(apps, schema_editor): CustomMail = apps.get_model("gestioncof", "CustomMail") db_alias = schema_editor.connection.alias if CustomMail.objects.filter(shortname="bienvenue").count() == 0: - CustomMail.objects.using(db_alias).bulk_create([ - CustomMail( - shortname="bienvenue", - title="Bienvenue au COF", - content="Mail de bienvenue au COF, envoyé automatiquement à " \ - + "l'inscription.\n\n" \ - + "Les balises {{ ... }} sont interprétées comme expliqué " \ + CustomMail.objects.using(db_alias).bulk_create( + [ + CustomMail( + shortname="bienvenue", + title="Bienvenue au COF", + content="Mail de bienvenue au COF, envoyé automatiquement à " + + "l'inscription.\n\n" + + "Les balises {{ ... }} sont interprétées comme expliqué " + "ci-dessous à l'envoi.", - comments="{{ nom }} \t fullname de la personne.\n"\ - + "{{ prenom }} \t prénom de la personne.") - ]) + comments="{{ nom }} \t fullname de la personne.\n" + + "{{ prenom }} \t prénom de la personne.", + ) + ] + ) class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0003_event_image'), - ] + dependencies = [("gestioncof", "0003_event_image")] operations = [ # Pas besoin de supprimer le mail lors de la migration dans l'autre # sens. - migrations.RunPython(create_mail, migrations.RunPython.noop), + migrations.RunPython(create_mail, migrations.RunPython.noop) ] diff --git a/gestioncof/migrations/0005_encoding.py b/gestioncof/migrations/0005_encoding.py index 4f565a5d..33c8502a 100644 --- a/gestioncof/migrations/0005_encoding.py +++ b/gestioncof/migrations/0005_encoding.py @@ -6,62 +6,71 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0004_registration_mail'), - ] + dependencies = [("gestioncof", "0004_registration_mail")] operations = [ migrations.AlterModelOptions( - name='custommail', - options={'verbose_name': 'Mail personnalisable', 'verbose_name_plural': 'Mails personnalisables'}, + name="custommail", + options={ + "verbose_name": "Mail personnalisable", + "verbose_name_plural": "Mails personnalisables", + }, ), migrations.AlterModelOptions( - name='eventoptionchoice', - options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'}, + name="eventoptionchoice", + options={"verbose_name": "Choix", "verbose_name_plural": "Choix"}, ), migrations.AlterField( - model_name='cofprofile', - name='is_buro', - field=models.BooleanField(default=False, verbose_name='Membre du Bur\xf4'), + model_name="cofprofile", + name="is_buro", + field=models.BooleanField(default=False, verbose_name="Membre du Bur\xf4"), ), migrations.AlterField( - model_name='cofprofile', - name='num', - field=models.IntegerField(default=0, verbose_name="Num\xe9ro d'adh\xe9rent", blank=True), + model_name="cofprofile", + name="num", + field=models.IntegerField( + default=0, verbose_name="Num\xe9ro d'adh\xe9rent", blank=True + ), ), migrations.AlterField( - model_name='cofprofile', - name='phone', - field=models.CharField(max_length=20, verbose_name='T\xe9l\xe9phone', blank=True), + model_name="cofprofile", + name="phone", + field=models.CharField( + max_length=20, verbose_name="T\xe9l\xe9phone", blank=True + ), ), migrations.AlterField( - model_name='event', - name='old', - field=models.BooleanField(default=False, verbose_name='Archiver (\xe9v\xe9nement fini)'), + model_name="event", + name="old", + field=models.BooleanField( + default=False, verbose_name="Archiver (\xe9v\xe9nement fini)" + ), ), migrations.AlterField( - model_name='event', - name='start_date', - field=models.DateField(null=True, verbose_name='Date de d\xe9but', blank=True), + model_name="event", + name="start_date", + field=models.DateField( + null=True, verbose_name="Date de d\xe9but", blank=True + ), ), migrations.AlterField( - model_name='eventcommentfield', - name='default', - field=models.TextField(verbose_name='Valeur par d\xe9faut', blank=True), + model_name="eventcommentfield", + name="default", + field=models.TextField(verbose_name="Valeur par d\xe9faut", blank=True), ), migrations.AlterField( - model_name='eventregistration', - name='paid', - field=models.BooleanField(default=False, verbose_name='A pay\xe9'), + model_name="eventregistration", + name="paid", + field=models.BooleanField(default=False, verbose_name="A pay\xe9"), ), migrations.AlterField( - model_name='survey', - name='details', - field=models.TextField(verbose_name='D\xe9tails', blank=True), + model_name="survey", + name="details", + field=models.TextField(verbose_name="D\xe9tails", blank=True), ), migrations.AlterField( - model_name='surveyquestionanswer', - name='answer', - field=models.CharField(max_length=200, verbose_name='R\xe9ponse'), + model_name="surveyquestionanswer", + name="answer", + field=models.CharField(max_length=200, verbose_name="R\xe9ponse"), ), ] diff --git a/gestioncof/migrations/0006_add_calendar.py b/gestioncof/migrations/0006_add_calendar.py index 27852f61..760fe56c 100644 --- a/gestioncof/migrations/0006_add_calendar.py +++ b/gestioncof/migrations/0006_add_calendar.py @@ -1,51 +1,66 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bda', '0004_mails-rappel'), + ("bda", "0004_mails-rappel"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('gestioncof', '0005_encoding'), + ("gestioncof", "0005_encoding"), ] operations = [ migrations.CreateModel( - name='CalendarSubscription', + name="CalendarSubscription", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, - auto_created=True, primary_key=True)), - ('token', models.UUIDField()), - ('subscribe_to_events', models.BooleanField(default=True)), - ('subscribe_to_my_shows', models.BooleanField(default=True)), - ('other_shows', models.ManyToManyField(to='bda.Spectacle')), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, - on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("token", models.UUIDField()), + ("subscribe_to_events", models.BooleanField(default=True)), + ("subscribe_to_my_shows", models.BooleanField(default=True)), + ("other_shows", models.ManyToManyField(to="bda.Spectacle")), + ( + "user", + models.OneToOneField( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], ), migrations.AlterModelOptions( - name='custommail', - options={'verbose_name': 'Mail personnalisable', - 'verbose_name_plural': 'Mails personnalisables'}, + name="custommail", + options={ + "verbose_name": "Mail personnalisable", + "verbose_name_plural": "Mails personnalisables", + }, ), migrations.AlterModelOptions( - name='eventoptionchoice', - options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'}, + name="eventoptionchoice", + options={"verbose_name": "Choix", "verbose_name_plural": "Choix"}, ), migrations.AlterField( - model_name='event', - name='end_date', - field=models.DateTimeField(null=True, verbose_name=b'Date de fin', - blank=True), - ), - migrations.AlterField( - model_name='event', - name='start_date', + model_name="event", + name="end_date", field=models.DateTimeField( - null=True, verbose_name=b'Date de d\xc3\xa9but', blank=True), + null=True, verbose_name=b"Date de fin", blank=True + ), + ), + migrations.AlterField( + model_name="event", + name="start_date", + field=models.DateTimeField( + null=True, verbose_name=b"Date de d\xc3\xa9but", blank=True + ), ), ] diff --git a/gestioncof/migrations/0007_alter_club.py b/gestioncof/migrations/0007_alter_club.py index 324c59a6..13603370 100644 --- a/gestioncof/migrations/0007_alter_club.py +++ b/gestioncof/migrations/0007_alter_club.py @@ -1,47 +1,44 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0006_add_calendar'), - ] + dependencies = [("gestioncof", "0006_add_calendar")] operations = [ migrations.AlterField( - model_name='club', - name='name', - field=models.CharField(unique=True, max_length=200, - verbose_name='Nom') + model_name="club", + name="name", + field=models.CharField(unique=True, max_length=200, verbose_name="Nom"), ), migrations.AlterField( - model_name='club', - name='description', - field=models.TextField(verbose_name='Description', blank=True) + model_name="club", + name="description", + field=models.TextField(verbose_name="Description", blank=True), ), migrations.AlterField( - model_name='club', - name='membres', - field=models.ManyToManyField(related_name='clubs', - to=settings.AUTH_USER_MODEL, - blank=True), + model_name="club", + name="membres", + field=models.ManyToManyField( + related_name="clubs", to=settings.AUTH_USER_MODEL, blank=True + ), ), migrations.AlterField( - model_name='club', - name='respos', - field=models.ManyToManyField(related_name='clubs_geres', - to=settings.AUTH_USER_MODEL, - blank=True), + model_name="club", + name="respos", + field=models.ManyToManyField( + related_name="clubs_geres", to=settings.AUTH_USER_MODEL, blank=True + ), ), migrations.AlterField( - model_name='event', - name='start_date', - field=models.DateTimeField(null=True, - verbose_name='Date de d\xe9but', - blank=True), + model_name="event", + name="start_date", + field=models.DateTimeField( + null=True, verbose_name="Date de d\xe9but", blank=True + ), ), ] diff --git a/gestioncof/migrations/0008_py3.py b/gestioncof/migrations/0008_py3.py index 7d94d7ce..f2b89aa4 100644 --- a/gestioncof/migrations/0008_py3.py +++ b/gestioncof/migrations/0008_py3.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models def forwards(apps, schema_editor): @@ -11,243 +11,266 @@ def forwards(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0007_alter_club'), - ] + dependencies = [("gestioncof", "0007_alter_club")] operations = [ migrations.AlterField( - model_name='clipper', - name='fullname', - field=models.CharField(verbose_name='Nom complet', max_length=200), + model_name="clipper", + name="fullname", + field=models.CharField(verbose_name="Nom complet", max_length=200), ), migrations.AlterField( - model_name='clipper', - name='username', - field=models.CharField(verbose_name='Identifiant', max_length=20), + model_name="clipper", + name="username", + field=models.CharField(verbose_name="Identifiant", max_length=20), ), migrations.AlterField( - model_name='cofprofile', - name='comments', + model_name="cofprofile", + name="comments", field=models.TextField( - verbose_name="Commentaires visibles par l'utilisateur", - blank=True), + verbose_name="Commentaires visibles par l'utilisateur", blank=True + ), ), migrations.AlterField( - model_name='cofprofile', - name='is_cof', - field=models.BooleanField(verbose_name='Membre du COF', - default=False), + model_name="cofprofile", + name="is_cof", + field=models.BooleanField(verbose_name="Membre du COF", default=False), ), migrations.AlterField( - model_name='cofprofile', - name='login_clipper', - field=models.CharField(verbose_name='Login clipper', max_length=8, - blank=True), + model_name="cofprofile", + name="login_clipper", + field=models.CharField( + verbose_name="Login clipper", max_length=8, blank=True + ), ), migrations.AlterField( - model_name='cofprofile', - name='mailing_bda', - field=models.BooleanField(verbose_name='Recevoir les mails BdA', - default=False), - ), - migrations.AlterField( - model_name='cofprofile', - name='mailing_bda_revente', + model_name="cofprofile", + name="mailing_bda", field=models.BooleanField( - verbose_name='Recevoir les mails de revente de places BdA', - default=False), + verbose_name="Recevoir les mails BdA", default=False + ), ), migrations.AlterField( - model_name='cofprofile', - name='mailing_cof', - field=models.BooleanField(verbose_name='Recevoir les mails COF', - default=False), + model_name="cofprofile", + name="mailing_bda_revente", + field=models.BooleanField( + verbose_name="Recevoir les mails de revente de places BdA", + default=False, + ), ), migrations.AlterField( - model_name='cofprofile', - name='occupation', - field=models.CharField(verbose_name='Occupation', - choices=[('exterieur', 'Extérieur'), - ('1A', '1A'), - ('2A', '2A'), - ('3A', '3A'), - ('4A', '4A'), - ('archicube', 'Archicube'), - ('doctorant', 'Doctorant'), - ('CST', 'CST')], - max_length=9, default='1A'), + model_name="cofprofile", + name="mailing_cof", + field=models.BooleanField( + verbose_name="Recevoir les mails COF", default=False + ), ), migrations.AlterField( - model_name='cofprofile', - name='petits_cours_accept', - field=models.BooleanField(verbose_name='Recevoir des petits cours', - default=False), + model_name="cofprofile", + name="occupation", + field=models.CharField( + verbose_name="Occupation", + choices=[ + ("exterieur", "Extérieur"), + ("1A", "1A"), + ("2A", "2A"), + ("3A", "3A"), + ("4A", "4A"), + ("archicube", "Archicube"), + ("doctorant", "Doctorant"), + ("CST", "CST"), + ], + max_length=9, + default="1A", + ), ), migrations.AlterField( - model_name='cofprofile', - name='petits_cours_remarques', + model_name="cofprofile", + name="petits_cours_accept", + field=models.BooleanField( + verbose_name="Recevoir des petits cours", default=False + ), + ), + migrations.AlterField( + model_name="cofprofile", + name="petits_cours_remarques", field=models.TextField( blank=True, - verbose_name='Remarques et précisions pour les petits cours', - default=''), + verbose_name="Remarques et précisions pour les petits cours", + default="", + ), ), migrations.AlterField( - model_name='cofprofile', - name='type_cotiz', + model_name="cofprofile", + name="type_cotiz", field=models.CharField( - verbose_name='Type de cotisation', - choices=[('etudiant', 'Normalien étudiant'), - ('normalien', 'Normalien élève'), - ('exterieur', 'Extérieur')], - max_length=9, default='normalien'), + verbose_name="Type de cotisation", + choices=[ + ("etudiant", "Normalien étudiant"), + ("normalien", "Normalien élève"), + ("exterieur", "Extérieur"), + ], + max_length=9, + default="normalien", + ), ), migrations.AlterField( - model_name='custommail', - name='comments', + model_name="custommail", + name="comments", field=models.TextField( - verbose_name='Informations contextuelles sur le mail', - blank=True), + verbose_name="Informations contextuelles sur le mail", blank=True + ), ), migrations.AlterField( - model_name='custommail', - name='content', - field=models.TextField(verbose_name='Contenu'), + model_name="custommail", + name="content", + field=models.TextField(verbose_name="Contenu"), ), migrations.AlterField( - model_name='custommail', - name='title', - field=models.CharField(verbose_name='Titre', max_length=200), + model_name="custommail", + name="title", + field=models.CharField(verbose_name="Titre", max_length=200), ), migrations.AlterField( - model_name='event', - name='description', - field=models.TextField(verbose_name='Description', blank=True), + model_name="event", + name="description", + field=models.TextField(verbose_name="Description", blank=True), ), migrations.AlterField( - model_name='event', - name='end_date', - field=models.DateTimeField(null=True, verbose_name='Date de fin', - blank=True), + model_name="event", + name="end_date", + field=models.DateTimeField( + null=True, verbose_name="Date de fin", blank=True + ), ), migrations.AlterField( - model_name='event', - name='image', - field=models.ImageField(upload_to='imgs/events/', null=True, - verbose_name='Image', blank=True), + model_name="event", + name="image", + field=models.ImageField( + upload_to="imgs/events/", null=True, verbose_name="Image", blank=True + ), ), migrations.AlterField( - model_name='event', - name='location', - field=models.CharField(verbose_name='Lieu', max_length=200), + model_name="event", + name="location", + field=models.CharField(verbose_name="Lieu", max_length=200), ), migrations.AlterField( - model_name='event', - name='registration_open', - field=models.BooleanField(verbose_name='Inscriptions ouvertes', - default=True), + model_name="event", + name="registration_open", + field=models.BooleanField( + verbose_name="Inscriptions ouvertes", default=True + ), ), migrations.AlterField( - model_name='event', - name='title', - field=models.CharField(verbose_name='Titre', max_length=200), + model_name="event", + name="title", + field=models.CharField(verbose_name="Titre", max_length=200), ), migrations.AlterField( - model_name='eventcommentfield', - name='fieldtype', - field=models.CharField(verbose_name='Type', - choices=[('text', 'Texte long'), - ('char', 'Texte court')], - max_length=10, default='text'), - ), - migrations.AlterField( - model_name='eventcommentfield', - name='name', - field=models.CharField(verbose_name='Champ', max_length=200), - ), - migrations.AlterField( - model_name='eventcommentvalue', - name='content', - field=models.TextField(null=True, verbose_name='Contenu', - blank=True), - ), - migrations.AlterField( - model_name='eventoption', - name='multi_choices', - field=models.BooleanField(verbose_name='Choix multiples', - default=False), - ), - migrations.AlterField( - model_name='eventoption', - name='name', - field=models.CharField(verbose_name='Option', max_length=200), - ), - migrations.AlterField( - model_name='eventoptionchoice', - name='value', - field=models.CharField(verbose_name='Valeur', max_length=200), - ), - migrations.AlterField( - model_name='petitcoursability', - name='niveau', + model_name="eventcommentfield", + name="fieldtype", field=models.CharField( - choices=[('college', 'Collège'), ('lycee', 'Lycée'), - ('prepa1styear', 'Prépa 1ère année / L1'), - ('prepa2ndyear', 'Prépa 2ème année / L2'), - ('licence3', 'Licence 3'), - ('other', 'Autre (préciser dans les commentaires)')], - max_length=12, verbose_name='Niveau'), + verbose_name="Type", + choices=[("text", "Texte long"), ("char", "Texte court")], + max_length=10, + default="text", + ), ), migrations.AlterField( - model_name='petitcoursattribution', - name='rank', + model_name="eventcommentfield", + name="name", + field=models.CharField(verbose_name="Champ", max_length=200), + ), + migrations.AlterField( + model_name="eventcommentvalue", + name="content", + field=models.TextField(null=True, verbose_name="Contenu", blank=True), + ), + migrations.AlterField( + model_name="eventoption", + name="multi_choices", + field=models.BooleanField(verbose_name="Choix multiples", default=False), + ), + migrations.AlterField( + model_name="eventoption", + name="name", + field=models.CharField(verbose_name="Option", max_length=200), + ), + migrations.AlterField( + model_name="eventoptionchoice", + name="value", + field=models.CharField(verbose_name="Valeur", max_length=200), + ), + migrations.AlterField( + model_name="petitcoursability", + name="niveau", + field=models.CharField( + choices=[ + ("college", "Collège"), + ("lycee", "Lycée"), + ("prepa1styear", "Prépa 1ère année / L1"), + ("prepa2ndyear", "Prépa 2ème année / L2"), + ("licence3", "Licence 3"), + ("other", "Autre (préciser dans les commentaires)"), + ], + max_length=12, + verbose_name="Niveau", + ), + ), + migrations.AlterField( + model_name="petitcoursattribution", + name="rank", field=models.IntegerField(verbose_name="Rang dans l'email"), ), migrations.AlterField( - model_name='petitcoursattributioncounter', - name='count', - field=models.IntegerField(verbose_name="Nombre d'envois", - default=0), + model_name="petitcoursattributioncounter", + name="count", + field=models.IntegerField(verbose_name="Nombre d'envois", default=0), ), migrations.AlterField( - model_name='petitcoursdemande', - name='niveau', + model_name="petitcoursdemande", + name="niveau", field=models.CharField( - verbose_name='Niveau', - choices=[('college', 'Collège'), ('lycee', 'Lycée'), - ('prepa1styear', 'Prépa 1ère année / L1'), - ('prepa2ndyear', 'Prépa 2ème année / L2'), - ('licence3', 'Licence 3'), - ('other', 'Autre (préciser dans les commentaires)')], - max_length=12, default=''), + verbose_name="Niveau", + choices=[ + ("college", "Collège"), + ("lycee", "Lycée"), + ("prepa1styear", "Prépa 1ère année / L1"), + ("prepa2ndyear", "Prépa 2ème année / L2"), + ("licence3", "Licence 3"), + ("other", "Autre (préciser dans les commentaires)"), + ], + max_length=12, + default="", + ), ), migrations.AlterField( - model_name='survey', - name='old', - field=models.BooleanField(verbose_name='Archiver (sondage fini)', - default=False), + model_name="survey", + name="old", + field=models.BooleanField( + verbose_name="Archiver (sondage fini)", default=False + ), ), migrations.AlterField( - model_name='survey', - name='survey_open', - field=models.BooleanField(verbose_name='Sondage ouvert', - default=True), + model_name="survey", + name="survey_open", + field=models.BooleanField(verbose_name="Sondage ouvert", default=True), ), migrations.AlterField( - model_name='survey', - name='title', - field=models.CharField(verbose_name='Titre', max_length=200), + model_name="survey", + name="title", + field=models.CharField(verbose_name="Titre", max_length=200), ), migrations.AlterField( - model_name='surveyquestion', - name='multi_answers', - field=models.BooleanField(verbose_name='Choix multiples', - default=False), + model_name="surveyquestion", + name="multi_answers", + field=models.BooleanField(verbose_name="Choix multiples", default=False), ), migrations.AlterField( - model_name='surveyquestion', - name='question', - field=models.CharField(verbose_name='Question', max_length=200), + model_name="surveyquestion", + name="question", + field=models.CharField(verbose_name="Question", max_length=200), ), migrations.RunPython(forwards, migrations.RunPython.noop), ] diff --git a/gestioncof/migrations/0009_delete_clipper.py b/gestioncof/migrations/0009_delete_clipper.py index e537107b..35362716 100644 --- a/gestioncof/migrations/0009_delete_clipper.py +++ b/gestioncof/migrations/0009_delete_clipper.py @@ -6,12 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0008_py3'), - ] + dependencies = [("gestioncof", "0008_py3")] - operations = [ - migrations.DeleteModel( - name='Clipper', - ), - ] + operations = [migrations.DeleteModel(name="Clipper")] diff --git a/gestioncof/migrations/0010_delete_custommail.py b/gestioncof/migrations/0010_delete_custommail.py index 63ebeca7..2434faf2 100644 --- a/gestioncof/migrations/0010_delete_custommail.py +++ b/gestioncof/migrations/0010_delete_custommail.py @@ -5,12 +5,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0009_delete_clipper'), - ] + dependencies = [("gestioncof", "0009_delete_clipper")] - operations = [ - migrations.DeleteModel( - name='CustomMail', - ), - ] + operations = [migrations.DeleteModel(name="CustomMail")] diff --git a/gestioncof/migrations/0011_longer_clippers.py b/gestioncof/migrations/0011_longer_clippers.py index 631d0ea8..777f79a8 100644 --- a/gestioncof/migrations/0011_longer_clippers.py +++ b/gestioncof/migrations/0011_longer_clippers.py @@ -6,14 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0010_delete_custommail'), - ] + dependencies = [("gestioncof", "0010_delete_custommail")] operations = [ migrations.AlterField( - model_name='cofprofile', - name='login_clipper', - field=models.CharField(verbose_name='Login clipper', blank=True, max_length=32), - ), + model_name="cofprofile", + name="login_clipper", + field=models.CharField( + verbose_name="Login clipper", blank=True, max_length=32 + ), + ) ] diff --git a/gestioncof/migrations/0011_remove_cofprofile_num.py b/gestioncof/migrations/0011_remove_cofprofile_num.py index f39ce367..abf97768 100644 --- a/gestioncof/migrations/0011_remove_cofprofile_num.py +++ b/gestioncof/migrations/0011_remove_cofprofile_num.py @@ -6,13 +6,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0010_delete_custommail'), - ] + dependencies = [("gestioncof", "0010_delete_custommail")] - operations = [ - migrations.RemoveField( - model_name='cofprofile', - name='num', - ), - ] + operations = [migrations.RemoveField(model_name="cofprofile", name="num")] diff --git a/gestioncof/migrations/0012_merge.py b/gestioncof/migrations/0012_merge.py index 39879346..5e23119d 100644 --- a/gestioncof/migrations/0012_merge.py +++ b/gestioncof/migrations/0012_merge.py @@ -7,9 +7,8 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('gestioncof', '0011_remove_cofprofile_num'), - ('gestioncof', '0011_longer_clippers'), + ("gestioncof", "0011_remove_cofprofile_num"), + ("gestioncof", "0011_longer_clippers"), ] - operations = [ - ] + operations = [] diff --git a/gestioncof/migrations/0013_pei.py b/gestioncof/migrations/0013_pei.py index 2fbddf1f..186d458d 100644 --- a/gestioncof/migrations/0013_pei.py +++ b/gestioncof/migrations/0013_pei.py @@ -6,42 +6,42 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0012_merge'), - ] + dependencies = [("gestioncof", "0012_merge")] operations = [ migrations.AlterField( - model_name='cofprofile', - name='occupation', + model_name="cofprofile", + name="occupation", field=models.CharField( - verbose_name='Occupation', + verbose_name="Occupation", max_length=9, - default='1A', + default="1A", choices=[ - ('exterieur', 'Extérieur'), - ('1A', '1A'), - ('2A', '2A'), - ('3A', '3A'), - ('4A', '4A'), - ('archicube', 'Archicube'), - ('doctorant', 'Doctorant'), - ('CST', 'CST'), - ('PEI', 'PEI') - ]), + ("exterieur", "Extérieur"), + ("1A", "1A"), + ("2A", "2A"), + ("3A", "3A"), + ("4A", "4A"), + ("archicube", "Archicube"), + ("doctorant", "Doctorant"), + ("CST", "CST"), + ("PEI", "PEI"), + ], + ), ), migrations.AlterField( - model_name='cofprofile', - name='type_cotiz', + model_name="cofprofile", + name="type_cotiz", field=models.CharField( - verbose_name='Type de cotisation', + verbose_name="Type de cotisation", max_length=9, - default='normalien', + default="normalien", choices=[ - ('etudiant', 'Normalien étudiant'), - ('normalien', 'Normalien élève'), - ('exterieur', 'Extérieur'), - ('gratis', 'Gratuit') - ]), + ("etudiant", "Normalien étudiant"), + ("normalien", "Normalien élève"), + ("exterieur", "Extérieur"), + ("gratis", "Gratuit"), + ], + ), ), ] diff --git a/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py b/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py index 1d842329..b849bfca 100644 --- a/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py +++ b/gestioncof/migrations/0014_cofprofile_mailing_unernestaparis.py @@ -7,14 +7,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0013_pei'), - ] + dependencies = [("gestioncof", "0013_pei")] operations = [ migrations.AddField( - model_name='cofprofile', - name='mailing_unernestaparis', - field=models.BooleanField(default=False, verbose_name='Recevoir les mails unErnestAParis'), - ), + model_name="cofprofile", + name="mailing_unernestaparis", + field=models.BooleanField( + default=False, verbose_name="Recevoir les mails unErnestAParis" + ), + ) ] diff --git a/gestioncof/models.py b/gestioncof/models.py index 8a5b6a53..227fa936 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -1,17 +1,13 @@ -from django.db import models -from django.dispatch import receiver from django.contrib.auth.models import User +from django.db import models +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ -from django.db.models.signals import post_save, post_delete - -from gestioncof.petits_cours_models import choices_length from bda.models import Spectacle +from gestioncof.petits_cours_models import choices_length -TYPE_COMMENT_FIELD = ( - ('text', _("Texte long")), - ('char', _("Texte court")), -) +TYPE_COMMENT_FIELD = (("text", _("Texte long")), ("char", _("Texte court"))) class CofProfile(models.Model): @@ -49,40 +45,39 @@ class CofProfile(models.Model): (COTIZ_GRATIS, _("Gratuit")), ) - user = models.OneToOneField( - User, on_delete=models.CASCADE, - related_name="profile", - ) - login_clipper = models.CharField( - "Login clipper", max_length=32, blank=True - ) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + login_clipper = models.CharField("Login clipper", max_length=32, blank=True) is_cof = models.BooleanField("Membre du COF", default=False) phone = models.CharField("Téléphone", max_length=20, blank=True) - occupation = models.CharField(_("Occupation"), - default="1A", - choices=OCCUPATION_CHOICES, - max_length=choices_length( - OCCUPATION_CHOICES)) - departement = models.CharField(_("Département"), max_length=50, - blank=True) - type_cotiz = models.CharField(_("Type de cotisation"), - default="normalien", - choices=TYPE_COTIZ_CHOICES, - max_length=choices_length( - TYPE_COTIZ_CHOICES)) + occupation = models.CharField( + _("Occupation"), + default="1A", + choices=OCCUPATION_CHOICES, + max_length=choices_length(OCCUPATION_CHOICES), + ) + departement = models.CharField(_("Département"), max_length=50, blank=True) + type_cotiz = models.CharField( + _("Type de cotisation"), + default="normalien", + choices=TYPE_COTIZ_CHOICES, + max_length=choices_length(TYPE_COTIZ_CHOICES), + ) mailing_cof = models.BooleanField("Recevoir les mails COF", default=False) mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False) - mailing_unernestaparis = models.BooleanField("Recevoir les mails unErnestAParis", default=False) + mailing_unernestaparis = models.BooleanField( + "Recevoir les mails unErnestAParis", default=False + ) mailing_bda_revente = models.BooleanField( - "Recevoir les mails de revente de places BdA", default=False) - comments = models.TextField( - "Commentaires visibles par l'utilisateur", blank=True) + "Recevoir les mails de revente de places BdA", default=False + ) + comments = models.TextField("Commentaires visibles par l'utilisateur", blank=True) is_buro = models.BooleanField("Membre du Burô", default=False) petits_cours_accept = models.BooleanField( - "Recevoir des petits cours", default=False) + "Recevoir des petits cours", default=False + ) petits_cours_remarques = models.TextField( - _("Remarques et précisions pour les petits cours"), - blank=True, default="") + _("Remarques et précisions pour les petits cours"), blank=True, default="" + ) class Meta: verbose_name = "Profil COF" @@ -106,8 +101,7 @@ def post_delete_user(sender, instance, *args, **kwargs): class Club(models.Model): name = models.CharField("Nom", max_length=200, unique=True) description = models.TextField("Description", blank=True) - respos = models.ManyToManyField(User, related_name="clubs_geres", - blank=True) + respos = models.ManyToManyField(User, related_name="clubs_geres", blank=True) membres = models.ManyToManyField(User, related_name="clubs", blank=True) def __str__(self): @@ -120,10 +114,8 @@ class Event(models.Model): start_date = models.DateTimeField("Date de début", blank=True, null=True) end_date = models.DateTimeField("Date de fin", blank=True, null=True) description = models.TextField("Description", blank=True) - image = models.ImageField("Image", blank=True, null=True, - upload_to="imgs/events/") - registration_open = models.BooleanField("Inscriptions ouvertes", - default=True) + image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/events/") + registration_open = models.BooleanField("Inscriptions ouvertes", default=True) old = models.BooleanField("Archiver (événement fini)", default=False) class Meta: @@ -135,12 +127,12 @@ class Event(models.Model): class EventCommentField(models.Model): event = models.ForeignKey( - Event, on_delete=models.CASCADE, - related_name="commentfields", + Event, on_delete=models.CASCADE, related_name="commentfields" ) name = models.CharField("Champ", max_length=200) - fieldtype = models.CharField("Type", max_length=10, - choices=TYPE_COMMENT_FIELD, default="text") + fieldtype = models.CharField( + "Type", max_length=10, choices=TYPE_COMMENT_FIELD, default="text" + ) default = models.TextField("Valeur par défaut", blank=True) class Meta: @@ -152,12 +144,10 @@ class EventCommentField(models.Model): class EventCommentValue(models.Model): commentfield = models.ForeignKey( - EventCommentField, on_delete=models.CASCADE, - related_name="values", + EventCommentField, on_delete=models.CASCADE, related_name="values" ) registration = models.ForeignKey( - "EventRegistration", on_delete=models.CASCADE, - related_name="comments", + "EventRegistration", on_delete=models.CASCADE, related_name="comments" ) content = models.TextField("Contenu", blank=True, null=True) @@ -166,10 +156,7 @@ class EventCommentValue(models.Model): class EventOption(models.Model): - event = models.ForeignKey( - Event, on_delete=models.CASCADE, - related_name="options", - ) + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="options") name = models.CharField("Option", max_length=200) multi_choices = models.BooleanField("Choix multiples", default=False) @@ -182,8 +169,7 @@ class EventOption(models.Model): class EventOptionChoice(models.Model): event_option = models.ForeignKey( - EventOption, on_delete=models.CASCADE, - related_name="choices", + EventOption, on_delete=models.CASCADE, related_name="choices" ) value = models.CharField("Valeur", max_length=200) @@ -199,8 +185,9 @@ class EventRegistration(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) event = models.ForeignKey(Event, on_delete=models.CASCADE) options = models.ManyToManyField(EventOptionChoice) - filledcomments = models.ManyToManyField(EventCommentField, - through=EventCommentValue) + filledcomments = models.ManyToManyField( + EventCommentField, through=EventCommentValue + ) paid = models.BooleanField("A payé", default=False) class Meta: @@ -226,8 +213,7 @@ class Survey(models.Model): class SurveyQuestion(models.Model): survey = models.ForeignKey( - Survey, on_delete=models.CASCADE, - related_name="questions", + Survey, on_delete=models.CASCADE, related_name="questions" ) question = models.CharField("Question", max_length=200) multi_answers = models.BooleanField("Choix multiples", default=False) @@ -241,8 +227,7 @@ class SurveyQuestion(models.Model): class SurveyQuestionAnswer(models.Model): survey_question = models.ForeignKey( - SurveyQuestion, on_delete=models.CASCADE, - related_name="answers", + SurveyQuestion, on_delete=models.CASCADE, related_name="answers" ) answer = models.CharField("Réponse", max_length=200) @@ -256,8 +241,7 @@ class SurveyQuestionAnswer(models.Model): class SurveyAnswer(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) survey = models.ForeignKey(Survey, on_delete=models.CASCADE) - answers = models.ManyToManyField(SurveyQuestionAnswer, - related_name="selected_by") + answers = models.ManyToManyField(SurveyQuestionAnswer, related_name="selected_by") class Meta: verbose_name = "Réponses" @@ -265,8 +249,9 @@ class SurveyAnswer(models.Model): def __str__(self): return "Réponse de %s sondage %s" % ( - self.user.get_full_name(), - self.survey.title) + self.user.get_full_name(), + self.survey.title, + ) class CalendarSubscription(models.Model): diff --git a/gestioncof/petits_cours_forms.py b/gestioncof/petits_cours_forms.py index e8f067bf..b9cfc067 100644 --- a/gestioncof/petits_cours_forms.py +++ b/gestioncof/petits_cours_forms.py @@ -1,11 +1,10 @@ from captcha.fields import ReCaptchaField - from django import forms -from django.forms import ModelForm -from django.forms.models import inlineformset_factory, BaseInlineFormSet from django.contrib.auth.models import User +from django.forms import ModelForm +from django.forms.models import BaseInlineFormSet, inlineformset_factory -from gestioncof.petits_cours_models import PetitCoursDemande, PetitCoursAbility +from gestioncof.petits_cours_models import PetitCoursAbility, PetitCoursDemande class BaseMatieresFormSet(BaseInlineFormSet): @@ -20,33 +19,44 @@ class BaseMatieresFormSet(BaseInlineFormSet): form = self.forms[i] if not form.cleaned_data: continue - matiere = form.cleaned_data['matiere'] - niveau = form.cleaned_data['niveau'] - delete = form.cleaned_data['DELETE'] + matiere = form.cleaned_data["matiere"] + niveau = form.cleaned_data["niveau"] + delete = form.cleaned_data["DELETE"] if not delete and (matiere, niveau) in matieres: raise forms.ValidationError( "Vous ne pouvez pas vous inscrire deux fois pour la " - "même matiere avec le même niveau.") + "même matiere avec le même niveau." + ) matieres.append((matiere, niveau)) class DemandeForm(ModelForm): - captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'}) + captcha = ReCaptchaField(attrs={"theme": "clean", "lang": "fr"}) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['matieres'].help_text = '' + self.fields["matieres"].help_text = "" class Meta: model = PetitCoursDemande - fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu', - 'matieres', 'agrege_requis', 'niveau', 'remarques') - widgets = {'matieres': forms.CheckboxSelectMultiple} + fields = ( + "name", + "email", + "phone", + "quand", + "freq", + "lieu", + "matieres", + "agrege_requis", + "niveau", + "remarques", + ) + widgets = {"matieres": forms.CheckboxSelectMultiple} MatieresFormSet = inlineformset_factory( User, PetitCoursAbility, fields=("matiere", "niveau", "agrege"), - formset=BaseMatieresFormSet + formset=BaseMatieresFormSet, ) diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py index 820f1292..40031877 100644 --- a/gestioncof/petits_cours_models.py +++ b/gestioncof/petits_cours_models.py @@ -1,28 +1,30 @@ from functools import reduce +from django.contrib.auth.models import User from django.db import models from django.db.models import Min -from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ def choices_length(choices): return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) + LEVELS_CHOICES = ( - ('college', _("Collège")), - ('lycee', _("Lycée")), - ('prepa1styear', _("Prépa 1ère année / L1")), - ('prepa2ndyear', _("Prépa 2ème année / L2")), - ('licence3', _("Licence 3")), - ('other', _("Autre (préciser dans les commentaires)")), + ("college", _("Collège")), + ("lycee", _("Lycée")), + ("prepa1styear", _("Prépa 1ère année / L1")), + ("prepa2ndyear", _("Prépa 2ème année / L2")), + ("licence3", _("Licence 3")), + ("other", _("Autre (préciser dans les commentaires)")), ) class PetitCoursSubject(models.Model): name = models.CharField(_("Matière"), max_length=30) - users = models.ManyToManyField(User, related_name="petits_cours_matieres", - through="PetitCoursAbility") + users = models.ManyToManyField( + User, related_name="petits_cours_matieres", through="PetitCoursAbility" + ) class Meta: verbose_name = "Matière de petits cours" @@ -35,12 +37,11 @@ class PetitCoursSubject(models.Model): class PetitCoursAbility(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) matiere = models.ForeignKey( - PetitCoursSubject, on_delete=models.CASCADE, - verbose_name=_("Matière"), + PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière") + ) + niveau = models.CharField( + _("Niveau"), choices=LEVELS_CHOICES, max_length=choices_length(LEVELS_CHOICES) ) - niveau = models.CharField(_("Niveau"), - choices=LEVELS_CHOICES, - max_length=choices_length(LEVELS_CHOICES)) agrege = models.BooleanField(_("Agrégé"), default=False) class Meta: @@ -56,41 +57,50 @@ class PetitCoursAbility(models.Model): class PetitCoursDemande(models.Model): name = models.CharField(_("Nom/prénom"), max_length=200) email = models.CharField(_("Adresse email"), max_length=300) - phone = models.CharField(_("Téléphone (facultatif)"), - max_length=20, blank=True) + phone = models.CharField(_("Téléphone (facultatif)"), max_length=20, blank=True) quand = models.CharField( _("Quand ?"), - help_text=_("Indiquez ici la période désirée pour les petits" - " cours (vacances scolaires, semaine, week-end)."), - max_length=300, blank=True) + help_text=_( + "Indiquez ici la période désirée pour les petits" + " cours (vacances scolaires, semaine, week-end)." + ), + max_length=300, + blank=True, + ) freq = models.CharField( _("Fréquence"), - help_text=_("Indiquez ici la fréquence envisagée " - "(hebdomadaire, 2 fois par semaine, ...)"), - max_length=300, blank=True) + help_text=_( + "Indiquez ici la fréquence envisagée " + "(hebdomadaire, 2 fois par semaine, ...)" + ), + max_length=300, + blank=True, + ) lieu = models.CharField( _("Lieu (si préférence)"), help_text=_("Si vous avez avez une préférence sur le lieu."), - max_length=300, blank=True) + max_length=300, + blank=True, + ) matieres = models.ManyToManyField( - PetitCoursSubject, verbose_name=_("Matières"), - related_name="demandes") + PetitCoursSubject, verbose_name=_("Matières"), related_name="demandes" + ) agrege_requis = models.BooleanField(_("Agrégé requis"), default=False) - niveau = models.CharField(_("Niveau"), - default="", - choices=LEVELS_CHOICES, - max_length=choices_length(LEVELS_CHOICES)) + niveau = models.CharField( + _("Niveau"), + default="", + choices=LEVELS_CHOICES, + max_length=choices_length(LEVELS_CHOICES), + ) remarques = models.TextField(_("Remarques et précisions"), blank=True) traitee = models.BooleanField(_("Traitée"), default=False) traitee_par = models.ForeignKey( - User, on_delete=models.CASCADE, - blank=True, null=True, + User, on_delete=models.CASCADE, blank=True, null=True ) - processed = models.DateTimeField(_("Date de traitement"), - blank=True, null=True) + processed = models.DateTimeField(_("Date de traitement"), blank=True, null=True) created = models.DateTimeField(_("Date de création"), auto_now_add=True) def get_candidates(self, redo=False): @@ -105,18 +115,15 @@ class PetitCoursDemande(models.Model): matiere=matiere, niveau=self.niveau, user__profile__is_cof=True, - user__profile__petits_cours_accept=True + user__profile__petits_cours_accept=True, ) if self.agrege_requis: candidates = candidates.filter(agrege=True) if redo: attrs = self.petitcoursattribution_set.filter(matiere=matiere) - already_proposed = [ - attr.user - for attr in attrs - ] + already_proposed = [attr.user for attr in attrs] candidates = candidates.exclude(user__in=already_proposed) - candidates = candidates.order_by('?').select_related().all() + candidates = candidates.order_by("?").select_related().all() yield (matiere, candidates) class Meta: @@ -124,25 +131,20 @@ class PetitCoursDemande(models.Model): verbose_name_plural = "Demandes de petits cours" def __str__(self): - return "Demande {:d} du {:s}".format( - self.id, self.created.strftime("%d %b %Y") - ) + return "Demande {:d} du {:s}".format(self.id, self.created.strftime("%d %b %Y")) class PetitCoursAttribution(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) demande = models.ForeignKey( - PetitCoursDemande, on_delete=models.CASCADE, - verbose_name=_("Demande"), + PetitCoursDemande, on_delete=models.CASCADE, verbose_name=_("Demande") ) matiere = models.ForeignKey( - PetitCoursSubject, on_delete=models.CASCADE, - verbose_name=_("Matière"), + PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière") ) date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True) rank = models.IntegerField("Rang dans l'email") - selected = models.BooleanField(_("Sélectionné par le demandeur"), - default=False) + selected = models.BooleanField(_("Sélectionné par le demandeur"), default=False) class Meta: verbose_name = "Attribution de petits cours" @@ -157,8 +159,7 @@ class PetitCoursAttribution(models.Model): class PetitCoursAttributionCounter(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) matiere = models.ForeignKey( - PetitCoursSubject, on_delete=models.CASCADE, - verbose_name=_("Matiere"), + PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matiere") ) count = models.IntegerField("Nombre d'envois", default=0) @@ -169,15 +170,12 @@ class PetitCoursAttributionCounter(models.Model): n'existe pas encore, il est initialisé avec le minimum des valeurs des compteurs de tout le monde. """ - counter, created = cls.objects.get_or_create( - user=user, - matiere=matiere, - ) + counter, created = cls.objects.get_or_create(user=user, matiere=matiere) if created: mincount = ( - cls.objects.filter(matiere=matiere).exclude(user=user) - .aggregate(Min('count')) - ['count__min'] + cls.objects.filter(matiere=matiere) + .exclude(user=user) + .aggregate(Min("count"))["count__min"] ) counter.count = mincount or 0 counter.save() diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index 6b8c8610..d640981a 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -1,31 +1,31 @@ import json + from custommail.shortcuts import render_custom_mail - -from django.shortcuts import render, get_object_or_404, redirect -from django.core import mail -from django.contrib.auth.models import User -from django.views.generic import ListView, DetailView -from django.views.decorators.csrf import csrf_exempt from django.conf import settings -from django.contrib.auth.decorators import login_required from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core import mail from django.db import transaction +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView, ListView -from gestioncof.models import CofProfile -from gestioncof.petits_cours_models import ( - PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter, - PetitCoursAbility -) -from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet from gestioncof.decorators import buro_required +from gestioncof.models import CofProfile +from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet +from gestioncof.petits_cours_models import ( + PetitCoursAbility, + PetitCoursAttribution, + PetitCoursAttributionCounter, + PetitCoursDemande, +) class DemandeListView(ListView): - queryset = ( - PetitCoursDemande.objects - .prefetch_related('matieres') - .order_by('traitee', '-id') + queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by( + "traitee", "-id" ) template_name = "petits_cours_demandes_list.html" paginate_by = 20 @@ -33,10 +33,8 @@ class DemandeListView(ListView): class DemandeDetailView(DetailView): model = PetitCoursDemande - queryset = ( - PetitCoursDemande.objects - .prefetch_related('petitcoursattribution_set', - 'matieres') + queryset = PetitCoursDemande.objects.prefetch_related( + "petitcoursattribution_set", "matieres" ) template_name = "gestioncof/details_demande_petit_cours.html" context_object_name = "demande" @@ -44,7 +42,7 @@ class DemandeDetailView(DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) obj = self.object - context['attributions'] = obj.petitcoursattribution_set.all() + context["attributions"] = obj.petitcoursattribution_set.all() return context @@ -64,13 +62,15 @@ def traitement(request, demande_id, redo=False): tuples = [] for candidate in candidates: user = candidate.user - tuples.append(( - candidate, - PetitCoursAttributionCounter.get_uptodate(user, matiere) - )) + tuples.append( + ( + candidate, + PetitCoursAttributionCounter.get_uptodate(user, matiere), + ) + ) tuples = sorted(tuples, key=lambda c: c[1].count) candidates, _ = zip(*tuples) - candidates = candidates[0:min(3, len(candidates))] + candidates = candidates[0 : min(3, len(candidates))] attribdata[matiere.id] = [] proposals[matiere] = [] for candidate in candidates: @@ -83,8 +83,9 @@ def traitement(request, demande_id, redo=False): proposed_for[user].append(matiere) else: unsatisfied.append(matiere) - return _finalize_traitement(request, demande, proposals, - proposed_for, unsatisfied, attribdata, redo) + return _finalize_traitement( + request, demande, proposals, proposed_for, unsatisfied, attribdata, redo + ) @buro_required @@ -92,43 +93,56 @@ def retraitement(request, demande_id): return traitement(request, demande_id, redo=True) -def _finalize_traitement(request, demande, proposals, proposed_for, - unsatisfied, attribdata, redo=False, errors=None): +def _finalize_traitement( + request, + demande, + proposals, + proposed_for, + unsatisfied, + attribdata, + redo=False, + errors=None, +): proposals = proposals.items() proposed_for = proposed_for.items() attribdata = list(attribdata.items()) proposed_mails = _generate_eleve_email(demande, proposed_for) - mainmail = render_custom_mail("petits-cours-mail-demandeur", { - "proposals": proposals, - "unsatisfied": unsatisfied, - "extra": - '' - }) + "", + }, + ) if errors is not None: for error in errors: messages.error(request, error) - return render(request, "gestioncof/traitement_demande_petit_cours.html", - {"demande": demande, - "unsatisfied": unsatisfied, - "proposals": proposals, - "proposed_for": proposed_for, - "proposed_mails": proposed_mails, - "mainmail": mainmail, - "attribdata": json.dumps(attribdata), - "redo": redo, - }) + return render( + request, + "gestioncof/traitement_demande_petit_cours.html", + { + "demande": demande, + "unsatisfied": unsatisfied, + "proposals": proposals, + "proposed_for": proposed_for, + "proposed_mails": proposed_mails, + "mainmail": mainmail, + "attribdata": json.dumps(attribdata), + "redo": redo, + }, + ) def _generate_eleve_email(demande, proposed_for): return [ ( user, - render_custom_mail('petit-cours-mail-eleve', { - "demande": demande, - "matieres": matieres - }) + render_custom_mail( + "petit-cours-mail-eleve", {"demande": demande, "matieres": matieres} + ), ) for user, matieres in proposed_for ] @@ -143,25 +157,30 @@ def _traitement_other_preparing(request, demande): errors = [] for matiere, candidates in demande.get_candidates(redo): if candidates: - candidates = dict([(candidate.user.id, candidate.user) - for candidate in candidates]) + candidates = dict( + [(candidate.user.id, candidate.user) for candidate in candidates] + ) attribdata[matiere.id] = [] proposals[matiere] = [] for choice_id in range(min(3, len(candidates))): choice = int( - request.POST["proposal-{:d}-{:d}" - .format(matiere.id, choice_id)] + request.POST["proposal-{:d}-{:d}".format(matiere.id, choice_id)] ) if choice == -1: continue if choice not in candidates: - errors.append("Choix invalide pour la proposition {:d}" - "en {!s}".format(choice_id + 1, matiere)) + errors.append( + "Choix invalide pour la proposition {:d}" + "en {!s}".format(choice_id + 1, matiere) + ) continue user = candidates[choice] if user in proposals[matiere]: - errors.append("La proposition {:d} en {!s} est un doublon" - .format(choice_id + 1, matiere)) + errors.append( + "La proposition {:d} en {!s} est un doublon".format( + choice_id + 1, matiere + ) + ) continue proposals[matiere].append(user) attribdata[matiere.id].append(user.id) @@ -172,15 +191,24 @@ def _traitement_other_preparing(request, demande): if not proposals[matiere]: errors.append("Aucune proposition pour {!s}".format(matiere)) elif len(proposals[matiere]) < 3: - errors.append("Seulement {:d} proposition{:s} pour {!s}" - .format( - len(proposals[matiere]), - "s" if len(proposals[matiere]) > 1 else "", - matiere)) + errors.append( + "Seulement {:d} proposition{:s} pour {!s}".format( + len(proposals[matiere]), + "s" if len(proposals[matiere]) > 1 else "", + matiere, + ) + ) else: unsatisfied.append(matiere) - return _finalize_traitement(request, demande, proposals, proposed_for, - unsatisfied, attribdata, errors=errors) + return _finalize_traitement( + request, + demande, + proposals, + proposed_for, + unsatisfied, + attribdata, + errors=errors, + ) def _traitement_other(request, demande, redo): @@ -198,10 +226,12 @@ def _traitement_other(request, demande, redo): tuples = [] for candidate in candidates: user = candidate.user - tuples.append(( - candidate, - PetitCoursAttributionCounter.get_uptodate(user, matiere) - )) + tuples.append( + ( + candidate, + PetitCoursAttributionCounter.get_uptodate(user, matiere), + ) + ) tuples = sorted(tuples, key=lambda c: c[1].count) candidates, _ = zip(*tuples) attribdata[matiere.id] = [] @@ -218,13 +248,16 @@ def _traitement_other(request, demande, redo): unsatisfied.append(matiere) proposals = proposals.items() proposed_for = proposed_for.items() - return render(request, - "gestioncof/traitement_demande_petit_cours_autre_niveau.html", - {"demande": demande, - "unsatisfied": unsatisfied, - "proposals": proposals, - "proposed_for": proposed_for, - }) + return render( + request, + "gestioncof/traitement_demande_petit_cours_autre_niveau.html", + { + "demande": demande, + "unsatisfied": unsatisfied, + "proposals": proposals, + "proposed_for": proposed_for, + }, + ) def _traitement_post(request, demande): @@ -252,24 +285,32 @@ def _traitement_post(request, demande): proposed_mails = _generate_eleve_email(demande, proposed_for) mainmail_object, mainmail_body = render_custom_mail( "petits-cours-mail-demandeur", - { - "proposals": proposals_list, - "unsatisfied": unsatisfied, - "extra": extra - } + {"proposals": proposals_list, "unsatisfied": unsatisfied, "extra": extra}, ) - frommail = settings.MAIL_DATA['petits_cours']['FROM'] - bccaddress = settings.MAIL_DATA['petits_cours']['BCC'] - replyto = settings.MAIL_DATA['petits_cours']['REPLYTO'] + frommail = settings.MAIL_DATA["petits_cours"]["FROM"] + bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"] + replyto = settings.MAIL_DATA["petits_cours"]["REPLYTO"] mails_to_send = [] for (user, (mail_object, body)) in proposed_mails: - msg = mail.EmailMessage(mail_object, body, frommail, [user.email], - [bccaddress], headers={'Reply-To': replyto}) + msg = mail.EmailMessage( + mail_object, + body, + frommail, + [user.email], + [bccaddress], + headers={"Reply-To": replyto}, + ) mails_to_send.append(msg) - mails_to_send.append(mail.EmailMessage(mainmail_object, mainmail_body, - frommail, [demande.email], - [bccaddress], - headers={'Reply-To': replyto})) + mails_to_send.append( + mail.EmailMessage( + mainmail_object, + mainmail_body, + frommail, + [demande.email], + [bccaddress], + headers={"Reply-To": replyto}, + ) + ) connection = mail.get_connection(fail_silently=False) connection.send_messages(mails_to_send) with transaction.atomic(): @@ -280,18 +321,19 @@ def _traitement_post(request, demande): ) counter.count += 1 counter.save() - attrib = PetitCoursAttribution(user=user, matiere=matiere, - demande=demande, rank=rank + 1) + attrib = PetitCoursAttribution( + user=user, matiere=matiere, demande=demande, rank=rank + 1 + ) attrib.save() demande.traitee = True demande.traitee_par = request.user demande.processed = timezone.now() demande.save() - return render(request, - "gestioncof/traitement_demande_petit_cours_success.html", - {"demande": demande, - "redo": redo, - }) + return render( + request, + "gestioncof/traitement_demande_petit_cours_success.html", + {"demande": demande, "redo": redo}, + ) @login_required @@ -308,22 +350,25 @@ def inscription(request): profile.petits_cours_remarques = request.POST["remarques"] profile.save() with transaction.atomic(): - abilities = ( - PetitCoursAbility.objects.filter(user=request.user).all() - ) + abilities = PetitCoursAbility.objects.filter(user=request.user).all() for ability in abilities: PetitCoursAttributionCounter.get_uptodate( - ability.user, - ability.matiere + ability.user, ability.matiere ) success = True formset = MatieresFormSet(instance=request.user) else: formset = MatieresFormSet(instance=request.user) - return render(request, "inscription-petit-cours.html", - {"formset": formset, "success": success, - "receive_proposals": profile.petits_cours_accept, - "remarques": profile.petits_cours_remarques}) + return render( + request, + "inscription-petit-cours.html", + { + "formset": formset, + "success": success, + "receive_proposals": profile.petits_cours_accept, + "remarques": profile.petits_cours_remarques, + }, + ) @csrf_exempt @@ -336,8 +381,9 @@ def demande(request): success = True else: form = DemandeForm() - return render(request, "demande-petit-cours.html", {"form": form, - "success": success}) + return render( + request, "demande-petit-cours.html", {"form": form, "success": success} + ) @csrf_exempt @@ -350,5 +396,6 @@ def demande_raw(request): success = True else: form = DemandeForm() - return render(request, "demande-petit-cours-raw.html", - {"form": form, "success": success}) + return render( + request, "demande-petit-cours-raw.html", {"form": form, "success": success} + ) diff --git a/gestioncof/shared.py b/gestioncof/shared.py index fdab9a45..87e19842 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -1,13 +1,11 @@ from django.conf import settings from django.contrib.sites.models import Site - from django_cas_ng.backends import CASBackend from gestioncof.models import CofProfile class COFCASBackend(CASBackend): - def clean_username(self, username): # Le CAS de l'ENS accepte les logins avec des espaces au début # et à la fin, ainsi qu’avec une casse variable. On normalise pour @@ -24,9 +22,6 @@ class COFCASBackend(CASBackend): def context_processor(request): - '''Append extra data to the context of the given request''' - data = { - "user": request.user, - "site": Site.objects.get_current(), - } + """Append extra data to the context of the given request""" + data = {"user": request.user, "site": Site.objects.get_current()} return data diff --git a/gestioncof/signals.py b/gestioncof/signals.py index 11cb55fc..3614b1c8 100644 --- a/gestioncof/signals.py +++ b/gestioncof/signals.py @@ -2,22 +2,21 @@ from django.contrib import messages from django.contrib.auth.signals import user_logged_in from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ - from django_cas_ng.signals import cas_user_authenticated @receiver(user_logged_in) def messages_on_out_login(request, user, **kwargs): - if user.backend.startswith('django.contrib.auth'): - msg = _('Connexion à GestioCOF réussie. Bienvenue {}.').format( - user.get_short_name(), + if user.backend.startswith("django.contrib.auth"): + msg = _("Connexion à GestioCOF réussie. Bienvenue {}.").format( + user.get_short_name() ) messages.success(request, msg) @receiver(cas_user_authenticated) def mesagges_on_cas_login(request, user, **kwargs): - msg = _('Connexion à GestioCOF par CAS réussie. Bienvenue {}.').format( - user.get_short_name(), + msg = _("Connexion à GestioCOF par CAS réussie. Bienvenue {}.").format( + user.get_short_name() ) messages.success(request, msg) diff --git a/gestioncof/templatetags/utils.py b/gestioncof/templatetags/utils.py index 2b732aec..21518614 100644 --- a/gestioncof/templatetags/utils.py +++ b/gestioncof/templatetags/utils.py @@ -1,8 +1,8 @@ +import re + from django import template from django.utils.safestring import mark_safe -import re - register = template.Library() @@ -12,6 +12,7 @@ def key(d, key_name): value = d[key_name] except KeyError: from django.conf import settings + value = settings.TEMPLATE_STRING_IF_INVALID return value @@ -19,16 +20,15 @@ def key(d, key_name): def highlight_text(text, q): q2 = "|".join(re.escape(word) for word in q.split()) pattern = re.compile(r"(?P%s)" % q2, re.IGNORECASE) - return mark_safe(re.sub(pattern, - r"\g", - text)) + return mark_safe( + re.sub(pattern, r"\g", text) + ) @register.filter def highlight_user(user, q): if user.first_name and user.last_name: - text = "%s %s (%s)" % (user.first_name, user.last_name, - user.username) + text = "%s %s (%s)" % (user.first_name, user.last_name, user.username) else: text = user.username return highlight_text(text, q) diff --git a/gestioncof/tests/test_legacy.py b/gestioncof/tests/test_legacy.py index 85673edd..cc7ddbf7 100644 --- a/gestioncof/tests/test_legacy.py +++ b/gestioncof/tests/test_legacy.py @@ -12,17 +12,17 @@ from gestioncof.models import CofProfile, User class SimpleTest(TestCase): 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 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. 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. u.save() - CofProfile.objects.get(user__username='foo').delete() - self.assertFalse(User.objects.filter(username='foo').exists()) + CofProfile.objects.get(user__username="foo").delete() + self.assertFalse(User.objects.filter(username="foo").exists()) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 1ab6a5f4..d65247c1 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -2,6 +2,7 @@ import csv import uuid from datetime import timedelta +from custommail.models import CustomMail from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.messages.api import get_messages @@ -13,29 +14,25 @@ from django.urls import reverse from bda.models import Salle, Tirage from gestioncof.autocomplete import Clipper -from gestioncof.models import ( - CalendarSubscription, Club, Event, Survey, SurveyAnswer -) +from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer from gestioncof.tests.testcases import ViewTestCaseMixin -from custommail.models import CustomMail - from .utils import create_member, create_root, create_user User = get_user_model() class RegistrationViewTests(ViewTestCaseMixin, TestCase): - url_name = 'registration' - url_expected = '/registration' + url_name = "registration" + url_expected = "/registration" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def requires_mails(self): - call_command('syncmails', verbosity=0) + call_command("syncmails", verbosity=0) def test_get(self): r = self.client.get(self.url) @@ -44,96 +41,108 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase): @property def _minimal_data(self): return { - 'first_name': '', - 'last_name': '', - 'email': '', - + "first_name": "", + "last_name": "", + "email": "", # 'is_cof': '1', - 'login_clipper': '', - 'phone': '', - 'occupation': '1A', - 'departement': '', - 'type_cotiz': 'normalien', - 'comments': '', - + "login_clipper": "", + "phone": "", + "occupation": "1A", + "departement": "", + "type_cotiz": "normalien", + "comments": "", # 'user_exists': '1', - - 'events-TOTAL_FORMS': '0', - 'events-INITIAL_FORMS': '0', - 'events-MIN_NUM_FORMS': '0', - 'events-MAX_NUM_FORMS': '1000', + "events-TOTAL_FORMS": "0", + "events-INITIAL_FORMS": "0", + "events-MIN_NUM_FORMS": "0", + "events-MAX_NUM_FORMS": "1000", } def test_post_new(self): self.requires_mails() - r = self.client.post(self.url, dict(self._minimal_data, **{ - 'username': 'username', - 'first_name': 'first', - 'last_name': 'last', - 'email': 'username@mail.net', - 'is_cof': '1', - })) + r = self.client.post( + self.url, + dict( + self._minimal_data, + **{ + "username": "username", + "first_name": "first", + "last_name": "last", + "email": "username@mail.net", + "is_cof": "1", + } + ), + ) self.assertEqual(r.status_code, 200) - u = User.objects.get(username='username') - expected_message = Message(messages.SUCCESS, ( - "L'inscription de first last (username@mail.net) a été " - "enregistrée avec succès.\n" - "Il est désormais membre du COF n°{} !" - .format(u.pk) - )) + u = User.objects.get(username="username") + expected_message = Message( + messages.SUCCESS, + ( + "L'inscription de first last (username@mail.net) a été " + "enregistrée avec succès.\n" + "Il est désormais membre du COF n°{} !".format(u.pk) + ), + ) self.assertIn(expected_message, get_messages(r.wsgi_request)) - self.assertEqual(u.first_name, 'first') - self.assertEqual(u.last_name, 'last') - self.assertEqual(u.email, 'username@mail.net') + self.assertEqual(u.first_name, "first") + self.assertEqual(u.last_name, "last") + self.assertEqual(u.email, "username@mail.net") def test_post_edit(self): self.requires_mails() - u = self.users['user'] + u = self.users["user"] - r = self.client.post(self.url, dict(self._minimal_data, **{ - 'username': 'user', - 'first_name': 'first', - 'last_name': 'last', - 'email': 'user@mail.net', - 'is_cof': '1', - 'user_exists': '1', - })) + r = self.client.post( + self.url, + dict( + self._minimal_data, + **{ + "username": "user", + "first_name": "first", + "last_name": "last", + "email": "user@mail.net", + "is_cof": "1", + "user_exists": "1", + } + ), + ) self.assertEqual(r.status_code, 200) u.refresh_from_db() - expected_message = Message(messages.SUCCESS, ( - "L'inscription de first last (user@mail.net) a été " - "enregistrée avec succès.\n" - "Il est désormais membre du COF n°{} !" - .format(u.pk) - )) + expected_message = Message( + messages.SUCCESS, + ( + "L'inscription de first last (user@mail.net) a été " + "enregistrée avec succès.\n" + "Il est désormais membre du COF n°{} !".format(u.pk) + ), + ) self.assertIn(expected_message, get_messages(r.wsgi_request)) - self.assertEqual(u.first_name, 'first') - self.assertEqual(u.last_name, 'last') - self.assertEqual(u.email, 'user@mail.net') + self.assertEqual(u.first_name, "first") + self.assertEqual(u.last_name, "last") + self.assertEqual(u.email, "user@mail.net") def _test_mail_welcome(self, was_cof, is_cof, expect_mail): self.requires_mails() - u = self.users['member'] if was_cof else self.users['user'] + u = self.users["member"] if was_cof else self.users["user"] - data = dict(self._minimal_data, **{ - 'username': u.username, - 'email': 'user@mail.net', - 'user_exists': '1', - }) + data = dict( + self._minimal_data, + **{"username": u.username, "email": "user@mail.net", "user_exists": "1"} + ) if is_cof: - data['is_cof'] = '1' + data["is_cof"] = "1" self.client.post(self.url, data) u.refresh_from_db() def _is_sent(): - cm = CustomMail.objects.get(shortname='welcome') - welcome_msg = cm.get_message({'member': u}) + cm = CustomMail.objects.get(shortname="welcome") + welcome_msg = cm.get_message({"member": u}) for m in mail.outbox: if m.subject == welcome_msg.subject: return True @@ -156,197 +165,184 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase): def test_events(self): e = Event.objects.create() - cf1 = e.commentfields.create(name='Comment Field 1') - cf2 = e.commentfields.create( - name='Comment Field 2', fieldtype='char', + cf1 = e.commentfields.create(name="Comment Field 1") + cf2 = e.commentfields.create(name="Comment Field 2", fieldtype="char") + + o1 = e.options.create(name="Option 1") + o2 = e.options.create(name="Option 2", multi_choices=True) + + oc1 = o1.choices.create(value="O1 - Choice 1") + o1.choices.create(value="O1 - Choice 2") + oc3 = o2.choices.create(value="O2 - Choice 1") + o2.choices.create(value="O2 - Choice 2") + + self.client.post( + self.url, + dict( + self._minimal_data, + **{ + "username": "user", + "user_exists": "1", + "events-TOTAL_FORMS": "1", + "events-INITIAL_FORMS": "0", + "events-MIN_NUM_FORMS": "0", + "events-MAX_NUM_FORMS": "1000", + "events-0-status": "paid", + "events-0-option_{}".format(o1.pk): [str(oc1.pk)], + "events-0-option_{}".format(o2.pk): [str(oc3.pk)], + "events-0-comment_{}".format(cf1.pk): "comment 1", + "events-0-comment_{}".format(cf2.pk): "", + } + ), ) - o1 = e.options.create(name='Option 1') - o2 = e.options.create(name='Option 2', multi_choices=True) - - oc1 = o1.choices.create(value='O1 - Choice 1') - o1.choices.create(value='O1 - Choice 2') - oc3 = o2.choices.create(value='O2 - Choice 1') - o2.choices.create(value='O2 - Choice 2') - - self.client.post(self.url, dict(self._minimal_data, **{ - 'username': 'user', - 'user_exists': '1', - 'events-TOTAL_FORMS': '1', - 'events-INITIAL_FORMS': '0', - 'events-MIN_NUM_FORMS': '0', - 'events-MAX_NUM_FORMS': '1000', - 'events-0-status': 'paid', - 'events-0-option_{}'.format(o1.pk): [str(oc1.pk)], - 'events-0-option_{}'.format(o2.pk): [str(oc3.pk)], - 'events-0-comment_{}'.format(cf1.pk): 'comment 1', - 'events-0-comment_{}'.format(cf2.pk): '', - })) - - er = e.eventregistration_set.get(user=self.users['user']) - self.assertQuerysetEqual( - er.options.all(), map(repr, [oc1, oc3]), - ordered=False, + er = e.eventregistration_set.get(user=self.users["user"]) + self.assertQuerysetEqual(er.options.all(), map(repr, [oc1, oc3]), ordered=False) + self.assertCountEqual( + er.comments.values_list("content", flat=True), ["comment 1"] ) - self.assertCountEqual(er.comments.values_list('content', flat=True), [ - 'comment 1', - ]) class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): urls_conf = [ + {"name": "empty-registration", "expected": "/registration/empty"}, { - 'name': 'empty-registration', - 'expected': '/registration/empty', + "name": "user-registration", + "kwargs": {"username": "user"}, + "expected": "/registration/user/user", }, { - 'name': 'user-registration', - 'kwargs': {'username': 'user'}, - 'expected': '/registration/user/user', - }, - { - 'name': 'clipper-registration', - 'kwargs': { - 'login_clipper': 'uid', - 'fullname': 'First Last1 Last2', - }, - 'expected': '/registration/clipper/uid/First%20Last1%20Last2', + "name": "clipper-registration", + "kwargs": {"login_clipper": "uid", "fullname": "First Last1 Last2"}, + "expected": "/registration/clipper/uid/First%20Last1%20Last2", }, ] - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test_empty(self): r = self.client.get(self.t_urls[0]) - self.assertIn('user_form', r.context) - self.assertIn('profile_form', r.context) - self.assertIn('event_formset', r.context) - self.assertIn('clubs_form', r.context) + self.assertIn("user_form", r.context) + self.assertIn("profile_form", r.context) + self.assertIn("event_formset", r.context) + self.assertIn("clubs_form", r.context) def test_username(self): - u = self.users['user'] - u.first_name = 'first' - u.last_name = 'last' + u = self.users["user"] + u.first_name = "first" + u.last_name = "last" u.save() r = self.client.get(self.t_urls[1]) - self.assertIn('user_form', r.context) - self.assertIn('profile_form', r.context) - self.assertIn('event_formset', r.context) - self.assertIn('clubs_form', r.context) - user_form = r.context['user_form'] - self.assertEqual(user_form['username'].initial, 'user') - self.assertEqual(user_form['first_name'].initial, 'first') - self.assertEqual(user_form['last_name'].initial, 'last') + self.assertIn("user_form", r.context) + self.assertIn("profile_form", r.context) + self.assertIn("event_formset", r.context) + self.assertIn("clubs_form", r.context) + user_form = r.context["user_form"] + self.assertEqual(user_form["username"].initial, "user") + self.assertEqual(user_form["first_name"].initial, "first") + self.assertEqual(user_form["last_name"].initial, "last") def test_clipper(self): r = self.client.get(self.t_urls[2]) - self.assertIn('user_form', r.context) - self.assertIn('profile_form', r.context) - self.assertIn('event_formset', r.context) - self.assertIn('clubs_form', r.context) - user_form = r.context['user_form'] - profile_form = r.context['profile_form'] - self.assertEqual(user_form['first_name'].initial, 'First') - self.assertEqual(user_form['last_name'].initial, 'Last1 Last2') - self.assertEqual(user_form['email'].initial, 'uid@clipper.ens.fr') - self.assertEqual(profile_form['login_clipper'].initial, 'uid') + self.assertIn("user_form", r.context) + self.assertIn("profile_form", r.context) + self.assertIn("event_formset", r.context) + self.assertIn("clubs_form", r.context) + user_form = r.context["user_form"] + profile_form = r.context["profile_form"] + self.assertEqual(user_form["first_name"].initial, "First") + self.assertEqual(user_form["last_name"].initial, "Last1 Last2") + self.assertEqual(user_form["email"].initial, "uid@clipper.ens.fr") + self.assertEqual(profile_form["login_clipper"].initial, "uid") -@override_settings(LDAP_SERVER_URL='ldap_url') +@override_settings(LDAP_SERVER_URL="ldap_url") class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): - url_name = 'cof.registration.autocomplete' - url_expected = '/autocomplete/registration' + url_name = "cof.registration.autocomplete" + url_expected = "/autocomplete/registration" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def setUp(self): super().setUp() - self.u1 = create_user('uu_u1', attrs={ - 'first_name': 'abc', 'last_name': 'xyz', - }) - self.u2 = create_user('uu_u2', attrs={ - 'first_name': 'wyz', 'last_name': 'abd', - }) - self.m1 = create_member('uu_m1', attrs={ - 'first_name': 'ebd', 'last_name': 'wyv', - }) + self.u1 = create_user("uu_u1", attrs={"first_name": "abc", "last_name": "xyz"}) + self.u2 = create_user("uu_u2", attrs={"first_name": "wyz", "last_name": "abd"}) + self.m1 = create_member( + "uu_m1", attrs={"first_name": "ebd", "last_name": "wyv"} + ) self.mockLDAP([]) - def _test( - self, query, expected_users, expected_members, expected_clippers, - ): - r = self.client.get(self.url, {'q': query}) + def _test(self, query, expected_users, expected_members, expected_clippers): + r = self.client.get(self.url, {"q": query}) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['users'], map(repr, expected_users), - ordered=False, + r.context["users"], map(repr, expected_users), ordered=False ) self.assertQuerysetEqual( - r.context['members'], + r.context["members"], map(lambda u: repr(u.profile), expected_members), ordered=False, ) self.assertCountEqual( - map(str, r.context.get('clippers', [])), - map(str, expected_clippers), + map(str, r.context.get("clippers", [])), map(str, expected_clippers) ) def test_username(self): - self._test('uu', [self.u1, self.u2], [self.m1], []) + self._test("uu", [self.u1, self.u2], [self.m1], []) def test_firstname(self): - self._test('ab', [self.u1, self.u2], [], []) + self._test("ab", [self.u1, self.u2], [], []) def test_lastname(self): - self._test('wy', [self.u2], [self.m1], []) + self._test("wy", [self.u2], [self.m1], []) def test_multi_query(self): - self._test('wy bd', [self.u2], [self.m1], []) + self._test("wy bd", [self.u2], [self.m1], []) def test_clipper(self): - mock_ldap = self.mockLDAP([('uid', 'first last')]) + mock_ldap = self.mockLDAP([("uid", "first last")]) - self._test('aa bb', [], [], [Clipper('uid', 'first last')]) + self._test("aa bb", [], [], [Clipper("uid", "first last")]) mock_ldap.search.assert_called_once_with( - 'dc=spi,dc=ens,dc=fr', - '(&(|(cn=*aa*)(uid=*aa*))(|(cn=*bb*)(uid=*bb*)))', - attributes=['uid', 'cn'], + "dc=spi,dc=ens,dc=fr", + "(&(|(cn=*aa*)(uid=*aa*))(|(cn=*bb*)(uid=*bb*)))", + attributes=["uid", "cn"], ) def test_clipper_escaped(self): mock_ldap = self.mockLDAP([]) - self._test('; & | (', [], [], []) + self._test("; & | (", [], [], []) mock_ldap.search.assert_not_called() def test_clipper_no_duplicate(self): - self.mockLDAP([('uid', 'uu_u1')]) + self.mockLDAP([("uid", "uu_u1")]) - self._test('uu u1', [self.u1], [], [Clipper('uid', 'uu_u1')]) + self._test("uu u1", [self.u1], [], [Clipper("uid", "uu_u1")]) - self.u1.profile.login_clipper = 'uid' + self.u1.profile.login_clipper = "uid" self.u1.profile.save() - self._test('uu u1', [self.u1], [], []) + self._test("uu u1", [self.u1], [], []) class HomeViewTests(ViewTestCaseMixin, TestCase): - url_name = 'home' - url_expected = '/' + url_name = "home" + url_expected = "/" - auth_user = 'user' + auth_user = "user" auth_forbidden = [None] def test(self): @@ -355,49 +351,52 @@ class HomeViewTests(ViewTestCaseMixin, TestCase): class ProfileViewTests(ViewTestCaseMixin, TestCase): - url_name = 'profile' - url_expected = '/profile' + url_name = "profile" + url_expected = "/profile" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'member' - auth_forbidden = [None, 'user'] + auth_user = "member" + auth_forbidden = [None, "user"] def test_get(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def test_post(self): - u = self.users['member'] + u = self.users["member"] - r = self.client.post(self.url, { - 'u-first_name': 'First', - 'u-last_name': 'Last', - 'p-phone': '', - # 'mailing_cof': '1', - # 'mailing_bda': '1', - # 'mailing_bda_revente': '1', - }) + r = self.client.post( + self.url, + { + "u-first_name": "First", + "u-last_name": "Last", + "p-phone": "", + # 'mailing_cof': '1', + # 'mailing_bda': '1', + # 'mailing_bda_revente': '1', + }, + ) self.assertEqual(r.status_code, 200) - expected_message = Message(messages.SUCCESS, ( - "Votre profil a été mis à jour avec succès !" - )) + expected_message = Message( + messages.SUCCESS, ("Votre profil a été mis à jour avec succès !") + ) self.assertIn(expected_message, get_messages(r.wsgi_request)) u.refresh_from_db() - self.assertEqual(u.first_name, 'First') - self.assertEqual(u.last_name, 'Last') + self.assertEqual(u.first_name, "First") + self.assertEqual(u.last_name, "Last") self.assertFalse(u.profile.mailing_cof) self.assertFalse(u.profile.mailing_bda) self.assertFalse(u.profile.mailing_bda_revente) class UtilsViewTests(ViewTestCaseMixin, TestCase): - url_name = 'utile_cof' - url_expected = '/utile_cof' + url_name = "utile_cof" + url_expected = "/utile_cof" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): r = self.client.get(self.url) @@ -405,92 +404,95 @@ class UtilsViewTests(ViewTestCaseMixin, TestCase): class MailingListDiffCof(ViewTestCaseMixin, TestCase): - url_name = 'ml_diffcof' - url_expected = '/utile_cof/diff_cof' + url_name = "ml_diffcof" + url_expected = "/utile_cof/diff_cof" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def setUp(self): super().setUp() - self.u1 = create_member('u1', attrs={'mailing_cof': True}) - self.u2 = create_member('u2', attrs={'mailing_cof': False}) - self.u3 = create_user('u3', attrs={'mailing_cof': True}) + self.u1 = create_member("u1", attrs={"mailing_cof": True}) + self.u2 = create_member("u2", attrs={"mailing_cof": False}) + self.u3 = create_user("u3", attrs={"mailing_cof": True}) def test(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['personnes'].get(), self.u1.profile) + self.assertEqual(r.context["personnes"].get(), self.u1.profile) class ConfigUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'config.edit' - url_expected = '/config' + url_name = "config.edit" + url_expected = "/config" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'root' - auth_forbidden = [None, 'user', 'member', 'staff'] + auth_user = "root" + auth_forbidden = [None, "user", "member", "staff"] def get_users_extra(self): - return { - 'root': create_root('root'), - } + return {"root": create_root("root")} def test_get(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def test_post(self): - r = self.client.post(self.url, { - 'gestion_banner': 'Announcement !', - }) + r = self.client.post(self.url, {"gestion_banner": "Announcement !"}) - self.assertRedirects(r, reverse('home')) + self.assertRedirects(r, reverse("home")) class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase): - url_name = 'cof-user-autocomplete' - url_expected = '/user/autocomplete' + url_name = "cof-user-autocomplete" + url_expected = "/user/autocomplete" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): - r = self.client.get(self.url, {'q': 'user'}) + r = self.client.get(self.url, {"q": "user"}) self.assertEqual(r.status_code, 200) class ExportMembersViewTests(ViewTestCaseMixin, TestCase): - url_name = 'cof.membres_export' - url_expected = '/export/members' + url_name = "cof.membres_export" + url_expected = "/export/members" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): - u1, u2 = self.users['member'], self.users['staff'] - u1.first_name = 'first' - u1.last_name = 'last' - u1.email = 'user@mail.net' + u1, u2 = self.users["member"], self.users["staff"] + u1.first_name = "first" + u1.last_name = "last" + u1.email = "user@mail.net" u1.save() - u1.profile.phone = '0123456789' - u1.profile.departement = 'Dept' + u1.profile.phone = "0123456789" + u1.profile.departement = "Dept" u1.profile.save() r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - data = list(csv.reader(r.content.decode('utf-8').split('\n')[:-1])) + data = list(csv.reader(r.content.decode("utf-8").split("\n")[:-1])) expected = [ [ - str(u1.pk), 'member', 'first', 'last', 'user@mail.net', - '0123456789', '1A', 'Dept', 'normalien', + str(u1.pk), + "member", + "first", + "last", + "user@mail.net", + "0123456789", + "1A", + "Dept", + "normalien", ], - [str(u2.pk), 'staff', '', '', '', '', '1A', '', 'normalien'], + [str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien"], ] # Sort before checking equality, the order of the output of csv.reader # does not seem deterministic @@ -503,34 +505,32 @@ class MegaHelpers: def setUp(self): super().setUp() - u1 = create_user('u1') - u1.first_name = 'first' - u1.last_name = 'last' - u1.email = 'user@mail.net' + u1 = create_user("u1") + u1.first_name = "first" + u1.last_name = "last" + u1.email = "user@mail.net" u1.save() - u1.profile.phone = '0123456789' - u1.profile.departement = 'Dept' - u1.profile.comments = 'profile.comments' + u1.profile.phone = "0123456789" + u1.profile.departement = "Dept" + u1.profile.comments = "profile.comments" u1.profile.save() - u2 = create_user('u2') + u2 = create_user("u2") u2.profile.save() - m = Event.objects.create(title='MEGA 2018') + m = Event.objects.create(title="MEGA 2018") - cf1 = m.commentfields.create(name='Commentaires') - cf2 = m.commentfields.create( - name='Comment Field 2', fieldtype='char', - ) + cf1 = m.commentfields.create(name="Commentaires") + cf2 = m.commentfields.create(name="Comment Field 2", fieldtype="char") - option_type = m.options.create(name='Orga ? Conscrit ?') - choice_orga = option_type.choices.create(value='Orga') - choice_conscrit = option_type.choices.create(value='Conscrit') + option_type = m.options.create(name="Orga ? Conscrit ?") + choice_orga = option_type.choices.create(value="Orga") + choice_conscrit = option_type.choices.create(value="Conscrit") mr1 = m.eventregistration_set.create(user=u1) mr1.options.add(choice_orga) - mr1.comments.create(commentfield=cf1, content='Comment 1') - mr1.comments.create(commentfield=cf2, content='Comment 2') + mr1.comments.create(commentfield=cf1, content="Comment 1") + mr1.comments.create(commentfield=cf2, content="Comment 2") mr2 = m.eventregistration_set.create(user=u2) mr2.options.add(choice_conscrit) @@ -543,96 +543,122 @@ class MegaHelpers: class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'cof.mega_export' - url_expected = '/export/mega' + url_name = "cof.mega_export" + url_expected = "/export/mega" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual(self.load_from_csv_response(r), [ + self.assertListEqual( + self.load_from_csv_response(r), [ - 'u1', 'first', 'last', 'user@mail.net', '0123456789', - str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2', + [ + "u1", + "first", + "last", + "user@mail.net", + "0123456789", + str(self.u1.pk), + "profile.comments", + "Comment 1---Comment 2", + ], + ["u2", "", "", "", "", str(self.u2.pk), "", ""], ], - ['u2', '', '', '', '', str(self.u2.pk), '', ''], - ]) + ) class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'cof.mega_export_orgas' - url_expected = '/export/mega/orgas' + url_name = "cof.mega_export_orgas" + url_expected = "/export/mega/orgas" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual(self.load_from_csv_response(r), [ + self.assertListEqual( + self.load_from_csv_response(r), [ - 'u1', 'first', 'last', 'user@mail.net', '0123456789', - str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2', + [ + "u1", + "first", + "last", + "user@mail.net", + "0123456789", + str(self.u1.pk), + "profile.comments", + "Comment 1---Comment 2", + ] ], - ]) + ) -class ExportMegaParticipantsViewTests( - MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'cof.mega_export_participants' - url_expected = '/export/mega/participants' +class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): + url_name = "cof.mega_export_participants" + url_expected = "/export/mega/participants" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual(self.load_from_csv_response(r), [ - ['u2', '', '', '', '', str(self.u2.pk), '', ''], - ]) + self.assertListEqual( + self.load_from_csv_response(r), + [["u2", "", "", "", "", str(self.u2.pk), "", ""]], + ) -class ExportMegaRemarksViewTests( - MegaHelpers, ViewTestCaseMixin, TestCase): - url_name = 'cof.mega_export_remarks' - url_expected = '/export/mega/avecremarques' +class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): + url_name = "cof.mega_export_remarks" + url_expected = "/export/mega/avecremarques" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] def test(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual(self.load_from_csv_response(r), [ + self.assertListEqual( + self.load_from_csv_response(r), [ - 'u1', 'first', 'last', 'user@mail.net', '0123456789', - str(self.u1.pk), 'profile.comments', 'Comment 1', + [ + "u1", + "first", + "last", + "user@mail.net", + "0123456789", + str(self.u1.pk), + "profile.comments", + "Comment 1", + ] ], - ]) + ) class ClubListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'liste-clubs' - url_expected = '/clubs/liste' + url_name = "liste-clubs" + url_expected = "/clubs/liste" - auth_user = 'member' - auth_forbidden = [None, 'user'] + auth_user = "member" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() - self.c1 = Club.objects.create(name='Club1') - self.c2 = Club.objects.create(name='Club2') + self.c1 = Club.objects.create(name="Club1") + self.c2 = Club.objects.create(name="Club2") - m = self.users['member'] + m = self.users["member"] self.c1.membres.add(m) self.c1.respos.add(m) @@ -640,11 +666,11 @@ class ClubListViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['owned_clubs'].get(), self.c1) - self.assertEqual(r.context['other_clubs'].get(), self.c2) + self.assertEqual(r.context["owned_clubs"].get(), self.c1) + self.assertEqual(r.context["other_clubs"].get(), self.c2) def test_as_staff(self): - u = self.users['staff'] + u = self.users["staff"] c = Client() c.force_login(u) @@ -652,32 +678,31 @@ class ClubListViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['owned_clubs'], map(repr, [self.c1, self.c2]), - ordered=False, + r.context["owned_clubs"], map(repr, [self.c1, self.c2]), ordered=False ) class ClubMembersViewTests(ViewTestCaseMixin, TestCase): - url_name = 'membres-club' + url_name = "membres-club" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] @property def url_kwargs(self): - return {'name': self.c.name} + return {"name": self.c.name} @property def url_expected(self): - return '/clubs/membres/{}'.format(self.c.name) + return "/clubs/membres/{}".format(self.c.name) def setUp(self): super().setUp() - self.u1 = create_user('u1') - self.u2 = create_user('u2') + self.u1 = create_user("u1") + self.u2 = create_user("u2") - self.c = Club.objects.create(name='Club') + self.c = Club.objects.create(name="Club") self.c.membres.add(self.u1, self.u2) self.c.respos.add(self.u1) @@ -685,10 +710,10 @@ class ClubMembersViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['members_no_respo'].get(), self.u2) + self.assertEqual(r.context["members_no_respo"].get(), self.u2) def test_as_respo(self): - u = self.users['user'] + u = self.users["user"] self.c.respos.add(u) c = Client() @@ -699,31 +724,27 @@ class ClubMembersViewTests(ViewTestCaseMixin, TestCase): class ClubChangeRespoViewTests(ViewTestCaseMixin, TestCase): - url_name = 'change-respo' + url_name = "change-respo" - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] @property def url_kwargs(self): - return {'club_name': self.c.name, 'user_id': self.users['user'].pk} + return {"club_name": self.c.name, "user_id": self.users["user"].pk} @property def url_expected(self): - return '/clubs/change_respo/{}/{}'.format( - self.c.name, self.users['user'].pk, - ) + return "/clubs/change_respo/{}/{}".format(self.c.name, self.users["user"].pk) def setUp(self): super().setUp() - self.c = Club.objects.create(name='Club') + self.c = Club.objects.create(name="Club") def test(self): - u = self.users['user'] - expected_redirect = reverse('membres-club', kwargs={ - 'name': self.c.name, - }) + u = self.users["user"] + expected_redirect = reverse("membres-club", kwargs={"name": self.c.name}) self.c.membres.add(u) r = self.client.get(self.url) @@ -735,40 +756,42 @@ class ClubChangeRespoViewTests(ViewTestCaseMixin, TestCase): class CalendarViewTests(ViewTestCaseMixin, TestCase): - url_name = 'calendar' - url_expected = '/calendar/subscription' + url_name = "calendar" + url_expected = "/calendar/subscription" - auth_user = 'member' - auth_forbidden = [None, 'user'] + auth_user = "member" + auth_forbidden = [None, "user"] post_expected_message = Message( - messages.SUCCESS, "Calendrier mis à jour avec succès.") + messages.SUCCESS, "Calendrier mis à jour avec succès." + ) def test_get(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def test_post_new(self): - r = self.client.post(self.url, { - 'subscribe_to_events': True, - 'subscribe_to_my_shows': True, - 'other_shows': [], - }) + r = self.client.post( + self.url, + { + "subscribe_to_events": True, + "subscribe_to_my_shows": True, + "other_shows": [], + }, + ) self.assertEqual(r.status_code, 200) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) - cs = self.users['member'].calendarsubscription + cs = self.users["member"].calendarsubscription self.assertTrue(cs.subscribe_to_events) self.assertTrue(cs.subscribe_to_my_shows) def test_post_edit(self): - u = self.users['member'] + u = self.users["member"] token = uuid.uuid4() cs = CalendarSubscription.objects.create(token=token, user=u) - r = self.client.post(self.url, { - 'other_shows': [], - }) + r = self.client.post(self.url, {"other_shows": []}) self.assertEqual(r.status_code, 200) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) @@ -778,34 +801,30 @@ class CalendarViewTests(ViewTestCaseMixin, TestCase): self.assertFalse(cs.subscribe_to_my_shows) def test_post_other_shows(self): - t = Tirage.objects.create( - ouverture=self.now, - fermeture=self.now, - active=True, - ) + t = Tirage.objects.create(ouverture=self.now, fermeture=self.now, active=True) location = Salle.objects.create() s = t.spectacle_set.create( - date=self.now, price=3.5, slots=20, location=location, - listing=True) + date=self.now, price=3.5, slots=20, location=location, listing=True + ) - r = self.client.post(self.url, {'other_shows': [str(s.pk)]}) + r = self.client.post(self.url, {"other_shows": [str(s.pk)]}) self.assertEqual(r.status_code, 200) class CalendarICSViewTests(ViewTestCaseMixin, TestCase): - url_name = 'calendar.ics' + url_name = "calendar.ics" auth_user = None auth_forbidden = [] @property def url_kwargs(self): - return {'token': self.token} + return {"token": self.token} @property def url_expected(self): - return '/calendar/{}/calendar.ics'.format(self.token) + return "/calendar/{}/calendar.ics".format(self.token) def setUp(self): super().setUp() @@ -813,32 +832,44 @@ class CalendarICSViewTests(ViewTestCaseMixin, TestCase): self.token = uuid.uuid4() self.t = Tirage.objects.create( - ouverture=self.now, - fermeture=self.now, - active=True, + ouverture=self.now, fermeture=self.now, active=True ) - location = Salle.objects.create(name='Location') + location = Salle.objects.create(name="Location") self.s1 = self.t.spectacle_set.create( - price=1, slots=10, location=location, listing=True, - title='Spectacle 1', date=self.now + timedelta(days=1), + price=1, + slots=10, + location=location, + listing=True, + title="Spectacle 1", + date=self.now + timedelta(days=1), ) self.s2 = self.t.spectacle_set.create( - price=2, slots=20, location=location, listing=True, - title='Spectacle 2', date=self.now + timedelta(days=2), + price=2, + slots=20, + location=location, + listing=True, + title="Spectacle 2", + date=self.now + timedelta(days=2), ) self.s3 = self.t.spectacle_set.create( - price=3, slots=30, location=location, listing=True, - title='Spectacle 3', date=self.now + timedelta(days=3), + price=3, + slots=30, + location=location, + listing=True, + title="Spectacle 3", + date=self.now + timedelta(days=3), ) def test(self): - u = self.users['user'] + u = self.users["user"] p = u.participant_set.create(tirage=self.t) p.attribution_set.create(spectacle=self.s1) self.cs = CalendarSubscription.objects.create( - user=u, token=self.token, - subscribe_to_my_shows=True, subscribe_to_events=True, + user=u, + token=self.token, + subscribe_to_my_shows=True, + subscribe_to_events=True, ) self.cs.other_shows.add(self.s2) @@ -847,91 +878,107 @@ class CalendarICSViewTests(ViewTestCaseMixin, TestCase): def get_dt_from_ical(v): return v.dt - self.assertCalEqual(r.content.decode('utf-8'), [ - { - 'summary': 'Spectacle 1', - 'dtstart': (get_dt_from_ical, ( - (self.now + timedelta(days=1)).replace(microsecond=0) - )), - 'dtend': (get_dt_from_ical, ( - (self.now + timedelta(days=1, hours=2)).replace( - microsecond=0) - )), - 'location': 'Location', - 'uid': 'show-{}-{}@example.com'.format(self.s1.pk, self.t.pk), - }, - { - 'summary': 'Spectacle 2', - 'dtstart': (get_dt_from_ical, ( - (self.now + timedelta(days=2)).replace(microsecond=0) - )), - 'dtend': (get_dt_from_ical, ( - (self.now + timedelta(days=2, hours=2)).replace( - microsecond=0) - )), - 'location': 'Location', - 'uid': 'show-{}-{}@example.com'.format(self.s2.pk, self.t.pk), - }, - ]) + self.assertCalEqual( + r.content.decode("utf-8"), + [ + { + "summary": "Spectacle 1", + "dtstart": ( + get_dt_from_ical, + ((self.now + timedelta(days=1)).replace(microsecond=0)), + ), + "dtend": ( + get_dt_from_ical, + ( + (self.now + timedelta(days=1, hours=2)).replace( + microsecond=0 + ) + ), + ), + "location": "Location", + "uid": "show-{}-{}@example.com".format(self.s1.pk, self.t.pk), + }, + { + "summary": "Spectacle 2", + "dtstart": ( + get_dt_from_ical, + ((self.now + timedelta(days=2)).replace(microsecond=0)), + ), + "dtend": ( + get_dt_from_ical, + ( + (self.now + timedelta(days=2, hours=2)).replace( + microsecond=0 + ) + ), + ), + "location": "Location", + "uid": "show-{}-{}@example.com".format(self.s2.pk, self.t.pk), + }, + ], + ) class EventViewTests(ViewTestCaseMixin, TestCase): - url_name = 'event.details' - http_methods = ['GET', 'POST'] + url_name = "event.details" + http_methods = ["GET", "POST"] - auth_user = 'user' + auth_user = "user" auth_forbidden = [None] - post_expected_message = Message(messages.SUCCESS, ( - "Votre inscription a bien été enregistrée ! Vous pouvez cependant la " - "modifier jusqu'à la fin des inscriptions." - )) + post_expected_message = Message( + messages.SUCCESS, + ( + "Votre inscription a bien été enregistrée ! Vous pouvez cependant la " + "modifier jusqu'à la fin des inscriptions." + ), + ) @property def url_kwargs(self): - return {'event_id': self.e.pk} + return {"event_id": self.e.pk} @property def url_expected(self): - return '/event/{}'.format(self.e.pk) + return "/event/{}".format(self.e.pk) def setUp(self): super().setUp() self.e = Event.objects.create() - self.ecf1 = self.e.commentfields.create(name='Comment Field 1') + self.ecf1 = self.e.commentfields.create(name="Comment Field 1") self.ecf2 = self.e.commentfields.create( - name='Comment Field 2', fieldtype='char', + name="Comment Field 2", fieldtype="char" ) - self.o1 = self.e.options.create(name='Option 1') - self.o2 = self.e.options.create(name='Option 2', multi_choices=True) + self.o1 = self.e.options.create(name="Option 1") + self.o2 = self.e.options.create(name="Option 2", multi_choices=True) - self.oc1 = self.o1.choices.create(value='O1 - Choice 1') - self.oc2 = self.o1.choices.create(value='O1 - Choice 2') - self.oc3 = self.o2.choices.create(value='O2 - Choice 1') - self.oc4 = self.o2.choices.create(value='O2 - Choice 2') + self.oc1 = self.o1.choices.create(value="O1 - Choice 1") + self.oc2 = self.o1.choices.create(value="O1 - Choice 2") + self.oc3 = self.o2.choices.create(value="O2 - Choice 1") + self.oc4 = self.o2.choices.create(value="O2 - Choice 2") def test_get(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def test_post_new(self): - r = self.client.post(self.url, { - 'option_{}'.format(self.o1.pk): [str(self.oc1.pk)], - 'option_{}'.format(self.o2.pk): [ - str(self.oc3.pk), str(self.oc4.pk), - ], - }) + r = self.client.post( + self.url, + { + "option_{}".format(self.o1.pk): [str(self.oc1.pk)], + "option_{}".format(self.o2.pk): [str(self.oc3.pk), str(self.oc4.pk)], + }, + ) self.assertEqual(r.status_code, 200) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) - er = self.e.eventregistration_set.get(user=self.users['user']) + er = self.e.eventregistration_set.get(user=self.users["user"]) self.assertQuerysetEqual( - er.options.all(), map(repr, [self.oc1, self.oc3, self.oc4]), - ordered=False, + er.options.all(), map(repr, [self.oc1, self.oc3, self.oc4]), ordered=False ) # TODO: Make the view care about comments. # self.assertQuerysetEqual( @@ -940,25 +987,23 @@ class EventViewTests(ViewTestCaseMixin, TestCase): # ) def test_post_edit(self): - er = self.e.eventregistration_set.create(user=self.users['user']) + er = self.e.eventregistration_set.create(user=self.users["user"]) er.options.add(self.oc1, self.oc3, self.oc4) - er.comments.create( - commentfield=self.ecf1, content='Comment 1', - ) + er.comments.create(commentfield=self.ecf1, content="Comment 1") - r = self.client.post(self.url, { - 'option_{}'.format(self.o1.pk): [], - 'option_{}'.format(self.o2.pk): [str(self.oc3.pk)], - }) + r = self.client.post( + self.url, + { + "option_{}".format(self.o1.pk): [], + "option_{}".format(self.o2.pk): [str(self.oc3.pk)], + }, + ) self.assertEqual(r.status_code, 200) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) er.refresh_from_db() - self.assertQuerysetEqual( - er.options.all(), map(repr, [self.oc3]), - ordered=False, - ) + self.assertQuerysetEqual(er.options.all(), map(repr, [self.oc3]), ordered=False) # TODO: Make the view care about comments. # self.assertQuerysetEqual( # er.comments.all(), map(repr, []), @@ -967,153 +1012,149 @@ class EventViewTests(ViewTestCaseMixin, TestCase): class EventStatusViewTests(ViewTestCaseMixin, TestCase): - url_name = 'event.details.status' + url_name = "event.details.status" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] @property def url_kwargs(self): - return {'event_id': self.e.pk} + return {"event_id": self.e.pk} @property def url_expected(self): - return '/event/{}/status'.format(self.e.pk) + return "/event/{}/status".format(self.e.pk) def setUp(self): super().setUp() self.e = Event.objects.create() - self.cf1 = self.e.commentfields.create(name='Comment Field 1') - self.cf2 = self.e.commentfields.create( - name='Comment Field 2', fieldtype='char', - ) + self.cf1 = self.e.commentfields.create(name="Comment Field 1") + self.cf2 = self.e.commentfields.create(name="Comment Field 2", fieldtype="char") - self.o1 = self.e.options.create(name='Option 1') - self.o2 = self.e.options.create(name='Option 2', multi_choices=True) + self.o1 = self.e.options.create(name="Option 1") + self.o2 = self.e.options.create(name="Option 2", multi_choices=True) - self.oc1 = self.o1.choices.create(value='O1 - Choice 1') - self.oc2 = self.o1.choices.create(value='O1 - Choice 2') - self.oc3 = self.o2.choices.create(value='O2 - Choice 1') - self.oc4 = self.o2.choices.create(value='O2 - Choice 2') + self.oc1 = self.o1.choices.create(value="O1 - Choice 1") + self.oc2 = self.o1.choices.create(value="O1 - Choice 2") + self.oc3 = self.o2.choices.create(value="O2 - Choice 1") + self.oc4 = self.o2.choices.create(value="O2 - Choice 2") - self.er1 = self.e.eventregistration_set.create(user=self.users['user']) + self.er1 = self.e.eventregistration_set.create(user=self.users["user"]) self.er1.options.add(self.oc1) - self.er2 = self.e.eventregistration_set.create( - user=self.users['member'], - ) + self.er2 = self.e.eventregistration_set.create(user=self.users["member"]) def _get_oc_filter_name(self, oc): - return 'option_{}_choice_{}'.format(oc.event_option.pk, oc.pk) + return "option_{}_choice_{}".format(oc.event_option.pk, oc.pk) def _test_filters(self, filters, expected): - r = self.client.post(self.url, { - self._get_oc_filter_name(oc): v for oc, v in filters - }) + r = self.client.post( + self.url, {self._get_oc_filter_name(oc): v for oc, v in filters} + ) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['user_choices'], map(repr, expected), - ordered=False, + r.context["user_choices"], map(repr, expected), ordered=False ) def test_filter_none(self): - self._test_filters([(self.oc1, 'none')], [self.er1, self.er2]) + self._test_filters([(self.oc1, "none")], [self.er1, self.er2]) def test_filter_yes(self): - self._test_filters([(self.oc1, 'yes')], [self.er1]) + self._test_filters([(self.oc1, "yes")], [self.er1]) def test_filter_no(self): - self._test_filters([(self.oc1, 'no')], [self.er2]) + self._test_filters([(self.oc1, "no")], [self.er2]) class SurveyViewTests(ViewTestCaseMixin, TestCase): - url_name = 'survey.details' - http_methods = ['GET', 'POST'] + url_name = "survey.details" + http_methods = ["GET", "POST"] - auth_user = 'user' + auth_user = "user" auth_forbidden = [None] - post_expected_message = Message(messages.SUCCESS, ( - "Votre réponse a bien été enregistrée ! Vous pouvez cependant la " - "modifier jusqu'à la fin du sondage." - )) + post_expected_message = Message( + messages.SUCCESS, + ( + "Votre réponse a bien été enregistrée ! Vous pouvez cependant la " + "modifier jusqu'à la fin du sondage." + ), + ) @property def url_kwargs(self): - return {'survey_id': self.s.pk} + return {"survey_id": self.s.pk} @property def url_expected(self): - return '/survey/{}'.format(self.s.pk) + return "/survey/{}".format(self.s.pk) def setUp(self): super().setUp() - self.s = Survey.objects.create(title='Title') + self.s = Survey.objects.create(title="Title") - self.q1 = self.s.questions.create(question='Question 1 ?') - self.q2 = self.s.questions.create( - question='Question 2 ?', - multi_answers=True, - ) + self.q1 = self.s.questions.create(question="Question 1 ?") + self.q2 = self.s.questions.create(question="Question 2 ?", multi_answers=True) - self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1') - self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2') - self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1') - self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2') + self.qa1 = self.q1.answers.create(answer="Q1 - Answer 1") + self.qa2 = self.q1.answers.create(answer="Q1 - Answer 2") + self.qa3 = self.q2.answers.create(answer="Q2 - Answer 1") + self.qa4 = self.q2.answers.create(answer="Q2 - Answer 2") def test_get(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def test_post_new(self): - r = self.client.post(self.url, { - 'question_{}'.format(self.q1.pk): [str(self.qa1.pk)], - 'question_{}'.format(self.q2.pk): [ - str(self.qa3.pk), str(self.qa4.pk), - ], - }) + r = self.client.post( + self.url, + { + "question_{}".format(self.q1.pk): [str(self.qa1.pk)], + "question_{}".format(self.q2.pk): [str(self.qa3.pk), str(self.qa4.pk)], + }, + ) self.assertEqual(r.status_code, 200) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) - a = self.s.surveyanswer_set.get(user=self.users['user']) + a = self.s.surveyanswer_set.get(user=self.users["user"]) self.assertQuerysetEqual( - a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]), - ordered=False, + a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]), ordered=False ) def test_post_edit(self): - a = self.s.surveyanswer_set.create(user=self.users['user']) + a = self.s.surveyanswer_set.create(user=self.users["user"]) a.answers.add(self.qa1, self.qa1, self.qa4) - r = self.client.post(self.url, { - 'question_{}'.format(self.q1.pk): [], - 'question_{}'.format(self.q2.pk): [str(self.qa3.pk)], - }) + r = self.client.post( + self.url, + { + "question_{}".format(self.q1.pk): [], + "question_{}".format(self.q2.pk): [str(self.qa3.pk)], + }, + ) self.assertEqual(r.status_code, 200) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) a.refresh_from_db() - self.assertQuerysetEqual( - a.answers.all(), map(repr, [self.qa3]), - ordered=False, - ) + self.assertQuerysetEqual(a.answers.all(), map(repr, [self.qa3]), ordered=False) def test_post_delete(self): - a = self.s.surveyanswer_set.create(user=self.users['user']) + a = self.s.surveyanswer_set.create(user=self.users["user"]) a.answers.add(self.qa1, self.qa4) - r = self.client.post(self.url, {'delete': '1'}) + r = self.client.post(self.url, {"delete": "1"}) self.assertEqual(r.status_code, 200) expected_message = Message( - messages.SUCCESS, "Votre réponse a bien été supprimée") + messages.SUCCESS, "Votre réponse a bien été supprimée" + ) self.assertIn(expected_message, get_messages(r.wsgi_request)) with self.assertRaises(SurveyAnswer.DoesNotExist): @@ -1137,64 +1178,60 @@ class SurveyViewTests(ViewTestCaseMixin, TestCase): class SurveyStatusViewTests(ViewTestCaseMixin, TestCase): - url_name = 'survey.details.status' + url_name = "survey.details.status" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'staff' - auth_forbidden = [None, 'user', 'member'] + auth_user = "staff" + auth_forbidden = [None, "user", "member"] @property def url_kwargs(self): - return {'survey_id': self.s.pk} + return {"survey_id": self.s.pk} @property def url_expected(self): - return '/survey/{}/status'.format(self.s.pk) + return "/survey/{}/status".format(self.s.pk) def setUp(self): super().setUp() - self.s = Survey.objects.create(title='Title') + self.s = Survey.objects.create(title="Title") - self.q1 = self.s.questions.create(question='Question 1 ?') - self.q2 = self.s.questions.create( - question='Question 2 ?', - multi_answers=True, - ) + self.q1 = self.s.questions.create(question="Question 1 ?") + self.q2 = self.s.questions.create(question="Question 2 ?", multi_answers=True) - self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1') - self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2') - self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1') - self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2') + self.qa1 = self.q1.answers.create(answer="Q1 - Answer 1") + self.qa2 = self.q1.answers.create(answer="Q1 - Answer 2") + self.qa3 = self.q2.answers.create(answer="Q2 - Answer 1") + self.qa4 = self.q2.answers.create(answer="Q2 - Answer 2") - self.a1 = self.s.surveyanswer_set.create(user=self.users['user']) + self.a1 = self.s.surveyanswer_set.create(user=self.users["user"]) self.a1.answers.add(self.qa1) - self.a2 = self.s.surveyanswer_set.create(user=self.users['member']) + self.a2 = self.s.surveyanswer_set.create(user=self.users["member"]) def test_get(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def _get_qa_filter_name(self, qa): - return 'question_{}_answer_{}'.format(qa.survey_question.pk, qa.pk) + return "question_{}_answer_{}".format(qa.survey_question.pk, qa.pk) def _test_filters(self, filters, expected): - r = self.client.post(self.url, { - self._get_qa_filter_name(qa): v for qa, v in filters - }) + r = self.client.post( + self.url, {self._get_qa_filter_name(qa): v for qa, v in filters} + ) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['user_answers'], map(repr, expected), - ordered=False, + r.context["user_answers"], map(repr, expected), ordered=False ) def test_filter_none(self): - self._test_filters([(self.qa1, 'none')], [self.a1, self.a2]) + self._test_filters([(self.qa1, "none")], [self.a1, self.a2]) def test_filter_yes(self): - self._test_filters([(self.qa1, 'yes')], [self.a1]) + self._test_filters([(self.qa1, "yes")], [self.a1]) def test_filter_no(self): - self._test_filters([(self.qa1, 'no')], [self.a2]) + self._test_filters([(self.qa1, "no")], [self.a2]) diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py index b53f2866..43f69bbc 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/testcases.py @@ -1,6 +1,6 @@ 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): @@ -18,7 +18,7 @@ class ViewTestCaseMixin(BaseViewTestCaseMixin): def get_users_base(self): return { - 'user': create_user('user'), - 'member': create_member('member'), - 'staff': create_staff('staff'), + "user": create_user("user"), + "member": create_member("member"), + "staff": create_staff("staff"), } diff --git a/gestioncof/tests/utils.py b/gestioncof/tests/utils.py index 7ba361b7..7325e350 100644 --- a/gestioncof/tests/utils.py +++ b/gestioncof/tests/utils.py @@ -7,28 +7,35 @@ def _create_user(username, is_cof=False, is_staff=False, attrs=None): if attrs is None: attrs = {} - password = attrs.pop('password', username) + password = attrs.pop("password", username) - user_keys = [ - 'first_name', 'last_name', 'email', 'is_staff', 'is_superuser', - ] + user_keys = ["first_name", "last_name", "email", "is_staff", "is_superuser"] user_attrs = {k: v for k, v in attrs.items() if k in user_keys} profile_keys = [ - 'is_cof', 'login_clipper', 'phone', 'occupation', 'departement', - 'type_cotiz', 'mailing_cof', 'mailing_bda', 'mailing_bda_revente', - 'comments', 'is_buro', 'petit_cours_accept', - 'petit_cours_remarques', + "is_cof", + "login_clipper", + "phone", + "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} if is_cof: - profile_attrs['is_cof'] = True + profile_attrs["is_cof"] = True if is_staff: # At the moment, admin is accessible by COF staff. - user_attrs['is_staff'] = True - profile_attrs['is_buro'] = True + user_attrs["is_staff"] = True + profile_attrs["is_buro"] = True user = User(username=username, **user_attrs) user.set_password(password) @@ -56,6 +63,6 @@ def create_staff(username, attrs=None): def create_root(username, attrs=None): if attrs is None: attrs = {} - attrs.setdefault('is_staff', True) - attrs.setdefault('is_superuser', True) + attrs.setdefault("is_staff", True) + attrs.setdefault("is_superuser", True) return _create_user(username, attrs=attrs) diff --git a/gestioncof/urls.py b/gestioncof/urls.py index f8ce8f6d..c4414fa5 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -1,67 +1,87 @@ 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.petits_cours_views import DemandeDetailView, DemandeListView export_patterns = [ - url(r'^members$', views.export_members, - name='cof.membres_export'), - url(r'^mega/avecremarques$', views.export_mega_remarksonly, - name='cof.mega_export_remarks'), - url(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"^members$", views.export_members, name="cof.membres_export"), + url( + r"^mega/avecremarques$", + views.export_mega_remarksonly, + name="cof.mega_export_remarks", + ), + url( + 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.+)$', views.export_mega_bytype), - url(r'^mega$', views.export_mega, - name='cof.mega_export'), + url(r"^mega$", views.export_mega, name="cof.mega_export"), ] petitcours_patterns = [ - url(r'^inscription$', petits_cours_views.inscription, - name='petits-cours-inscription'), - url(r'^demande$', petits_cours_views.demande, - name='petits-cours-demande'), - url(r'^demande-raw$', petits_cours_views.demande_raw, - name='petits-cours-demande-raw'), - url(r'^demandes$', + url( + r"^inscription$", + petits_cours_views.inscription, + name="petits-cours-inscription", + ), + url(r"^demande$", petits_cours_views.demande, name="petits-cours-demande"), + url( + r"^demande-raw$", + petits_cours_views.demande_raw, + name="petits-cours-demande-raw", + ), + url( + r"^demandes$", buro_required(DemandeListView.as_view()), - name='petits-cours-demandes-list'), - url(r'^demandes/(?P\d+)$', + name="petits-cours-demandes-list", + ), + url( + r"^demandes/(?P\d+)$", buro_required(DemandeDetailView.as_view()), - name='petits-cours-demande-details'), - url(r'^demandes/(?P\d+)/traitement$', + name="petits-cours-demande-details", + ), + url( + r"^demandes/(?P\d+)/traitement$", petits_cours_views.traitement, - name='petits-cours-demande-traitement'), - url(r'^demandes/(?P\d+)/retraitement$', + name="petits-cours-demande-traitement", + ), + url( + r"^demandes/(?P\d+)/retraitement$", petits_cours_views.retraitement, - name='petits-cours-demande-retraitement'), + name="petits-cours-demande-retraitement", + ), ] surveys_patterns = [ - url(r'^(?P\d+)/status$', views.survey_status, - name='survey.details.status'), - url(r'^(?P\d+)$', views.survey, - name='survey.details'), + url( + r"^(?P\d+)/status$", + views.survey_status, + name="survey.details.status", + ), + url(r"^(?P\d+)$", views.survey, name="survey.details"), ] events_patterns = [ - url(r'^(?P\d+)$', views.event, - name='event.details'), - url(r'^(?P\d+)/status$', views.event_status, - name='event.details.status'), + url(r"^(?P\d+)$", views.event, name="event.details"), + url(r"^(?P\d+)/status$", views.event_status, name="event.details.status"), ] calendar_patterns = [ - url(r'^subscription$', views.calendar, - name='calendar'), - url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics, - name='calendar.ics'), + url(r"^subscription$", views.calendar, name="calendar"), + url( + r"^(?P[a-z0-9-]+)/calendar.ics$", views.calendar_ics, name="calendar.ics" + ), ] clubs_patterns = [ - url(r'^membres/(?P\w+)', views.membres_club, name='membres-club'), - url(r'^liste', views.liste_clubs, name='liste-clubs'), - url(r'^change_respo/(?P\w+)/(?P\d+)', - views.change_respo, name='change-respo'), + url(r"^membres/(?P\w+)", views.membres_club, name="membres-club"), + url(r"^liste", views.liste_clubs, name="liste-clubs"), + url( + r"^change_respo/(?P\w+)/(?P\d+)", + views.change_respo, + name="change-respo", + ), ] diff --git a/gestioncof/views.py b/gestioncof/views.py index d77794bb..618fb24a 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -1,59 +1,74 @@ -import unicodecsv import uuid from datetime import timedelta -from icalendar import Calendar, Event as Vevent -from custommail.shortcuts import send_custom_mail -from django.shortcuts import redirect, get_object_or_404, render -from django.http import Http404, HttpResponse, HttpResponseForbidden +import unicodecsv +from custommail.shortcuts import send_custom_mail +from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User from django.contrib.auth.views import ( - login as django_login_view, logout as django_logout_view, + login as django_login_view, + logout as django_logout_view, redirect_to_login, ) -from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core.urlresolvers import reverse_lazy -from django.views.generic import FormView +from django.http import Http404, HttpResponse, HttpResponseForbidden +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.contrib import messages - +from django.views.generic import FormView from django_cas_ng.views import logout as cas_logout_view +from icalendar import Calendar, Event as Vevent -from utils.views.autocomplete import Select2QuerySetView - -from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ - SurveyQuestionAnswer -from gestioncof.models import Event, EventRegistration, EventOption, \ - EventOptionChoice -from gestioncof.models import EventCommentField, EventCommentValue, \ - CalendarSubscription -from gestioncof.models import CofProfile, Club +from bda.models import Spectacle, Tirage from gestioncof.decorators import buro_required, cof_required from gestioncof.forms import ( - UserForm, ProfileForm, - EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm, - RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm, - EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm + CalendarForm, + ClubsForm, + EventForm, + EventFormset, + EventStatusFilterForm, + GestioncofConfigForm, + ProfileForm, + RegistrationPassUserForm, + RegistrationProfileForm, + RegistrationUserForm, + SurveyForm, + SurveyStatusFilterForm, + UserForm, ) - -from bda.models import Tirage, Spectacle +from gestioncof.models import ( + CalendarSubscription, + Club, + CofProfile, + Event, + EventCommentField, + EventCommentValue, + EventOption, + EventOptionChoice, + EventRegistration, + Survey, + SurveyAnswer, + SurveyQuestion, + SurveyQuestionAnswer, +) +from utils.views.autocomplete import Select2QuerySetView @login_required def home(request): - data = {"surveys": Survey.objects.filter(old=False).all(), - "events": Event.objects.filter(old=False).all(), - "open_surveys": - Survey.objects.filter(survey_open=True, old=False).all(), - "open_events": - Event.objects.filter(registration_open=True, old=False).all(), - "active_tirages": Tirage.objects.filter(active=True).all(), - "open_tirages": - Tirage.objects.filter(active=True, - ouverture__lte=timezone.now()).all(), - "now": timezone.now()} + data = { + "surveys": Survey.objects.filter(old=False).all(), + "events": Event.objects.filter(old=False).all(), + "open_surveys": Survey.objects.filter(survey_open=True, old=False).all(), + "open_events": Event.objects.filter(registration_open=True, old=False).all(), + "active_tirages": Tirage.objects.filter(active=True).all(), + "open_tirages": Tirage.objects.filter( + active=True, ouverture__lte=timezone.now() + ).all(), + "now": timezone.now(), + } return render(request, "home.html", data) @@ -61,8 +76,8 @@ def login(request): if request.user.is_authenticated: return redirect("home") context = {} - if request.method == "GET" and 'next' in request.GET: - context['next'] = request.GET['next'] + if request.method == "GET" and "next" in request.GET: + context["next"] = request.GET["next"] return render(request, "login_switch.html", context) @@ -73,34 +88,33 @@ def login_ext(request): if not user.has_usable_password() or user.password in ("", "!"): profile, created = CofProfile.objects.get_or_create(user=user) if profile.login_clipper: - return render(request, "error.html", - {"error_type": "use_clipper_login"}) + return render( + request, "error.html", {"error_type": "use_clipper_login"} + ) else: - return render(request, "error.html", - {"error_type": "no_password"}) + return render(request, "error.html", {"error_type": "no_password"}) except User.DoesNotExist: pass context = {} - if request.method == "GET" and 'next' in request.GET: - context['next'] = request.GET['next'] - if request.method == "POST" and 'next' in request.POST: - context['next'] = request.POST['next'] - return django_login_view(request, template_name='login.html', - extra_context=context) + if request.method == "GET" and "next" in request.GET: + context["next"] = request.GET["next"] + if request.method == "POST" and "next" in request.POST: + context["next"] = request.POST["next"] + return django_login_view(request, template_name="login.html", extra_context=context) @login_required def logout(request, next_page=None): if next_page is None: - next_page = request.GET.get('next', None) + next_page = request.GET.get("next", None) - profile = getattr(request.user, 'profile', None) + profile = getattr(request.user, "profile", None) if profile and profile.login_clipper: - msg = _('Déconnexion de GestioCOF et CAS réussie. À bientôt {}.') + msg = _("Déconnexion de GestioCOF et CAS réussie. À bientôt {}.") logout_view = cas_logout_view else: - msg = _('Déconnexion de GestioCOF réussie. À bientôt {}.') + msg = _("Déconnexion de GestioCOF réussie. À bientôt {}.") logout_view = django_logout_view messages.success(request, msg.format(request.user.get_short_name())) @@ -110,8 +124,7 @@ def logout(request, next_page=None): @login_required def survey(request, survey_id): survey = get_object_or_404( - Survey.objects.prefetch_related('questions', 'questions__answers'), - id=survey_id, + Survey.objects.prefetch_related("questions", "questions__answers"), id=survey_id ) if not survey.survey_open or survey.old: raise Http404 @@ -119,10 +132,11 @@ def survey(request, survey_id): deleted = False if request.method == "POST": form = SurveyForm(request.POST, survey=survey) - if request.POST.get('delete'): + if request.POST.get("delete"): try: - current_answer = SurveyAnswer.objects.get(user=request.user, - survey=survey) + current_answer = SurveyAnswer.objects.get( + user=request.user, survey=survey + ) current_answer.delete() current_answer = None except SurveyAnswer.DoesNotExist: @@ -134,9 +148,9 @@ def survey(request, survey_id): if form.is_valid(): all_answers = [] for question_id, answers_ids in form.answers(): - question = get_object_or_404(SurveyQuestion, - id=question_id, - survey=survey) + question = get_object_or_404( + SurveyQuestion, id=question_id, survey=survey + ) if type(answers_ids) != list: answers_ids = [answers_ids] if not question.multi_answers and len(answers_ids) > 1: @@ -146,50 +160,48 @@ def survey(request, survey_id): continue answer_id = int(answer_id) answer = SurveyQuestionAnswer.objects.get( - id=answer_id, - survey_question=question) + id=answer_id, survey_question=question + ) all_answers.append(answer) try: current_answer = SurveyAnswer.objects.get( - user=request.user, survey=survey) + user=request.user, survey=survey + ) except SurveyAnswer.DoesNotExist: - current_answer = SurveyAnswer(user=request.user, - survey=survey) + current_answer = SurveyAnswer(user=request.user, survey=survey) current_answer.save() current_answer.answers = all_answers current_answer.save() success = True else: try: - current_answer = SurveyAnswer.objects.get(user=request.user, - survey=survey) - form = SurveyForm(survey=survey, - current_answers=current_answer.answers) + current_answer = SurveyAnswer.objects.get(user=request.user, survey=survey) + form = SurveyForm(survey=survey, current_answers=current_answer.answers) except SurveyAnswer.DoesNotExist: current_answer = None form = SurveyForm(survey=survey) # Messages if success: if deleted: - messages.success(request, - "Votre réponse a bien été supprimée") + messages.success(request, "Votre réponse a bien été supprimée") else: - messages.success(request, - "Votre réponse a bien été enregistrée ! Vous " - "pouvez cependant la modifier jusqu'à la fin " - "du sondage.") - return render(request, "gestioncof/survey.html", { - "survey": survey, - "form": form, - "current_answer": current_answer - }) + messages.success( + request, + "Votre réponse a bien été enregistrée ! Vous " + "pouvez cependant la modifier jusqu'à la fin " + "du sondage.", + ) + return render( + request, + "gestioncof/survey.html", + {"survey": survey, "form": form, "current_answer": current_answer}, + ) def get_event_form_choices(event, form): all_choices = [] for option_id, choices_ids in form.choices(): - option = get_object_or_404(EventOption, id=option_id, - event=event) + option = get_object_or_404(EventOption, id=option_id, event=event) if type(choices_ids) != list: choices_ids = [choices_ids] if not option.multi_choices and len(choices_ids) > 1: @@ -198,22 +210,19 @@ def get_event_form_choices(event, form): if not choice_id: continue choice_id = int(choice_id) - choice = EventOptionChoice.objects.get( - id=choice_id, - event_option=option) + choice = EventOptionChoice.objects.get(id=choice_id, event_option=option) all_choices.append(choice) return all_choices def update_event_form_comments(event, form, registration): for commentfield_id, value in form.comments(): - field = get_object_or_404(EventCommentField, id=commentfield_id, - event=event) + field = get_object_or_404(EventCommentField, id=commentfield_id, event=event) if value == field.default: continue (storage, _) = EventCommentValue.objects.get_or_create( - commentfield=field, - registration=registration) + commentfield=field, registration=registration + ) storage.content = value storage.save() @@ -228,27 +237,29 @@ def event(request, event_id): form = EventForm(request.POST, event=event) if form.is_valid(): all_choices = get_event_form_choices(event, form) - (current_registration, _) = \ - EventRegistration.objects.get_or_create(user=request.user, - event=event) + (current_registration, _) = EventRegistration.objects.get_or_create( + user=request.user, event=event + ) current_registration.options = all_choices current_registration.save() success = True else: try: - current_registration = \ - EventRegistration.objects.get(user=request.user, event=event) - form = EventForm(event=event, - current_choices=current_registration.options) + current_registration = EventRegistration.objects.get( + user=request.user, event=event + ) + form = EventForm(event=event, current_choices=current_registration.options) except EventRegistration.DoesNotExist: form = EventForm(event=event) # Messages if success: - messages.success(request, "Votre inscription a bien été enregistrée ! " - "Vous pouvez cependant la modifier jusqu'à " - "la fin des inscriptions.") - return render(request, "gestioncof/event.html", - {"event": event, "form": form}) + messages.success( + request, + "Votre inscription a bien été enregistrée ! " + "Vous pouvez cependant la modifier jusqu'à " + "la fin des inscriptions.", + ) + return render(request, "gestioncof/event.html", {"event": event, "form": form}) def clean_post_for_status(initial): @@ -272,19 +283,21 @@ def event_status(request, event_id): if value == "yes": registrations_query = registrations_query.filter(paid=True) elif value == "no": - registrations_query = registrations_query.filter( - paid=False) + registrations_query = registrations_query.filter(paid=False) continue - choice = get_object_or_404(EventOptionChoice, id=choice_id, - event_option__id=option_id) + choice = get_object_or_404( + EventOptionChoice, id=choice_id, event_option__id=option_id + ) if value == "none": continue if value == "yes": registrations_query = registrations_query.filter( - options__id__exact=choice.id) + options__id__exact=choice.id + ) elif value == "no": registrations_query = registrations_query.exclude( - options__id__exact=choice.id) + options__id__exact=choice.id + ) user_choices = registrations_query.prefetch_related("user").all() options = EventOption.objects.filter(event=event).all() choices_count = {} @@ -294,10 +307,17 @@ def event_status(request, event_id): for user_choice in user_choices: for choice in user_choice.options.all(): choices_count[choice.id] += 1 - return render(request, "event_status.html", - {"event": event, "user_choices": user_choices, - "options": options, "choices_count": choices_count, - "form": form}) + return render( + request, + "event_status.html", + { + "event": event, + "user_choices": user_choices, + "options": options, + "choices_count": choices_count, + "form": form, + }, + ) @buro_required @@ -308,16 +328,15 @@ def survey_status(request, survey_id): form = SurveyStatusFilterForm(post_data or None, survey=survey) if form.is_valid(): for question_id, answer_id, value in form.filters(): - answer = get_object_or_404(SurveyQuestionAnswer, id=answer_id, - survey_question__id=question_id) + answer = get_object_or_404( + SurveyQuestionAnswer, id=answer_id, survey_question__id=question_id + ) if value == "none": continue if value == "yes": - answers_query = answers_query.filter( - answers__id__exact=answer.id) + answers_query = answers_query.filter(answers__id__exact=answer.id) elif value == "no": - answers_query = answers_query.exclude( - answers__id__exact=answer.id) + answers_query = answers_query.exclude(answers__id__exact=answer.id) user_answers = answers_query.prefetch_related("user").all() questions = SurveyQuestion.objects.filter(survey=survey).all() answers_count = {} @@ -327,10 +346,17 @@ def survey_status(request, survey_id): for user_answer in user_answers: for answer in user_answer.answers.all(): answers_count[answer.id] += 1 - return render(request, "survey_status.html", - {"survey": survey, "user_answers": user_answers, - "questions": questions, "answers_count": answers_count, - "form": form}) + return render( + request, + "survey_status.html", + { + "survey": survey, + "user_answers": user_answers, + "questions": questions, + "answers_count": answers_count, + "form": form, + }, + ) @cof_required @@ -343,22 +369,18 @@ def profile(request): if user_form.is_valid() and profile_form.is_valid(): user_form.save() profile_form.save() - messages.success( - request, - _("Votre profil a été mis à jour avec succès !") - ) + messages.success(request, _("Votre profil a été mis à jour avec succès !")) context = {"user_form": user_form, "profile_form": profile_form} return render(request, "gestioncof/profile.html", context) def registration_set_ro_fields(user_form, profile_form): - user_form.fields['username'].widget.attrs['readonly'] = True - profile_form.fields['login_clipper'].widget.attrs['readonly'] = True + user_form.fields["username"].widget.attrs["readonly"] = True + profile_form.fields["login_clipper"].widget.attrs["readonly"] = True @buro_required -def registration_form2(request, login_clipper=None, username=None, - fullname=None): +def registration_form2(request, login_clipper=None, username=None, fullname=None): events = Event.objects.filter(old=False).all() member = None if login_clipper: @@ -369,20 +391,24 @@ def registration_form2(request, login_clipper=None, username=None, except User.DoesNotExist: # new user, but prefill # user - user_form = RegistrationUserForm(initial={ - 'username': login_clipper, - 'email': "%s@clipper.ens.fr" % login_clipper}) + user_form = RegistrationUserForm( + initial={ + "username": login_clipper, + "email": "%s@clipper.ens.fr" % login_clipper, + } + ) if fullname: bits = fullname.split(" ") - user_form.fields['first_name'].initial = bits[0] + user_form.fields["first_name"].initial = bits[0] if len(bits) > 1: - user_form.fields['last_name'].initial = " ".join(bits[1:]) + user_form.fields["last_name"].initial = " ".join(bits[1:]) # profile - profile_form = RegistrationProfileForm(initial={ - 'login_clipper': login_clipper}) + profile_form = RegistrationProfileForm( + initial={"login_clipper": login_clipper} + ) registration_set_ro_fields(user_form, profile_form) # events & clubs - event_formset = EventFormset(events=events, prefix='events') + event_formset = EventFormset(events=events, prefix="events") clubs_form = ClubsForm() if username: member = get_object_or_404(User, username=username) @@ -396,26 +422,33 @@ def registration_form2(request, login_clipper=None, username=None, for event in events: try: current_registrations.append( - EventRegistration.objects.get(user=member, event=event)) + EventRegistration.objects.get(user=member, event=event) + ) except EventRegistration.DoesNotExist: current_registrations.append(None) event_formset = EventFormset( - events=events, prefix='events', - current_registrations=current_registrations) + events=events, prefix="events", current_registrations=current_registrations + ) # Clubs - clubs_form = ClubsForm(initial={'clubs': member.clubs.all()}) + clubs_form = ClubsForm(initial={"clubs": member.clubs.all()}) elif not login_clipper: # new user user_form = RegistrationPassUserForm() profile_form = RegistrationProfileForm() - event_formset = EventFormset(events=events, prefix='events') + event_formset = EventFormset(events=events, prefix="events") clubs_form = ClubsForm() - return render(request, "gestioncof/registration_form.html", - {"member": member, "login_clipper": login_clipper, - "user_form": user_form, - "profile_form": profile_form, - "event_formset": event_formset, - "clubs_form": clubs_form}) + return render( + request, + "gestioncof/registration_form.html", + { + "member": member, + "login_clipper": login_clipper, + "user_form": user_form, + "profile_form": profile_form, + "event_formset": event_formset, + "clubs_form": clubs_form, + }, + ) @buro_required @@ -429,15 +462,14 @@ def registration(request): # Remplissage des formulaires # ----- - if 'password1' in request_dict or 'password2' in request_dict: + if "password1" in request_dict or "password2" in request_dict: user_form = RegistrationPassUserForm(request_dict) else: user_form = RegistrationUserForm(request_dict) profile_form = RegistrationProfileForm(request_dict) clubs_form = ClubsForm(request_dict) events = Event.objects.filter(old=False).all() - event_formset = EventFormset(events=events, data=request_dict, - prefix='events') + event_formset = EventFormset(events=events, data=request_dict, prefix="events") if "user_exists" in request_dict and request_dict["user_exists"]: username = request_dict["username"] try: @@ -459,38 +491,44 @@ def registration(request): profile, _ = CofProfile.objects.get_or_create(user=member) was_cof = profile.is_cof # Maintenant on remplit le formulaire de profil - profile_form = RegistrationProfileForm(request_dict, - instance=profile) - if (profile_form.is_valid() and event_formset.is_valid() - and clubs_form.is_valid()): + profile_form = RegistrationProfileForm(request_dict, instance=profile) + if ( + profile_form.is_valid() + and event_formset.is_valid() + and clubs_form.is_valid() + ): # Enregistrement du profil profile = profile_form.save() if profile.is_cof and not was_cof: send_custom_mail( - "welcome", "cof@ens.fr", [member.email], - context={'member': member}, + "welcome", + "cof@ens.fr", + [member.email], + context={"member": member}, ) # Enregistrement des inscriptions aux événements for form in event_formset: - if 'status' not in form.cleaned_data: - form.cleaned_data['status'] = 'no' - if form.cleaned_data['status'] == 'no': + if "status" not in form.cleaned_data: + form.cleaned_data["status"] = "no" + if form.cleaned_data["status"] == "no": try: - current_registration = EventRegistration.objects \ - .get(user=member, event=form.event) + current_registration = EventRegistration.objects.get( + user=member, event=form.event + ) current_registration.delete() except EventRegistration.DoesNotExist: pass continue all_choices = get_event_form_choices(form.event, form) - (current_registration, created_reg) = \ - EventRegistration.objects.get_or_create( - user=member, event=form.event) - update_event_form_comments(form.event, form, - current_registration) + ( + current_registration, + created_reg, + ) = EventRegistration.objects.get_or_create( + user=member, event=form.event + ) + update_event_form_comments(form.event, form, current_registration) current_registration.options = all_choices - current_registration.paid = \ - (form.cleaned_data['status'] == 'paid') + current_registration.paid = form.cleaned_data["status"] == "paid" current_registration.save() # if form.event.title == "Mega 15" and created_reg: # field = EventCommentField.objects.get( @@ -508,7 +546,7 @@ def registration(request): # send_custom_mail(...) # Enregistrement des inscriptions aux clubs member.clubs.clear() - for club in clubs_form.cleaned_data['clubs']: + for club in clubs_form.cleaned_data["clubs"]: club.membres.add(member) club.save() @@ -516,20 +554,29 @@ def registration(request): # Success # --- - msg = ("L'inscription de {:s} ({:s}) a été " - "enregistrée avec succès." - .format(member.get_full_name(), member.email)) + msg = ( + "L'inscription de {:s} ({:s}) a été " + "enregistrée avec succès.".format( + member.get_full_name(), member.email + ) + ) if profile.is_cof: msg += "\nIl est désormais membre du COF n°{:d} !".format( - member.profile.id) - messages.success(request, msg, extra_tags='safe') - return render(request, "gestioncof/registration_post.html", - {"user_form": user_form, - "profile_form": profile_form, - "member": member, - "login_clipper": login_clipper, - "event_formset": event_formset, - "clubs_form": clubs_form}) + member.profile.id + ) + messages.success(request, msg, extra_tags="safe") + return render( + request, + "gestioncof/registration_post.html", + { + "user_form": user_form, + "profile_form": profile_form, + "member": member, + "login_clipper": login_clipper, + "event_formset": event_formset, + "clubs_form": clubs_form, + }, + ) else: return render(request, "registration.html") @@ -545,13 +592,14 @@ def membres_club(request, name): # ou respo du club. user = request.user club = get_object_or_404(Club, name=name) - if not request.user.profile.is_buro \ - and club not in user.clubs_geres.all(): - return HttpResponseForbidden('

    Permission denied

    ') + if not request.user.profile.is_buro and club not in user.clubs_geres.all(): + return HttpResponseForbidden("

    Permission denied

    ") members_no_respo = club.membres.exclude(clubs_geres=club).all() - return render(request, 'membres_clubs.html', - {'club': club, - 'members_no_respo': members_no_respo}) + return render( + request, + "membres_clubs.html", + {"club": club, "members_no_respo": members_no_respo}, + ) @buro_required @@ -564,31 +612,41 @@ def change_respo(request, club_name, user_id): club.respos.add(user) else: raise Http404 - return redirect('membres-club', name=club_name) + return redirect("membres-club", name=club_name) @cof_required def liste_clubs(request): clubs = Club.objects if request.user.profile.is_buro: - data = {'owned_clubs': clubs.all()} + data = {"owned_clubs": clubs.all()} else: - data = {'owned_clubs': request.user.clubs_geres.all(), - 'other_clubs': clubs.exclude(respos=request.user)} - return render(request, 'liste_clubs.html', data) + data = { + "owned_clubs": request.user.clubs_geres.all(), + "other_clubs": clubs.exclude(respos=request.user), + } + return render(request, "liste_clubs.html", data) @buro_required def export_members(request): - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=membres_cof.csv' + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = "attachment; filename=membres_cof.csv" writer = unicodecsv.writer(response) for profile in CofProfile.objects.filter(is_cof=True).all(): user = profile.user - bits = [user.id, user.username, user.first_name, user.last_name, - user.email, profile.phone, profile.occupation, - profile.departement, profile.type_cotiz] + bits = [ + user.id, + user.username, + user.first_name, + user.last_name, + user.email, + profile.phone, + profile.occupation, + profile.departement, + profile.type_cotiz, + ] writer.writerow([str(bit) for bit in bits]) return response @@ -608,18 +666,24 @@ MEGA_ORGA = "Orga" def csv_export_mega(filename, qs): - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=' + filename + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = "attachment; filename=" + filename writer = unicodecsv.writer(response) for reg in qs.all(): user = reg.user profile = user.profile - comments = "---".join( - [comment.content for comment in reg.comments.all()]) - bits = [user.username, user.first_name, user.last_name, user.email, - profile.phone, user.id, - profile.comments if profile.comments else "", comments] + comments = "---".join([comment.content for comment in reg.comments.all()]) + bits = [ + user.username, + user.first_name, + user.last_name, + user.email, + profile.phone, + user.id, + profile.comments if profile.comments else "", + comments, + ] writer.writerow([str(bit) for bit in bits]) @@ -628,9 +692,9 @@ def csv_export_mega(filename, qs): @buro_required def export_mega_remarksonly(request): - filename = 'remarques_mega_{}.csv'.format(MEGA_YEAR) - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=' + filename + filename = "remarques_mega_{}.csv".format(MEGA_YEAR) + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = "attachment; filename=" + filename writer = unicodecsv.writer(response) event = Event.objects.get(title=MEGA_EVENT_NAME) @@ -639,8 +703,16 @@ def export_mega_remarksonly(request): reg = val.registration user = reg.user profile = user.profile - bits = [user.username, user.first_name, user.last_name, user.email, - profile.phone, profile.id, profile.comments, val.content] + bits = [ + user.username, + user.first_name, + user.last_name, + user.email, + profile.phone, + profile.id, + profile.comments, + val.content, + ] writer.writerow([str(bit) for bit in bits]) return response @@ -672,7 +744,7 @@ def export_mega_orgas(request): qs = EventRegistration.objects.filter(event=event).filter( options__id=participant_type ) - return csv_export_mega('orgas_mega_{}.csv'.format(MEGA_YEAR), qs) + return csv_export_mega("orgas_mega_{}.csv".format(MEGA_YEAR), qs) @buro_required @@ -683,15 +755,15 @@ def export_mega_participants(request): qs = EventRegistration.objects.filter(event=event).filter( options__id=participant_type ) - return csv_export_mega('conscrits_mega_{}.csv'.format(MEGA_YEAR), qs) + return csv_export_mega("conscrits_mega_{}.csv".format(MEGA_YEAR), qs) @buro_required def export_mega(request): event = Event.objects.filter(title=MEGA_EVENT_NAME) - qs = EventRegistration.objects.filter(event=event) \ - .order_by("user__username") - return csv_export_mega('all_mega_{}.csv'.format(MEGA_YEAR), qs) + qs = EventRegistration.objects.filter(event=event).order_by("user__username") + return csv_export_mega("all_mega_{}.csv".format(MEGA_YEAR), qs) + # ------------------------------ # Fin des exports Mega hardcodés @@ -706,32 +778,28 @@ def utile_cof(request): @buro_required def utile_bda(request): tirages = Tirage.objects.all() - return render(request, "utile_bda.html", {'tirages': tirages}) + return render(request, "utile_bda.html", {"tirages": tirages}) @buro_required def liste_bdadiff(request): titre = "BdA diffusion" personnes = CofProfile.objects.filter(mailing_bda=True, is_cof=True).all() - return render(request, "liste_mails.html", - {"titre": titre, "personnes": personnes}) + return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) @buro_required def liste_bdarevente(request): titre = "BdA revente" - personnes = CofProfile.objects.filter(mailing_bda_revente=True, - is_cof=True).all() - return render(request, "liste_mails.html", {"titre": titre, - "personnes": personnes}) + personnes = CofProfile.objects.filter(mailing_bda_revente=True, is_cof=True).all() + return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) @buro_required def liste_diffcof(request): titre = "Diffusion COF" personnes = CofProfile.objects.filter(mailing_cof=True, is_cof=True).all() - return render(request, "liste_mails.html", {"titre": titre, - "personnes": personnes}) + return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) @cof_required @@ -740,7 +808,7 @@ def calendar(request): instance = CalendarSubscription.objects.get(user=request.user) except CalendarSubscription.DoesNotExist: instance = None - if request.method == 'POST': + if request.method == "POST": form = CalendarForm(request.POST, instance=instance) if form.is_valid(): subscription = form.save(commit=False) @@ -749,19 +817,26 @@ def calendar(request): subscription.token = uuid.uuid4() subscription.save() form.save_m2m() - messages.success(request, - "Calendrier mis à jour avec succès.") - return render(request, "gestioncof/calendar_subscription.html", - {'form': form, - 'token': str(subscription.token)}) + messages.success(request, "Calendrier mis à jour avec succès.") + return render( + request, + "gestioncof/calendar_subscription.html", + {"form": form, "token": str(subscription.token)}, + ) else: messages.error(request, "Formulaire incorrect.") - return render(request, "gestioncof/calendar_subscription.html", - {'form': form}) + return render( + request, "gestioncof/calendar_subscription.html", {"form": form} + ) else: - return render(request, "gestioncof/calendar_subscription.html", - {'form': CalendarForm(instance=instance), - 'token': instance.token if instance else None}) + return render( + request, + "gestioncof/calendar_subscription.html", + { + "form": CalendarForm(instance=instance), + "token": instance.token if instance else None, + }, + ) def calendar_ics(request, token): @@ -769,33 +844,33 @@ def calendar_ics(request, token): shows = subscription.other_shows.all() if subscription.subscribe_to_my_shows: shows |= Spectacle.objects.filter( - attribues__participant__user=subscription.user, - tirage__active=True) + attribues__participant__user=subscription.user, tirage__active=True + ) shows = shows.distinct() vcal = Calendar() site = Site.objects.get_current() for show in shows: vevent = Vevent() - vevent.add('dtstart', show.date) - vevent.add('dtend', show.date + timedelta(seconds=7200)) - vevent.add('summary', show.title) - vevent.add('location', show.location.name) - vevent.add('uid', 'show-{:d}-{:d}@{:s}'.format( - show.pk, show.tirage_id, site.domain)) + vevent.add("dtstart", show.date) + vevent.add("dtend", show.date + timedelta(seconds=7200)) + vevent.add("summary", show.title) + vevent.add("location", show.location.name) + vevent.add( + "uid", "show-{:d}-{:d}@{:s}".format(show.pk, show.tirage_id, site.domain) + ) vcal.add_component(vevent) if subscription.subscribe_to_events: for event in Event.objects.filter(old=False).all(): vevent = Vevent() - vevent.add('dtstart', event.start_date) - vevent.add('dtend', event.end_date) - vevent.add('summary', event.title) - vevent.add('location', event.location) - vevent.add('description', event.description) - vevent.add('uid', 'event-{:d}@{:s}'.format( - event.pk, site.domain)) + vevent.add("dtstart", event.start_date) + vevent.add("dtend", event.end_date) + vevent.add("summary", event.title) + vevent.add("location", event.location) + vevent.add("description", event.description) + vevent.add("uid", "event-{:d}@{:s}".format(event.pk, site.domain)) vcal.add_component(vevent) response = HttpResponse(content=vcal.to_ical()) - response['Content-Type'] = "text/calendar" + response["Content-Type"] = "text/calendar" return response @@ -823,7 +898,7 @@ class ConfigUpdate(FormView): class UserAutocomplete(Select2QuerySetView): model = User - search_fields = ('username', 'first_name', 'last_name') + search_fields = ("username", "first_name", "last_name") user_autocomplete = buro_required(UserAutocomplete.as_view()) diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index 906f7b15..49125896 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -1,5 +1,5 @@ -from django.forms.widgets import Widget from django.forms.utils import flatatt +from django.forms.widgets import Widget from django.utils.safestring import mark_safe @@ -13,8 +13,8 @@ class TriStateCheckbox(Widget): def render(self, name, value, attrs=None, choices=()): if value is None: - value = 'none' - attrs['value'] = value + value = "none" + attrs["value"] = value final_attrs = self.build_attrs(self.attrs, attrs) - output = ["" % flatatt(final_attrs)] - return mark_safe('\n'.join(output)) + output = ['' % flatatt(final_attrs)] + return mark_safe("\n".join(output)) diff --git a/kfet/__init__.py b/kfet/__init__.py index 5d6c8f97..42ea33b1 100644 --- a/kfet/__init__.py +++ b/kfet/__init__.py @@ -1 +1 @@ -default_app_config = 'kfet.apps.KFetConfig' +default_app_config = "kfet.apps.KFetConfig" diff --git a/kfet/apps.py b/kfet/apps.py index 7a6c97a2..f3c7b07b 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig class KFetConfig(AppConfig): - name = 'kfet' + name = "kfet" verbose_name = "Application K-Fêt" def ready(self): @@ -11,4 +11,5 @@ class KFetConfig(AppConfig): def register_config(self): import djconfig from kfet.forms import KFetConfigForm + djconfig.register(KFetConfigForm) diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py index 00926030..ef2486a7 100644 --- a/kfet/auth/__init__.py +++ b/kfet/auth/__init__.py @@ -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_TRIGRAMME = 'GNR' +KFET_GENERIC_USERNAME = "kfet_genericteam" +KFET_GENERIC_TRIGRAMME = "GNR" diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py index d91931f5..5b4fe7fd 100644 --- a/kfet/auth/apps.py +++ b/kfet/auth/apps.py @@ -4,11 +4,12 @@ from django.utils.translation import ugettext_lazy as _ class KFetAuthConfig(AppConfig): - name = 'kfet.auth' - label = 'kfetauth' + name = "kfet.auth" + label = "kfetauth" verbose_name = _("K-Fêt - Authentification et Autorisation") def ready(self): from . import signals # noqa from .utils import setup_kfet_generic_user + post_migrate.connect(setup_kfet_generic_user, sender=self) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index d8ef3001..55e18458 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model + from kfet.models import Account, GenericTeamToken from .utils import get_kfet_generic_user @@ -12,11 +13,7 @@ class BaseKFetBackend: Add extra select related up to Account. """ try: - return ( - User.objects - .select_related('profile__account_kfet') - .get(pk=user_id) - ) + return User.objects.select_related("profile__account_kfet").get(pk=user_id) except User.DoesNotExist: return None diff --git a/kfet/auth/context_processors.py b/kfet/auth/context_processors.py index 7b59b88b..183cc56a 100644 --- a/kfet/auth/context_processors.py +++ b/kfet/auth/context_processors.py @@ -2,9 +2,6 @@ from django.contrib.auth.context_processors import PermWrapper def temporary_auth(request): - if hasattr(request, 'real_user'): - return { - 'user': request.real_user, - 'perms': PermWrapper(request.real_user), - } + if hasattr(request, "real_user"): + return {"user": request.real_user, "perms": PermWrapper(request.real_user)} return {} diff --git a/kfet/auth/fields.py b/kfet/auth/fields.py index 28ba1c9e..a5544787 100644 --- a/kfet/auth/fields.py +++ b/kfet/auth/fields.py @@ -5,15 +5,12 @@ from django.forms import widgets class KFetPermissionsField(forms.ModelMultipleChoiceField): - def __init__(self, *args, **kwargs): queryset = Permission.objects.filter( - content_type__in=ContentType.objects.filter(app_label="kfet"), + content_type__in=ContentType.objects.filter(app_label="kfet") ) super().__init__( - queryset=queryset, - widget=widgets.CheckboxSelectMultiple, - *args, **kwargs + queryset=queryset, widget=widgets.CheckboxSelectMultiple, *args, **kwargs ) def label_from_instance(self, obj): diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py index 876e8814..b1628af0 100644 --- a/kfet/auth/forms.py +++ b/kfet/auth/forms.py @@ -8,11 +8,11 @@ class GroupForm(forms.ModelForm): permissions = KFetPermissionsField() def clean_name(self): - name = self.cleaned_data['name'] - return 'K-Fêt %s' % name + name = self.cleaned_data["name"] + return "K-Fêt %s" % name 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 # used. # other_groups = self.instance.permissions.difference( @@ -21,28 +21,29 @@ class GroupForm(forms.ModelForm): if self.instance.pk is None: return kfet_perms other_perms = self.instance.permissions.exclude( - pk__in=[p.pk for p in self.fields['permissions'].queryset], + pk__in=[p.pk for p in self.fields["permissions"].queryset] ) return list(kfet_perms) + list(other_perms) class Meta: model = Group - fields = ['name', 'permissions'] + fields = ["name", "permissions"] class UserGroupForm(forms.ModelForm): groups = forms.ModelMultipleChoiceField( - Group.objects.filter(name__icontains='K-Fêt'), - label='Statut équipe', - required=False) + Group.objects.filter(name__icontains="K-Fêt"), + label="Statut équipe", + required=False, + ) def clean_groups(self): - kfet_groups = self.cleaned_data.get('groups') + kfet_groups = self.cleaned_data.get("groups") if self.instance.pk is None: return kfet_groups - other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') + other_groups = self.instance.groups.exclude(name__icontains="K-Fêt") return list(kfet_groups) + list(other_groups) class Meta: model = User - fields = ['groups'] + fields = ["groups"] diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 2f3bd33b..43a920e1 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -12,21 +12,19 @@ class TemporaryAuthMiddleware: values from CofProfile and Account of this user. """ + def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if request.user.is_authenticated: # avoid multiple db accesses in views and templates - request.user = ( - User.objects - .select_related('profile__account_kfet') - .get(pk=request.user.pk) + request.user = User.objects.select_related("profile__account_kfet").get( + pk=request.user.pk ) temp_request_user = AccountBackend().authenticate( - request, - kfet_password=self.get_kfet_password(request), + request, kfet_password=self.get_kfet_password(request) ) if temp_request_user: @@ -36,7 +34,4 @@ class TemporaryAuthMiddleware: return self.get_response(request) def get_kfet_password(self, request): - return ( - request.META.get('HTTP_KFETPASSWORD') or - request.POST.get('KFETPASSWORD') - ) + return request.META.get("HTTP_KFETPASSWORD") or request.POST.get("KFETPASSWORD") diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py index 061570a8..caf5d786 100644 --- a/kfet/auth/migrations/0001_initial.py +++ b/kfet/auth/migrations/0001_initial.py @@ -7,18 +7,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('auth', '0006_require_contenttypes_0002'), + ("auth", "0006_require_contenttypes_0002"), # Following dependency allows using Account model to set up the kfet # generic user in post_migrate receiver. - ('kfet', '0058_delete_genericteamtoken'), + ("kfet", "0058_delete_genericteamtoken"), ] operations = [ migrations.CreateModel( - name='GenericTeamToken', + name="GenericTeamToken", fields=[ - ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), - ('token', models.CharField(unique=True, max_length=50)), + ( + "id", + models.AutoField( + verbose_name="ID", + auto_created=True, + serialize=False, + primary_key=True, + ), + ), + ("token", models.CharField(unique=True, max_length=50)), ], - ), + ) ] diff --git a/kfet/auth/models.py b/kfet/auth/models.py index ecd40091..73a70c22 100644 --- a/kfet/auth/models.py +++ b/kfet/auth/models.py @@ -3,7 +3,6 @@ from django.utils.crypto import get_random_string class GenericTeamTokenManager(models.Manager): - def create_token(self): token = get_random_string(50) while self.filter(token=token).exists(): diff --git a/kfet/auth/signals.py b/kfet/auth/signals.py index 3d7af18b..c13cde09 100644 --- a/kfet/auth/signals.py +++ b/kfet/auth/signals.py @@ -19,22 +19,26 @@ def suggest_auth_generic(sender, request, user, **kwargs): - logged in user is a kfet staff member (except the generic user). """ # Filter against the next page. - if not(hasattr(request, 'GET') and 'next' in request.GET): + if not (hasattr(request, "GET") and "next" in request.GET): return - next_page = request.GET['next'] - generic_url = reverse('kfet.login.generic') + next_page = request.GET["next"] + generic_url = reverse("kfet.login.generic") - if not('k-fet' in next_page and not next_page.startswith(generic_url)): + if not ("k-fet" in next_page and not next_page.startswith(generic_url)): return # Filter against the logged in user. - if not(user.has_perm('kfet.is_team') and user != get_kfet_generic_user()): + if not (user.has_perm("kfet.is_team") and user != get_kfet_generic_user()): return # Seems legit to add message. text = _("K-Fêt — Ouvrir une session partagée ?") - messages.info(request, mark_safe( - '{}' - .format(generic_url, text) - )) + messages.info( + request, + mark_safe( + '{}'.format( + generic_url, text + ) + ), + ) diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 62f870e8..e4330eef 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -1,8 +1,8 @@ from unittest import mock +from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.core import signing from django.core.urlresolvers import reverse -from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.test import RequestFactory, TestCase from kfet.forms import UserGroupForm @@ -15,11 +15,11 @@ from .models import GenericTeamToken from .utils import get_kfet_generic_user from .views import GenericLoginView - ## # Forms ## + class UserGroupFormTests(TestCase): """Test suite for UserGroupForm.""" @@ -31,8 +31,7 @@ class UserGroupFormTests(TestCase): prefix_name = "K-Fêt " names = ["Group 1", "Group 2", "Group 3"] self.kfet_groups = [ - Group.objects.create(name=prefix_name+name) - for name in names + Group.objects.create(name=prefix_name + name) for name in names ] # create a non-K-Fêt group @@ -41,11 +40,9 @@ class UserGroupFormTests(TestCase): def test_choices(self): """Only K-Fêt groups are selectable.""" form = UserGroupForm(instance=self.user) - groups_field = form.fields['groups'] + groups_field = form.fields["groups"] self.assertQuerysetEqual( - groups_field.queryset, - [repr(g) for g in self.kfet_groups], - ordered=False, + groups_field.queryset, [repr(g) for g in self.kfet_groups], ordered=False ) def test_keep_others(self): @@ -56,9 +53,7 @@ class UserGroupFormTests(TestCase): user.groups.add(self.other_group) # add user to some K-Fêt groups through UserGroupForm - data = { - 'groups': [group.pk for group in self.kfet_groups], - } + data = {"groups": [group.pk for group in self.kfet_groups]} form = UserGroupForm(data, instance=user) form.is_valid() @@ -71,7 +66,6 @@ class UserGroupFormTests(TestCase): class KFetGenericUserTests(TestCase): - def test_exists(self): """ The account is set up when app is ready, so it should exist. @@ -86,44 +80,39 @@ class KFetGenericUserTests(TestCase): # Backends ## -class AccountBackendTests(TestCase): +class AccountBackendTests(TestCase): def setUp(self): - self.request = RequestFactory().get('/') + self.request = RequestFactory().get("/") def test_valid(self): - acc = Account(trigramme='000') - acc.change_pwd('valid') - acc.save({'username': 'user'}) + acc = Account(trigramme="000") + acc.change_pwd("valid") + acc.save({"username": "user"}) - auth = AccountBackend().authenticate( - self.request, kfet_password='valid') + auth = AccountBackend().authenticate(self.request, kfet_password="valid") self.assertEqual(auth, acc.user) def test_invalid(self): - auth = AccountBackend().authenticate( - self.request, kfet_password='invalid') + auth = AccountBackend().authenticate(self.request, kfet_password="invalid") self.assertIsNone(auth) class GenericBackendTests(TestCase): - def setUp(self): - self.request = RequestFactory().get('/') + self.request = RequestFactory().get("/") def test_valid(self): token = GenericTeamToken.objects.create_token() - auth = GenericBackend().authenticate( - self.request, kfet_token=token.token) + auth = GenericBackend().authenticate(self.request, kfet_token=token.token) self.assertEqual(auth, get_kfet_generic_user()) self.assertEqual(GenericTeamToken.objects.all().count(), 0) def test_invalid(self): - auth = GenericBackend().authenticate( - self.request, kfet_token='invalid') + auth = GenericBackend().authenticate(self.request, kfet_token="invalid") self.assertIsNone(auth) @@ -131,78 +120,74 @@ class GenericBackendTests(TestCase): # Views ## -class GenericLoginViewTests(TestCase): +class GenericLoginViewTests(TestCase): def setUp(self): - patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) - user_acc = Account(trigramme='000') - user_acc.save({'username': 'user'}) + user_acc = Account(trigramme="000") + user_acc.save({"username": "user"}) self.user = user_acc.user - self.user.set_password('user') + self.user.set_password("user") self.user.save() - team_acc = Account(trigramme='100') - team_acc.save({'username': 'team'}) + team_acc = Account(trigramme="100") + team_acc.save({"username": "team"}) self.team = team_acc.user - self.team.set_password('team') + self.team.set_password("team") self.team.save() self.team.user_permissions.add( - Permission.objects.get( - content_type__app_label='kfet', codename='is_team'), + Permission.objects.get(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() 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): """ 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) 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): """ POST request without token in COOKIES sets a token and redirects to logout url. """ - self.client.login(username='team', password='team') + self.client.login(username="team", password="team") r = self.client.post(self.url) self.assertRedirects( - r, '/logout?next={}'.format(self.url), - fetch_redirect_response=False, + r, "/logout?next={}".format(self.url), fetch_redirect_response=False ) def test_notoken_not_team(self): """ Logged in user must be a team user to initiate login as generic user. """ - self.client.login(username='user', password='user') + self.client.login(username="user", password="user") # With GET. r = self.client.get(self.url) self.assertRedirects( - r, '/login?next={}'.format(self.url), - fetch_redirect_response=False, + r, "/login?next={}".format(self.url), fetch_redirect_response=False ) # Also with POST. r = self.client.post(self.url) self.assertRedirects( - r, '/login?next={}'.format(self.url), - fetch_redirect_response=False, + r, "/login?next={}".format(self.url), fetch_redirect_response=False ) def _set_signed_cookie(self, client, key, value): @@ -216,10 +201,9 @@ class GenericLoginViewTests(TestCase): try: cookie = client.cookies[key] # It also can be emptied. - self.assertEqual(cookie.value, '') - self.assertEqual( - cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT') - self.assertEqual(cookie['max-age'], 0) + self.assertEqual(cookie.value, "") + self.assertEqual(cookie["expires"], "Thu, 01-Jan-1970 00:00:00 GMT") + self.assertEqual(cookie["max-age"], 0) except AssertionError: raise AssertionError("The cookie '%s' still exists." % key) @@ -227,16 +211,16 @@ class GenericLoginViewTests(TestCase): """ The kfet generic user is logged in. """ - token = GenericTeamToken.objects.create(token='valid') + token = GenericTeamToken.objects.create(token="valid") self._set_signed_cookie( - self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'valid') + self.client, GenericLoginView.TOKEN_COOKIE_NAME, "valid" + ) 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._is_cookie_deleted( - self.client, GenericLoginView.TOKEN_COOKIE_NAME) + self._is_cookie_deleted(self.client, GenericLoginView.TOKEN_COOKIE_NAME) with self.assertRaises(GenericTeamToken.DoesNotExist): token.refresh_from_db() @@ -245,27 +229,26 @@ class GenericLoginViewTests(TestCase): If token is invalid, delete it and try again. """ self._set_signed_cookie( - self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'invalid') + self.client, GenericLoginView.TOKEN_COOKIE_NAME, "invalid" + ) r = self.client.get(self.url) self.assertRedirects(r, self.url, fetch_redirect_response=False) self.assertEqual(r.wsgi_request.user, AnonymousUser()) - self._is_cookie_deleted( - self.client, GenericLoginView.TOKEN_COOKIE_NAME) + self._is_cookie_deleted(self.client, GenericLoginView.TOKEN_COOKIE_NAME) def test_flow_ok(self): """ A team user is logged in as the kfet generic user. """ - self.client.login(username='team', password='team') - next_url = '/k-fet/' + self.client.login(username="team", password="team") + next_url = "/k-fet/" - r = self.client.post( - '{}?next={}'.format(self.url, next_url), follow=True) + r = self.client.post("{}?next={}".format(self.url, next_url), follow=True) self.assertEqual(r.wsgi_request.user, self.generic_user) - self.assertEqual(r.wsgi_request.path, '/k-fet/') + self.assertEqual(r.wsgi_request.path, "/k-fet/") ## @@ -276,10 +259,10 @@ class GenericLoginViewTests(TestCase): # - temporary_auth context processor ## -class TemporaryAuthTests(TestCase): +class TemporaryAuthTests(TestCase): def setUp(self): - patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) @@ -287,22 +270,23 @@ class TemporaryAuthTests(TestCase): self.middleware = TemporaryAuthMiddleware(mock.Mock()) - user1_acc = Account(trigramme='000') - user1_acc.change_pwd('kfet_user1') - user1_acc.save({'username': 'user1'}) + user1_acc = Account(trigramme="000") + user1_acc.change_pwd("kfet_user1") + user1_acc.save({"username": "user1"}) self.user1 = user1_acc.user - self.user1.set_password('user1') + self.user1.set_password("user1") self.user1.save() - user2_acc = Account(trigramme='100') - user2_acc.change_pwd('kfet_user2') - user2_acc.save({'username': 'user2'}) + user2_acc = Account(trigramme="100") + user2_acc.change_pwd("kfet_user2") + user2_acc.save({"username": "user2"}) self.user2 = user2_acc.user - self.user2.set_password('user2') + self.user2.set_password("user2") self.user2.save() 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) def test_middleware_header(self): @@ -310,7 +294,7 @@ class TemporaryAuthTests(TestCase): A user can be authenticated if ``HTTP_KFETPASSWORD`` header of a request contains a valid kfet password. """ - request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') + request = self.factory.get("/", HTTP_KFETPASSWORD="kfet_user2") request.user = self.user1 self.middleware(request) @@ -323,7 +307,7 @@ class TemporaryAuthTests(TestCase): A user can be authenticated if ``KFETPASSWORD`` of POST data contains a valid kfet password. """ - request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) + request = self.factory.post("/", {"KFETPASSWORD": "kfet_user2"}) request.user = self.user1 self.middleware(request) @@ -335,34 +319,33 @@ class TemporaryAuthTests(TestCase): """ 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 self.middleware(request) self.assertEqual(request.user, self.user1) - self.assertFalse(hasattr(request, 'real_user')) + self.assertFalse(hasattr(request, "real_user")) def test_context_processor(self): """ Context variables give the real authenticated user and his permissions. """ - self.client.login(username='user1', password='user1') + 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.assertNotIn('kfet.is_team', r.context['perms']) + self.assertEqual(r.context["user"], self.user1) + self.assertNotIn("kfet.is_team", r.context["perms"]) def test_auth_not_persistent(self): """ The authentication is temporary, i.e. for one request. """ - self.client.login(username='user1', password='user1') + self.client.login(username="user1", password="user1") - r1 = self.client.get( - '/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + r1 = self.client.get("/k-fet/accounts/", HTTP_KFETPASSWORD="kfet_user2") self.assertEqual(r1.wsgi_request.user, self.user2) - r2 = self.client.get('/k-fet/accounts/') + r2 = self.client.get("/k-fet/accounts/") self.assertEqual(r2.wsgi_request.user, self.user1) diff --git a/kfet/auth/utils.py b/kfet/auth/utils.py index 0edc555d..ed63bb67 100644 --- a/kfet/auth/utils.py +++ b/kfet/auth/utils.py @@ -23,12 +23,9 @@ def setup_kfet_generic_user(**kwargs): """ generic = get_kfet_generic_user() generic.user_permissions.add( - Permission.objects.get( - content_type__app_label='kfet', - codename='is_team', - ) + Permission.objects.get(content_type__app_label="kfet", codename="is_team") ) def hash_password(password): - return hashlib.sha256(password.encode('utf-8')).hexdigest() + return hashlib.sha256(password.encode("utf-8")).hexdigest() diff --git a/kfet/auth/views.py b/kfet/auth/views.py index 7b9f4099..35a6eedf 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -1,17 +1,17 @@ from django.contrib import messages -from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import Group, User from django.contrib.auth.views import redirect_to_login +from django.contrib.messages.views import SuccessMessageMixin from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Prefetch from django.http import QueryDict from django.shortcuts import redirect, render from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ -from django.views.generic import View from django.views.decorators.http import require_http_methods +from django.views.generic import View from django.views.generic.edit import CreateView, UpdateView from .forms import GroupForm @@ -30,28 +30,33 @@ class GenericLoginView(View): provider, which can be external. Session is unusable as it will be cleared on logout. """ - TOKEN_COOKIE_NAME = 'kfettoken' - @method_decorator(require_http_methods(['GET', 'POST'])) + TOKEN_COOKIE_NAME = "kfettoken" + + @method_decorator(require_http_methods(["GET", "POST"])) def dispatch(self, request, *args, **kwargs): token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) if not token: - if not request.user.has_perm('kfet.is_team'): + if not request.user.has_perm("kfet.is_team"): return redirect_to_login(request.get_full_path()) - if request.method == 'POST': + if request.method == "POST": # Step 1: set token and logout user. return self.prepare_auth() else: # GET request should not change server/client states. Send a # confirmation template to emit a POST request. - return render(request, 'kfet/confirm_form.html', { - 'title': _("Ouvrir une session partagée"), - 'text': _( - "Êtes-vous sûr·e de vouloir ouvrir une session " - "partagée ?" - ), - }) + return render( + request, + "kfet/confirm_form.html", + { + "title": _("Ouvrir une session partagée"), + "text": _( + "Êtes-vous sûr·e de vouloir ouvrir une session " + "partagée ?" + ), + }, + ) else: # Step 2: validate token. return self.validate_auth(token) @@ -62,20 +67,19 @@ class GenericLoginView(View): # Prepare callback of logout. here_url = reverse(login_generic) - if 'next' in self.request.GET: + if "next" in self.request.GET: # Keep given next page. here_qd = QueryDict(mutable=True) - here_qd['next'] = self.request.GET['next'] - here_url += '?{}'.format(here_qd.urlencode()) + here_qd["next"] = self.request.GET["next"] + here_url += "?{}".format(here_qd.urlencode()) - logout_url = reverse('cof-logout') + logout_url = reverse("cof-logout") logout_qd = QueryDict(mutable=True) - logout_qd['next'] = here_url - logout_url += '?{}'.format(logout_qd.urlencode(safe='/')) + logout_qd["next"] = here_url + logout_url += "?{}".format(logout_qd.urlencode(safe="/")) resp = redirect(logout_url) - resp.set_signed_cookie( - self.TOKEN_COOKIE_NAME, token.token, httponly=True) + resp.set_signed_cookie(self.TOKEN_COOKIE_NAME, token.token, httponly=True) return resp def validate_auth(self, token): @@ -85,9 +89,9 @@ class GenericLoginView(View): if user: # Log in generic user. login(self.request, user) - messages.success(self.request, _( - "K-Fêt — Ouverture d'une session partagée." - )) + messages.success( + self.request, _("K-Fêt — Ouverture d'une session partagée.") + ) resp = redirect(self.get_next_url()) else: # Try again. @@ -98,39 +102,34 @@ class GenericLoginView(View): return resp 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() -@permission_required('kfet.manage_perms') +@permission_required("kfet.manage_perms") def account_group(request): user_pre = Prefetch( - 'user_set', - queryset=User.objects.select_related('profile__account_kfet'), + "user_set", queryset=User.objects.select_related("profile__account_kfet") ) - groups = ( - Group.objects - .filter(name__icontains='K-Fêt') - .prefetch_related('permissions', user_pre) + groups = Group.objects.filter(name__icontains="K-Fêt").prefetch_related( + "permissions", user_pre ) - return render(request, 'kfet/account_group.html', { - 'groups': groups, - }) + return render(request, "kfet/account_group.html", {"groups": groups}) class AccountGroupCreate(SuccessMessageMixin, CreateView): model = Group - template_name = 'kfet/account_group_form.html' + template_name = "kfet/account_group_form.html" form_class = GroupForm - success_message = 'Nouveau groupe : %(name)s' - success_url = reverse_lazy('kfet.account.group') + success_message = "Nouveau groupe : %(name)s" + success_url = reverse_lazy("kfet.account.group") class AccountGroupUpdate(SuccessMessageMixin, UpdateView): - queryset = Group.objects.filter(name__icontains='K-Fêt') - template_name = 'kfet/account_group_form.html' + queryset = Group.objects.filter(name__icontains="K-Fêt") + template_name = "kfet/account_group_form.html" form_class = GroupForm - success_message = 'Groupe modifié : %(name)s' - success_url = reverse_lazy('kfet.account.group') + success_message = "Groupe modifié : %(name)s" + success_url = reverse_lazy("kfet.account.group") diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index 0d1904d6..d7448194 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -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.db.models import Q +from django.http import Http404 +from django.shortcuts import render +from ldap3 import Connection from gestioncof.models import User from kfet.decorators import teamkfet_required @@ -25,81 +25,80 @@ def account_create(request): raise Http404 q = request.GET.get("q") - if (len(q) == 0): + if len(q) == 0: return render(request, "kfet/account_create_autocomplete.html") - data = {'q': q} + data = {"q": q} queries = {} search_words = q.split() # Fetching data from User, CofProfile and Account tables - queries['kfet'] = Account.objects - queries['users_cof'] = User.objects.filter(profile__is_cof=True) - queries['users_notcof'] = User.objects.filter(profile__is_cof=False) + queries["kfet"] = Account.objects + queries["users_cof"] = User.objects.filter(profile__is_cof=True) + queries["users_notcof"] = User.objects.filter(profile__is_cof=False) for word in search_words: - queries['kfet'] = queries['kfet'].filter( + queries["kfet"] = queries["kfet"].filter( Q(cofprofile__user__username__icontains=word) | Q(cofprofile__user__first_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(first_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(first_name__icontains=word) | Q(last_name__icontains=word) ) # Clearing redundancies - queries['kfet'] = queries['kfet'].distinct() + queries["kfet"] = queries["kfet"].distinct() usernames = set( - queries['kfet'].values_list('cofprofile__user__username', flat=True)) - queries['kfet'] = [ - (account, account.cofprofile.user) - for account in queries['kfet'] + queries["kfet"].values_list("cofprofile__user__username", flat=True) + ) + queries["kfet"] = [ + (account, account.cofprofile.user) for account in queries["kfet"] ] - queries['users_cof'] = \ - queries['users_cof'].exclude(username__in=usernames).distinct() - queries['users_notcof'] = \ - queries['users_notcof'].exclude(username__in=usernames).distinct() - usernames |= set( - queries['users_cof'].values_list('username', flat=True)) - usernames |= set( - queries['users_notcof'].values_list('username', flat=True)) + queries["users_cof"] = ( + queries["users_cof"].exclude(username__in=usernames).distinct() + ) + queries["users_notcof"] = ( + queries["users_notcof"].exclude(username__in=usernames).distinct() + ) + usernames |= set(queries["users_cof"].values_list("username", flat=True)) + usernames |= set(queries["users_notcof"].values_list("username", flat=True)) # Fetching data from the SPI - if getattr(settings, 'LDAP_SERVER_URL', None): + if getattr(settings, "LDAP_SERVER_URL", None): # Fetching - ldap_query = '(&{:s})'.format(''.join( - '(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=word) - for word in search_words if word.isalnum() - )) + ldap_query = "(&{:s})".format( + "".join( + "(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=word) + for word in search_words + if word.isalnum() + ) + ) if ldap_query != "(&)": # If none of the bits were legal, we do not perform the query entries = None with Connection(settings.LDAP_SERVER_URL) as conn: - conn.search( - 'dc=spi,dc=ens,dc=fr', ldap_query, - attributes=['uid', 'cn'] - ) + conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"]) entries = conn.entries # Clearing redundancies - queries['clippers'] = [ + queries["clippers"] = [ Clipper(entry.uid.value, entry.cn.value) for entry in entries - if entry.uid.value - and entry.uid.value not in usernames + if entry.uid.value and entry.uid.value not in usernames ] # Resulting data data.update(queries) - data['options'] = sum([len(query) for query in queries]) + data["options"] = sum([len(query) for query in queries]) return render(request, "kfet/account_create_autocomplete.html", data) @@ -111,17 +110,19 @@ def account_search(request): q = request.GET.get("q") words = q.split() - data = {'q': q} + data = {"q": q} for word in words: query = Account.objects.filter( - Q(cofprofile__user__username__icontains=word) | - Q(cofprofile__user__first_name__icontains=word) | - Q(cofprofile__user__last_name__icontains=word) - ).distinct() + Q(cofprofile__user__username__icontains=word) + | Q(cofprofile__user__first_name__icontains=word) + | Q(cofprofile__user__last_name__icontains=word) + ).distinct() - query = [(account.trigramme, account.cofprofile.user.get_full_name()) - for account in query] + query = [ + (account.trigramme, account.cofprofile.user.get_full_name()) + for account in query + ] - data['accounts'] = query - return render(request, 'kfet/account_search_autocomplete.html', data) + data["accounts"] = query + return render(request, "kfet/account_search_autocomplete.html", data) diff --git a/kfet/cms/__init__.py b/kfet/cms/__init__.py index 0f6cab45..f6aabddc 100644 --- a/kfet/cms/__init__.py +++ b/kfet/cms/__init__.py @@ -1 +1 @@ -default_app_config = 'kfet.cms.apps.KFetCMSAppConfig' +default_app_config = "kfet.cms.apps.KFetCMSAppConfig" diff --git a/kfet/cms/apps.py b/kfet/cms/apps.py index 1db0e043..d4928ac6 100644 --- a/kfet/cms/apps.py +++ b/kfet/cms/apps.py @@ -2,9 +2,9 @@ from django.apps import AppConfig class KFetCMSAppConfig(AppConfig): - name = 'kfet.cms' - label = 'kfetcms' - verbose_name = 'CMS K-Fêt' + name = "kfet.cms" + label = "kfetcms" + verbose_name = "CMS K-Fêt" def ready(self): from . import hooks diff --git a/kfet/cms/context_processors.py b/kfet/cms/context_processors.py index 34f175d1..700170ee 100644 --- a/kfet/cms/context_processors.py +++ b/kfet/cms/context_processors.py @@ -3,18 +3,14 @@ from kfet.models import Article def get_articles(request=None): articles = ( - Article.objects - .filter(is_sold=True, hidden=False) - .select_related('category') - .order_by('category__name', 'name') + Article.objects.filter(is_sold=True, hidden=False) + .select_related("category") + .order_by("category__name", "name") ) pressions, others = [], [] for article in articles: - if article.category.name == 'Pression': + if article.category.name == "Pression": pressions.append(article) else: others.append(article) - return { - 'pressions': pressions, - 'articles': others, - } + return {"pressions": pressions, "articles": others} diff --git a/kfet/cms/hooks.py b/kfet/cms/hooks.py index e58aeef5..a3070cfb 100644 --- a/kfet/cms/hooks.py +++ b/kfet/cms/hooks.py @@ -1,12 +1,10 @@ from django.contrib.staticfiles.templatetags.staticfiles import static from django.utils.html import format_html - from wagtail.wagtailcore import hooks -@hooks.register('insert_editor_css') +@hooks.register("insert_editor_css") def editor_css(): return format_html( - '', - static('kfetcms/css/editor.css'), + '', static("kfetcms/css/editor.css") ) diff --git a/kfet/cms/management/commands/kfet_loadwagtail.py b/kfet/cms/management/commands/kfet_loadwagtail.py index 86b94d3e..566cca43 100644 --- a/kfet/cms/management/commands/kfet_loadwagtail.py +++ b/kfet/cms/management/commands/kfet_loadwagtail.py @@ -1,7 +1,6 @@ from django.contrib.auth.models import Group from django.core.management import call_command from django.core.management.base import BaseCommand - from wagtail.wagtailcore.models import Page, Site @@ -9,7 +8,7 @@ class Command(BaseCommand): help = "Importe des données pour Wagtail" def add_arguments(self, parser): - parser.add_argument('--file', default='kfet_wagtail_17_05') + parser.add_argument("--file", default="kfet_wagtail_17_05") def handle(self, *args, **options): @@ -18,12 +17,10 @@ class Command(BaseCommand): # Nettoyage des données initiales posées par Wagtail dans la migration # wagtailcore/0002 - Group.objects.filter(name__in=('Moderators', 'Editors')).delete() + Group.objects.filter(name__in=("Moderators", "Editors")).delete() try: - homepage = Page.objects.get( - title="Welcome to your new Wagtail site!" - ) + homepage = Page.objects.get(title="Welcome to your new Wagtail site!") homepage.delete() Site.objects.filter(root_page=homepage).delete() except Page.DoesNotExist: @@ -32,4 +29,4 @@ class Command(BaseCommand): # Import des données # Par défaut, il s'agit d'une copie du site K-Fêt (17-05) - call_command('loaddata', options['file']) + call_command("loaddata", options["file"]) diff --git a/kfet/cms/migrations/0001_initial.py b/kfet/cms/migrations/0001_initial.py index ed0b0948..8c075903 100644 --- a/kfet/cms/migrations/0001_initial.py +++ b/kfet/cms/migrations/0001_initial.py @@ -1,49 +1,214 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models -import wagtail.wagtailsnippets.blocks +import django.db.models.deletion import wagtail.wagtailcore.blocks import wagtail.wagtailcore.fields -import django.db.models.deletion +import wagtail.wagtailsnippets.blocks +from django.db import migrations, models + import kfet.cms.models class Migration(migrations.Migration): dependencies = [ - ('wagtailcore', '0033_remove_golive_expiry_help_text'), - ('wagtailimages', '0019_delete_filter'), + ("wagtailcore", "0033_remove_golive_expiry_help_text"), + ("wagtailimages", "0019_delete_filter"), ] operations = [ migrations.CreateModel( - name='KFetPage', + name="KFetPage", fields=[ - ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page', on_delete=models.CASCADE)), - ('no_header', models.BooleanField(verbose_name='Sans en-tête', help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", default=False)), - ('content', wagtail.wagtailcore.fields.StreamField((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses'))))), ('group', wagtail.wagtailcore.blocks.StreamBlock((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses')))))), label='Contenu groupé'))), verbose_name='Contenu')), - ('layout', models.CharField(max_length=255, choices=[('kfet/base_col_1.html', 'Une colonne : centrée sur la page'), ('kfet/base_col_2.html', 'Deux colonnes : fixe à gauche, contenu à droite'), ('kfet/base_col_mult.html', 'Contenu scindé sur plusieurs colonnes')], help_text='Comment cette page devrait être affichée ?', verbose_name='Template', default='kfet/base_col_mult.html')), - ('main_size', models.CharField(max_length=255, blank=True, verbose_name='Taille de la colonne de contenu')), - ('col_count', models.CharField(max_length=255, blank=True, verbose_name='Nombre de colonnes', help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes")), + ( + "page_ptr", + models.OneToOneField( + serialize=False, + primary_key=True, + parent_link=True, + auto_created=True, + to="wagtailcore.Page", + on_delete=models.CASCADE, + ), + ), + ( + "no_header", + models.BooleanField( + verbose_name="Sans en-tête", + help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", + default=False, + ), + ), + ( + "content", + wagtail.wagtailcore.fields.StreamField( + ( + ( + "rich", + wagtail.wagtailcore.blocks.RichTextBlock( + label="Éditeur" + ), + ), + ("carte", kfet.cms.models.MenuBlock()), + ( + "group_team", + wagtail.wagtailcore.blocks.StructBlock( + ( + ( + "show_only", + wagtail.wagtailcore.blocks.IntegerBlock( + help_text="Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.", + required=False, + label="Montrer seulement", + ), + ), + ( + "members", + wagtail.wagtailcore.blocks.ListBlock( + wagtail.wagtailsnippets.blocks.SnippetChooserBlock( + kfet.cms.models.MemberTeam + ), + classname="team-group", + label="K-Fêt-eux-ses", + ), + ), + ) + ), + ), + ( + "group", + wagtail.wagtailcore.blocks.StreamBlock( + ( + ( + "rich", + wagtail.wagtailcore.blocks.RichTextBlock( + label="Éditeur" + ), + ), + ("carte", kfet.cms.models.MenuBlock()), + ( + "group_team", + wagtail.wagtailcore.blocks.StructBlock( + ( + ( + "show_only", + wagtail.wagtailcore.blocks.IntegerBlock( + help_text="Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.", + required=False, + label="Montrer seulement", + ), + ), + ( + "members", + wagtail.wagtailcore.blocks.ListBlock( + wagtail.wagtailsnippets.blocks.SnippetChooserBlock( + kfet.cms.models.MemberTeam + ), + classname="team-group", + label="K-Fêt-eux-ses", + ), + ), + ) + ), + ), + ), + label="Contenu groupé", + ), + ), + ), + verbose_name="Contenu", + ), + ), + ( + "layout", + models.CharField( + max_length=255, + choices=[ + ( + "kfet/base_col_1.html", + "Une colonne : centrée sur la page", + ), + ( + "kfet/base_col_2.html", + "Deux colonnes : fixe à gauche, contenu à droite", + ), + ( + "kfet/base_col_mult.html", + "Contenu scindé sur plusieurs colonnes", + ), + ], + help_text="Comment cette page devrait être affichée ?", + verbose_name="Template", + default="kfet/base_col_mult.html", + ), + ), + ( + "main_size", + models.CharField( + max_length=255, + blank=True, + verbose_name="Taille de la colonne de contenu", + ), + ), + ( + "col_count", + models.CharField( + max_length=255, + blank=True, + verbose_name="Nombre de colonnes", + help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes", + ), + ), ], options={ - 'verbose_name': 'page K-Fêt', - 'verbose_name_plural': 'pages K-Fêt', + "verbose_name": "page K-Fêt", + "verbose_name_plural": "pages K-Fêt", }, - bases=('wagtailcore.page',), + bases=("wagtailcore.page",), ), migrations.CreateModel( - name='MemberTeam', + name="MemberTeam", fields=[ - ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), - ('first_name', models.CharField(blank=True, max_length=255, verbose_name='Prénom', default='')), - ('last_name', models.CharField(blank=True, max_length=255, verbose_name='Nom', default='')), - ('nick_name', models.CharField(verbose_name='Alias', blank=True, default='', max_length=255)), - ('photo', models.ForeignKey(null=True, related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='Photo', blank=True, to='wagtailimages.Image')), + ( + "id", + models.AutoField( + verbose_name="ID", + auto_created=True, + serialize=False, + primary_key=True, + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=255, verbose_name="Prénom", default="" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=255, verbose_name="Nom", default="" + ), + ), + ( + "nick_name", + models.CharField( + verbose_name="Alias", blank=True, default="", max_length=255 + ), + ), + ( + "photo", + models.ForeignKey( + null=True, + related_name="+", + on_delete=django.db.models.deletion.SET_NULL, + verbose_name="Photo", + blank=True, + to="wagtailimages.Image", + ), + ), ], - options={ - 'verbose_name': 'K-Fêt-eux-se', - }, + options={"verbose_name": "K-Fêt-eux-se"}, ), ] diff --git a/kfet/cms/migrations/0002_alter_kfetpage_colcount.py b/kfet/cms/migrations/0002_alter_kfetpage_colcount.py index fe91d3e6..4eace262 100644 --- a/kfet/cms/migrations/0002_alter_kfetpage_colcount.py +++ b/kfet/cms/migrations/0002_alter_kfetpage_colcount.py @@ -6,14 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfetcms', '0001_initial'), - ] + dependencies = [("kfetcms", "0001_initial")] operations = [ migrations.AlterField( - model_name='kfetpage', - name='col_count', - field=models.CharField(blank=True, max_length=255, verbose_name='Nombre de colonnes', help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes."), - ), + model_name="kfetpage", + name="col_count", + field=models.CharField( + blank=True, + max_length=255, + verbose_name="Nombre de colonnes", + help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes.", + ), + ) ] diff --git a/kfet/cms/models.py b/kfet/cms/models.py index 0dff183f..6d108f31 100644 --- a/kfet/cms/models.py +++ b/kfet/cms/models.py @@ -1,8 +1,10 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ - from wagtail.wagtailadmin.edit_handlers import ( - FieldPanel, FieldRowPanel, MultiFieldPanel, StreamFieldPanel + FieldPanel, + FieldRowPanel, + MultiFieldPanel, + StreamFieldPanel, ) from wagtail.wagtailcore import blocks from wagtail.wagtailcore.fields import StreamField @@ -17,47 +19,45 @@ from kfet.cms.context_processors import get_articles @register_snippet class MemberTeam(models.Model): first_name = models.CharField( - verbose_name=_('Prénom'), - blank=True, default='', max_length=255, + verbose_name=_("Prénom"), blank=True, default="", max_length=255 ) last_name = models.CharField( - verbose_name=_('Nom'), - blank=True, default='', max_length=255, + verbose_name=_("Nom"), blank=True, default="", max_length=255 ) nick_name = models.CharField( - verbose_name=_('Alias'), - blank=True, default='', max_length=255, + verbose_name=_("Alias"), blank=True, default="", max_length=255 ) photo = models.ForeignKey( - 'wagtailimages.Image', - verbose_name=_('Photo'), + "wagtailimages.Image", + verbose_name=_("Photo"), on_delete=models.SET_NULL, - null=True, blank=True, - related_name='+', + null=True, + blank=True, + related_name="+", ) class Meta: - verbose_name = _('K-Fêt-eux-se') + verbose_name = _("K-Fêt-eux-se") panels = [ - FieldPanel('first_name'), - FieldPanel('last_name'), - FieldPanel('nick_name'), - ImageChooserPanel('photo'), + FieldPanel("first_name"), + FieldPanel("last_name"), + FieldPanel("nick_name"), + ImageChooserPanel("photo"), ] def __str__(self): return self.get_full_name() def get_full_name(self): - return '{} {}'.format(self.first_name, self.last_name).strip() + return "{} {}".format(self.first_name, self.last_name).strip() class MenuBlock(blocks.StaticBlock): class Meta: - icon = 'list-ul' - label = _('Carte') - template = 'kfetcms/block_menu.html' + icon = "list-ul" + label = _("Carte") + template = "kfetcms/block_menu.html" def get_context(self, *args, **kwargs): context = super().get_context(*args, **kwargs) @@ -67,71 +67,68 @@ class MenuBlock(blocks.StaticBlock): class GroupTeamBlock(blocks.StructBlock): show_only = blocks.IntegerBlock( - label=_('Montrer seulement'), + label=_("Montrer seulement"), required=False, help_text=_( - 'Nombre initial de membres affichés. Laisser vide pour tou-te-s ' - 'les afficher.' + "Nombre initial de membres affichés. Laisser vide pour tou-te-s " + "les afficher." ), ) members = blocks.ListBlock( SnippetChooserBlock(MemberTeam), - label=_('K-Fêt-eux-ses'), - classname='team-group', + label=_("K-Fêt-eux-ses"), + classname="team-group", ) class Meta: - icon = 'group' - label = _('Groupe de K-Fêt-eux-ses') - template = 'kfetcms/block_teamgroup.html' + icon = "group" + label = _("Groupe de K-Fêt-eux-ses") + template = "kfetcms/block_teamgroup.html" class ChoicesStreamBlock(blocks.StreamBlock): - rich = blocks.RichTextBlock(label=_('Éditeur')) + rich = blocks.RichTextBlock(label=_("Éditeur")) carte = MenuBlock() group_team = GroupTeamBlock() class KFetStreamBlock(ChoicesStreamBlock): - group = ChoicesStreamBlock(label=_('Contenu groupé')) + group = ChoicesStreamBlock(label=_("Contenu groupé")) class KFetPage(Page): - content = StreamField(KFetStreamBlock, verbose_name=_('Contenu')) + content = StreamField(KFetStreamBlock, verbose_name=_("Contenu")) # Layout fields - TEMPLATE_COL_1 = 'kfet/base_col_1.html' - TEMPLATE_COL_2 = 'kfet/base_col_2.html' - TEMPLATE_COL_MULT = 'kfet/base_col_mult.html' + TEMPLATE_COL_1 = "kfet/base_col_1.html" + TEMPLATE_COL_2 = "kfet/base_col_2.html" + TEMPLATE_COL_MULT = "kfet/base_col_mult.html" no_header = models.BooleanField( - verbose_name=_('Sans en-tête'), + verbose_name=_("Sans en-tête"), default=False, - help_text=_( - "Coché, l'en-tête (avec le titre) de la page n'est pas affiché." - ), + help_text=_("Coché, l'en-tête (avec le titre) de la page n'est pas affiché."), ) layout = models.CharField( - verbose_name=_('Template'), + verbose_name=_("Template"), choices=[ - (TEMPLATE_COL_1, _('Une colonne : centrée sur la page')), - (TEMPLATE_COL_2, _('Deux colonnes : fixe à gauche, contenu à droite')), - (TEMPLATE_COL_MULT, _('Contenu scindé sur plusieurs colonnes')), + (TEMPLATE_COL_1, _("Une colonne : centrée sur la page")), + (TEMPLATE_COL_2, _("Deux colonnes : fixe à gauche, contenu à droite")), + (TEMPLATE_COL_MULT, _("Contenu scindé sur plusieurs colonnes")), ], - default=TEMPLATE_COL_MULT, max_length=255, - help_text=_( - "Comment cette page devrait être affichée ?" - ), + default=TEMPLATE_COL_MULT, + max_length=255, + help_text=_("Comment cette page devrait être affichée ?"), ) main_size = models.CharField( - verbose_name=_('Taille de la colonne de contenu'), - blank=True, max_length=255, + verbose_name=_("Taille de la colonne de contenu"), blank=True, max_length=255 ) col_count = models.CharField( - verbose_name=_('Nombre de colonnes'), - blank=True, max_length=255, + verbose_name=_("Nombre de colonnes"), + blank=True, + max_length=255, help_text=_( "S'applique au page dont le contenu est scindé sur plusieurs colonnes." ), @@ -139,34 +136,29 @@ class KFetPage(Page): # Panels - content_panels = Page.content_panels + [ - StreamFieldPanel('content'), - ] + content_panels = Page.content_panels + [StreamFieldPanel("content")] layout_panel = [ - FieldPanel('no_header'), - FieldPanel('layout'), - FieldRowPanel([ - FieldPanel('main_size'), - FieldPanel('col_count'), - ]), + FieldPanel("no_header"), + FieldPanel("layout"), + FieldRowPanel([FieldPanel("main_size"), FieldPanel("col_count")]), ] settings_panels = [ - MultiFieldPanel(layout_panel, _('Affichage')) + MultiFieldPanel(layout_panel, _("Affichage")) ] + Page.settings_panels # Base template template = "kfetcms/base.html" class Meta: - verbose_name = _('page K-Fêt') - verbose_name_plural = _('pages K-Fêt') + verbose_name = _("page K-Fêt") + verbose_name_plural = _("pages K-Fêt") def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) - page = context['page'] + page = context["page"] if not page.seo_title: page.seo_title = page.title diff --git a/kfet/config.py b/kfet/config.py index 452c2a09..a8f7f0eb 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -1,8 +1,7 @@ +import djconfig from django.core.exceptions import ValidationError from django.db import models -import djconfig - class KFetConfig(object): """kfet app configuration. @@ -10,7 +9,8 @@ class KFetConfig(object): Enhance isolation with backend used to store config. """ - prefix = 'kfet_' + + prefix = "kfet_" def __init__(self): # Set this to False again to reload the config, e.g for testing @@ -28,11 +28,11 @@ class KFetConfig(object): def __getattr__(self, key): self._check_init() - if key == 'subvention_cof': + if key == "subvention_cof": # Allows accessing to the reduction as a subvention # Other reason: backward compatibility - reduction_mult = 1 - self.reduction_cof/100 - return (1/reduction_mult - 1) * 100 + reduction_mult = 1 - self.reduction_cof / 100 + return (1 / reduction_mult - 1) * 100 return getattr(djconfig.config, self._get_dj_key(key)) def list(self): @@ -44,12 +44,15 @@ class KFetConfig(object): """ # prevent circular imports from kfet.forms import KFetConfigForm + self._check_init() - return [(field.label, getattr(djconfig.config, name), ) - for name, field in KFetConfigForm.base_fields.items()] + return [ + (field.label, getattr(djconfig.config, name)) + for name, field in KFetConfigForm.base_fields.items() + ] def _get_dj_key(self, key): - return '{}{}'.format(self.prefix, key) + return "{}{}".format(self.prefix, key) def set(self, **kwargs): """Update configuration value(s). @@ -78,8 +81,9 @@ class KFetConfig(object): cfg_form.save() else: raise ValidationError( - 'Invalid values in kfet_config.set: %(fields)s', - params={'fields': list(cfg_form.errors)}) + "Invalid values in kfet_config.set: %(fields)s", + params={"fields": list(cfg_form.errors)}, + ) kfet_config = KFetConfig() diff --git a/kfet/consumers.py b/kfet/consumers.py index a53bbb72..2655c86b 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -2,5 +2,5 @@ from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): - groups = ['kfet.kpsul'] - perms_connect = ['kfet.is_team'] + groups = ["kfet.kpsul"] + perms_connect = ["kfet.is_team"] diff --git a/kfet/context_processors.py b/kfet/context_processors.py index 89678f62..6668eebf 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -2,4 +2,4 @@ from kfet.config import kfet_config def config(request): - return {'kfet_config': kfet_config} + return {"kfet_config": kfet_config} diff --git a/kfet/decorators.py b/kfet/decorators.py index 66c9d71c..70848820 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -2,6 +2,7 @@ from django.contrib.auth.decorators import user_passes_test def kfet_is_team(user): - return user.has_perm('kfet.is_team') + return user.has_perm("kfet.is_team") + teamkfet_required = user_passes_test(kfet_is_team) diff --git a/kfet/forms.py b/kfet/forms.py index 9d0fadd8..b253fd67 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -2,79 +2,87 @@ from datetime import timedelta from decimal import Decimal from django import forms -from django.core.exceptions import ValidationError -from django.core import validators from django.contrib.auth.models import User +from django.core import validators +from django.core.exceptions import ValidationError from django.forms import modelformset_factory from django.utils import timezone - from djconfig.forms import ConfigForm -from kfet.models import ( - Account, Checkout, Article, OperationGroup, Operation, - CheckoutStatement, ArticleCategory, AccountNegative, Transfer, - TransferGroup, Supplier) from gestioncof.models import CofProfile +from kfet.models import ( + Account, + AccountNegative, + Article, + ArticleCategory, + Checkout, + CheckoutStatement, + Operation, + OperationGroup, + Supplier, + Transfer, + TransferGroup, +) from .auth.forms import UserGroupForm # noqa - # ----- # Widgets # ----- + class DateTimeWidget(forms.DateTimeInput): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.attrs['format'] = '%Y-%m-%d %H:%M' + self.attrs["format"] = "%Y-%m-%d %H:%M" class Media: - css = { - 'all': ('kfet/css/bootstrap-datetimepicker.min.css',) - } - js = ('kfet/js/bootstrap-datetimepicker.min.js',) + css = {"all": ("kfet/css/bootstrap-datetimepicker.min.css",)} + js = ("kfet/js/bootstrap-datetimepicker.min.js",) # ----- # Account forms # ----- + class AccountForm(forms.ModelForm): # Surcharge pour passer data à Account.save() - def save(self, data = {}, *args, **kwargs): - obj = super().save(commit = False, *args, **kwargs) - obj.save(data = data) + def save(self, data={}, *args, **kwargs): + obj = super().save(commit=False, *args, **kwargs) + obj.save(data=data) return obj class Meta: - model = Account - fields = ['trigramme', 'promo', 'nickname', 'is_frozen'] - widgets = { - 'trigramme': forms.TextInput(attrs={'autocomplete': 'off'}), - } + model = Account + fields = ["trigramme", "promo", "nickname", "is_frozen"] + widgets = {"trigramme": forms.TextInput(attrs={"autocomplete": "off"})} + class AccountBalanceForm(forms.ModelForm): class Meta: model = Account - fields = ['balance'] + fields = ["balance"] + class AccountTriForm(AccountForm): - def clean_trigramme(self): - trigramme = self.cleaned_data['trigramme'] + trigramme = self.cleaned_data["trigramme"] return trigramme.upper() class Meta(AccountForm.Meta): - fields = ['trigramme'] + fields = ["trigramme"] + class AccountNoTriForm(AccountForm): class Meta(AccountForm.Meta): - exclude = ['trigramme'] + exclude = ["trigramme"] + class AccountRestrictForm(AccountForm): class Meta(AccountForm.Meta): - fields = ['is_frozen'] + fields = ["is_frozen"] class AccountPwdForm(forms.Form): @@ -85,14 +93,12 @@ class AccountPwdForm(forms.Form): widget=forms.PasswordInput, ) pwd2 = forms.CharField( - label="Confirmer le mot de passe", - required=False, - widget=forms.PasswordInput, + label="Confirmer le mot de passe", required=False, widget=forms.PasswordInput ) def clean(self): - pwd1 = self.cleaned_data.get('pwd1', '') - pwd2 = self.cleaned_data.get('pwd2', '') + pwd1 = self.cleaned_data.get("pwd1", "") + pwd2 = self.cleaned_data.get("pwd2", "") if len(pwd1) < 8: raise ValidationError("Mot de passe trop court") if pwd1 != pwd2: @@ -102,63 +108,66 @@ class AccountPwdForm(forms.Form): class CofForm(forms.ModelForm): def clean_is_cof(self): - instance = getattr(self, 'instance', None) + instance = getattr(self, "instance", None) if instance and instance.pk: return instance.is_cof else: return False + class Meta: - model = CofProfile - fields = ['login_clipper', 'is_cof', 'departement'] + model = CofProfile + fields = ["login_clipper", "is_cof", "departement"] + class CofRestrictForm(CofForm): class Meta(CofForm.Meta): - fields = ['departement'] + fields = ["departement"] class UserForm(forms.ModelForm): class Meta: model = User - fields = ['username', 'first_name', 'last_name', 'email'] - help_texts = { - 'username': '' - } + fields = ["username", "first_name", "last_name", "email"] + help_texts = {"username": ""} class UserRestrictForm(UserForm): class Meta(UserForm.Meta): - fields = ['first_name', 'last_name'] + fields = ["first_name", "last_name"] + class UserRestrictTeamForm(UserForm): class Meta(UserForm.Meta): - fields = ['first_name', 'last_name', 'email'] + fields = ["first_name", "last_name", "email"] class AccountNegativeForm(forms.ModelForm): class Meta: - model = AccountNegative - fields = ['authz_overdraft_amount', 'authz_overdraft_until', - 'balance_offset', 'comment'] - widgets = { - 'authz_overdraft_until': DateTimeWidget(), - } + model = AccountNegative + fields = [ + "authz_overdraft_amount", + "authz_overdraft_until", + "balance_offset", + "comment", + ] + widgets = {"authz_overdraft_until": DateTimeWidget()} + # ----- # Checkout forms # ----- + class CheckoutForm(forms.ModelForm): class Meta: - model = Checkout - fields = ['name', 'valid_from', 'valid_to', 'balance', 'is_protected'] - widgets = { - 'valid_from': DateTimeWidget(), - 'valid_to' : DateTimeWidget(), - } + model = Checkout + fields = ["name", "valid_from", "valid_to", "balance", "is_protected"] + widgets = {"valid_from": DateTimeWidget(), "valid_to": DateTimeWidget()} + class CheckoutRestrictForm(CheckoutForm): class Meta(CheckoutForm.Meta): - fields = ['name', 'valid_from', 'valid_to'] + fields = ["name", "valid_from", "valid_to"] class CheckoutStatementCreateForm(forms.ModelForm): @@ -180,173 +189,206 @@ class CheckoutStatementCreateForm(forms.ModelForm): class Meta: model = CheckoutStatement - exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken', - 'balance_old', 'balance_new'] + exclude = [ + "by", + "at", + "checkout", + "amount_error", + "amount_taken", + "balance_old", + "balance_new", + ] def clean(self): - not_count = self.cleaned_data['not_count'] + not_count = self.cleaned_data["not_count"] if not not_count and ( - self.cleaned_data['balance_001'] is None - or self.cleaned_data['balance_002'] is None - or self.cleaned_data['balance_005'] is None - or self.cleaned_data['balance_01'] is None - or self.cleaned_data['balance_02'] is None - or self.cleaned_data['balance_05'] is None - or self.cleaned_data['balance_1'] is None - or self.cleaned_data['balance_2'] is None - or self.cleaned_data['balance_5'] is None - or self.cleaned_data['balance_10'] is None - or self.cleaned_data['balance_20'] is None - or self.cleaned_data['balance_50'] is None - or self.cleaned_data['balance_100'] is None - or self.cleaned_data['balance_200'] is None - or self.cleaned_data['balance_500'] is None): - raise ValidationError("Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp (et t'as pas idée de comment c'est long de vérifier que t'as mis des valeurs de partout...)") + self.cleaned_data["balance_001"] is None + or self.cleaned_data["balance_002"] is None + or self.cleaned_data["balance_005"] is None + or self.cleaned_data["balance_01"] is None + or self.cleaned_data["balance_02"] is None + or self.cleaned_data["balance_05"] is None + or self.cleaned_data["balance_1"] is None + or self.cleaned_data["balance_2"] is None + or self.cleaned_data["balance_5"] is None + or self.cleaned_data["balance_10"] is None + or self.cleaned_data["balance_20"] is None + or self.cleaned_data["balance_50"] is None + or self.cleaned_data["balance_100"] is None + or self.cleaned_data["balance_200"] is None + or self.cleaned_data["balance_500"] is None + ): + raise ValidationError( + "Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp (et t'as pas idée de comment c'est long de vérifier que t'as mis des valeurs de partout...)" + ) super().clean() + class CheckoutStatementUpdateForm(forms.ModelForm): class Meta: model = CheckoutStatement - exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken'] + exclude = ["by", "at", "checkout", "amount_error", "amount_taken"] # ----- # Category # ----- + class CategoryForm(forms.ModelForm): class Meta: model = ArticleCategory - fields = ['name', 'has_addcost'] + fields = ["name", "has_addcost"] + # ----- # Article forms # ----- + class ArticleForm(forms.ModelForm): category_new = forms.CharField( - label="Créer une catégorie", - max_length=45, - required = False) + label="Créer une catégorie", max_length=45, required=False + ) category = forms.ModelChoiceField( - label="Catégorie", - queryset = ArticleCategory.objects.all(), - required = False) + label="Catégorie", queryset=ArticleCategory.objects.all(), required=False + ) suppliers = forms.ModelMultipleChoiceField( - label="Fournisseurs", - queryset = Supplier.objects.all(), - required = False) + label="Fournisseurs", queryset=Supplier.objects.all(), required=False + ) supplier_new = forms.CharField( - label="Créer un fournisseur", - max_length = 45, - required = False) + label="Créer un fournisseur", max_length=45, required=False + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: - self.initial['suppliers'] = self.instance.suppliers.values_list('pk', flat=True) + self.initial["suppliers"] = self.instance.suppliers.values_list( + "pk", flat=True + ) def clean(self): - category = self.cleaned_data.get('category') - category_new = self.cleaned_data.get('category_new') + category = self.cleaned_data.get("category") + category_new = self.cleaned_data.get("category_new") if not category and not category_new: - raise ValidationError('Sélectionnez une catégorie ou créez en une') + raise ValidationError("Sélectionnez une catégorie ou créez en une") elif not category: category, _ = ArticleCategory.objects.get_or_create(name=category_new) - self.cleaned_data['category'] = category + self.cleaned_data["category"] = category super().clean() class Meta: - model = Article - fields = ['name', 'is_sold', 'hidden', 'price', 'stock', 'category', 'box_type', - 'box_capacity'] + model = Article + fields = [ + "name", + "is_sold", + "hidden", + "price", + "stock", + "category", + "box_type", + "box_capacity", + ] + class ArticleRestrictForm(ArticleForm): class Meta(ArticleForm.Meta): - fields = ['name', 'is_sold', 'hidden', 'price', 'category', 'box_type', - 'box_capacity'] + fields = [ + "name", + "is_sold", + "hidden", + "price", + "category", + "box_type", + "box_capacity", + ] + # ----- # K-Psul forms # ----- + class KPsulOperationGroupForm(forms.ModelForm): # FIXME(AD): Use timezone.now instead of timezone.now() to avoid using a # fixed datetime (application boot here). # One may even use: Checkout.objects.is_valid() if changing # to now = timezone.now is ok in 'is_valid' definition. checkout = forms.ModelChoiceField( - queryset = Checkout.objects.filter( - is_protected=False, valid_from__lte=timezone.now(), - valid_to__gte=timezone.now()), - widget = forms.HiddenInput()) + queryset=Checkout.objects.filter( + is_protected=False, + valid_from__lte=timezone.now(), + valid_to__gte=timezone.now(), + ), + widget=forms.HiddenInput(), + ) on_acc = forms.ModelChoiceField( - queryset = Account.objects.exclude(trigramme='GNR'), - widget = forms.HiddenInput()) + queryset=Account.objects.exclude(trigramme="GNR"), widget=forms.HiddenInput() + ) + class Meta: - model = OperationGroup - fields = ['on_acc', 'checkout', 'comment'] + model = OperationGroup + fields = ["on_acc", "checkout", "comment"] + class KPsulAccountForm(forms.ModelForm): class Meta: - model = Account - fields = ['trigramme'] + model = Account + fields = ["trigramme"] widgets = { - 'trigramme': forms.TextInput( - attrs={ - 'autocomplete': 'off', - 'spellcheck': 'false', - }), + "trigramme": forms.TextInput( + attrs={"autocomplete": "off", "spellcheck": "false"} + ) } class KPsulCheckoutForm(forms.Form): checkout = forms.ModelChoiceField( - queryset=None, - widget=forms.Select(attrs={'id': 'id_checkout_select'}), + queryset=None, widget=forms.Select(attrs={"id": "id_checkout_select"}) ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Create the queryset on form instanciation to use the current time. - self.fields['checkout'].queryset = ( - Checkout.objects.is_valid().filter(is_protected=False)) + self.fields["checkout"].queryset = Checkout.objects.is_valid().filter( + is_protected=False + ) class KPsulOperationForm(forms.ModelForm): article = forms.ModelChoiceField( - queryset=Article.objects.select_related('category').all(), + queryset=Article.objects.select_related("category").all(), required=False, - widget = forms.HiddenInput()) + widget=forms.HiddenInput(), + ) article_nb = forms.IntegerField( required=False, initial=None, validators=[validators.MinValueValidator(1)], widget=forms.HiddenInput(), ) + class Meta: - model = Operation - fields = ['type', 'amount', 'article', 'article_nb'] - widgets = { - 'type': forms.HiddenInput(), - 'amount': forms.HiddenInput(), - } + model = Operation + fields = ["type", "amount", "article", "article_nb"] + widgets = {"type": forms.HiddenInput(), "amount": forms.HiddenInput()} def clean(self): super().clean() - type_ope = self.cleaned_data.get('type') - amount = self.cleaned_data.get('amount') - article = self.cleaned_data.get('article') - article_nb = self.cleaned_data.get('article_nb') + type_ope = self.cleaned_data.get("type") + amount = self.cleaned_data.get("amount") + article = self.cleaned_data.get("article") + article_nb = self.cleaned_data.get("article_nb") errors = [] if type_ope and type_ope == Operation.PURCHASE: if not article or article_nb is None or article_nb < 1: - errors.append(ValidationError( - "Un achat nécessite un article et une quantité")) + errors.append( + ValidationError("Un achat nécessite un article et une quantité") + ) elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]: if not amount or article or article_nb: errors.append(ValidationError("Bad request")) @@ -355,8 +397,8 @@ class KPsulOperationForm(forms.ModelForm): errors.append(ValidationError("Charge non positive")) elif type_ope == Operation.WITHDRAW and amount >= 0: errors.append(ValidationError("Retrait non négatif")) - self.cleaned_data['article'] = None - self.cleaned_data['article_nb'] = None + self.cleaned_data["article"] = None + self.cleaned_data["article_nb"] = None if errors: raise ValidationError(errors) @@ -364,26 +406,29 @@ class KPsulOperationForm(forms.ModelForm): KPsulOperationFormSet = modelformset_factory( Operation, - form = KPsulOperationForm, - can_delete = True, - extra = 0, - min_num = 1, validate_min = True) + form=KPsulOperationForm, + can_delete=True, + extra=0, + min_num=1, + validate_min=True, +) + class AddcostForm(forms.Form): - trigramme = forms.CharField(required = False) + trigramme = forms.CharField(required=False) amount = forms.DecimalField( - required = False, - max_digits=6,decimal_places=2,min_value=Decimal(0)) + required=False, max_digits=6, decimal_places=2, min_value=Decimal(0) + ) def clean(self): - trigramme = self.cleaned_data.get('trigramme') + trigramme = self.cleaned_data.get("trigramme") if trigramme: try: Account.objects.get(trigramme=trigramme) except Account.DoesNotExist: - raise ValidationError('Compte invalide') + raise ValidationError("Compte invalide") else: - self.cleaned_data['amount'] = 0 + self.cleaned_data["amount"] = 0 super().clean() @@ -395,34 +440,40 @@ class AddcostForm(forms.Form): class KFetConfigForm(ConfigForm): kfet_reduction_cof = forms.DecimalField( - label='Réduction COF', initial=Decimal('20'), - max_digits=6, decimal_places=2, + label="Réduction COF", + initial=Decimal("20"), + max_digits=6, + decimal_places=2, help_text="Réduction, à donner en pourcentage, appliquée lors d'un " - "achat par un-e membre du COF sur le montant en euros.", + "achat par un-e membre du COF sur le montant en euros.", ) kfet_addcost_amount = forms.DecimalField( - label='Montant de la majoration (en €)', initial=Decimal('0'), + label="Montant de la majoration (en €)", + initial=Decimal("0"), required=False, - max_digits=6, decimal_places=2, + max_digits=6, + decimal_places=2, ) kfet_addcost_for = forms.ModelChoiceField( - label='Destinataire de la majoration', initial=None, required=False, - help_text='Laissez vide pour désactiver la majoration.', - queryset=(Account.objects - .select_related('cofprofile', 'cofprofile__user') - .all()), + label="Destinataire de la majoration", + initial=None, + required=False, + help_text="Laissez vide pour désactiver la majoration.", + queryset=( + Account.objects.select_related("cofprofile", "cofprofile__user").all() + ), ) kfet_overdraft_duration = forms.DurationField( - label='Durée du découvert autorisé par défaut', - initial=timedelta(days=1), + label="Durée du découvert autorisé par défaut", initial=timedelta(days=1) ) kfet_overdraft_amount = forms.DecimalField( - label='Montant du découvert autorisé par défaut (en €)', - initial=Decimal('20'), - max_digits=6, decimal_places=2, + label="Montant du découvert autorisé par défaut (en €)", + initial=Decimal("20"), + max_digits=6, + decimal_places=2, ) kfet_cancel_duration = forms.DurationField( - label='Durée pour annuler une commande sans mot de passe', + label="Durée pour annuler une commande sans mot de passe", initial=timedelta(minutes=5), ) @@ -438,105 +489,98 @@ class FilterHistoryForm(forms.Form): # Transfer forms # ----- + class TransferGroupForm(forms.ModelForm): class Meta: - model = TransferGroup - fields = ['comment'] + model = TransferGroup + fields = ["comment"] + class TransferForm(forms.ModelForm): from_acc = forms.ModelChoiceField( - queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13', 'GNR']), - widget = forms.HiddenInput() + queryset=Account.objects.exclude(trigramme__in=["LIQ", "#13", "GNR"]), + widget=forms.HiddenInput(), ) to_acc = forms.ModelChoiceField( - queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13', 'GNR']), - widget = forms.HiddenInput() + queryset=Account.objects.exclude(trigramme__in=["LIQ", "#13", "GNR"]), + widget=forms.HiddenInput(), ) def clean_amount(self): - amount = self.cleaned_data['amount'] + amount = self.cleaned_data["amount"] if amount <= 0: raise forms.ValidationError("Montant invalide") return amount class Meta: - model = Transfer - fields = ['from_acc', 'to_acc', 'amount'] + model = Transfer + fields = ["from_acc", "to_acc", "amount"] + TransferFormSet = modelformset_factory( - Transfer, - form = TransferForm, - min_num = 1, validate_min = True, - extra = 9, + Transfer, form=TransferForm, min_num=1, validate_min=True, extra=9 ) # ----- # Inventory forms # ----- + class InventoryArticleForm(forms.Form): article = forms.ModelChoiceField( - queryset = Article.objects.all(), - widget = forms.HiddenInput(), - ) + queryset=Article.objects.all(), widget=forms.HiddenInput() + ) stock_new = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if 'initial' in kwargs: - self.name = kwargs['initial']['name'] - self.stock_old = kwargs['initial']['stock_old'] - self.category = kwargs['initial']['category'] - self.category_name = kwargs['initial']['category__name'] - self.box_capacity = kwargs['initial']['box_capacity'] + if "initial" in kwargs: + self.name = kwargs["initial"]["name"] + self.stock_old = kwargs["initial"]["stock_old"] + self.category = kwargs["initial"]["category"] + self.category_name = kwargs["initial"]["category__name"] + self.box_capacity = kwargs["initial"]["box_capacity"] + # ----- # Order forms # ----- - class OrderArticleForm(forms.Form): article = forms.ModelChoiceField( - queryset=Article.objects.all(), - widget=forms.HiddenInput(), - ) + queryset=Article.objects.all(), widget=forms.HiddenInput() + ) quantity_ordered = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if 'initial' in kwargs: - self.name = kwargs['initial']['name'] - self.stock = kwargs['initial']['stock'] - self.category = kwargs['initial']['category'] - self.category_name = kwargs['initial']['category__name'] - self.box_capacity = kwargs['initial']['box_capacity'] - self.v_all = kwargs['initial']['v_all'] - self.v_moy = kwargs['initial']['v_moy'] - self.v_et = kwargs['initial']['v_et'] - self.v_prev = kwargs['initial']['v_prev'] - self.c_rec = kwargs['initial']['c_rec'] + if "initial" in kwargs: + self.name = kwargs["initial"]["name"] + self.stock = kwargs["initial"]["stock"] + self.category = kwargs["initial"]["category"] + self.category_name = kwargs["initial"]["category__name"] + self.box_capacity = kwargs["initial"]["box_capacity"] + self.v_all = kwargs["initial"]["v_all"] + self.v_moy = kwargs["initial"]["v_moy"] + self.v_et = kwargs["initial"]["v_et"] + self.v_prev = kwargs["initial"]["v_prev"] + self.c_rec = kwargs["initial"]["c_rec"] + class OrderArticleToInventoryForm(forms.Form): article = forms.ModelChoiceField( - queryset = Article.objects.all(), - widget = forms.HiddenInput(), - ) - price_HT = forms.DecimalField( - max_digits = 7, decimal_places = 4, - required = False) - TVA = forms.DecimalField( - max_digits = 7, decimal_places = 2, - required = False) - rights = forms.DecimalField( - max_digits = 7, decimal_places = 4, - required = False) + queryset=Article.objects.all(), widget=forms.HiddenInput() + ) + price_HT = forms.DecimalField(max_digits=7, decimal_places=4, required=False) + TVA = forms.DecimalField(max_digits=7, decimal_places=2, required=False) + rights = forms.DecimalField(max_digits=7, decimal_places=4, required=False) quantity_received = forms.IntegerField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if 'initial' in kwargs: - self.name = kwargs['initial']['name'] - self.category = kwargs['initial']['category'] - self.category_name = kwargs['initial']['category__name'] - self.quantity_ordered = kwargs['initial']['quantity_ordered'] + if "initial" in kwargs: + self.name = kwargs["initial"]["name"] + self.category = kwargs["initial"]["category"] + self.category_name = kwargs["initial"]["category__name"] + self.quantity_ordered = kwargs["initial"]["quantity_ordered"] diff --git a/kfet/management/commands/createopes.py b/kfet/management/commands/createopes.py index 5a7699ae..03458ea0 100644 --- a/kfet/management/commands/createopes.py +++ b/kfet/management/commands/createopes.py @@ -1,4 +1,3 @@ - """ Crée des opérations aléatoires réparties sur une période de temps spécifiée """ @@ -6,29 +5,40 @@ Crée des opérations aléatoires réparties sur une période de temps spécifi import random from datetime import timedelta from decimal import Decimal -from django.utils import timezone -from django.core.management.base import BaseCommand -from kfet.models import (Account, Article, OperationGroup, Operation, - Checkout, Transfer, TransferGroup) +from django.core.management.base import BaseCommand +from django.utils import timezone + +from kfet.models import ( + Account, + Article, + Checkout, + Operation, + OperationGroup, + Transfer, + TransferGroup, +) class Command(BaseCommand): - help = ("Crée des opérations réparties uniformément " - "sur une période de temps") + help = "Crée des opérations réparties uniformément " "sur une période de temps" def add_arguments(self, parser): # Nombre d'opérations à créer - parser.add_argument('opes', type=int, - help='Number of opegroups to create') + parser.add_argument("opes", type=int, help="Number of opegroups to create") # Période sur laquelle créer (depuis num_days avant maintenant) - parser.add_argument('days', type=int, - help='Period in which to create opegroups') + parser.add_argument( + "days", type=int, help="Period in which to create opegroups" + ) # Optionnel : nombre de transfert à créer (défaut 0) - parser.add_argument('--transfers', type=int, default=0, - help='Number of transfers to create (default 0)') + parser.add_argument( + "--transfers", + type=int, + default=0, + help="Number of transfers to create (default 0)", + ) def handle(self, *args, **options): @@ -39,19 +49,19 @@ class Command(BaseCommand): purchases = 0 transfers = 0 - num_ops = options['opes'] - num_transfers = options['transfers'] + num_ops = options["opes"] + num_transfers = options["transfers"] # Convert to seconds - time = options['days'] * 24 * 3600 + time = options["days"] * 24 * 3600 now = timezone.now() checkout = Checkout.objects.first() articles = Article.objects.all() - accounts = Account.objects.exclude(trigramme='LIQ') - liq_account = Account.objects.get(trigramme='LIQ') + accounts = Account.objects.exclude(trigramme="LIQ") + liq_account = Account.objects.get(trigramme="LIQ") try: con_account = Account.objects.get( - cofprofile__user__first_name='Assurancetourix' + cofprofile__user__first_name="Assurancetourix" ) except Account.DoesNotExist: con_account = random.choice(accounts) @@ -78,12 +88,12 @@ class Command(BaseCommand): if random.random() < 0.2: addcost = True addcost_for = con_account - addcost_amount = Decimal('0.5') + addcost_amount = Decimal("0.5") else: addcost = False # Initialize opegroup amount - amount = Decimal('0') + amount = Decimal("0") # Generating operations ope_list = [] @@ -95,19 +105,18 @@ class Command(BaseCommand): if typevar > 0.9 and account != liq_account: ope = Operation( type=Operation.DEPOSIT, - amount=Decimal(random.randint(1, 99)/10) + amount=Decimal(random.randint(1, 99) / 10), ) # 0.05 probability to have a withdrawal elif typevar > 0.85 and account != liq_account: ope = Operation( type=Operation.WITHDRAW, - amount=-Decimal(random.randint(1, 99)/10) + amount=-Decimal(random.randint(1, 99) / 10), ) # 0.05 probability to have an edition elif typevar > 0.8 and account != liq_account: ope = Operation( - type=Operation.EDIT, - amount=Decimal(random.randint(1, 99)/10) + type=Operation.EDIT, amount=Decimal(random.randint(1, 99) / 10) ) else: article = random.choice(articles) @@ -115,9 +124,9 @@ class Command(BaseCommand): ope = Operation( type=Operation.PURCHASE, - amount=-article.price*nb, + amount=-article.price * nb, article=article, - article_nb=nb + article_nb=nb, ) purchases += 1 @@ -130,23 +139,23 @@ class Command(BaseCommand): ope_list.append(ope) amount += ope.amount - opegroup_list.append(OperationGroup( - on_acc=account, - checkout=checkout, - at=at, - is_cof=account.cofprofile.is_cof, - amount=amount, - )) + opegroup_list.append( + OperationGroup( + on_acc=account, + checkout=checkout, + at=at, + is_cof=account.cofprofile.is_cof, + amount=amount, + ) + ) at_list.append(at) - ope_by_grp.append((at, ope_list, )) + ope_by_grp.append((at, ope_list)) OperationGroup.objects.bulk_create(opegroup_list) # Fetch created OperationGroup objects pk by at - opegroups = (OperationGroup.objects - .filter(at__in=at_list) - .values('id', 'at')) - opegroups_by = {grp['at']: grp['id'] for grp in opegroups} + opegroups = OperationGroup.objects.filter(at__in=at_list).values("id", "at") + opegroups_by = {grp["at"]: grp["id"] for grp in opegroups} all_ope = [] for _ in range(num_ops): @@ -175,30 +184,28 @@ class Command(BaseCommand): else: comment = "" - transfergroup_list.append(TransferGroup( - at=at, - comment=comment, - valid_by=random.choice(accounts), - )) + transfergroup_list.append( + TransferGroup(at=at, comment=comment, valid_by=random.choice(accounts)) + ) at_list.append(at) # Randomly generate transfer transfer_list = [] for i in range(random.randint(1, 4)): - transfer_list.append(Transfer( - from_acc=random.choice(accounts), - to_acc=random.choice(accounts), - amount=Decimal(random.randint(1, 99)/10) - )) + transfer_list.append( + Transfer( + from_acc=random.choice(accounts), + to_acc=random.choice(accounts), + amount=Decimal(random.randint(1, 99) / 10), + ) + ) - transfer_by_grp.append((at, transfer_list, )) + transfer_by_grp.append((at, transfer_list)) TransferGroup.objects.bulk_create(transfergroup_list) - transfergroups = (TransferGroup.objects - .filter(at__in=at_list) - .values('id', 'at')) - transfergroups_by = {grp['at']: grp['id'] for grp in transfergroups} + transfergroups = TransferGroup.objects.filter(at__in=at_list).values("id", "at") + transfergroups_by = {grp["at"]: grp["id"] for grp in transfergroups} all_transfer = [] for _ in range(num_transfers): @@ -211,9 +218,10 @@ class Command(BaseCommand): transfers += len(all_transfer) self.stdout.write( - "- {:d} opérations créées dont {:d} commandes d'articles" - .format(opes_created, purchases)) + "- {:d} opérations créées dont {:d} commandes d'articles".format( + opes_created, purchases + ) + ) if transfers: - self.stdout.write("- {:d} transferts créés" - .format(transfers)) + self.stdout.write("- {:d} transferts créés".format(transfers)) diff --git a/kfet/management/commands/loadkfetdevdata.py b/kfet/management/commands/loadkfetdevdata.py index 6dd25f29..0543be80 100644 --- a/kfet/management/commands/loadkfetdevdata.py +++ b/kfet/management/commands/loadkfetdevdata.py @@ -6,18 +6,23 @@ import os import random from datetime import timedelta -from django.utils import timezone -from django.contrib.auth.models import User, Group, Permission, ContentType +from django.contrib.auth.models import ContentType, Group, Permission, User from django.core.management import call_command +from django.utils import timezone from gestioncof.management.base import MyBaseCommand from gestioncof.models import CofProfile -from kfet.models import (Account, Checkout, CheckoutStatement, Supplier, - SupplierArticle, Article) +from kfet.models import ( + Account, + Article, + Checkout, + CheckoutStatement, + Supplier, + SupplierArticle, +) # Où sont stockés les fichiers json -DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), - 'data') +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") class Command(MyBaseCommand): @@ -28,7 +33,7 @@ class Command(MyBaseCommand): # Groupes # --- - Group.objects.filter(name__icontains='K-Fêt').delete() + Group.objects.filter(name__icontains="K-Fêt").delete() group_chef = Group(name="K-Fêt César") group_boy = Group(name="K-Fêt Légionnaire") @@ -37,10 +42,11 @@ class Command(MyBaseCommand): group_boy.save() permissions_chef = Permission.objects.filter( - content_type__in=ContentType.objects.filter( - app_label='kfet')) + content_type__in=ContentType.objects.filter(app_label="kfet") + ) permissions_boy = Permission.objects.filter( - codename__in=['is_team', 'perform_deposit']) + codename__in=["is_team", "perform_deposit"] + ) group_chef.permissions.add(*permissions_chef) group_boy.permissions.add(*permissions_boy) @@ -51,11 +57,11 @@ class Command(MyBaseCommand): self.stdout.write("Création des comptes K-Fêt") - gaulois = CofProfile.objects.filter(user__last_name='Gaulois') - gaulois_trigramme = map('{:03d}'.format, range(50)) + gaulois = CofProfile.objects.filter(user__last_name="Gaulois") + gaulois_trigramme = map("{:03d}".format, range(50)) - romains = CofProfile.objects.filter(user__last_name='Romain') - romains_trigramme = map(lambda x: str(100+x), range(99)) + romains = CofProfile.objects.filter(user__last_name="Romain") + romains_trigramme = map(lambda x: str(100 + x), range(99)) created_accounts = 0 team_accounts = 0 @@ -64,18 +70,18 @@ class Command(MyBaseCommand): account, created = Account.objects.get_or_create( trigramme=trigramme, cofprofile=profile, - defaults={'balance': random.randint(1, 999)/10} + defaults={"balance": random.randint(1, 999) / 10}, ) created_accounts += int(created) - if profile.user.first_name == 'Abraracourcix': + if profile.user.first_name == "Abraracourcix": profile.user.groups.add(group_chef) for (profile, trigramme) in zip(romains, romains_trigramme): account, created = Account.objects.get_or_create( trigramme=trigramme, cofprofile=profile, - defaults={'balance': random.randint(1, 999)/10} + defaults={"balance": random.randint(1, 999) / 10}, ) created_accounts += int(created) @@ -83,47 +89,50 @@ class Command(MyBaseCommand): profile.user.groups.add(group_boy) team_accounts += 1 - self.stdout.write("- {:d} comptes créés, {:d} dans l'équipe K-Fêt" - .format(created_accounts, team_accounts)) + self.stdout.write( + "- {:d} comptes créés, {:d} dans l'équipe K-Fêt".format( + created_accounts, team_accounts + ) + ) # Compte liquide self.stdout.write("Création du compte liquide") - liq_user, _ = User.objects.get_or_create(username='liquide') + liq_user, _ = User.objects.get_or_create(username="liquide") liq_profile, _ = CofProfile.objects.get_or_create(user=liq_user) - liq_account, _ = Account.objects.get_or_create(cofprofile=liq_profile, - trigramme='LIQ') + liq_account, _ = Account.objects.get_or_create( + cofprofile=liq_profile, trigramme="LIQ" + ) # Root account if existing - root_profile = CofProfile.objects.filter(user__username='root') + root_profile = CofProfile.objects.filter(user__username="root") if root_profile.exists(): self.stdout.write("Création du compte K-Fêt root") root_profile = root_profile.get() - Account.objects.get_or_create(cofprofile=root_profile, - trigramme='AAA') + Account.objects.get_or_create(cofprofile=root_profile, trigramme="AAA") # --- # Caisse # --- checkout, created = Checkout.objects.get_or_create( - created_by=Account.objects.get(trigramme='000'), - name='Chaudron', + created_by=Account.objects.get(trigramme="000"), + name="Chaudron", defaults={ - 'valid_from': timezone.now(), - 'valid_to': timezone.now() + timedelta(days=730) + "valid_from": timezone.now(), + "valid_to": timezone.now() + timedelta(days=730), }, ) if created: CheckoutStatement.objects.create( - by=Account.objects.get(trigramme='000'), + by=Account.objects.get(trigramme="000"), checkout=checkout, balance_old=0, balance_new=0, amount_taken=0, - amount_error=0 + amount_error=0, ) # --- @@ -135,10 +144,7 @@ class Command(MyBaseCommand): articles = random.sample(list(Article.objects.all()), 40) to_create = [] for article in articles: - to_create.append(SupplierArticle( - supplier=supplier, - article=article - )) + to_create.append(SupplierArticle(supplier=supplier, article=article)) SupplierArticle.objects.bulk_create(to_create) @@ -146,10 +152,10 @@ class Command(MyBaseCommand): # Opérations # --- - call_command('createopes', '100', '7', '--transfers=20') + call_command("createopes", "100", "7", "--transfers=20") # --- # Wagtail CMS # --- - call_command('kfet_loadwagtail') + call_command("kfet_loadwagtail") diff --git a/kfet/migrations/0001_initial.py b/kfet/migrations/0001_initial.py index 8f9b14fa..8dbad4a8 100644 --- a/kfet/migrations/0001_initial.py +++ b/kfet/migrations/0001_initial.py @@ -1,257 +1,713 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations -import django.db.models.deletion -import django.core.validators import datetime +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ - ('gestioncof', '0007_alter_club'), - ] + dependencies = [("gestioncof", "0007_alter_club")] operations = [ migrations.CreateModel( - name='Account', + name="Account", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('trigramme', models.CharField(max_length=3, validators=[django.core.validators.RegexValidator(regex='^[^a-z]{3}$')], unique=True)), - ('balance', models.DecimalField(decimal_places=2, default=0, max_digits=6)), - ('frozen', models.BooleanField(default=False)), - ('promo', models.IntegerField(null=True, blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016)], default=2015)), - ('nickname', models.CharField(max_length=255, blank=True, default='')), - ('password', models.CharField(max_length=255, blank=True, null=True, unique=True, default=None)), - ('cofprofile', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='account_kfet', to='gestioncof.CofProfile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "trigramme", + models.CharField( + max_length=3, + validators=[ + django.core.validators.RegexValidator(regex="^[^a-z]{3}$") + ], + unique=True, + ), + ), + ( + "balance", + models.DecimalField(decimal_places=2, default=0, max_digits=6), + ), + ("frozen", models.BooleanField(default=False)), + ( + "promo", + models.IntegerField( + null=True, + blank=True, + choices=[ + (1980, 1980), + (1981, 1981), + (1982, 1982), + (1983, 1983), + (1984, 1984), + (1985, 1985), + (1986, 1986), + (1987, 1987), + (1988, 1988), + (1989, 1989), + (1990, 1990), + (1991, 1991), + (1992, 1992), + (1993, 1993), + (1994, 1994), + (1995, 1995), + (1996, 1996), + (1997, 1997), + (1998, 1998), + (1999, 1999), + (2000, 2000), + (2001, 2001), + (2002, 2002), + (2003, 2003), + (2004, 2004), + (2005, 2005), + (2006, 2006), + (2007, 2007), + (2008, 2008), + (2009, 2009), + (2010, 2010), + (2011, 2011), + (2012, 2012), + (2013, 2013), + (2014, 2014), + (2015, 2015), + (2016, 2016), + ], + default=2015, + ), + ), + ("nickname", models.CharField(max_length=255, blank=True, default="")), + ( + "password", + models.CharField( + max_length=255, blank=True, null=True, unique=True, default=None + ), + ), + ( + "cofprofile", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="account_kfet", + to="gestioncof.CofProfile", + ), + ), ], ), migrations.CreateModel( - name='AccountNegative', + name="AccountNegative", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start', models.DateTimeField(default=datetime.datetime(2016, 8, 2, 10, 22, 1, 569492))), - ('balance_offset', models.DecimalField(decimal_places=2, max_digits=6)), - ('authorized_overdraft', models.DecimalField(decimal_places=2, default=0, max_digits=6)), - ('comment', models.CharField(max_length=255, blank=True)), - ('account', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='negative', to='kfet.Account')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "start", + models.DateTimeField( + default=datetime.datetime(2016, 8, 2, 10, 22, 1, 569492) + ), + ), + ("balance_offset", models.DecimalField(decimal_places=2, max_digits=6)), + ( + "authorized_overdraft", + models.DecimalField(decimal_places=2, default=0, max_digits=6), + ), + ("comment", models.CharField(max_length=255, blank=True)), + ( + "account", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="negative", + to="kfet.Account", + ), + ), ], ), migrations.CreateModel( - name='Article', + name="Article", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=45)), - ('is_sold', models.BooleanField(default=True)), - ('price', models.DecimalField(decimal_places=2, max_digits=6)), - ('stock', models.IntegerField(default=0)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=45)), + ("is_sold", models.BooleanField(default=True)), + ("price", models.DecimalField(decimal_places=2, max_digits=6)), + ("stock", models.IntegerField(default=0)), ], ), migrations.CreateModel( - name='ArticleCategory', + name="ArticleCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=45)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=45)), ], ), migrations.CreateModel( - name='ArticleRule', + name="ArticleRule", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ratio', models.PositiveSmallIntegerField()), - ('article_on', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='rule_on', to='kfet.Article')), - ('article_to', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='rule_to', to='kfet.Article')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("ratio", models.PositiveSmallIntegerField()), + ( + "article_on", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="rule_on", + to="kfet.Article", + ), + ), + ( + "article_to", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="rule_to", + to="kfet.Article", + ), + ), ], ), migrations.CreateModel( - name='Checkout', + name="Checkout", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=45)), - ('valid_from', models.DateTimeField()), - ('valid_to', models.DateTimeField()), - ('balance', models.DecimalField(decimal_places=2, max_digits=6)), - ('is_protected', models.BooleanField(default=False)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='kfet.Account')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=45)), + ("valid_from", models.DateTimeField()), + ("valid_to", models.DateTimeField()), + ("balance", models.DecimalField(decimal_places=2, max_digits=6)), + ("is_protected", models.BooleanField(default=False)), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="kfet.Account", + ), + ), ], ), migrations.CreateModel( - name='CheckoutTransfer', + name="CheckoutTransfer", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.DecimalField(decimal_places=2, max_digits=6)), - ('from_checkout', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers_from', to='kfet.Checkout')), - ('to_checkout', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers_to', to='kfet.Checkout')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=6)), + ( + "from_checkout", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="transfers_from", + to="kfet.Checkout", + ), + ), + ( + "to_checkout", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="transfers_to", + to="kfet.Checkout", + ), + ), ], ), migrations.CreateModel( - name='Inventory', + name="Inventory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('at', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("at", models.DateTimeField(auto_now_add=True)), ], ), migrations.CreateModel( - name='InventoryArticle', + name="InventoryArticle", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('stock_old', models.IntegerField()), - ('stock_new', models.IntegerField()), - ('stock_error', models.IntegerField(default=0)), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kfet.Article')), - ('inventory', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kfet.Inventory')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("stock_old", models.IntegerField()), + ("stock_new", models.IntegerField()), + ("stock_error", models.IntegerField(default=0)), + ( + "article", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="kfet.Article" + ), + ), + ( + "inventory", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="kfet.Inventory" + ), + ), ], ), migrations.CreateModel( - name='Operation', + name="Operation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(max_length=8, choices=[('purchase', 'Achat'), ('deposit', 'Charge'), ('withdraw', 'Retrait')])), - ('amount', models.DecimalField(decimal_places=2, max_digits=6)), - ('on_checkout', models.BooleanField(default=True)), - ('canceled_at', models.DateTimeField(blank=True, null=True, default=None)), - ('addcost_amount', models.DecimalField(decimal_places=2, max_digits=6)), - ('addcost_for', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='addcosts', to='kfet.Account', null=True, default=None)), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='operations', to='kfet.Article', null=True, default=None)), - ('canceled_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='+', to='kfet.Account', null=True, default=None)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.CharField( + max_length=8, + choices=[ + ("purchase", "Achat"), + ("deposit", "Charge"), + ("withdraw", "Retrait"), + ], + ), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=6)), + ("on_checkout", models.BooleanField(default=True)), + ( + "canceled_at", + models.DateTimeField(blank=True, null=True, default=None), + ), + ("addcost_amount", models.DecimalField(decimal_places=2, max_digits=6)), + ( + "addcost_for", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="addcosts", + to="kfet.Account", + null=True, + default=None, + ), + ), + ( + "article", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="operations", + to="kfet.Article", + null=True, + default=None, + ), + ), + ( + "canceled_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="+", + to="kfet.Account", + null=True, + default=None, + ), + ), ], ), migrations.CreateModel( - name='OperationGroup', + name="OperationGroup", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('at', models.DateTimeField(auto_now_add=True)), - ('amount', models.IntegerField()), - ('is_cof', models.BooleanField(default=False)), - ('comment', models.CharField(max_length=255, blank=True, default='')), - ('checkout', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='operations', to='kfet.Checkout')), - ('on_acc', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='operations', to='kfet.Account')), - ('valid_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='+', to='kfet.Account', null=True, default=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("at", models.DateTimeField(auto_now_add=True)), + ("amount", models.IntegerField()), + ("is_cof", models.BooleanField(default=False)), + ("comment", models.CharField(max_length=255, blank=True, default="")), + ( + "checkout", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="operations", + to="kfet.Checkout", + ), + ), + ( + "on_acc", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="operations", + to="kfet.Account", + ), + ), + ( + "valid_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="+", + to="kfet.Account", + null=True, + default=True, + ), + ), ], ), migrations.CreateModel( - name='Order', + name="Order", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('at', models.DateTimeField(auto_now_add=True)), - ('amount', models.DecimalField(decimal_places=2, max_digits=6)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("at", models.DateTimeField(auto_now_add=True)), + ("amount", models.DecimalField(decimal_places=2, max_digits=6)), ], ), migrations.CreateModel( - name='OrderArticle', + name="OrderArticle", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity_ordered', models.IntegerField()), - ('quantity_received', models.IntegerField()), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kfet.Article')), - ('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kfet.Order')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("quantity_ordered", models.IntegerField()), + ("quantity_received", models.IntegerField()), + ( + "article", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="kfet.Article" + ), + ), + ( + "order", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="kfet.Order" + ), + ), ], ), migrations.CreateModel( - name='Statement', + name="Statement", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('balance_old', models.DecimalField(decimal_places=2, max_digits=6)), - ('balance_new', models.DecimalField(decimal_places=2, max_digits=6)), - ('amount_taken', models.DecimalField(decimal_places=2, max_digits=6)), - ('amount_error', models.DecimalField(decimal_places=2, max_digits=6)), - ('at', models.DateTimeField(auto_now_add=True)), - ('by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='kfet.Account')), - ('checkout', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='statements', to='kfet.Checkout')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("balance_old", models.DecimalField(decimal_places=2, max_digits=6)), + ("balance_new", models.DecimalField(decimal_places=2, max_digits=6)), + ("amount_taken", models.DecimalField(decimal_places=2, max_digits=6)), + ("amount_error", models.DecimalField(decimal_places=2, max_digits=6)), + ("at", models.DateTimeField(auto_now_add=True)), + ( + "by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="kfet.Account", + ), + ), + ( + "checkout", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="statements", + to="kfet.Checkout", + ), + ), ], ), migrations.CreateModel( - name='Supplier', + name="Supplier", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=45)), - ('address', models.TextField()), - ('email', models.EmailField(max_length=254)), - ('phone', models.CharField(max_length=10)), - ('comment', models.TextField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=45)), + ("address", models.TextField()), + ("email", models.EmailField(max_length=254)), + ("phone", models.CharField(max_length=10)), + ("comment", models.TextField()), ], ), migrations.CreateModel( - name='SupplierArticle', + name="SupplierArticle", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('box_type', models.CharField(max_length=7, choices=[('caisse', 'Caisse'), ('carton', 'Carton'), ('palette', 'Palette'), ('fût', 'Fût')])), - ('box_capacity', models.PositiveSmallIntegerField()), - ('price_HT', models.DecimalField(decimal_places=4, max_digits=7)), - ('TVA', models.DecimalField(decimal_places=2, max_digits=4)), - ('rights', models.DecimalField(decimal_places=4, max_digits=7)), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kfet.Article')), - ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kfet.Supplier')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "box_type", + models.CharField( + max_length=7, + choices=[ + ("caisse", "Caisse"), + ("carton", "Carton"), + ("palette", "Palette"), + ("fût", "Fût"), + ], + ), + ), + ("box_capacity", models.PositiveSmallIntegerField()), + ("price_HT", models.DecimalField(decimal_places=4, max_digits=7)), + ("TVA", models.DecimalField(decimal_places=2, max_digits=4)), + ("rights", models.DecimalField(decimal_places=4, max_digits=7)), + ( + "article", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="kfet.Article" + ), + ), + ( + "supplier", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="kfet.Supplier" + ), + ), ], ), migrations.CreateModel( - name='Transfer', + name="Transfer", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.DecimalField(decimal_places=2, max_digits=6)), - ('canceled_at', models.DateTimeField(blank=True, null=True, default=None)), - ('canceled_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='+', to='kfet.Account', null=True, default=None)), - ('from_acc', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers_from', to='kfet.Account')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=6)), + ( + "canceled_at", + models.DateTimeField(blank=True, null=True, default=None), + ), + ( + "canceled_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="+", + to="kfet.Account", + null=True, + default=None, + ), + ), + ( + "from_acc", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="transfers_from", + to="kfet.Account", + ), + ), ], ), migrations.CreateModel( - name='TransferGroup', + name="TransferGroup", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('at', models.DateTimeField(auto_now_add=True)), - ('comment', models.CharField(max_length=255, blank=True, default='')), - ('valid_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='+', to='kfet.Account', null=True, default=None)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("at", models.DateTimeField(auto_now_add=True)), + ("comment", models.CharField(max_length=255, blank=True, default="")), + ( + "valid_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="+", + to="kfet.Account", + null=True, + default=None, + ), + ), ], ), migrations.AddField( - model_name='transfer', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers', to='kfet.TransferGroup'), + model_name="transfer", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="transfers", + to="kfet.TransferGroup", + ), ), migrations.AddField( - model_name='transfer', - name='to_acc', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers_to', to='kfet.Account'), + model_name="transfer", + name="to_acc", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="transfers_to", + to="kfet.Account", + ), ), migrations.AddField( - model_name='supplier', - name='articles', - field=models.ManyToManyField(related_name='suppliers', through='kfet.SupplierArticle', to='kfet.Article'), + model_name="supplier", + name="articles", + field=models.ManyToManyField( + related_name="suppliers", + through="kfet.SupplierArticle", + to="kfet.Article", + ), ), migrations.AddField( - model_name='order', - name='articles', - field=models.ManyToManyField(related_name='orders', through='kfet.OrderArticle', to='kfet.Article'), + model_name="order", + name="articles", + field=models.ManyToManyField( + related_name="orders", through="kfet.OrderArticle", to="kfet.Article" + ), ), migrations.AddField( - model_name='order', - name='supplier', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='kfet.Supplier'), + model_name="order", + name="supplier", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="orders", + to="kfet.Supplier", + ), ), migrations.AddField( - model_name='operation', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='kfet.OperationGroup'), + model_name="operation", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="kfet.OperationGroup", + ), ), migrations.AddField( - model_name='inventory', - name='articles', - field=models.ManyToManyField(related_name='inventories', through='kfet.InventoryArticle', to='kfet.Article'), + model_name="inventory", + name="articles", + field=models.ManyToManyField( + related_name="inventories", + through="kfet.InventoryArticle", + to="kfet.Article", + ), ), migrations.AddField( - model_name='inventory', - name='by', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='kfet.Account'), + model_name="inventory", + name="by", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="kfet.Account", + ), ), migrations.AddField( - model_name='inventory', - name='order', - field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, blank=True, related_name='inventory', to='kfet.Order', null=True, default=None), + model_name="inventory", + name="order", + field=models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + blank=True, + related_name="inventory", + to="kfet.Order", + null=True, + default=None, + ), ), migrations.AddField( - model_name='article', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='articles', to='kfet.ArticleCategory'), + model_name="article", + name="category", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="articles", + to="kfet.ArticleCategory", + ), ), ] diff --git a/kfet/migrations/0002_auto_20160802_2139.py b/kfet/migrations/0002_auto_20160802_2139.py index 0a59de44..39ccbbe5 100644 --- a/kfet/migrations/0002_auto_20160802_2139.py +++ b/kfet/migrations/0002_auto_20160802_2139.py @@ -1,24 +1,25 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import datetime +from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0001_initial'), - ] + dependencies = [("kfet", "0001_initial")] operations = [ migrations.AlterModelOptions( - name='account', - options={'permissions': (('is_team', 'Is part of the team'),)}, + name="account", + options={"permissions": (("is_team", "Is part of the team"),)}, ), migrations.AlterField( - model_name='accountnegative', - name='start', - field=models.DateTimeField(default=datetime.datetime(2016, 8, 2, 21, 39, 30, 52279)), + model_name="accountnegative", + name="start", + field=models.DateTimeField( + default=datetime.datetime(2016, 8, 2, 21, 39, 30, 52279) + ), ), ] diff --git a/kfet/migrations/0003_auto_20160802_2142.py b/kfet/migrations/0003_auto_20160802_2142.py index 586146de..c3bfda52 100644 --- a/kfet/migrations/0003_auto_20160802_2142.py +++ b/kfet/migrations/0003_auto_20160802_2142.py @@ -1,20 +1,19 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import datetime +from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0002_auto_20160802_2139'), - ] + dependencies = [("kfet", "0002_auto_20160802_2139")] operations = [ migrations.AlterField( - model_name='accountnegative', - name='start', + model_name="accountnegative", + name="start", field=models.DateTimeField(default=datetime.datetime.now), - ), + ) ] diff --git a/kfet/migrations/0004_auto_20160802_2144.py b/kfet/migrations/0004_auto_20160802_2144.py index b9e9d0d3..48646cef 100644 --- a/kfet/migrations/0004_auto_20160802_2144.py +++ b/kfet/migrations/0004_auto_20160802_2144.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0003_auto_20160802_2142'), - ] + dependencies = [("kfet", "0003_auto_20160802_2142")] operations = [ migrations.AlterField( - model_name='accountnegative', - name='balance_offset', + model_name="accountnegative", + name="balance_offset", field=models.DecimalField(decimal_places=2, max_digits=6, default=0), - ), + ) ] diff --git a/kfet/migrations/0005_auto_20160802_2154.py b/kfet/migrations/0005_auto_20160802_2154.py index a01419fc..49c96b1c 100644 --- a/kfet/migrations/0005_auto_20160802_2154.py +++ b/kfet/migrations/0005_auto_20160802_2154.py @@ -1,28 +1,31 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0004_auto_20160802_2144'), - ] + dependencies = [("kfet", "0004_auto_20160802_2144")] operations = [ migrations.CreateModel( - name='GlobalPermissions', + name="GlobalPermissions", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + verbose_name="ID", + auto_created=True, + ), + ) ], options={ - 'permissions': (('is_team', 'Is part of the team'),), - 'managed': False, + "permissions": (("is_team", "Is part of the team"),), + "managed": False, }, ), - migrations.AlterModelOptions( - name='account', - options={}, - ), + migrations.AlterModelOptions(name="account", options={}), ] diff --git a/kfet/migrations/0006_auto_20160804_0600.py b/kfet/migrations/0006_auto_20160804_0600.py index 063504b9..524e1352 100644 --- a/kfet/migrations/0006_auto_20160804_0600.py +++ b/kfet/migrations/0006_auto_20160804_0600.py @@ -1,28 +1,23 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0005_auto_20160802_2154'), - ] + dependencies = [("kfet", "0005_auto_20160802_2154")] operations = [ migrations.AlterModelOptions( - name='checkout', - options={'ordering': ['-valid_to']}, + name="checkout", options={"ordering": ["-valid_to"]} ), migrations.RenameField( - model_name='account', - old_name='frozen', - new_name='is_frozen', + model_name="account", old_name="frozen", new_name="is_frozen" ), migrations.AlterField( - model_name='checkout', - name='balance', + model_name="checkout", + name="balance", field=models.DecimalField(max_digits=6, default=0, decimal_places=2), ), ] diff --git a/kfet/migrations/0007_auto_20160804_0641.py b/kfet/migrations/0007_auto_20160804_0641.py index 70bff402..fb870a71 100644 --- a/kfet/migrations/0007_auto_20160804_0641.py +++ b/kfet/migrations/0007_auto_20160804_0641.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0006_auto_20160804_0600'), - ] + dependencies = [("kfet", "0006_auto_20160804_0600")] operations = [ migrations.AlterField( - model_name='article', - name='price', + model_name="article", + name="price", field=models.DecimalField(default=0, max_digits=6, decimal_places=2), - ), + ) ] diff --git a/kfet/migrations/0008_auto_20160804_1736.py b/kfet/migrations/0008_auto_20160804_1736.py index 1abbb76a..930562b0 100644 --- a/kfet/migrations/0008_auto_20160804_1736.py +++ b/kfet/migrations/0008_auto_20160804_1736.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0007_auto_20160804_0641'), - ] + dependencies = [("kfet", "0007_auto_20160804_0641")] operations = [ migrations.AlterField( - model_name='account', - name='trigramme', - field=models.CharField(unique=True, validators=[django.core.validators.RegexValidator(regex='^[^a-z]{3}$')], db_index=True, max_length=3), - ), + model_name="account", + name="trigramme", + field=models.CharField( + unique=True, + validators=[django.core.validators.RegexValidator(regex="^[^a-z]{3}$")], + db_index=True, + max_length=3, + ), + ) ] diff --git a/kfet/migrations/0009_auto_20160805_0720.py b/kfet/migrations/0009_auto_20160805_0720.py index 8e9a4db9..90a19749 100644 --- a/kfet/migrations/0009_auto_20160805_0720.py +++ b/kfet/migrations/0009_auto_20160805_0720.py @@ -1,24 +1,20 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0008_auto_20160804_1736'), - ] + dependencies = [("kfet", "0008_auto_20160804_1736")] operations = [ migrations.RenameField( - model_name='operation', - old_name='on_checkout', - new_name='is_checkout', + model_name="operation", old_name="on_checkout", new_name="is_checkout" ), migrations.AddField( - model_name='operation', - name='article_nb', + model_name="operation", + name="article_nb", field=models.PositiveSmallIntegerField(default=None, null=True, blank=True), ), ] diff --git a/kfet/migrations/0010_auto_20160806_2343.py b/kfet/migrations/0010_auto_20160806_2343.py index 60c8cc93..84267a6d 100644 --- a/kfet/migrations/0010_auto_20160806_2343.py +++ b/kfet/migrations/0010_auto_20160806_2343.py @@ -1,30 +1,35 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0009_auto_20160805_0720'), - ] + dependencies = [("kfet", "0009_auto_20160805_0720")] operations = [ migrations.AlterField( - model_name='operation', - name='addcost_amount', + model_name="operation", + name="addcost_amount", field=models.DecimalField(max_digits=6, default=0, decimal_places=2), ), migrations.AlterField( - model_name='operationgroup', - name='amount', + model_name="operationgroup", + name="amount", field=models.DecimalField(max_digits=6, default=0, decimal_places=2), ), migrations.AlterField( - model_name='operationgroup', - name='valid_by', - field=models.ForeignKey(default=None, related_name='+', to='kfet.Account', blank=True, null=True, on_delete=django.db.models.deletion.PROTECT), + model_name="operationgroup", + name="valid_by", + field=models.ForeignKey( + default=None, + related_name="+", + to="kfet.Account", + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + ), ), ] diff --git a/kfet/migrations/0011_auto_20160807_1720.py b/kfet/migrations/0011_auto_20160807_1720.py index 97525676..53064235 100644 --- a/kfet/migrations/0011_auto_20160807_1720.py +++ b/kfet/migrations/0011_auto_20160807_1720.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0010_auto_20160806_2343'), - ] + dependencies = [("kfet", "0010_auto_20160806_2343")] operations = [ migrations.AlterField( - model_name='operation', - name='amount', - field=models.DecimalField(decimal_places=2, max_digits=6, default=0, blank=True), - ), + model_name="operation", + name="amount", + field=models.DecimalField( + decimal_places=2, max_digits=6, default=0, blank=True + ), + ) ] diff --git a/kfet/migrations/0012_settings.py b/kfet/migrations/0012_settings.py index 8f0f6247..1bae911d 100644 --- a/kfet/migrations/0012_settings.py +++ b/kfet/migrations/0012_settings.py @@ -1,22 +1,37 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0011_auto_20160807_1720'), - ] + dependencies = [("kfet", "0011_auto_20160807_1720")] operations = [ migrations.CreateModel( - name='Settings', + name="Settings", fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), - ('name', models.CharField(max_length=45)), - ('value_decimal', models.DecimalField(null=True, max_digits=6, decimal_places=2, blank=True, default=None)), + ( + "id", + models.AutoField( + serialize=False, + auto_created=True, + primary_key=True, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=45)), + ( + "value_decimal", + models.DecimalField( + null=True, + max_digits=6, + decimal_places=2, + blank=True, + default=None, + ), + ), ], - ), + ) ] diff --git a/kfet/migrations/0013_auto_20160807_1840.py b/kfet/migrations/0013_auto_20160807_1840.py index d7ce2c75..9a9ac3b1 100644 --- a/kfet/migrations/0013_auto_20160807_1840.py +++ b/kfet/migrations/0013_auto_20160807_1840.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0012_settings'), - ] + dependencies = [("kfet", "0012_settings")] operations = [ migrations.AlterField( - model_name='settings', - name='name', + model_name="settings", + name="name", field=models.CharField(unique=True, max_length=45), - ), + ) ] diff --git a/kfet/migrations/0014_auto_20160807_2314.py b/kfet/migrations/0014_auto_20160807_2314.py index 50417091..ecaee428 100644 --- a/kfet/migrations/0014_auto_20160807_2314.py +++ b/kfet/migrations/0014_auto_20160807_2314.py @@ -1,18 +1,22 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0013_auto_20160807_1840'), - ] + dependencies = [("kfet", "0013_auto_20160807_1840")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('can_perform_deposit', 'Peut effectuer une charge')), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("can_perform_deposit", "Peut effectuer une charge"), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0015_auto_20160807_2324.py b/kfet/migrations/0015_auto_20160807_2324.py index a1789fc2..fa2af882 100644 --- a/kfet/migrations/0015_auto_20160807_2324.py +++ b/kfet/migrations/0015_auto_20160807_2324.py @@ -1,18 +1,26 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0014_auto_20160807_2314'), - ] + dependencies = [("kfet", "0014_auto_20160807_2314")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('can_perform_deposit', 'Peut effectuer une charge'), ('can_perform_negative_operations', 'Peut enregistrer des commandes en négatif')), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("can_perform_deposit", "Peut effectuer une charge"), + ( + "can_perform_negative_operations", + "Peut enregistrer des commandes en négatif", + ), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0016_settings_value_account.py b/kfet/migrations/0016_settings_value_account.py index 53793938..e10eb682 100644 --- a/kfet/migrations/0016_settings_value_account.py +++ b/kfet/migrations/0016_settings_value_account.py @@ -1,20 +1,24 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0015_auto_20160807_2324'), - ] + dependencies = [("kfet", "0015_auto_20160807_2324")] operations = [ migrations.AddField( - model_name='settings', - name='value_account', - field=models.ForeignKey(to='kfet.Account', on_delete=django.db.models.deletion.PROTECT, default=None, null=True, blank=True), - ), + model_name="settings", + name="value_account", + field=models.ForeignKey( + to="kfet.Account", + on_delete=django.db.models.deletion.PROTECT, + default=None, + null=True, + blank=True, + ), + ) ] diff --git a/kfet/migrations/0017_auto_20160808_0234.py b/kfet/migrations/0017_auto_20160808_0234.py index e8aa8ec0..795078a9 100644 --- a/kfet/migrations/0017_auto_20160808_0234.py +++ b/kfet/migrations/0017_auto_20160808_0234.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0016_settings_value_account'), - ] + dependencies = [("kfet", "0016_settings_value_account")] operations = [ migrations.AlterField( - model_name='operation', - name='addcost_amount', - field=models.DecimalField(blank=True, null=True, decimal_places=2, default=None, max_digits=6), - ), + model_name="operation", + name="addcost_amount", + field=models.DecimalField( + blank=True, null=True, decimal_places=2, default=None, max_digits=6 + ), + ) ] diff --git a/kfet/migrations/0018_auto_20160808_0341.py b/kfet/migrations/0018_auto_20160808_0341.py index 384e82b2..3b29b716 100644 --- a/kfet/migrations/0018_auto_20160808_0341.py +++ b/kfet/migrations/0018_auto_20160808_0341.py @@ -1,18 +1,27 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0017_auto_20160808_0234'), - ] + dependencies = [("kfet", "0017_auto_20160808_0234")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('can_perform_deposit', 'Effectuer une charge'), ('can_perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte")), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("can_perform_deposit", "Effectuer une charge"), + ( + "can_perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0019_auto_20160808_0343.py b/kfet/migrations/0019_auto_20160808_0343.py index 6512b7ea..f500032a 100644 --- a/kfet/migrations/0019_auto_20160808_0343.py +++ b/kfet/migrations/0019_auto_20160808_0343.py @@ -1,18 +1,27 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0018_auto_20160808_0341'), - ] + dependencies = [("kfet", "0018_auto_20160808_0341")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"))}, - ), + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ), + }, + ) ] diff --git a/kfet/migrations/0020_auto_20160808_0450.py b/kfet/migrations/0020_auto_20160808_0450.py index 2ecc18ee..d3424bac 100644 --- a/kfet/migrations/0020_auto_20160808_0450.py +++ b/kfet/migrations/0020_auto_20160808_0450.py @@ -1,20 +1,21 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import datetime +from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0019_auto_20160808_0343'), - ] + dependencies = [("kfet", "0019_auto_20160808_0343")] operations = [ migrations.AlterField( - model_name='accountnegative', - name='start', - field=models.DateTimeField(default=datetime.datetime.now, blank=True, null=True), - ), + model_name="accountnegative", + name="start", + field=models.DateTimeField( + default=datetime.datetime.now, blank=True, null=True + ), + ) ] diff --git a/kfet/migrations/0021_auto_20160808_0506.py b/kfet/migrations/0021_auto_20160808_0506.py index 61a7ef65..2ef48232 100644 --- a/kfet/migrations/0021_auto_20160808_0506.py +++ b/kfet/migrations/0021_auto_20160808_0506.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0020_auto_20160808_0450'), - ] + dependencies = [("kfet", "0020_auto_20160808_0450")] operations = [ migrations.AlterField( - model_name='accountnegative', - name='start', + model_name="accountnegative", + name="start", field=models.DateTimeField(default=None, blank=True, null=True), - ), + ) ] diff --git a/kfet/migrations/0022_auto_20160808_0512.py b/kfet/migrations/0022_auto_20160808_0512.py index ba5de03e..3701e856 100644 --- a/kfet/migrations/0022_auto_20160808_0512.py +++ b/kfet/migrations/0022_auto_20160808_0512.py @@ -1,24 +1,26 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0021_auto_20160808_0506'), - ] + dependencies = [("kfet", "0021_auto_20160808_0506")] operations = [ migrations.AlterField( - model_name='accountnegative', - name='authorized_overdraft', - field=models.DecimalField(blank=True, decimal_places=2, null=True, default=None, max_digits=6), + model_name="accountnegative", + name="authorized_overdraft", + field=models.DecimalField( + blank=True, decimal_places=2, null=True, default=None, max_digits=6 + ), ), migrations.AlterField( - model_name='accountnegative', - name='balance_offset', - field=models.DecimalField(blank=True, decimal_places=2, null=True, default=None, max_digits=6), + model_name="accountnegative", + name="balance_offset", + field=models.DecimalField( + blank=True, decimal_places=2, null=True, default=None, max_digits=6 + ), ), ] diff --git a/kfet/migrations/0023_auto_20160808_0535.py b/kfet/migrations/0023_auto_20160808_0535.py index 7e4d7051..03a8d1c3 100644 --- a/kfet/migrations/0023_auto_20160808_0535.py +++ b/kfet/migrations/0023_auto_20160808_0535.py @@ -1,24 +1,22 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0022_auto_20160808_0512'), - ] + dependencies = [("kfet", "0022_auto_20160808_0512")] operations = [ migrations.RenameField( - model_name='accountnegative', - old_name='authorized_overdraft', - new_name='authz_overdraft_amount', + model_name="accountnegative", + old_name="authorized_overdraft", + new_name="authz_overdraft_amount", ), migrations.AddField( - model_name='accountnegative', - name='authz_overdraft_until', + model_name="accountnegative", + name="authz_overdraft_until", field=models.DateTimeField(null=True, default=None, blank=True), ), ] diff --git a/kfet/migrations/0024_settings_value_duration.py b/kfet/migrations/0024_settings_value_duration.py index 1f90b1c9..56b22812 100644 --- a/kfet/migrations/0024_settings_value_duration.py +++ b/kfet/migrations/0024_settings_value_duration.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0023_auto_20160808_0535'), - ] + dependencies = [("kfet", "0023_auto_20160808_0535")] operations = [ migrations.AddField( - model_name='settings', - name='value_duration', + model_name="settings", + name="value_duration", field=models.DurationField(null=True, default=None, blank=True), - ), + ) ] diff --git a/kfet/migrations/0025_auto_20160809_0750.py b/kfet/migrations/0025_auto_20160809_0750.py index 8ba90e2d..51f3b5a3 100644 --- a/kfet/migrations/0025_auto_20160809_0750.py +++ b/kfet/migrations/0025_auto_20160809_0750.py @@ -1,18 +1,28 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0024_settings_value_duration'), - ] + dependencies = [("kfet", "0024_settings_value_duration")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes')), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0026_auto_20160809_0810.py b/kfet/migrations/0026_auto_20160809_0810.py index 7e96c937..942eaf26 100644 --- a/kfet/migrations/0026_auto_20160809_0810.py +++ b/kfet/migrations/0026_auto_20160809_0810.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0025_auto_20160809_0750'), - ] + dependencies = [("kfet", "0025_auto_20160809_0750")] operations = [ migrations.AlterField( - model_name='settings', - name='name', + model_name="settings", + name="name", field=models.CharField(db_index=True, max_length=45, unique=True), - ), + ) ] diff --git a/kfet/migrations/0027_auto_20160811_0648.py b/kfet/migrations/0027_auto_20160811_0648.py index 25bd826b..a0084e8a 100644 --- a/kfet/migrations/0027_auto_20160811_0648.py +++ b/kfet/migrations/0027_auto_20160811_0648.py @@ -1,39 +1,51 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0026_auto_20160809_0810'), - ] + dependencies = [("kfet", "0026_auto_20160809_0810")] operations = [ migrations.CreateModel( - name='CheckoutStatement', + name="CheckoutStatement", fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('balance_old', models.DecimalField(decimal_places=2, max_digits=6)), - ('balance_new', models.DecimalField(decimal_places=2, max_digits=6)), - ('amount_taken', models.DecimalField(decimal_places=2, max_digits=6)), - ('amount_error', models.DecimalField(decimal_places=2, max_digits=6)), - ('at', models.DateTimeField(auto_now_add=True)), - ('by', models.ForeignKey(to='kfet.Account', on_delete=django.db.models.deletion.PROTECT, related_name='+')), - ('checkout', models.ForeignKey(to='kfet.Checkout', on_delete=django.db.models.deletion.PROTECT, related_name='statements')), + ( + "id", + models.AutoField( + verbose_name="ID", + primary_key=True, + serialize=False, + auto_created=True, + ), + ), + ("balance_old", models.DecimalField(decimal_places=2, max_digits=6)), + ("balance_new", models.DecimalField(decimal_places=2, max_digits=6)), + ("amount_taken", models.DecimalField(decimal_places=2, max_digits=6)), + ("amount_error", models.DecimalField(decimal_places=2, max_digits=6)), + ("at", models.DateTimeField(auto_now_add=True)), + ( + "by", + models.ForeignKey( + to="kfet.Account", + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + ), + ), + ( + "checkout", + models.ForeignKey( + to="kfet.Checkout", + on_delete=django.db.models.deletion.PROTECT, + related_name="statements", + ), + ), ], ), - migrations.RemoveField( - model_name='statement', - name='by', - ), - migrations.RemoveField( - model_name='statement', - name='checkout', - ), - migrations.DeleteModel( - name='Statement', - ), + migrations.RemoveField(model_name="statement", name="by"), + migrations.RemoveField(model_name="statement", name="checkout"), + migrations.DeleteModel(name="Statement"), ] diff --git a/kfet/migrations/0028_auto_20160820_0146.py b/kfet/migrations/0028_auto_20160820_0146.py index 5f8fa377..a1b046cf 100644 --- a/kfet/migrations/0028_auto_20160820_0146.py +++ b/kfet/migrations/0028_auto_20160820_0146.py @@ -1,30 +1,40 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0027_auto_20160811_0648'), - ] + dependencies = [("kfet", "0027_auto_20160811_0648")] operations = [ migrations.AlterField( - model_name='operation', - name='group', - field=models.ForeignKey(to='kfet.OperationGroup', on_delete=django.db.models.deletion.PROTECT, related_name='opes'), + model_name="operation", + name="group", + field=models.ForeignKey( + to="kfet.OperationGroup", + on_delete=django.db.models.deletion.PROTECT, + related_name="opes", + ), ), migrations.AlterField( - model_name='operationgroup', - name='checkout', - field=models.ForeignKey(to='kfet.Checkout', on_delete=django.db.models.deletion.PROTECT, related_name='opesgroup'), + model_name="operationgroup", + name="checkout", + field=models.ForeignKey( + to="kfet.Checkout", + on_delete=django.db.models.deletion.PROTECT, + related_name="opesgroup", + ), ), migrations.AlterField( - model_name='operationgroup', - name='on_acc', - field=models.ForeignKey(to='kfet.Account', on_delete=django.db.models.deletion.PROTECT, related_name='opesgroup'), + model_name="operationgroup", + name="on_acc", + field=models.ForeignKey( + to="kfet.Account", + on_delete=django.db.models.deletion.PROTECT, + related_name="opesgroup", + ), ), ] diff --git a/kfet/migrations/0029_genericteamtoken.py b/kfet/migrations/0029_genericteamtoken.py index ba13674c..c5a81f05 100644 --- a/kfet/migrations/0029_genericteamtoken.py +++ b/kfet/migrations/0029_genericteamtoken.py @@ -1,21 +1,27 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0028_auto_20160820_0146'), - ] + dependencies = [("kfet", "0028_auto_20160820_0146")] operations = [ migrations.CreateModel( - name='GenericTeamToken', + name="GenericTeamToken", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('token', models.CharField(unique=True, max_length=50)), + ( + "id", + models.AutoField( + serialize=False, + primary_key=True, + auto_created=True, + verbose_name="ID", + ), + ), + ("token", models.CharField(unique=True, max_length=50)), ], - ), + ) ] diff --git a/kfet/migrations/0030_auto_20160821_0029.py b/kfet/migrations/0030_auto_20160821_0029.py index ed54efa9..1858522d 100644 --- a/kfet/migrations/0030_auto_20160821_0029.py +++ b/kfet/migrations/0030_auto_20160821_0029.py @@ -1,18 +1,29 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0029_genericteamtoken'), - ] + dependencies = [("kfet", "0029_genericteamtoken")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt')), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0031_auto_20160822_0523.py b/kfet/migrations/0031_auto_20160822_0523.py index e7ca4d6f..23aec1be 100644 --- a/kfet/migrations/0031_auto_20160822_0523.py +++ b/kfet/migrations/0031_auto_20160822_0523.py @@ -1,18 +1,30 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0030_auto_20160821_0029'), - ] + dependencies = [("kfet", "0030_auto_20160821_0029")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations')), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0032_auto_20160822_2350.py b/kfet/migrations/0032_auto_20160822_2350.py index 142fb29d..7cba0da3 100644 --- a/kfet/migrations/0032_auto_20160822_2350.py +++ b/kfet/migrations/0032_auto_20160822_2350.py @@ -1,94 +1,92 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0031_auto_20160822_0523'), - ] + dependencies = [("kfet", "0031_auto_20160822_0523")] operations = [ migrations.AddField( - model_name='checkoutstatement', - name='taken_001', + model_name="checkoutstatement", + name="taken_001", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_002', + model_name="checkoutstatement", + name="taken_002", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_005', + model_name="checkoutstatement", + name="taken_005", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_01', + model_name="checkoutstatement", + name="taken_01", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_02', + model_name="checkoutstatement", + name="taken_02", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_05', + model_name="checkoutstatement", + name="taken_05", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_1', + model_name="checkoutstatement", + name="taken_1", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_10', + model_name="checkoutstatement", + name="taken_10", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_100', + model_name="checkoutstatement", + name="taken_100", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_2', + model_name="checkoutstatement", + name="taken_2", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_20', + model_name="checkoutstatement", + name="taken_20", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_200', + model_name="checkoutstatement", + name="taken_200", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_5', + model_name="checkoutstatement", + name="taken_5", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_50', + model_name="checkoutstatement", + name="taken_50", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_500', + model_name="checkoutstatement", + name="taken_500", field=models.PositiveSmallIntegerField(default=0), ), migrations.AddField( - model_name='checkoutstatement', - name='taken_cheque', + model_name="checkoutstatement", + name="taken_cheque", field=models.PositiveSmallIntegerField(default=0), ), ] diff --git a/kfet/migrations/0033_checkoutstatement_not_count.py b/kfet/migrations/0033_checkoutstatement_not_count.py index 50c58256..dd445406 100644 --- a/kfet/migrations/0033_checkoutstatement_not_count.py +++ b/kfet/migrations/0033_checkoutstatement_not_count.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0032_auto_20160822_2350'), - ] + dependencies = [("kfet", "0032_auto_20160822_2350")] operations = [ migrations.AddField( - model_name='checkoutstatement', - name='not_count', + model_name="checkoutstatement", + name="not_count", field=models.BooleanField(default=False), - ), + ) ] diff --git a/kfet/migrations/0034_auto_20160823_0206.py b/kfet/migrations/0034_auto_20160823_0206.py index 90d0965c..1b28e289 100644 --- a/kfet/migrations/0034_auto_20160823_0206.py +++ b/kfet/migrations/0034_auto_20160823_0206.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0033_checkoutstatement_not_count'), - ] + dependencies = [("kfet", "0033_checkoutstatement_not_count")] operations = [ migrations.AlterField( - model_name='checkoutstatement', - name='taken_cheque', + model_name="checkoutstatement", + name="taken_cheque", field=models.DecimalField(max_digits=6, decimal_places=2, default=0), - ), + ) ] diff --git a/kfet/migrations/0035_auto_20160823_1505.py b/kfet/migrations/0035_auto_20160823_1505.py index 5fd73ae8..e2a98ca7 100644 --- a/kfet/migrations/0035_auto_20160823_1505.py +++ b/kfet/migrations/0035_auto_20160823_1505.py @@ -1,18 +1,34 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0034_auto_20160823_0206'), - ] + dependencies = [("kfet", "0034_auto_20160823_0206")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'))}, - ), + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ), + }, + ) ] diff --git a/kfet/migrations/0036_auto_20160823_1910.py b/kfet/migrations/0036_auto_20160823_1910.py index 2d29fd7a..09726e37 100644 --- a/kfet/migrations/0036_auto_20160823_1910.py +++ b/kfet/migrations/0036_auto_20160823_1910.py @@ -1,18 +1,35 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0035_auto_20160823_1505'), - ] + dependencies = [("kfet", "0035_auto_20160823_1505")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'), ('view_negs', 'Voir la liste des négatifs'))}, - ), + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ("view_negs", "Voir la liste des négatifs"), + ), + }, + ) ] diff --git a/kfet/migrations/0037_auto_20160826_2333.py b/kfet/migrations/0037_auto_20160826_2333.py index 9f937b60..6ebc921d 100644 --- a/kfet/migrations/0037_auto_20160826_2333.py +++ b/kfet/migrations/0037_auto_20160826_2333.py @@ -1,39 +1,54 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0036_auto_20160823_1910'), - ] + dependencies = [("kfet", "0036_auto_20160823_1910")] operations = [ migrations.AlterField( - model_name='supplierarticle', - name='TVA', - field=models.DecimalField(null=True, max_digits=4, decimal_places=2, default=None, blank=True), + model_name="supplierarticle", + name="TVA", + field=models.DecimalField( + null=True, max_digits=4, decimal_places=2, default=None, blank=True + ), ), migrations.AlterField( - model_name='supplierarticle', - name='box_capacity', + model_name="supplierarticle", + name="box_capacity", field=models.PositiveSmallIntegerField(null=True, default=None, blank=True), ), migrations.AlterField( - model_name='supplierarticle', - name='box_type', - field=models.CharField(null=True, max_length=7, choices=[('caisse', 'Caisse'), ('carton', 'Carton'), ('palette', 'Palette'), ('fût', 'Fût')], default=None, blank=True), + model_name="supplierarticle", + name="box_type", + field=models.CharField( + null=True, + max_length=7, + choices=[ + ("caisse", "Caisse"), + ("carton", "Carton"), + ("palette", "Palette"), + ("fût", "Fût"), + ], + default=None, + blank=True, + ), ), migrations.AlterField( - model_name='supplierarticle', - name='price_HT', - field=models.DecimalField(null=True, max_digits=7, decimal_places=4, default=None, blank=True), + model_name="supplierarticle", + name="price_HT", + field=models.DecimalField( + null=True, max_digits=7, decimal_places=4, default=None, blank=True + ), ), migrations.AlterField( - model_name='supplierarticle', - name='rights', - field=models.DecimalField(null=True, max_digits=7, decimal_places=4, default=None, blank=True), + model_name="supplierarticle", + name="rights", + field=models.DecimalField( + null=True, max_digits=7, decimal_places=4, default=None, blank=True + ), ), ] diff --git a/kfet/migrations/0038_auto_20160828_0402.py b/kfet/migrations/0038_auto_20160828_0402.py index 215591b7..be8d847e 100644 --- a/kfet/migrations/0038_auto_20160828_0402.py +++ b/kfet/migrations/0038_auto_20160828_0402.py @@ -1,36 +1,36 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0037_auto_20160826_2333'), - ] + dependencies = [("kfet", "0037_auto_20160826_2333")] operations = [ - migrations.AlterModelOptions( - name='inventory', - options={'ordering': ['-at']}, - ), - migrations.RemoveField( - model_name='supplierarticle', - name='box_capacity', - ), - migrations.RemoveField( - model_name='supplierarticle', - name='box_type', - ), + migrations.AlterModelOptions(name="inventory", options={"ordering": ["-at"]}), + migrations.RemoveField(model_name="supplierarticle", name="box_capacity"), + migrations.RemoveField(model_name="supplierarticle", name="box_type"), migrations.AddField( - model_name='article', - name='box_capacity', + model_name="article", + name="box_capacity", field=models.PositiveSmallIntegerField(blank=True, null=True, default=None), ), migrations.AddField( - model_name='article', - name='box_type', - field=models.CharField(max_length=7, blank=True, null=True, default=None, choices=[('caisse', 'Caisse'), ('carton', 'Carton'), ('palette', 'Palette'), ('fût', 'Fût')]), + model_name="article", + name="box_type", + field=models.CharField( + max_length=7, + blank=True, + null=True, + default=None, + choices=[ + ("caisse", "Caisse"), + ("carton", "Carton"), + ("palette", "Palette"), + ("fût", "Fût"), + ], + ), ), ] diff --git a/kfet/migrations/0039_auto_20160828_0430.py b/kfet/migrations/0039_auto_20160828_0430.py index 271fda68..2611d578 100644 --- a/kfet/migrations/0039_auto_20160828_0430.py +++ b/kfet/migrations/0039_auto_20160828_0430.py @@ -1,24 +1,22 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0038_auto_20160828_0402'), - ] + dependencies = [("kfet", "0038_auto_20160828_0402")] operations = [ migrations.AlterField( - model_name='order', - name='amount', + model_name="order", + name="amount", field=models.DecimalField(default=0, decimal_places=2, max_digits=6), ), migrations.AlterField( - model_name='orderarticle', - name='quantity_received', + model_name="orderarticle", + name="quantity_received", field=models.IntegerField(default=0), ), ] diff --git a/kfet/migrations/0040_auto_20160829_2035.py b/kfet/migrations/0040_auto_20160829_2035.py index 78b577a8..16bd2b36 100644 --- a/kfet/migrations/0040_auto_20160829_2035.py +++ b/kfet/migrations/0040_auto_20160829_2035.py @@ -1,31 +1,41 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations import datetime + +from django.db import migrations, models from django.utils.timezone import utc class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0039_auto_20160828_0430'), - ] + dependencies = [("kfet", "0039_auto_20160828_0430")] operations = [ - migrations.AlterModelOptions( - name='order', - options={'ordering': ['-at']}, - ), + migrations.AlterModelOptions(name="order", options={"ordering": ["-at"]}), migrations.AddField( - model_name='supplierarticle', - name='at', - field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 8, 29, 18, 35, 3, 419033, tzinfo=utc)), + model_name="supplierarticle", + name="at", + field=models.DateTimeField( + auto_now_add=True, + default=datetime.datetime(2016, 8, 29, 18, 35, 3, 419033, tzinfo=utc), + ), preserve_default=False, ), migrations.AlterField( - model_name='article', - name='box_type', - field=models.CharField(choices=[('caisse', 'caisse'), ('carton', 'carton'), ('palette', 'palette'), ('fût', 'fût')], null=True, max_length=7, blank=True, default=None), + model_name="article", + name="box_type", + field=models.CharField( + choices=[ + ("caisse", "caisse"), + ("carton", "carton"), + ("palette", "palette"), + ("fût", "fût"), + ], + null=True, + max_length=7, + blank=True, + default=None, + ), ), ] diff --git a/kfet/migrations/0041_auto_20160830_1502.py b/kfet/migrations/0041_auto_20160830_1502.py index 40c83907..488d33ff 100644 --- a/kfet/migrations/0041_auto_20160830_1502.py +++ b/kfet/migrations/0041_auto_20160830_1502.py @@ -1,18 +1,39 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0040_auto_20160829_2035'), - ] + dependencies = [("kfet", "0040_auto_20160829_2035")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'), ('view_negs', 'Voir la liste des négatifs'), ('order_to_inventory', "Générer un inventaire à partir d'une commande")), 'managed': False}, - ), + name="globalpermissions", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ("view_negs", "Voir la liste des négatifs"), + ( + "order_to_inventory", + "Générer un inventaire à partir d'une commande", + ), + ), + "managed": False, + }, + ) ] diff --git a/kfet/migrations/0042_auto_20160831_0126.py b/kfet/migrations/0042_auto_20160831_0126.py index 3306c401..70adbad5 100644 --- a/kfet/migrations/0042_auto_20160831_0126.py +++ b/kfet/migrations/0042_auto_20160831_0126.py @@ -1,18 +1,40 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0041_auto_20160830_1502'), - ] + dependencies = [("kfet", "0041_auto_20160830_1502")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'), ('view_negs', 'Voir la liste des négatifs'), ('order_to_inventory', "Générer un inventaire à partir d'une commande"), ('edit_balance_account', "Modifier la balance d'un compte"))}, - ), + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ("view_negs", "Voir la liste des négatifs"), + ( + "order_to_inventory", + "Générer un inventaire à partir d'une commande", + ), + ("edit_balance_account", "Modifier la balance d'un compte"), + ), + }, + ) ] diff --git a/kfet/migrations/0043_auto_20160901_0046.py b/kfet/migrations/0043_auto_20160901_0046.py index 2d9bf12a..b5132335 100644 --- a/kfet/migrations/0043_auto_20160901_0046.py +++ b/kfet/migrations/0043_auto_20160901_0046.py @@ -1,19 +1,60 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0042_auto_20160831_0126'), - ] + dependencies = [("kfet", "0042_auto_20160831_0126")] operations = [ migrations.AlterField( - model_name='account', - name='promo', - field=models.IntegerField(blank=True, default=2016, null=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016)]), - ), + model_name="account", + name="promo", + field=models.IntegerField( + blank=True, + default=2016, + null=True, + choices=[ + (1980, 1980), + (1981, 1981), + (1982, 1982), + (1983, 1983), + (1984, 1984), + (1985, 1985), + (1986, 1986), + (1987, 1987), + (1988, 1988), + (1989, 1989), + (1990, 1990), + (1991, 1991), + (1992, 1992), + (1993, 1993), + (1994, 1994), + (1995, 1995), + (1996, 1996), + (1997, 1997), + (1998, 1998), + (1999, 1999), + (2000, 2000), + (2001, 2001), + (2002, 2002), + (2003, 2003), + (2004, 2004), + (2005, 2005), + (2006, 2006), + (2007, 2007), + (2008, 2008), + (2009, 2009), + (2010, 2010), + (2011, 2011), + (2012, 2012), + (2013, 2013), + (2014, 2014), + (2015, 2015), + (2016, 2016), + ], + ), + ) ] diff --git a/kfet/migrations/0044_auto_20160901_1614.py b/kfet/migrations/0044_auto_20160901_1614.py index 2a91206a..f81ae828 100644 --- a/kfet/migrations/0044_auto_20160901_1614.py +++ b/kfet/migrations/0044_auto_20160901_1614.py @@ -1,18 +1,44 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0043_auto_20160901_0046'), - ] + dependencies = [("kfet", "0043_auto_20160901_0046")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en n\xe9gatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non r\xe9centes'), ('manage_perms', 'G\xe9rer les permissions K-F\xeat'), ('manage_addcosts', 'G\xe9rer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'), ('view_negs', 'Voir la liste des n\xe9gatifs'), ('order_to_inventory', "G\xe9n\xe9rer un inventaire \xe0 partir d'une commande"), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'\xe9quipe"))}, - ), + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en n\xe9gatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non r\xe9centes"), + ("manage_perms", "G\xe9rer les permissions K-F\xeat"), + ("manage_addcosts", "G\xe9rer les majorations"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ("view_negs", "Voir la liste des n\xe9gatifs"), + ( + "order_to_inventory", + "G\xe9n\xe9rer un inventaire \xe0 partir d'une commande", + ), + ("edit_balance_account", "Modifier la balance d'un compte"), + ( + "change_account_password", + "Modifier le mot de passe d'une personne de l'\xe9quipe", + ), + ), + }, + ) ] diff --git a/kfet/migrations/0045_auto_20160905_0705.py b/kfet/migrations/0045_auto_20160905_0705.py index 0673fdca..0f98c56a 100644 --- a/kfet/migrations/0045_auto_20160905_0705.py +++ b/kfet/migrations/0045_auto_20160905_0705.py @@ -1,23 +1,61 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0044_auto_20160901_1614'), - ] + dependencies = [("kfet", "0044_auto_20160901_1614")] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en n\xe9gatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non r\xe9centes'), ('manage_perms', 'G\xe9rer les permissions K-F\xeat'), ('manage_addcosts', 'G\xe9rer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'), ('view_negs', 'Voir la liste des n\xe9gatifs'), ('order_to_inventory', "G\xe9n\xe9rer un inventaire \xe0 partir d'une commande"), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'\xe9quipe"), ('special_add_account', 'Cr\xe9er un compte avec une balance initiale'))}, + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("is_team", "Is part of the team"), + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en n\xe9gatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non r\xe9centes"), + ("manage_perms", "G\xe9rer les permissions K-F\xeat"), + ("manage_addcosts", "G\xe9rer les majorations"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ("view_negs", "Voir la liste des n\xe9gatifs"), + ( + "order_to_inventory", + "G\xe9n\xe9rer un inventaire \xe0 partir d'une commande", + ), + ("edit_balance_account", "Modifier la balance d'un compte"), + ( + "change_account_password", + "Modifier le mot de passe d'une personne de l'\xe9quipe", + ), + ( + "special_add_account", + "Cr\xe9er un compte avec une balance initiale", + ), + ), + }, ), migrations.AlterField( - model_name='operation', - name='type', - field=models.CharField(max_length=8, choices=[('purchase', 'Achat'), ('deposit', 'Charge'), ('withdraw', 'Retrait'), ('initial', 'Initial')]), + model_name="operation", + name="type", + field=models.CharField( + max_length=8, + choices=[ + ("purchase", "Achat"), + ("deposit", "Charge"), + ("withdraw", "Retrait"), + ("initial", "Initial"), + ], + ), ), ] diff --git a/kfet/migrations/0046_account_created_at.py b/kfet/migrations/0046_account_created_at.py index a624c0fb..a0274432 100644 --- a/kfet/migrations/0046_account_created_at.py +++ b/kfet/migrations/0046_account_created_at.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0045_auto_20160905_0705'), - ] + dependencies = [("kfet", "0045_auto_20160905_0705")] operations = [ migrations.AddField( - model_name='account', - name='created_at', + model_name="account", + name="created_at", field=models.DateTimeField(auto_now_add=True, null=True), - ), + ) ] diff --git a/kfet/migrations/0047_auto_20170104_1528.py b/kfet/migrations/0047_auto_20170104_1528.py index d59447af..d391e1f4 100644 --- a/kfet/migrations/0047_auto_20170104_1528.py +++ b/kfet/migrations/0047_auto_20170104_1528.py @@ -6,14 +6,56 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0046_account_created_at'), - ] + dependencies = [("kfet", "0046_account_created_at")] operations = [ migrations.AlterField( - model_name='account', - name='promo', - field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017)], default=2016, null=True), - ), + model_name="account", + name="promo", + field=models.IntegerField( + blank=True, + choices=[ + (1980, 1980), + (1981, 1981), + (1982, 1982), + (1983, 1983), + (1984, 1984), + (1985, 1985), + (1986, 1986), + (1987, 1987), + (1988, 1988), + (1989, 1989), + (1990, 1990), + (1991, 1991), + (1992, 1992), + (1993, 1993), + (1994, 1994), + (1995, 1995), + (1996, 1996), + (1997, 1997), + (1998, 1998), + (1999, 1999), + (2000, 2000), + (2001, 2001), + (2002, 2002), + (2003, 2003), + (2004, 2004), + (2005, 2005), + (2006, 2006), + (2007, 2007), + (2008, 2008), + (2009, 2009), + (2010, 2010), + (2011, 2011), + (2012, 2012), + (2013, 2013), + (2014, 2014), + (2015, 2015), + (2016, 2016), + (2017, 2017), + ], + default=2016, + null=True, + ), + ) ] diff --git a/kfet/migrations/0048_article_hidden.py b/kfet/migrations/0048_article_hidden.py index 63869f77..d4d89022 100644 --- a/kfet/migrations/0048_article_hidden.py +++ b/kfet/migrations/0048_article_hidden.py @@ -6,14 +6,15 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0047_auto_20170104_1528'), - ] + dependencies = [("kfet", "0047_auto_20170104_1528")] operations = [ migrations.AddField( - model_name='article', - name='hidden', - field=models.BooleanField(help_text='Si oui, ne sera pas affiché au public ; par exemple sur la carte.', default=False), - ), + model_name="article", + name="hidden", + field=models.BooleanField( + help_text="Si oui, ne sera pas affiché au public ; par exemple sur la carte.", + default=False, + ), + ) ] diff --git a/kfet/migrations/0048_default_datetime.py b/kfet/migrations/0048_default_datetime.py index c9bacf1e..d5408c59 100644 --- a/kfet/migrations/0048_default_datetime.py +++ b/kfet/migrations/0048_default_datetime.py @@ -1,25 +1,23 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0047_auto_20170104_1528'), - ] + dependencies = [("kfet", "0047_auto_20170104_1528")] operations = [ migrations.AlterField( - model_name='operationgroup', - name='at', + model_name="operationgroup", + name="at", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AlterField( - model_name='transfergroup', - name='at', + model_name="transfergroup", + name="at", field=models.DateTimeField(default=django.utils.timezone.now), ), ] diff --git a/kfet/migrations/0049_merge.py b/kfet/migrations/0049_merge.py index 0ce9a525..e9bcb47a 100644 --- a/kfet/migrations/0049_merge.py +++ b/kfet/migrations/0049_merge.py @@ -6,10 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0048_article_hidden'), - ('kfet', '0048_default_datetime'), - ] + dependencies = [("kfet", "0048_article_hidden"), ("kfet", "0048_default_datetime")] - operations = [ - ] + operations = [] diff --git a/kfet/migrations/0050_remove_checkout.py b/kfet/migrations/0050_remove_checkout.py index b712c2d8..8cfd370a 100644 --- a/kfet/migrations/0050_remove_checkout.py +++ b/kfet/migrations/0050_remove_checkout.py @@ -7,32 +7,36 @@ from django.db import migrations, models def adapt_operation_types(apps, schema_editor): Operation = apps.get_model("kfet", "Operation") Operation.objects.filter( - is_checkout=False, - type__in=['withdraw', 'deposit']).update(type='edit') + is_checkout=False, type__in=["withdraw", "deposit"] + ).update(type="edit") def revert_operation_types(apps, schema_editor): Operation = apps.get_model("kfet", "Operation") - edits = Operation.objects.filter(type='edit') - edits.filter(amount__gt=0).update(type='deposit') - edits.filter(amount__lte=0).update(type='withdraw') + edits = Operation.objects.filter(type="edit") + edits.filter(amount__gt=0).update(type="deposit") + edits.filter(amount__lte=0).update(type="withdraw") class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0049_merge'), - ] + dependencies = [("kfet", "0049_merge")] operations = [ migrations.AlterField( - model_name='operation', - name='type', - field=models.CharField(choices=[('purchase', 'Achat'), ('deposit', 'Charge'), ('withdraw', 'Retrait'), ('initial', 'Initial'), ('edit', 'Édition')], max_length=8), + model_name="operation", + name="type", + field=models.CharField( + choices=[ + ("purchase", "Achat"), + ("deposit", "Charge"), + ("withdraw", "Retrait"), + ("initial", "Initial"), + ("edit", "Édition"), + ], + max_length=8, + ), ), migrations.RunPython(adapt_operation_types, revert_operation_types), - migrations.RemoveField( - model_name='operation', - name='is_checkout', - ), + migrations.RemoveField(model_name="operation", name="is_checkout"), ] diff --git a/kfet/migrations/0051_verbose_names.py b/kfet/migrations/0051_verbose_names.py index ae407fac..9892af71 100644 --- a/kfet/migrations/0051_verbose_names.py +++ b/kfet/migrations/0051_verbose_names.py @@ -1,210 +1,303 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0050_remove_checkout'), - ] + dependencies = [("kfet", "0050_remove_checkout")] operations = [ migrations.AlterField( - model_name='account', - name='is_frozen', - field=models.BooleanField(default=False, verbose_name='est gelé'), + model_name="account", + name="is_frozen", + field=models.BooleanField(default=False, verbose_name="est gelé"), ), migrations.AlterField( - model_name='account', - name='nickname', - field=models.CharField(default='', max_length=255, verbose_name='surnom(s)', blank=True), + model_name="account", + name="nickname", + field=models.CharField( + default="", max_length=255, verbose_name="surnom(s)", blank=True + ), ), migrations.AlterField( - model_name='accountnegative', - name='authz_overdraft_amount', - field=models.DecimalField(max_digits=6, blank=True, default=None, null=True, verbose_name='négatif autorisé', decimal_places=2), + model_name="accountnegative", + name="authz_overdraft_amount", + field=models.DecimalField( + max_digits=6, + blank=True, + default=None, + null=True, + verbose_name="négatif autorisé", + decimal_places=2, + ), ), migrations.AlterField( - model_name='accountnegative', - name='authz_overdraft_until', - field=models.DateTimeField(default=None, null=True, verbose_name='expiration du négatif', blank=True), + model_name="accountnegative", + name="authz_overdraft_until", + field=models.DateTimeField( + default=None, + null=True, + verbose_name="expiration du négatif", + blank=True, + ), ), migrations.AlterField( - model_name='accountnegative', - name='balance_offset', - field=models.DecimalField(blank=True, max_digits=6, help_text="Montant non compris dans l'autorisation de négatif", default=None, null=True, verbose_name='décalage de balance', decimal_places=2), + model_name="accountnegative", + name="balance_offset", + field=models.DecimalField( + blank=True, + max_digits=6, + help_text="Montant non compris dans l'autorisation de négatif", + default=None, + null=True, + verbose_name="décalage de balance", + decimal_places=2, + ), ), migrations.AlterField( - model_name='accountnegative', - name='comment', - field=models.CharField(blank=True, max_length=255, verbose_name='commentaire'), + model_name="accountnegative", + name="comment", + field=models.CharField( + blank=True, max_length=255, verbose_name="commentaire" + ), ), migrations.AlterField( - model_name='article', - name='box_capacity', - field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='capacité du contenant', blank=True), + model_name="article", + name="box_capacity", + field=models.PositiveSmallIntegerField( + default=None, + null=True, + verbose_name="capacité du contenant", + blank=True, + ), ), migrations.AlterField( - model_name='article', - name='box_type', - field=models.CharField(blank=True, max_length=7, choices=[('caisse', 'caisse'), ('carton', 'carton'), ('palette', 'palette'), ('fût', 'fût')], default=None, null=True, verbose_name='type de contenant'), + model_name="article", + name="box_type", + field=models.CharField( + blank=True, + max_length=7, + choices=[ + ("caisse", "caisse"), + ("carton", "carton"), + ("palette", "palette"), + ("fût", "fût"), + ], + default=None, + null=True, + verbose_name="type de contenant", + ), ), migrations.AlterField( - model_name='article', - name='category', - field=models.ForeignKey(related_name='articles', to='kfet.ArticleCategory', on_delete=django.db.models.deletion.PROTECT, verbose_name='catégorie'), + model_name="article", + name="category", + field=models.ForeignKey( + related_name="articles", + to="kfet.ArticleCategory", + on_delete=django.db.models.deletion.PROTECT, + verbose_name="catégorie", + ), ), migrations.AlterField( - model_name='article', - name='hidden', - field=models.BooleanField(default=False, verbose_name='caché', help_text='Si oui, ne sera pas affiché au public ; par exemple sur la carte.'), + model_name="article", + name="hidden", + field=models.BooleanField( + default=False, + verbose_name="caché", + help_text="Si oui, ne sera pas affiché au public ; par exemple sur la carte.", + ), ), migrations.AlterField( - model_name='article', - name='is_sold', - field=models.BooleanField(default=True, verbose_name='en vente'), + model_name="article", + name="is_sold", + field=models.BooleanField(default=True, verbose_name="en vente"), ), migrations.AlterField( - model_name='article', - name='name', - field=models.CharField(max_length=45, verbose_name='nom'), + model_name="article", + name="name", + field=models.CharField(max_length=45, verbose_name="nom"), ), migrations.AlterField( - model_name='article', - name='price', - field=models.DecimalField(default=0, verbose_name='prix', decimal_places=2, max_digits=6), + model_name="article", + name="price", + field=models.DecimalField( + default=0, verbose_name="prix", decimal_places=2, max_digits=6 + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='amount_error', - field=models.DecimalField(max_digits=6, verbose_name="montant de l'erreur", decimal_places=2), + model_name="checkoutstatement", + name="amount_error", + field=models.DecimalField( + max_digits=6, verbose_name="montant de l'erreur", decimal_places=2 + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='amount_taken', - field=models.DecimalField(max_digits=6, verbose_name='montant pris', decimal_places=2), + model_name="checkoutstatement", + name="amount_taken", + field=models.DecimalField( + max_digits=6, verbose_name="montant pris", decimal_places=2 + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='balance_new', - field=models.DecimalField(max_digits=6, verbose_name='nouvelle balance', decimal_places=2), + model_name="checkoutstatement", + name="balance_new", + field=models.DecimalField( + max_digits=6, verbose_name="nouvelle balance", decimal_places=2 + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='balance_old', - field=models.DecimalField(max_digits=6, verbose_name='ancienne balance', decimal_places=2), + model_name="checkoutstatement", + name="balance_old", + field=models.DecimalField( + max_digits=6, verbose_name="ancienne balance", decimal_places=2 + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='not_count', - field=models.BooleanField(default=False, verbose_name='caisse non comptée'), + model_name="checkoutstatement", + name="not_count", + field=models.BooleanField(default=False, verbose_name="caisse non comptée"), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_001', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 1¢'), + model_name="checkoutstatement", + name="taken_001", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 1¢" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_002', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 2¢'), + model_name="checkoutstatement", + name="taken_002", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 2¢" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_005', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 5¢'), + model_name="checkoutstatement", + name="taken_005", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 5¢" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_01', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 10¢'), + model_name="checkoutstatement", + name="taken_01", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 10¢" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_02', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 20¢'), + model_name="checkoutstatement", + name="taken_02", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 20¢" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_05', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 50¢'), + model_name="checkoutstatement", + name="taken_05", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 50¢" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_1', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 1€'), + model_name="checkoutstatement", + name="taken_1", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 1€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_10', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 10€'), + model_name="checkoutstatement", + name="taken_10", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 10€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_100', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 100€'), + model_name="checkoutstatement", + name="taken_100", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 100€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_2', - field=models.PositiveSmallIntegerField(default=0, verbose_name='pièces de 2€'), + model_name="checkoutstatement", + name="taken_2", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="pièces de 2€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_20', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 20€'), + model_name="checkoutstatement", + name="taken_20", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 20€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_200', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 200€'), + model_name="checkoutstatement", + name="taken_200", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 200€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_5', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 5€'), + model_name="checkoutstatement", + name="taken_5", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 5€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_50', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 50€'), + model_name="checkoutstatement", + name="taken_50", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 50€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_500', - field=models.PositiveSmallIntegerField(default=0, verbose_name='billets de 500€'), + model_name="checkoutstatement", + name="taken_500", + field=models.PositiveSmallIntegerField( + default=0, verbose_name="billets de 500€" + ), ), migrations.AlterField( - model_name='checkoutstatement', - name='taken_cheque', - field=models.DecimalField(default=0, verbose_name='montant des chèques', decimal_places=2, max_digits=6), + model_name="checkoutstatement", + name="taken_cheque", + field=models.DecimalField( + default=0, + verbose_name="montant des chèques", + decimal_places=2, + max_digits=6, + ), ), migrations.AlterField( - model_name='supplier', - name='address', - field=models.TextField(verbose_name='adresse'), + model_name="supplier", + name="address", + field=models.TextField(verbose_name="adresse"), ), migrations.AlterField( - model_name='supplier', - name='comment', - field=models.TextField(verbose_name='commentaire'), + model_name="supplier", + name="comment", + field=models.TextField(verbose_name="commentaire"), ), migrations.AlterField( - model_name='supplier', - name='email', - field=models.EmailField(max_length=254, verbose_name='adresse mail'), + model_name="supplier", + name="email", + field=models.EmailField(max_length=254, verbose_name="adresse mail"), ), migrations.AlterField( - model_name='supplier', - name='name', - field=models.CharField(max_length=45, verbose_name='nom'), + model_name="supplier", + name="name", + field=models.CharField(max_length=45, verbose_name="nom"), ), migrations.AlterField( - model_name='supplier', - name='phone', - field=models.CharField(max_length=10, verbose_name='téléphone'), + model_name="supplier", + name="phone", + field=models.CharField(max_length=10, verbose_name="téléphone"), ), ] diff --git a/kfet/migrations/0052_category_addcost.py b/kfet/migrations/0052_category_addcost.py index 83346a1a..8a8da85f 100644 --- a/kfet/migrations/0052_category_addcost.py +++ b/kfet/migrations/0052_category_addcost.py @@ -6,19 +6,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0051_verbose_names'), - ] + dependencies = [("kfet", "0051_verbose_names")] operations = [ migrations.AddField( - model_name='articlecategory', - name='has_addcost', - field=models.BooleanField(default=True, help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", verbose_name='majorée'), + model_name="articlecategory", + name="has_addcost", + field=models.BooleanField( + default=True, + help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", + verbose_name="majorée", + ), ), migrations.AlterField( - model_name='articlecategory', - name='name', - field=models.CharField(max_length=45, verbose_name='nom'), + model_name="articlecategory", + name="name", + field=models.CharField(max_length=45, verbose_name="nom"), ), ] diff --git a/kfet/migrations/0053_created_at.py b/kfet/migrations/0053_created_at.py index a868de33..6da14568 100644 --- a/kfet/migrations/0053_created_at.py +++ b/kfet/migrations/0053_created_at.py @@ -1,20 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0052_category_addcost'), - ] + dependencies = [("kfet", "0052_category_addcost")] operations = [ migrations.AlterField( - model_name='account', - name='created_at', + model_name="account", + name="created_at", field=models.DateTimeField(default=django.utils.timezone.now), - ), + ) ] diff --git a/kfet/migrations/0054_delete_settings.py b/kfet/migrations/0054_delete_settings.py index 80ee1d24..7294c8bf 100644 --- a/kfet/migrations/0054_delete_settings.py +++ b/kfet/migrations/0054_delete_settings.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models from kfet.forms import KFetConfigForm def adapt_settings(apps, schema_editor): - Settings = apps.get_model('kfet', 'Settings') + Settings = apps.get_model("kfet", "Settings") db_alias = schema_editor.connection.alias obj = Settings.objects.using(db_alias) @@ -22,17 +22,17 @@ def adapt_settings(apps, schema_editor): pass try: - subvention = obj.get(name='SUBVENTION_COF').value_decimal - subvention_mult = 1 + subvention/100 - reduction = (1 - 1/subvention_mult) * 100 - cfg['kfet_reduction_cof'] = reduction + subvention = obj.get(name="SUBVENTION_COF").value_decimal + subvention_mult = 1 + subvention / 100 + reduction = (1 - 1 / subvention_mult) * 100 + cfg["kfet_reduction_cof"] = reduction except Settings.DoesNotExist: pass - try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal') - try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account') - try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration') - try_get('kfet_overdraft_amount', 'OVERDRAFT_AMOUNT', 'value_decimal') - try_get('kfet_cancel_duration', 'CANCEL_DURATION', 'value_duration') + try_get("kfet_addcost_amount", "ADDCOST_AMOUNT", "value_decimal") + try_get("kfet_addcost_for", "ADDCOST_FOR", "value_account") + try_get("kfet_overdraft_duration", "OVERDRAFT_DURATION", "value_duration") + try_get("kfet_overdraft_amount", "OVERDRAFT_AMOUNT", "value_decimal") + try_get("kfet_cancel_duration", "CANCEL_DURATION", "value_duration") cfg_form = KFetConfigForm(initial=cfg) if cfg_form.is_valid(): @@ -41,18 +41,10 @@ def adapt_settings(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0053_created_at'), - ('djconfig', '0001_initial'), - ] + dependencies = [("kfet", "0053_created_at"), ("djconfig", "0001_initial")] operations = [ migrations.RunPython(adapt_settings), - migrations.RemoveField( - model_name='settings', - name='value_account', - ), - migrations.DeleteModel( - name='Settings', - ), + migrations.RemoveField(model_name="settings", name="value_account"), + migrations.DeleteModel(name="Settings"), ] diff --git a/kfet/migrations/0054_update_promos.py b/kfet/migrations/0054_update_promos.py index 2691e903..0f86779b 100644 --- a/kfet/migrations/0054_update_promos.py +++ b/kfet/migrations/0054_update_promos.py @@ -6,14 +6,56 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0053_created_at'), - ] + dependencies = [("kfet", "0053_created_at")] operations = [ migrations.AlterField( - model_name='account', - name='promo', - field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017)], default=2017, null=True), - ), + model_name="account", + name="promo", + field=models.IntegerField( + blank=True, + choices=[ + (1980, 1980), + (1981, 1981), + (1982, 1982), + (1983, 1983), + (1984, 1984), + (1985, 1985), + (1986, 1986), + (1987, 1987), + (1988, 1988), + (1989, 1989), + (1990, 1990), + (1991, 1991), + (1992, 1992), + (1993, 1993), + (1994, 1994), + (1995, 1995), + (1996, 1996), + (1997, 1997), + (1998, 1998), + (1999, 1999), + (2000, 2000), + (2001, 2001), + (2002, 2002), + (2003, 2003), + (2004, 2004), + (2005, 2005), + (2006, 2006), + (2007, 2007), + (2008, 2008), + (2009, 2009), + (2010, 2010), + (2011, 2011), + (2012, 2012), + (2013, 2013), + (2014, 2014), + (2015, 2015), + (2016, 2016), + (2017, 2017), + ], + default=2017, + null=True, + ), + ) ] diff --git a/kfet/migrations/0055_move_permissions.py b/kfet/migrations/0055_move_permissions.py index a418124c..9db3f793 100644 --- a/kfet/migrations/0055_move_permissions.py +++ b/kfet/migrations/0055_move_permissions.py @@ -13,41 +13,42 @@ def forwards_perms(apps, schema_editor): permissions which are assumed unused. """ - ContentType = apps.get_model('contenttypes', 'contenttype') + ContentType = apps.get_model("contenttypes", "contenttype") try: ctype_global = ContentType.objects.get( - app_label="kfet", model="globalpermissions", + app_label="kfet", model="globalpermissions" ) except ContentType.DoesNotExist: # We are not migrating from existing data, nothing to do. return perms = { - 'account': ( - 'is_team', 'manage_perms', 'manage_addcosts', - 'edit_balance_account', 'change_account_password', - 'special_add_account', + "account": ( + "is_team", + "manage_perms", + "manage_addcosts", + "edit_balance_account", + "change_account_password", + "special_add_account", ), - 'accountnegative': ('view_negs',), - 'inventory': ('order_to_inventory',), - 'operation': ( - 'perform_deposit', 'perform_negative_operations', - 'override_frozen_protection', 'cancel_old_operations', - 'perform_commented_operations', + "accountnegative": ("view_negs",), + "inventory": ("order_to_inventory",), + "operation": ( + "perform_deposit", + "perform_negative_operations", + "override_frozen_protection", + "cancel_old_operations", + "perform_commented_operations", ), } - Permission = apps.get_model('auth', 'permission') + Permission = apps.get_model("auth", "permission") global_perms = Permission.objects.filter(content_type=ctype_global) for modelname, codenames in perms.items(): - model = apps.get_model('kfet', modelname) + model = apps.get_model("kfet", modelname) ctype = ContentType.objects.get_for_model(model) - ( - global_perms - .filter(codename__in=codenames) - .update(content_type=ctype) - ) + (global_perms.filter(codename__in=codenames).update(content_type=ctype)) ctype_global.delete() @@ -55,27 +56,64 @@ def forwards_perms(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('kfet', '0054_delete_settings'), - ('contenttypes', '__latest__'), - ('auth', '__latest__'), + ("kfet", "0054_delete_settings"), + ("contenttypes", "__latest__"), + ("auth", "__latest__"), ] operations = [ migrations.AlterModelOptions( - name='account', - options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'))}, + name="account", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ("edit_balance_account", "Modifier la balance d'un compte"), + ( + "change_account_password", + "Modifier le mot de passe d'une personne de l'équipe", + ), + ( + "special_add_account", + "Créer un compte avec une balance initiale", + ), + ) + }, ), migrations.AlterModelOptions( - name='accountnegative', - options={'permissions': (('view_negs', 'Voir la liste des négatifs'),)}, + name="accountnegative", + options={"permissions": (("view_negs", "Voir la liste des négatifs"),)}, ), migrations.AlterModelOptions( - name='inventory', - options={'ordering': ['-at'], 'permissions': (('order_to_inventory', "Générer un inventaire à partir d'une commande"),)}, + name="inventory", + options={ + "ordering": ["-at"], + "permissions": ( + ( + "order_to_inventory", + "Générer un inventaire à partir d'une commande", + ), + ), + }, ), migrations.AlterModelOptions( - name='operation', - options={'permissions': (('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'))}, + name="operation", + options={ + "permissions": ( + ("perform_deposit", "Effectuer une charge"), + ( + "perform_negative_operations", + "Enregistrer des commandes en négatif", + ), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), + ) + }, ), migrations.RunPython(forwards_perms), ] diff --git a/kfet/migrations/0056_change_account_meta.py b/kfet/migrations/0056_change_account_meta.py index 3992bf3c..27e51417 100644 --- a/kfet/migrations/0056_change_account_meta.py +++ b/kfet/migrations/0056_change_account_meta.py @@ -6,13 +6,27 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0055_move_permissions'), - ] + dependencies = [("kfet", "0055_move_permissions")] operations = [ migrations.AlterModelOptions( - name='account', - options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'))}, - ), + name="account", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ("edit_balance_account", "Modifier la balance d'un compte"), + ( + "change_account_password", + "Modifier le mot de passe d'une personne de l'équipe", + ), + ( + "special_add_account", + "Créer un compte avec une balance initiale", + ), + ("can_force_close", "Fermer manuellement la K-Fêt"), + ) + }, + ) ] diff --git a/kfet/migrations/0057_merge.py b/kfet/migrations/0057_merge.py index 48f63399..456bbeb0 100644 --- a/kfet/migrations/0057_merge.py +++ b/kfet/migrations/0057_merge.py @@ -7,9 +7,8 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('kfet', '0056_change_account_meta'), - ('kfet', '0054_update_promos'), + ("kfet", "0056_change_account_meta"), + ("kfet", "0054_update_promos"), ] - operations = [ - ] + operations = [] diff --git a/kfet/migrations/0058_delete_genericteamtoken.py b/kfet/migrations/0058_delete_genericteamtoken.py index ea8b55cd..3b3216e9 100644 --- a/kfet/migrations/0058_delete_genericteamtoken.py +++ b/kfet/migrations/0058_delete_genericteamtoken.py @@ -6,12 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0057_merge'), - ] + dependencies = [("kfet", "0057_merge")] - operations = [ - migrations.DeleteModel( - name='GenericTeamToken', - ), - ] + operations = [migrations.DeleteModel(name="GenericTeamToken")] diff --git a/kfet/migrations/0059_create_generic.py b/kfet/migrations/0059_create_generic.py index 4f04770c..7408a300 100644 --- a/kfet/migrations/0059_create_generic.py +++ b/kfet/migrations/0059_create_generic.py @@ -15,31 +15,22 @@ def setup_kfet_generic_user(apps, schema_editor): See also setup_kfet_generic_user from kfet.auth.utils module. """ - User = apps.get_model('auth', 'User') - CofProfile = apps.get_model('gestioncof', 'CofProfile') - Account = apps.get_model('kfet', 'Account') + User = apps.get_model("auth", "User") + CofProfile = apps.get_model("gestioncof", "CofProfile") + Account = apps.get_model("kfet", "Account") user, _ = User.objects.update_or_create( username=KFET_GENERIC_USERNAME, - defaults={ - 'first_name': 'Compte générique K-Fêt', - }, + defaults={"first_name": "Compte générique K-Fêt"}, ) profile, _ = CofProfile.objects.update_or_create(user=user) account, _ = Account.objects.update_or_create( - cofprofile=profile, - defaults={ - 'trigramme': KFET_GENERIC_TRIGRAMME, - }, + cofprofile=profile, defaults={"trigramme": KFET_GENERIC_TRIGRAMME} ) class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0058_delete_genericteamtoken'), - ] + dependencies = [("kfet", "0058_delete_genericteamtoken")] - operations = [ - migrations.RunPython(setup_kfet_generic_user), - ] + operations = [migrations.RunPython(setup_kfet_generic_user)] diff --git a/kfet/migrations/0060_amend_supplier.py b/kfet/migrations/0060_amend_supplier.py index 4eb569f8..0a56640d 100644 --- a/kfet/migrations/0060_amend_supplier.py +++ b/kfet/migrations/0060_amend_supplier.py @@ -6,34 +6,39 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0059_create_generic'), - ] + dependencies = [("kfet", "0059_create_generic")] operations = [ migrations.AlterField( - model_name='supplier', - name='address', - field=models.TextField(verbose_name='adresse', blank=True), + model_name="supplier", + name="address", + field=models.TextField(verbose_name="adresse", blank=True), ), migrations.AlterField( - model_name='supplier', - name='articles', - field=models.ManyToManyField(verbose_name='articles vendus', through='kfet.SupplierArticle', related_name='suppliers', to='kfet.Article'), + model_name="supplier", + name="articles", + field=models.ManyToManyField( + verbose_name="articles vendus", + through="kfet.SupplierArticle", + related_name="suppliers", + to="kfet.Article", + ), ), migrations.AlterField( - model_name='supplier', - name='comment', - field=models.TextField(verbose_name='commentaire', blank=True), + model_name="supplier", + name="comment", + field=models.TextField(verbose_name="commentaire", blank=True), ), migrations.AlterField( - model_name='supplier', - name='email', - field=models.EmailField(max_length=254, verbose_name='adresse mail', blank=True), + model_name="supplier", + name="email", + field=models.EmailField( + max_length=254, verbose_name="adresse mail", blank=True + ), ), migrations.AlterField( - model_name='supplier', - name='phone', - field=models.CharField(max_length=20, verbose_name='téléphone', blank=True), + model_name="supplier", + name="phone", + field=models.CharField(max_length=20, verbose_name="téléphone", blank=True), ), ] diff --git a/kfet/migrations/0061_add_perms_config.py b/kfet/migrations/0061_add_perms_config.py index 01bdf51d..7d10da31 100644 --- a/kfet/migrations/0061_add_perms_config.py +++ b/kfet/migrations/0061_add_perms_config.py @@ -6,13 +6,29 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0060_amend_supplier'), - ] + dependencies = [("kfet", "0060_amend_supplier")] operations = [ migrations.AlterModelOptions( - name='account', - options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'), ('see_config', 'Voir la configuration K-Fêt'), ('change_config', 'Modifier la configuration K-Fêt'))}, - ), + name="account", + options={ + "permissions": ( + ("is_team", "Is part of the team"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ("edit_balance_account", "Modifier la balance d'un compte"), + ( + "change_account_password", + "Modifier le mot de passe d'une personne de l'équipe", + ), + ( + "special_add_account", + "Créer un compte avec une balance initiale", + ), + ("can_force_close", "Fermer manuellement la K-Fêt"), + ("see_config", "Voir la configuration K-Fêt"), + ("change_config", "Modifier la configuration K-Fêt"), + ) + }, + ) ] diff --git a/kfet/migrations/0062_delete_globalpermissions.py b/kfet/migrations/0062_delete_globalpermissions.py index ee245412..9a357a2d 100644 --- a/kfet/migrations/0062_delete_globalpermissions.py +++ b/kfet/migrations/0062_delete_globalpermissions.py @@ -3,12 +3,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0061_add_perms_config'), - ] + dependencies = [("kfet", "0061_add_perms_config")] - operations = [ - migrations.DeleteModel( - name='GlobalPermissions', - ), - ] + operations = [migrations.DeleteModel(name="GlobalPermissions")] diff --git a/kfet/migrations/0063_promo.py b/kfet/migrations/0063_promo.py index 3fac5a8a..de04573a 100644 --- a/kfet/migrations/0063_promo.py +++ b/kfet/migrations/0063_promo.py @@ -7,14 +7,57 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0062_delete_globalpermissions'), - ] + dependencies = [("kfet", "0062_delete_globalpermissions")] operations = [ migrations.AlterField( - model_name='account', - name='promo', - field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018)], default=2017, null=True), - ), + model_name="account", + name="promo", + field=models.IntegerField( + blank=True, + choices=[ + (1980, 1980), + (1981, 1981), + (1982, 1982), + (1983, 1983), + (1984, 1984), + (1985, 1985), + (1986, 1986), + (1987, 1987), + (1988, 1988), + (1989, 1989), + (1990, 1990), + (1991, 1991), + (1992, 1992), + (1993, 1993), + (1994, 1994), + (1995, 1995), + (1996, 1996), + (1997, 1997), + (1998, 1998), + (1999, 1999), + (2000, 2000), + (2001, 2001), + (2002, 2002), + (2003, 2003), + (2004, 2004), + (2005, 2005), + (2006, 2006), + (2007, 2007), + (2008, 2008), + (2009, 2009), + (2010, 2010), + (2011, 2011), + (2012, 2012), + (2013, 2013), + (2014, 2014), + (2015, 2015), + (2016, 2016), + (2017, 2017), + (2018, 2018), + ], + default=2017, + null=True, + ), + ) ] diff --git a/kfet/migrations/0064_promo_2018.py b/kfet/migrations/0064_promo_2018.py index c99d85b5..7fe5e160 100644 --- a/kfet/migrations/0064_promo_2018.py +++ b/kfet/migrations/0064_promo_2018.py @@ -7,14 +7,57 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('kfet', '0063_promo'), - ] + dependencies = [("kfet", "0063_promo")] operations = [ migrations.AlterField( - model_name='account', - name='promo', - field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018)], default=2018, null=True), - ), + model_name="account", + name="promo", + field=models.IntegerField( + blank=True, + choices=[ + (1980, 1980), + (1981, 1981), + (1982, 1982), + (1983, 1983), + (1984, 1984), + (1985, 1985), + (1986, 1986), + (1987, 1987), + (1988, 1988), + (1989, 1989), + (1990, 1990), + (1991, 1991), + (1992, 1992), + (1993, 1993), + (1994, 1994), + (1995, 1995), + (1996, 1996), + (1997, 1997), + (1998, 1998), + (1999, 1999), + (2000, 2000), + (2001, 2001), + (2002, 2002), + (2003, 2003), + (2004, 2004), + (2005, 2005), + (2006, 2006), + (2007, 2007), + (2008, 2008), + (2009, 2009), + (2010, 2010), + (2011, 2011), + (2012, 2012), + (2013, 2013), + (2014, 2014), + (2015, 2015), + (2016, 2016), + (2017, 2017), + (2018, 2018), + ], + default=2018, + null=True, + ), + ) ] diff --git a/kfet/models.py b/kfet/models.py index e952e85a..6b16505e 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,30 +1,31 @@ +import re +from datetime import date from functools import reduce -from django.db import models -from django.core.validators import RegexValidator from django.contrib.auth.models import User -from gestioncof.models import CofProfile -from django.urls import reverse -from django.utils.six.moves import reduce -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ -from django.db import transaction +from django.core.validators import RegexValidator +from django.db import models, transaction from django.db.models import F -from datetime import date -import re +from django.urls import reverse +from django.utils import timezone +from django.utils.six.moves import reduce +from django.utils.translation import ugettext_lazy as _ + +from gestioncof.models import CofProfile from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken # noqa - from .config import kfet_config from .utils import to_ukf + def choices_length(choices): return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) + def default_promo(): now = date.today() - return now.month <= 8 and now.year-1 or now.year + return now.month <= 8 and now.year - 1 or now.year class AccountManager(models.Manager): @@ -32,8 +33,7 @@ class AccountManager(models.Manager): def get_queryset(self): """Always append related data to this Account.""" - return super().get_queryset().select_related('cofprofile__user', - 'negative') + return super().get_queryset().select_related("cofprofile__user", "negative") def get_generic(self): """ @@ -48,6 +48,7 @@ class AccountManager(models.Manager): Raises Account.DoesNotExist if no Account has this password. """ from .auth.utils import hash_password + if password is None: raise self.model.DoesNotExist return self.get(password=hash_password(password)) @@ -57,69 +58,71 @@ class Account(models.Model): objects = AccountManager() cofprofile = models.OneToOneField( - CofProfile, on_delete = models.PROTECT, - related_name = "account_kfet") + CofProfile, on_delete=models.PROTECT, related_name="account_kfet" + ) trigramme = models.CharField( - unique = True, - max_length = 3, - validators = [RegexValidator(regex='^[^a-z]{3}$')], - db_index = True) - balance = models.DecimalField( - max_digits = 6, decimal_places = 2, - default = 0) - is_frozen = models.BooleanField("est gelé", default = False) + unique=True, + max_length=3, + validators=[RegexValidator(regex="^[^a-z]{3}$")], + db_index=True, + ) + balance = models.DecimalField(max_digits=6, decimal_places=2, default=0) + is_frozen = models.BooleanField("est gelé", default=False) created_at = models.DateTimeField(default=timezone.now) # Optional - PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)] + PROMO_CHOICES = [(r, r) for r in range(1980, date.today().year + 1)] promo = models.IntegerField( - choices = PROMO_CHOICES, - blank = True, null = True, default = default_promo()) - nickname = models.CharField( - "surnom(s)", - max_length = 255, - blank = True, default = "") + choices=PROMO_CHOICES, blank=True, null=True, default=default_promo() + ) + nickname = models.CharField("surnom(s)", max_length=255, blank=True, default="") password = models.CharField( - max_length = 255, - unique = True, - blank = True, null = True, default = None) + max_length=255, unique=True, blank=True, null=True, default=None + ) class Meta: permissions = ( - ('is_team', 'Is part of the team'), - ('manage_perms', 'Gérer les permissions K-Fêt'), - ('manage_addcosts', 'Gérer les majorations'), - ('edit_balance_account', "Modifier la balance d'un compte"), - ('change_account_password', - "Modifier le mot de passe d'une personne de l'équipe"), - ('special_add_account', - "Créer un compte avec une balance initiale"), - ('can_force_close', "Fermer manuellement la K-Fêt"), - ('see_config', "Voir la configuration K-Fêt"), - ('change_config', "Modifier la configuration K-Fêt"), + ("is_team", "Is part of the team"), + ("manage_perms", "Gérer les permissions K-Fêt"), + ("manage_addcosts", "Gérer les majorations"), + ("edit_balance_account", "Modifier la balance d'un compte"), + ( + "change_account_password", + "Modifier le mot de passe d'une personne de l'équipe", + ), + ("special_add_account", "Créer un compte avec une balance initiale"), + ("can_force_close", "Fermer manuellement la K-Fêt"), + ("see_config", "Voir la configuration K-Fêt"), + ("change_config", "Modifier la configuration K-Fêt"), ) def __str__(self): - return '%s (%s)' % (self.trigramme, self.name) + return "%s (%s)" % (self.trigramme, self.name) # Propriétés pour accéder aux attributs de cofprofile et user @property def user(self): return self.cofprofile.user + @property def username(self): return self.cofprofile.user.username + @property def first_name(self): return self.cofprofile.user.first_name + @property def last_name(self): return self.cofprofile.user.last_name + @property def email(self): return self.cofprofile.user.email + @property def departement(self): return self.cofprofile.departement + @property def is_cof(self): return self.cofprofile.is_cof @@ -131,7 +134,7 @@ class Account(models.Model): @property def real_balance(self): - if hasattr(self, 'negative') and self.negative.balance_offset: + if hasattr(self, "negative") and self.negative.balance_offset: return self.balance - self.negative.balance_offset return self.balance @@ -141,29 +144,29 @@ class Account(models.Model): @property def is_cash(self): - return self.trigramme == 'LIQ' + return self.trigramme == "LIQ" @property def need_comment(self): - return self.trigramme == '#13' + return self.trigramme == "#13" @property def readable(self): - return self.trigramme != 'GNR' + return self.trigramme != "GNR" @property def is_team(self): - return self.has_perm('kfet.is_team') + return self.has_perm("kfet.is_team") @staticmethod def is_validandfree(trigramme): - data = { 'is_valid' : False, 'is_free' : False } + data = {"is_valid": False, "is_free": False} pattern = re.compile("^[^a-z]{3}$") - data['is_valid'] = pattern.match(trigramme) and True or False + data["is_valid"] = pattern.match(trigramme) and True or False try: account = Account.objects.get(trigramme=trigramme) except Account.DoesNotExist: - data['is_free'] = True + data["is_free"] = True return data def perms_to_perform_operation(self, amount): @@ -176,31 +179,34 @@ class Account(models.Model): # Yes, so no perms and no stop return set(), False if self.need_comment: - perms.add('kfet.perform_commented_operations') + perms.add("kfet.perform_commented_operations") # Checking is frozen account if self.is_frozen: - perms.add('kfet.override_frozen_protection') + perms.add("kfet.override_frozen_protection") new_balance = self.balance + amount if new_balance < 0 and amount < 0: # Retrieving overdraft amount limit - if (hasattr(self, 'negative') - and self.negative.authz_overdraft_amount is not None): - overdraft_amount = - self.negative.authz_overdraft_amount + if ( + hasattr(self, "negative") + and self.negative.authz_overdraft_amount is not None + ): + overdraft_amount = -self.negative.authz_overdraft_amount else: - overdraft_amount = - overdraft_amount_max + overdraft_amount = -overdraft_amount_max # Retrieving overdraft datetime limit - if (hasattr(self, 'negative') - and self.negative.authz_overdraft_until is not None): + if ( + hasattr(self, "negative") + and self.negative.authz_overdraft_until is not None + ): overdraft_until = self.negative.authz_overdraft_until - elif hasattr(self, 'negative'): - overdraft_until = \ - self.negative.start + overdraft_duration_max + elif hasattr(self, "negative"): + overdraft_until = self.negative.start + overdraft_duration_max else: overdraft_until = timezone.now() + overdraft_duration_max # Checking it doesn't break 1 rule if new_balance < overdraft_amount or timezone.now() > overdraft_until: stop_ope = True - perms.add('kfet.perform_negative_operations') + perms.add("kfet.perform_negative_operations") return perms, stop_ope # Surcharge Méthode save() avec gestions de User et CofProfile @@ -209,7 +215,7 @@ class Account(models.Model): # Action: # - Enregistre User, CofProfile à partir de "data" # - Enregistre Account - def save(self, data = {}, *args, **kwargs): + def save(self, data={}, *args, **kwargs): if self.pk and data: # Account update @@ -217,8 +223,8 @@ class Account(models.Model): # Updating User with data user = self.user user.first_name = data.get("first_name", user.first_name) - user.last_name = data.get("last_name", user.last_name) - user.email = data.get("email", user.email) + user.last_name = data.get("last_name", user.last_name) + user.email = data.get("email", user.email) user.save() # Updating CofProfile with data cof = self.cofprofile @@ -240,18 +246,18 @@ class Account(models.Model): # Creating or updating User instance (user, _) = User.objects.get_or_create(username=username) if "first_name" in data: - user.first_name = data['first_name'] + user.first_name = data["first_name"] if "last_name" in data: - user.last_name = data['last_name'] + user.last_name = data["last_name"] if "email" in data: - user.email = data['email'] + user.email = data["email"] user.save() # Creating or updating CofProfile instance (cof, _) = CofProfile.objects.get_or_create(user=user) if "login_clipper" in data: - cof.login_clipper = data['login_clipper'] + cof.login_clipper = data["login_clipper"] if "departement" in data: - cof.departement = data['departement'] + cof.departement = data["departement"] cof.save() if data: self.cofprofile = cof @@ -259,6 +265,7 @@ class Account(models.Model): def change_pwd(self, clear_password): from .auth.utils import hash_password + self.password = hash_password(clear_password) # Surcharge de delete @@ -269,23 +276,21 @@ class Account(models.Model): def update_negative(self): if self.real_balance < 0: - if hasattr(self, 'negative') and not self.negative.start: + if hasattr(self, "negative") and not self.negative.start: self.negative.start = timezone.now() self.negative.save() - elif not hasattr(self, 'negative'): - self.negative = ( - AccountNegative.objects.create( - account=self, start=timezone.now(), - ) + elif not hasattr(self, "negative"): + self.negative = AccountNegative.objects.create( + account=self, start=timezone.now() ) - elif hasattr(self, 'negative'): + elif hasattr(self, "negative"): # self.real_balance >= 0 balance_offset = self.negative.balance_offset if balance_offset: ( - Account.objects - .filter(pk=self.pk) - .update(balance=F('balance')-balance_offset) + Account.objects.filter(pk=self.pk).update( + balance=F("balance") - balance_offset + ) ) self.refresh_from_db() self.negative.delete() @@ -299,41 +304,40 @@ class AccountNegativeManager(models.Manager): """Manager for AccountNegative model.""" def get_queryset(self): - return ( - super().get_queryset() - .select_related('account__cofprofile__user') - ) + return super().get_queryset().select_related("account__cofprofile__user") class AccountNegative(models.Model): objects = AccountNegativeManager() account = models.OneToOneField( - Account, on_delete=models.PROTECT, - related_name="negative", + Account, on_delete=models.PROTECT, related_name="negative" ) start = models.DateTimeField(blank=True, null=True, default=None) balance_offset = models.DecimalField( "décalage de balance", help_text="Montant non compris dans l'autorisation de négatif", - max_digits=6, decimal_places=2, - blank=True, null=True, default=None, + max_digits=6, + decimal_places=2, + blank=True, + null=True, + default=None, ) authz_overdraft_amount = models.DecimalField( "négatif autorisé", - max_digits=6, decimal_places=2, - blank=True, null=True, default=None, + max_digits=6, + decimal_places=2, + blank=True, + null=True, + default=None, ) authz_overdraft_until = models.DateTimeField( - "expiration du négatif", - blank=True, null=True, default=None, + "expiration du négatif", blank=True, null=True, default=None ) comment = models.CharField("commentaire", max_length=255, blank=True) class Meta: - permissions = ( - ('view_negs', 'Voir la liste des négatifs'), - ) + permissions = (("view_negs", "Voir la liste des négatifs"),) @property def until_default(self): @@ -341,31 +345,26 @@ class AccountNegative(models.Model): class CheckoutQuerySet(models.QuerySet): - def is_valid(self): now = timezone.now() return self.filter(valid_from__lte=now, valid_to__gte=now) class Checkout(models.Model): - created_by = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "+") - name = models.CharField(max_length = 45) + created_by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") + name = models.CharField(max_length=45) valid_from = models.DateTimeField() - valid_to = models.DateTimeField() - balance = models.DecimalField( - max_digits = 6, decimal_places = 2, - default = 0) - is_protected = models.BooleanField(default = False) + valid_to = models.DateTimeField() + balance = models.DecimalField(max_digits=6, decimal_places=2, default=0) + is_protected = models.BooleanField(default=False) objects = CheckoutQuerySet.as_manager() def get_absolute_url(self): - return reverse('kfet.checkout.read', kwargs={'pk': self.pk}) + return reverse("kfet.checkout.read", kwargs={"pk": self.pk}) class Meta: - ordering = ['-valid_to'] + ordering = ["-valid_to"] def __str__(self): return self.name @@ -388,31 +387,30 @@ class Checkout(models.Model): class CheckoutTransfer(models.Model): from_checkout = models.ForeignKey( - Checkout, on_delete = models.PROTECT, - related_name = "transfers_from") + Checkout, on_delete=models.PROTECT, related_name="transfers_from" + ) to_checkout = models.ForeignKey( - Checkout, on_delete = models.PROTECT, - related_name = "transfers_to") - amount = models.DecimalField( - max_digits = 6, decimal_places = 2) + Checkout, on_delete=models.PROTECT, related_name="transfers_to" + ) + amount = models.DecimalField(max_digits=6, decimal_places=2) class CheckoutStatement(models.Model): - by = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "+") + by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") checkout = models.ForeignKey( - Checkout, on_delete = models.PROTECT, - related_name = "statements") - balance_old = models.DecimalField("ancienne balance", - max_digits = 6, decimal_places = 2) - balance_new = models.DecimalField("nouvelle balance", - max_digits = 6, decimal_places = 2) - amount_taken = models.DecimalField("montant pris", - max_digits = 6, decimal_places = 2) - amount_error = models.DecimalField("montant de l'erreur", - max_digits = 6, decimal_places = 2) - at = models.DateTimeField(auto_now_add = True) + Checkout, on_delete=models.PROTECT, related_name="statements" + ) + balance_old = models.DecimalField( + "ancienne balance", max_digits=6, decimal_places=2 + ) + balance_new = models.DecimalField( + "nouvelle balance", max_digits=6, decimal_places=2 + ) + amount_taken = models.DecimalField("montant pris", max_digits=6, decimal_places=2) + amount_error = models.DecimalField( + "montant de l'erreur", max_digits=6, decimal_places=2 + ) + at = models.DateTimeField(auto_now_add=True) not_count = models.BooleanField("caisse non comptée", default=False) taken_001 = models.PositiveSmallIntegerField("pièces de 1¢", default=0) @@ -431,68 +429,76 @@ class CheckoutStatement(models.Model): taken_200 = models.PositiveSmallIntegerField("billets de 200€", default=0) taken_500 = models.PositiveSmallIntegerField("billets de 500€", default=0) taken_cheque = models.DecimalField( - "montant des chèques", - default=0, max_digits=6, decimal_places=2) + "montant des chèques", default=0, max_digits=6, decimal_places=2 + ) def __str__(self): - return '%s %s' % (self.checkout, self.at) + return "%s %s" % (self.checkout, self.at) def save(self, *args, **kwargs): if not self.pk: checkout_id = self.checkout_id - self.balance_old = (Checkout.objects - .values_list('balance', flat=True).get(pk=checkout_id)) + self.balance_old = Checkout.objects.values_list("balance", flat=True).get( + pk=checkout_id + ) if self.not_count: self.balance_new = self.balance_old - self.amount_taken - self.amount_error = ( - self.balance_new + self.amount_taken - self.balance_old) + self.amount_error = self.balance_new + self.amount_taken - self.balance_old with transaction.atomic(): Checkout.objects.filter(pk=checkout_id).update(balance=self.balance_new) super().save(*args, **kwargs) else: - self.amount_error = ( - self.balance_new + self.amount_taken - self.balance_old) + self.amount_error = self.balance_new + self.amount_taken - self.balance_old # Si on modifie le dernier relevé d'une caisse et que la nouvelle # balance est modifiée alors on modifie la balance actuelle de la caisse - last_statement = (CheckoutStatement.objects - .filter(checkout=self.checkout) - .order_by('at') - .last()) - if (last_statement.pk == self.pk - and last_statement.balance_new != self.balance_new): + last_statement = ( + CheckoutStatement.objects.filter(checkout=self.checkout) + .order_by("at") + .last() + ) + if ( + last_statement.pk == self.pk + and last_statement.balance_new != self.balance_new + ): Checkout.objects.filter(pk=self.checkout_id).update( - balance=F('balance') - last_statement.balance_new + self.balance_new) + balance=F("balance") - last_statement.balance_new + self.balance_new + ) super().save(*args, **kwargs) class ArticleCategory(models.Model): name = models.CharField("nom", max_length=45) - has_addcost = models.BooleanField("majorée", default=True, - help_text="Si oui et qu'une majoration " - "est active, celle-ci sera " - "appliquée aux articles de " - "cette catégorie.") + has_addcost = models.BooleanField( + "majorée", + default=True, + help_text="Si oui et qu'une majoration " + "est active, celle-ci sera " + "appliquée aux articles de " + "cette catégorie.", + ) def __str__(self): return self.name class Article(models.Model): - name = models.CharField("nom", max_length = 45) - is_sold = models.BooleanField("en vente", default = True) - hidden = models.BooleanField("caché", - default=False, - help_text="Si oui, ne sera pas affiché " - "au public ; par exemple " - "sur la carte.") - price = models.DecimalField( - "prix", - max_digits = 6, decimal_places = 2, - default = 0) - stock = models.IntegerField(default = 0) + name = models.CharField("nom", max_length=45) + is_sold = models.BooleanField("en vente", default=True) + hidden = models.BooleanField( + "caché", + default=False, + help_text="Si oui, ne sera pas affiché " + "au public ; par exemple " + "sur la carte.", + ) + price = models.DecimalField("prix", max_digits=6, decimal_places=2, default=0) + stock = models.IntegerField(default=0) category = models.ForeignKey( - ArticleCategory, on_delete = models.PROTECT, - related_name = "articles", verbose_name='catégorie') + ArticleCategory, + on_delete=models.PROTECT, + related_name="articles", + verbose_name="catégorie", + ) BOX_TYPE_CHOICES = ( ("caisse", "caisse"), ("carton", "carton"), @@ -501,18 +507,21 @@ class Article(models.Model): ) box_type = models.CharField( "type de contenant", - choices = BOX_TYPE_CHOICES, - max_length = choices_length(BOX_TYPE_CHOICES), - blank = True, null = True, default = None) + choices=BOX_TYPE_CHOICES, + max_length=choices_length(BOX_TYPE_CHOICES), + blank=True, + null=True, + default=None, + ) box_capacity = models.PositiveSmallIntegerField( - "capacité du contenant", - blank = True, null = True, default = None) + "capacité du contenant", blank=True, null=True, default=None + ) def __str__(self): - return '%s - %s' % (self.category.name, self.name) + return "%s - %s" % (self.category.name, self.name) def get_absolute_url(self): - return reverse('kfet.article.read', kwargs={'pk': self.pk}) + return reverse("kfet.article.read", kwargs={"pk": self.pk}) def price_ukf(self): return to_ukf(self.price) @@ -520,43 +529,43 @@ class Article(models.Model): class ArticleRule(models.Model): article_on = models.OneToOneField( - Article, on_delete = models.PROTECT, - related_name = "rule_on") + Article, on_delete=models.PROTECT, related_name="rule_on" + ) article_to = models.OneToOneField( - Article, on_delete = models.PROTECT, - related_name = "rule_to") + Article, on_delete=models.PROTECT, related_name="rule_to" + ) ratio = models.PositiveSmallIntegerField() + class Inventory(models.Model): articles = models.ManyToManyField( - Article, - through = 'InventoryArticle', - related_name = "inventories") - by = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "+") - at = models.DateTimeField(auto_now_add = True) + Article, through="InventoryArticle", related_name="inventories" + ) + by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") + at = models.DateTimeField(auto_now_add=True) # Optional order = models.OneToOneField( - 'Order', on_delete = models.PROTECT, - related_name = "inventory", - blank = True, null = True, default = None) + "Order", + on_delete=models.PROTECT, + related_name="inventory", + blank=True, + null=True, + default=None, + ) class Meta: - ordering = ['-at'] + ordering = ["-at"] permissions = ( - ('order_to_inventory', "Générer un inventaire à partir d'une commande"), + ("order_to_inventory", "Générer un inventaire à partir d'une commande"), ) class InventoryArticle(models.Model): - inventory = models.ForeignKey( - Inventory, on_delete = models.PROTECT) - article = models.ForeignKey( - Article, on_delete = models.PROTECT) - stock_old = models.IntegerField() - stock_new = models.IntegerField() - stock_error = models.IntegerField(default = 0) + inventory = models.ForeignKey(Inventory, on_delete=models.PROTECT) + article = models.ForeignKey(Article, on_delete=models.PROTECT) + stock_old = models.IntegerField() + stock_new = models.IntegerField() + stock_error = models.IntegerField(default=0) def save(self, *args, **kwargs): # S'il s'agit d'un inventaire provenant d'une livraison, il n'y a pas @@ -570,8 +579,8 @@ class Supplier(models.Model): articles = models.ManyToManyField( Article, verbose_name=_("articles vendus"), - through='SupplierArticle', - related_name='suppliers', + through="SupplierArticle", + related_name="suppliers", ) name = models.CharField(_("nom"), max_length=45) address = models.TextField(_("adresse"), blank=True) @@ -584,175 +593,187 @@ class Supplier(models.Model): class SupplierArticle(models.Model): - supplier = models.ForeignKey( - Supplier, on_delete = models.PROTECT) - article = models.ForeignKey( - Article, on_delete = models.PROTECT) - at = models.DateTimeField(auto_now_add = True) + supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT) + article = models.ForeignKey(Article, on_delete=models.PROTECT) + at = models.DateTimeField(auto_now_add=True) price_HT = models.DecimalField( - max_digits = 7, decimal_places = 4, - blank = True, null = True, default = None) + max_digits=7, decimal_places=4, blank=True, null=True, default=None + ) TVA = models.DecimalField( - max_digits = 4, decimal_places = 2, - blank = True, null = True, default = None) + max_digits=4, decimal_places=2, blank=True, null=True, default=None + ) rights = models.DecimalField( - max_digits = 7, decimal_places = 4, - blank = True, null = True, default = None) + max_digits=7, decimal_places=4, blank=True, null=True, default=None + ) + class Order(models.Model): supplier = models.ForeignKey( - Supplier, on_delete = models.PROTECT, - related_name = "orders") + Supplier, on_delete=models.PROTECT, related_name="orders" + ) articles = models.ManyToManyField( - Article, - through = "OrderArticle", - related_name = "orders") - at = models.DateTimeField(auto_now_add = True) - amount = models.DecimalField( - max_digits = 6, decimal_places = 2, default = 0) + Article, through="OrderArticle", related_name="orders" + ) + at = models.DateTimeField(auto_now_add=True) + amount = models.DecimalField(max_digits=6, decimal_places=2, default=0) class Meta: - ordering = ['-at'] + ordering = ["-at"] + class OrderArticle(models.Model): - order = models.ForeignKey( - Order, on_delete = models.PROTECT) - article = models.ForeignKey( - Article, on_delete = models.PROTECT) + order = models.ForeignKey(Order, on_delete=models.PROTECT) + article = models.ForeignKey(Article, on_delete=models.PROTECT) quantity_ordered = models.IntegerField() - quantity_received = models.IntegerField(default = 0) + quantity_received = models.IntegerField(default=0) + class TransferGroup(models.Model): at = models.DateTimeField(default=timezone.now) # Optional - comment = models.CharField( - max_length = 255, - blank = True, default = "") + comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "+", - blank = True, null = True, default = None) + Account, + on_delete=models.PROTECT, + related_name="+", + blank=True, + null=True, + default=None, + ) class Transfer(models.Model): group = models.ForeignKey( - TransferGroup, on_delete=models.PROTECT, - related_name="transfers") + TransferGroup, on_delete=models.PROTECT, related_name="transfers" + ) from_acc = models.ForeignKey( - Account, on_delete=models.PROTECT, - related_name="transfers_from") + Account, on_delete=models.PROTECT, related_name="transfers_from" + ) to_acc = models.ForeignKey( - Account, on_delete=models.PROTECT, - related_name="transfers_to") + Account, on_delete=models.PROTECT, related_name="transfers_to" + ) amount = models.DecimalField(max_digits=6, decimal_places=2) # Optional canceled_by = models.ForeignKey( - Account, on_delete=models.PROTECT, - null=True, blank=True, default=None, - related_name="+") - canceled_at = models.DateTimeField( - null=True, blank=True, default=None) + Account, + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + related_name="+", + ) + canceled_at = models.DateTimeField(null=True, blank=True, default=None) def __str__(self): - return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount) + return "{} -> {}: {}€".format(self.from_acc, self.to_acc, self.amount) class OperationGroup(models.Model): on_acc = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "opesgroup") + Account, on_delete=models.PROTECT, related_name="opesgroup" + ) checkout = models.ForeignKey( - Checkout, on_delete = models.PROTECT, - related_name = "opesgroup") + Checkout, on_delete=models.PROTECT, related_name="opesgroup" + ) at = models.DateTimeField(default=timezone.now) - amount = models.DecimalField( - max_digits = 6, decimal_places = 2, - default = 0) - is_cof = models.BooleanField(default = False) + amount = models.DecimalField(max_digits=6, decimal_places=2, default=0) + is_cof = models.BooleanField(default=False) # Optional - comment = models.CharField( - max_length = 255, - blank = True, default = "") + comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "+", - blank = True, null = True, default = None) + Account, + on_delete=models.PROTECT, + related_name="+", + blank=True, + null=True, + default=None, + ) def __str__(self): - return ', '.join(map(str, self.opes.all())) + return ", ".join(map(str, self.opes.all())) class Operation(models.Model): - PURCHASE = 'purchase' - DEPOSIT = 'deposit' - WITHDRAW = 'withdraw' - INITIAL = 'initial' - EDIT = 'edit' + PURCHASE = "purchase" + DEPOSIT = "deposit" + WITHDRAW = "withdraw" + INITIAL = "initial" + EDIT = "edit" TYPE_ORDER_CHOICES = ( - (PURCHASE, 'Achat'), - (DEPOSIT, 'Charge'), - (WITHDRAW, 'Retrait'), - (INITIAL, 'Initial'), - (EDIT, 'Édition'), + (PURCHASE, "Achat"), + (DEPOSIT, "Charge"), + (WITHDRAW, "Retrait"), + (INITIAL, "Initial"), + (EDIT, "Édition"), ) group = models.ForeignKey( - OperationGroup, on_delete=models.PROTECT, - related_name="opes") + OperationGroup, on_delete=models.PROTECT, related_name="opes" + ) type = models.CharField( - choices=TYPE_ORDER_CHOICES, - max_length=choices_length(TYPE_ORDER_CHOICES)) - amount = models.DecimalField( - max_digits=6, decimal_places=2, - blank=True, default=0) + choices=TYPE_ORDER_CHOICES, max_length=choices_length(TYPE_ORDER_CHOICES) + ) + amount = models.DecimalField(max_digits=6, decimal_places=2, blank=True, default=0) # Optional article = models.ForeignKey( - Article, on_delete=models.PROTECT, + Article, + on_delete=models.PROTECT, related_name="operations", - blank=True, null=True, default=None) - article_nb = models.PositiveSmallIntegerField( - blank=True, null=True, default=None) + blank=True, + null=True, + default=None, + ) + article_nb = models.PositiveSmallIntegerField(blank=True, null=True, default=None) canceled_by = models.ForeignKey( - Account, on_delete=models.PROTECT, + Account, + on_delete=models.PROTECT, related_name="+", - blank=True, null=True, default=None) - canceled_at = models.DateTimeField( - blank=True, null=True, default=None) + blank=True, + null=True, + default=None, + ) + canceled_at = models.DateTimeField(blank=True, null=True, default=None) addcost_for = models.ForeignKey( - Account, on_delete=models.PROTECT, + Account, + on_delete=models.PROTECT, related_name="addcosts", - blank=True, null=True, default=None) + blank=True, + null=True, + default=None, + ) addcost_amount = models.DecimalField( - max_digits=6, decimal_places=2, - blank=True, null=True, default=None) + max_digits=6, decimal_places=2, blank=True, null=True, default=None + ) class Meta: permissions = ( - ('perform_deposit', 'Effectuer une charge'), - ('perform_negative_operations', - 'Enregistrer des commandes en négatif'), - ('override_frozen_protection', "Forcer le gel d'un compte"), - ('cancel_old_operations', 'Annuler des commandes non récentes'), - ('perform_commented_operations', - 'Enregistrer des commandes avec commentaires'), + ("perform_deposit", "Effectuer une charge"), + ("perform_negative_operations", "Enregistrer des commandes en négatif"), + ("override_frozen_protection", "Forcer le gel d'un compte"), + ("cancel_old_operations", "Annuler des commandes non récentes"), + ( + "perform_commented_operations", + "Enregistrer des commandes avec commentaires", + ), ) @property def is_checkout(self): - return (self.type == Operation.DEPOSIT or - self.type == Operation.WITHDRAW or - (self.type == Operation.PURCHASE and self.group.on_acc.is_cash) - ) + return ( + self.type == Operation.DEPOSIT + or self.type == Operation.WITHDRAW + or (self.type == Operation.PURCHASE and self.group.on_acc.is_cash) + ) def __str__(self): templates = { - self.PURCHASE: "{nb} {article.name} ({amount}€)", - self.DEPOSIT: "charge ({amount}€)", - self.WITHDRAW: "retrait ({amount}€)", - self.INITIAL: "initial ({amount}€)", - self.EDIT: "édition ({amount}€)", - } - return templates[self.type].format(nb=self.article_nb, - article=self.article, - amount=self.amount) + self.PURCHASE: "{nb} {article.name} ({amount}€)", + self.DEPOSIT: "charge ({amount}€)", + self.WITHDRAW: "retrait ({amount}€)", + self.INITIAL: "initial ({amount}€)", + self.EDIT: "édition ({amount}€)", + } + return templates[self.type].format( + nb=self.article_nb, article=self.article, amount=self.amount + ) diff --git a/kfet/open/consumers.py b/kfet/open/consumers.py index b28a4664..8b800c76 100644 --- a/kfet/open/consumers.py +++ b/kfet/open/consumers.py @@ -1,6 +1,5 @@ from ..decorators import kfet_is_team from ..utils import DjangoJsonWebsocketConsumer, PermConsumerMixin - from .open import kfet_open @@ -16,8 +15,8 @@ class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer): def connection_groups(self, user, **kwargs): """Select which group the user should be connected.""" if kfet_is_team(user): - return ['kfet.open.team'] - return ['kfet.open.base'] + return ["kfet.open.team"] + return ["kfet.open.base"] def connect(self, message, *args, **kwargs): """Send current status on connect.""" diff --git a/kfet/open/open.py b/kfet/open/open.py index 82d6217a..d0e0c901 100644 --- a/kfet/open/open.py +++ b/kfet/open/open.py @@ -15,23 +15,20 @@ class OpenKfet(CachedMixin, object): Current state persists through cache. """ + # status is unknown after this duration time_unknown = timedelta(minutes=15) # status - OPENED = 'opened' - CLOSED = 'closed' - UNKNOWN = 'unknown' + OPENED = "opened" + CLOSED = "closed" + UNKNOWN = "unknown" # admin status - FAKE_CLOSED = 'fake_closed' + FAKE_CLOSED = "fake_closed" # cached attributes config - cached = { - '_raw_open': False, - '_last_update': None, - 'force_close': False, - } - cache_prefix = 'kfetopen' + cached = {"_raw_open": False, "_last_update": None, "force_close": False} + cache_prefix = "kfetopen" @property def raw_open(self): @@ -54,8 +51,10 @@ class OpenKfet(CachedMixin, object): return False if self.force_close else self.raw_open def status(self): - if (self.last_update is None or - timezone.now() - self.last_update >= self.time_unknown): + if ( + self.last_update is None + or timezone.now() - self.last_update >= self.time_unknown + ): return self.UNKNOWN return self.OPENED if self.is_open else self.CLOSED @@ -78,12 +77,10 @@ class OpenKfet(CachedMixin, object): """ status = self.status() - base = { - 'status': status, - } + base = {"status": status} restrict = { - 'admin_status': self.admin_status(status), - 'force_close': self.force_close, + "admin_status": self.admin_status(status), + "force_close": self.force_close, } return base, dict(base, **restrict) @@ -101,9 +98,10 @@ class OpenKfet(CachedMixin, object): def send_ws(self): """Send internal state to websocket channels.""" from .consumers import OpenKfetConsumer + base, team = self._export() - OpenKfetConsumer.group_send('kfet.open.base', base) - OpenKfetConsumer.group_send('kfet.open.team', team) + OpenKfetConsumer.group_send("kfet.open.base", base) + OpenKfetConsumer.group_send("kfet.open.team", team) kfet_open = OpenKfet() diff --git a/kfet/open/routing.py b/kfet/open/routing.py index 681bfab2..811ae56e 100644 --- a/kfet/open/routing.py +++ b/kfet/open/routing.py @@ -2,7 +2,4 @@ from channels.routing import route_class from . import consumers - -routing = [ - route_class(consumers.OpenKfetConsumer) -] +routing = [route_class(consumers.OpenKfetConsumer)] diff --git a/kfet/open/tests.py b/kfet/open/tests.py index 476eb6c0..75a9bf8a 100644 --- a/kfet/open/tests.py +++ b/kfet/open/tests.py @@ -2,14 +2,13 @@ import json from datetime import timedelta from unittest import mock +from channels.channel import Group +from channels.test import ChannelTestCase, WSClient from django.contrib.auth.models import AnonymousUser, Permission, User from django.test import Client from django.utils import timezone -from channels.channel import Group -from channels.test import ChannelTestCase, WSClient - -from . import kfet_open, OpenKfet +from . import OpenKfet, kfet_open from .consumers import OpenKfetConsumer @@ -79,40 +78,28 @@ class OpenKfetTest(ChannelTestCase): def test_export_user(self): """Export is limited for an anonymous user.""" export = self.kfet_open.export(AnonymousUser()) - self.assertSetEqual( - set(['status']), - set(export), - ) + self.assertSetEqual(set(["status"]), set(export)) def test_export_team(self): """Export all values for a team member.""" - user = User.objects.create_user('team', '', 'team') - user.user_permissions.add(Permission.objects.get(codename='is_team')) + user = User.objects.create_user("team", "", "team") + user.user_permissions.add(Permission.objects.get(codename="is_team")) export = self.kfet_open.export(user) - self.assertSetEqual( - set(['status', 'admin_status', 'force_close']), - set(export), - ) + self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(export)) def test_send_ws(self): - Group('kfet.open.base').add('test.open.base') - Group('kfet.open.team').add('test.open.team') + Group("kfet.open.base").add("test.open.base") + Group("kfet.open.team").add("test.open.team") self.kfet_open.send_ws() - recv_base = self.get_next_message('test.open.base', require=True) - base = json.loads(recv_base['text']) - self.assertSetEqual( - set(['status']), - set(base), - ) + recv_base = self.get_next_message("test.open.base", require=True) + base = json.loads(recv_base["text"]) + self.assertSetEqual(set(["status"]), set(base)) - recv_admin = self.get_next_message('test.open.team', require=True) - admin = json.loads(recv_admin['text']) - self.assertSetEqual( - set(['status', 'admin_status', 'force_close']), - set(admin), - ) + recv_admin = self.get_next_message("test.open.team", require=True) + admin = json.loads(recv_admin["text"]) + self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(admin)) class OpenKfetViewsTest(ChannelTestCase): @@ -120,34 +107,34 @@ class OpenKfetViewsTest(ChannelTestCase): def setUp(self): # Need this (and here) because of '.login' in setUp - patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) # get some permissions perms = { - 'kfet.is_team': Permission.objects.get(codename='is_team'), - 'kfet.can_force_close': Permission.objects.get(codename='can_force_close'), + "kfet.is_team": Permission.objects.get(codename="is_team"), + "kfet.can_force_close": Permission.objects.get(codename="can_force_close"), } # authenticated user and its client - self.u = User.objects.create_user('user', '', 'user') + self.u = User.objects.create_user("user", "", "user") self.c = Client() - self.c.login(username='user', password='user') + self.c.login(username="user", password="user") # team user and its clients - self.t = User.objects.create_user('team', '', 'team') - self.t.user_permissions.add(perms['kfet.is_team']) + self.t = User.objects.create_user("team", "", "team") + self.t.user_permissions.add(perms["kfet.is_team"]) self.c_t = Client() - self.c_t.login(username='team', password='team') + self.c_t.login(username="team", password="team") # admin user and its client - self.a = User.objects.create_user('admin', '', 'admin') + self.a = User.objects.create_user("admin", "", "admin") self.a.user_permissions.add( - perms['kfet.is_team'], perms['kfet.can_force_close'], + perms["kfet.is_team"], perms["kfet.can_force_close"] ) self.c_a = Client() - self.c_a.login(username='admin', password='admin') + self.c_a.login(username="admin", password="admin") def tearDown(self): kfet_open.clear_cache() @@ -155,17 +142,16 @@ class OpenKfetViewsTest(ChannelTestCase): def test_door(self): """Edit raw_status.""" for sent, expected in [(1, True), (0, False)]: - resp = Client().post('/k-fet/open/raw_open', { - 'raw_open': sent, - 'token': 'plop', - }) + resp = Client().post( + "/k-fet/open/raw_open", {"raw_open": sent, "token": "plop"} + ) self.assertEqual(200, resp.status_code) self.assertEqual(expected, kfet_open.raw_open) def test_force_close(self): """Edit force_close.""" for sent, expected in [(1, True), (0, False)]: - resp = self.c_a.post('/k-fet/open/force_close', {'force_close': sent}) + resp = self.c_a.post("/k-fet/open/force_close", {"force_close": sent}) self.assertEqual(200, resp.status_code) self.assertEqual(expected, kfet_open.force_close) @@ -173,7 +159,7 @@ class OpenKfetViewsTest(ChannelTestCase): """Can't edit force_close without kfet.can_force_close permission.""" clients = [Client(), self.c, self.c_t] for client in clients: - resp = client.post('/k-fet/open/force_close', {'force_close': 0}) + resp = client.post("/k-fet/open/force_close", {"force_close": 0}) self.assertEqual(403, resp.status_code) @@ -186,44 +172,44 @@ class OpenKfetConsumerTest(ChannelTestCase): c = WSClient() # connect - c.send_and_consume('websocket.connect', path='/ws/k-fet/open', - fail_on_none=True) + c.send_and_consume( + "websocket.connect", path="/ws/k-fet/open", fail_on_none=True + ) # initialization data is replied on connection self.assertIsNotNone(c.receive()) # client belongs to the 'kfet.open' group... - OpenKfetConsumer.group_send('kfet.open.base', {'test': 'plop'}) - self.assertEqual(c.receive(), {'test': 'plop'}) + OpenKfetConsumer.group_send("kfet.open.base", {"test": "plop"}) + self.assertEqual(c.receive(), {"test": "plop"}) # ...but not to the 'kfet.open.admin' one - OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'}) + OpenKfetConsumer.group_send("kfet.open.team", {"test": "plop"}) self.assertIsNone(c.receive()) - @mock.patch('gestioncof.signals.messages') + @mock.patch("gestioncof.signals.messages") def test_team_user(self, mock_messages): """Team user is added to kfet.open.team group.""" # setup team user and its client - t = User.objects.create_user('team', '', 'team') - t.user_permissions.add( - Permission.objects.get(codename='is_team') - ) + t = User.objects.create_user("team", "", "team") + t.user_permissions.add(Permission.objects.get(codename="is_team")) c = WSClient() c.force_login(t) # connect - c.send_and_consume('websocket.connect', path='/ws/k-fet/open', - fail_on_none=True) + c.send_and_consume( + "websocket.connect", path="/ws/k-fet/open", fail_on_none=True + ) # initialization data is replied on connection self.assertIsNotNone(c.receive()) # client belongs to the 'kfet.open.admin' group... - OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'}) - self.assertEqual(c.receive(), {'test': 'plop'}) + OpenKfetConsumer.group_send("kfet.open.team", {"test": "plop"}) + self.assertEqual(c.receive(), {"test": "plop"}) # ... but not to the 'kfet.open' one - OpenKfetConsumer.group_send('kfet.open.base', {'test': 'plop'}) + OpenKfetConsumer.group_send("kfet.open.base", {"test": "plop"}) self.assertIsNone(c.receive()) @@ -232,7 +218,7 @@ class OpenKfetScenarioTest(ChannelTestCase): def setUp(self): # Need this (and here) because of '.login' in setUp - patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) @@ -242,10 +228,10 @@ class OpenKfetScenarioTest(ChannelTestCase): self.c_ws = WSClient() # root user - self.r = User.objects.create_superuser('root', '', 'root') + self.r = User.objects.create_superuser("root", "", "root") # its client (for views) self.r_c = Client() - self.r_c.login(username='root', password='root') + self.r_c.login(username="root", password="root") # its client (for websockets) self.r_c_ws = WSClient() self.r_c_ws.force_login(self.r) @@ -255,8 +241,7 @@ class OpenKfetScenarioTest(ChannelTestCase): def ws_connect(self, ws_client): ws_client.send_and_consume( - 'websocket.connect', path='/ws/k-fet/open', - fail_on_none=True, + "websocket.connect", path="/ws/k-fet/open", fail_on_none=True ) return ws_client.receive(json=True) @@ -264,17 +249,11 @@ class OpenKfetScenarioTest(ChannelTestCase): """Clients connect.""" # test for anonymous user msg = self.ws_connect(self.c_ws) - self.assertSetEqual( - set(['status']), - set(msg), - ) + self.assertSetEqual(set(["status"]), set(msg)) # test for root user msg = self.ws_connect(self.r_c_ws) - self.assertSetEqual( - set(['status', 'admin_status', 'force_close']), - set(msg), - ) + self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(msg)) def test_scenario_1(self): """Clients connect, door opens, enable force close.""" @@ -282,33 +261,30 @@ class OpenKfetScenarioTest(ChannelTestCase): self.ws_connect(self.r_c_ws) # door sent "I'm open!" - self.c.post('/k-fet/open/raw_open', { - 'raw_open': True, - 'token': 'plop', - }) + self.c.post("/k-fet/open/raw_open", {"raw_open": True, "token": "plop"}) # anonymous user agree msg = self.c_ws.receive(json=True) - self.assertEqual(OpenKfet.OPENED, msg['status']) + self.assertEqual(OpenKfet.OPENED, msg["status"]) # root user too msg = self.r_c_ws.receive(json=True) - self.assertEqual(OpenKfet.OPENED, msg['status']) - self.assertEqual(OpenKfet.OPENED, msg['admin_status']) + self.assertEqual(OpenKfet.OPENED, msg["status"]) + self.assertEqual(OpenKfet.OPENED, msg["admin_status"]) # admin says "no it's closed" - self.r_c.post('/k-fet/open/force_close', {'force_close': True}) + self.r_c.post("/k-fet/open/force_close", {"force_close": True}) # so anonymous user see it's closed msg = self.c_ws.receive(json=True) - self.assertEqual(OpenKfet.CLOSED, msg['status']) + self.assertEqual(OpenKfet.CLOSED, msg["status"]) # root user too msg = self.r_c_ws.receive(json=True) - self.assertEqual(OpenKfet.CLOSED, msg['status']) + self.assertEqual(OpenKfet.CLOSED, msg["status"]) # but root knows things - self.assertEqual(OpenKfet.FAKE_CLOSED, msg['admin_status']) - self.assertTrue(msg['force_close']) + self.assertEqual(OpenKfet.FAKE_CLOSED, msg["admin_status"]) + self.assertTrue(msg["force_close"]) def test_scenario_2(self): """Starting falsely closed, clients connect, disable force close.""" @@ -316,19 +292,19 @@ class OpenKfetScenarioTest(ChannelTestCase): kfet_open.force_close = True msg = self.ws_connect(self.c_ws) - self.assertEqual(OpenKfet.CLOSED, msg['status']) + self.assertEqual(OpenKfet.CLOSED, msg["status"]) msg = self.ws_connect(self.r_c_ws) - self.assertEqual(OpenKfet.CLOSED, msg['status']) - self.assertEqual(OpenKfet.FAKE_CLOSED, msg['admin_status']) - self.assertTrue(msg['force_close']) + self.assertEqual(OpenKfet.CLOSED, msg["status"]) + self.assertEqual(OpenKfet.FAKE_CLOSED, msg["admin_status"]) + self.assertTrue(msg["force_close"]) - self.r_c.post('/k-fet/open/force_close', {'force_close': False}) + self.r_c.post("/k-fet/open/force_close", {"force_close": False}) msg = self.c_ws.receive(json=True) - self.assertEqual(OpenKfet.OPENED, msg['status']) + self.assertEqual(OpenKfet.OPENED, msg["status"]) msg = self.r_c_ws.receive(json=True) - self.assertEqual(OpenKfet.OPENED, msg['status']) - self.assertEqual(OpenKfet.OPENED, msg['admin_status']) - self.assertFalse(msg['force_close']) + self.assertEqual(OpenKfet.OPENED, msg["status"]) + self.assertEqual(OpenKfet.OPENED, msg["admin_status"]) + self.assertFalse(msg["force_close"]) diff --git a/kfet/open/urls.py b/kfet/open/urls.py index bd227b96..c38b9ce4 100644 --- a/kfet/open/urls.py +++ b/kfet/open/urls.py @@ -2,10 +2,7 @@ from django.conf.urls import url from . import views - urlpatterns = [ - url(r'^raw_open$', views.raw_open, - name='kfet.open.edit_raw_open'), - url(r'^force_close$', views.force_close, - name='kfet.open.edit_force_close'), + url(r"^raw_open$", views.raw_open, name="kfet.open.edit_raw_open"), + url(r"^force_close$", views.force_close, name="kfet.open.edit_force_close"), ] diff --git a/kfet/open/views.py b/kfet/open/views.py index 4f1efa5f..49b91f4a 100644 --- a/kfet/open/views.py +++ b/kfet/open/views.py @@ -1,32 +1,31 @@ from django.conf import settings -from django.core.exceptions import PermissionDenied from django.contrib.auth.decorators import permission_required +from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from .open import kfet_open - -TRUE_STR = ['1', 'True', 'true'] +TRUE_STR = ["1", "True", "true"] @csrf_exempt @require_POST def raw_open(request): - token = request.POST.get('token') + token = request.POST.get("token") if token != settings.KFETOPEN_TOKEN: raise PermissionDenied - raw_open = request.POST.get('raw_open') in TRUE_STR + raw_open = request.POST.get("raw_open") in TRUE_STR kfet_open.raw_open = raw_open kfet_open.send_ws() return HttpResponse() -@permission_required('kfet.can_force_close', raise_exception=True) +@permission_required("kfet.can_force_close", raise_exception=True) @require_POST def force_close(request): - force_close = request.POST.get('force_close') in TRUE_STR + force_close = request.POST.get("force_close") in TRUE_STR kfet_open.force_close = force_close kfet_open.send_ws() return HttpResponse() diff --git a/kfet/routing.py b/kfet/routing.py index f1305d4b..ceafca06 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -2,8 +2,7 @@ from channels.routing import include, route_class from . import consumers - routing = [ - route_class(consumers.KPsul, path=r'^/k-psul/$'), - include('kfet.open.routing.routing', path=r'^/open'), + route_class(consumers.KPsul, path=r"^/k-psul/$"), + include("kfet.open.routing.routing", path=r"^/open"), ] diff --git a/kfet/statistic.py b/kfet/statistic.py index 0aba4dda..02171267 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,11 +1,10 @@ from datetime import date, datetime, time, timedelta -from dateutil.relativedelta import relativedelta -from dateutil.parser import parse as dateutil_parse import pytz - -from django.utils import timezone +from dateutil.parser import parse as dateutil_parse +from dateutil.relativedelta import relativedelta from django.db.models import Sum +from django.utils import timezone KFET_WAKES_UP_AT = time(7, 0) @@ -13,7 +12,7 @@ KFET_WAKES_UP_AT = time(7, 0) def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): """datetime wrapper with time offset.""" naive = datetime.combine(date(year, month, day), start_at) - return pytz.timezone('Europe/Paris').localize(naive, is_dst=None) + return pytz.timezone("Europe/Paris").localize(naive, is_dst=None) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): @@ -27,8 +26,7 @@ class Scale(object): name = None step = None - def __init__(self, n_steps=0, begin=None, end=None, - last=False, std_chunk=True): + def __init__(self, n_steps=0, begin=None, end=None, last=False, std_chunk=True): self.std_chunk = std_chunk if last: end = timezone.now() @@ -48,9 +46,11 @@ class Scale(object): self.begin = begin self.end = end else: - raise Exception('Two of these args must be specified: ' - 'n_steps, begin, end; ' - 'or use last and n_steps') + raise Exception( + "Two of these args must be specified: " + "n_steps, begin, end; " + "or use last and n_steps" + ) self.datetimes = self.get_datetimes() @@ -65,7 +65,7 @@ class Scale(object): return self.std_chunk and self.get_chunk_start(dt) or dt def __getitem__(self, i): - return self.datetimes[i], self.datetimes[i+1] + return self.datetimes[i], self.datetimes[i + 1] def __len__(self): return len(self.datetimes) - 1 @@ -85,21 +85,18 @@ class Scale(object): if label_fmt is None: label_fmt = self.label_fmt return [ - begin.strftime(label_fmt.format(i=i, rev_i=len(self)-i)) + begin.strftime(label_fmt.format(i=i, rev_i=len(self) - i)) for i, (begin, end) in enumerate(self) ] def chunkify_qs(self, qs, field=None): if field is None: - field = 'at' - begin_f = '{}__gte'.format(field) - end_f = '{}__lte'.format(field) - return [ - qs.filter(**{begin_f: begin, end_f: end}) - for begin, end in self - ] + field = "at" + begin_f = "{}__gte".format(field) + end_f = "{}__lte".format(field) + return [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] - def get_by_chunks(self, qs, field_callback=None, field_db='at'): + def get_by_chunks(self, qs, field_callback=None, field_db="at"): """Objects of queryset ranked according to the scale. Returns a generator whose each item, corresponding to a scale chunk, @@ -122,16 +119,14 @@ class Scale(object): """ if field_callback is None: + def field_callback(obj): return getattr(obj, field_db) - begin_f = '{}__gte'.format(field_db) - end_f = '{}__lte'.format(field_db) + begin_f = "{}__gte".format(field_db) + end_f = "{}__lte".format(field_db) - qs = ( - qs - .filter(**{begin_f: self.begin, end_f: self.end}) - ) + qs = qs.filter(**{begin_f: self.begin, end_f: self.end}) obj_iter = iter(qs) @@ -184,9 +179,9 @@ class Scale(object): class DayScale(Scale): - name = 'day' + name = "day" step = timedelta(days=1) - label_fmt = '%A' + label_fmt = "%A" @classmethod def get_chunk_start(cls, dt): @@ -194,9 +189,9 @@ class DayScale(Scale): class WeekScale(Scale): - name = 'week' + name = "week" step = timedelta(days=7) - label_fmt = 'Semaine %W' + label_fmt = "Semaine %W" @classmethod def get_chunk_start(cls, dt): @@ -206,62 +201,63 @@ class WeekScale(Scale): class MonthScale(Scale): - name = 'month' + name = "month" step = relativedelta(months=1) - label_fmt = '%B' + label_fmt = "%B" @classmethod def get_chunk_start(cls, dt): return to_kfet_day(dt).replace(day=1) -def stat_manifest(scales_def=None, scale_args=None, scale_prefix=None, - **other_url_params): +def stat_manifest( + scales_def=None, scale_args=None, scale_prefix=None, **other_url_params +): if scale_prefix is None: - scale_prefix = 'scale_' + scale_prefix = "scale_" if scales_def is None: scales_def = [] if scale_args is None: scale_args = {} manifest = [] for label, cls in scales_def: - url_params = {scale_prefix+'name': cls.name} - url_params.update({scale_prefix+key: value - for key, value in scale_args.items()}) + url_params = {scale_prefix + "name": cls.name} + url_params.update( + {scale_prefix + key: value for key, value in scale_args.items()} + ) url_params.update(other_url_params) - manifest.append(dict( - label=label, - url_params=url_params, - )) + manifest.append(dict(label=label, url_params=url_params)) return manifest -def last_stats_manifest(scales_def=None, scale_args=None, scale_prefix=None, - **url_params): +def last_stats_manifest( + scales_def=None, scale_args=None, scale_prefix=None, **url_params +): scales_def = [ - ('Derniers mois', MonthScale, ), - ('Dernières semaines', WeekScale, ), - ('Derniers jours', DayScale, ), + ("Derniers mois", MonthScale), + ("Dernières semaines", WeekScale), + ("Derniers jours", DayScale), ] if scale_args is None: scale_args = {} - scale_args.update(dict( - last=True, - n_steps=7, - )) - return stat_manifest(scales_def=scales_def, scale_args=scale_args, - scale_prefix=scale_prefix, **url_params) + scale_args.update(dict(last=True, n_steps=7)) + return stat_manifest( + scales_def=scales_def, + scale_args=scale_args, + scale_prefix=scale_prefix, + **url_params + ) # Étant donné un queryset d'operations # rend la somme des article_nb def tot_ventes(queryset): - res = queryset.aggregate(Sum('article_nb'))['article_nb__sum'] + res = queryset.aggregate(Sum("article_nb"))["article_nb__sum"] return res and res or 0 class ScaleMixin(object): - scale_args_prefix = 'scale_' + scale_args_prefix = "scale_" def get_scale_args(self, params=None, prefix=None): """Retrieve scale args from params. @@ -282,26 +278,25 @@ class ScaleMixin(object): scale_args = {} - name = params.get(prefix+'name', None) + name = params.get(prefix + "name", None) if name is not None: - scale_args['name'] = name + scale_args["name"] = name - n_steps = params.get(prefix+'n_steps', None) + n_steps = params.get(prefix + "n_steps", None) if n_steps is not None: - scale_args['n_steps'] = int(n_steps) + scale_args["n_steps"] = int(n_steps) - begin = params.get(prefix+'begin', None) + begin = params.get(prefix + "begin", None) if begin is not None: - scale_args['begin'] = dateutil_parse(begin) + scale_args["begin"] = dateutil_parse(begin) - end = params.get(prefix+'send', None) + end = params.get(prefix + "send", None) if end is not None: - scale_args['end'] = dateutil_parse(end) + scale_args["end"] = dateutil_parse(end) - last = params.get(prefix+'last', None) + last = params.get(prefix + "last", None) if last is not None: - scale_args['last'] = ( - last in ['true', 'True', '1'] and True or False) + scale_args["last"] = last in ["true", "True", "1"] and True or False return scale_args @@ -309,7 +304,7 @@ class ScaleMixin(object): context = super().get_context_data(*args, **kwargs) scale_args = self.get_scale_args() - scale_name = scale_args.pop('name', None) + scale_name = scale_args.pop("name", None) scale_cls = Scale.by_name(scale_name) if scale_cls is None: @@ -318,7 +313,7 @@ class ScaleMixin(object): scale = scale_cls(**scale_args) self.scale = scale - context['labels'] = scale.get_labels() + context["labels"] = scale.get_labels() return context def get_default_scale(self): diff --git a/kfet/templatetags/dictionary_extras.py b/kfet/templatetags/dictionary_extras.py index fafaad8d..181fd012 100644 --- a/kfet/templatetags/dictionary_extras.py +++ b/kfet/templatetags/dictionary_extras.py @@ -1,5 +1,6 @@ from django.template.defaulttags import register + @register.filter def get_item(dictionary, key): return dictionary.get(key) diff --git a/kfet/templatetags/kfet_tags.py b/kfet/templatetags/kfet_tags.py index 68b74738..4c26dd17 100644 --- a/kfet/templatetags/kfet_tags.py +++ b/kfet/templatetags/kfet_tags.py @@ -6,10 +6,9 @@ from django.utils.safestring import mark_safe from ..utils import to_ukf - register = template.Library() -register.filter('ukf', to_ukf) +register.filter("ukf", to_ukf) @register.filter() diff --git a/kfet/tests/test_config.py b/kfet/tests/test_config.py index 43497ca8..02ff1ff0 100644 --- a/kfet/tests/test_config.py +++ b/kfet/tests/test_config.py @@ -1,10 +1,9 @@ from decimal import Decimal +import djconfig from django.test import TestCase from django.utils import timezone -import djconfig - from gestioncof.models import User from kfet.config import kfet_config from kfet.models import Account @@ -18,18 +17,18 @@ class ConfigTest(TestCase): djconfig.reload_maybe() def test_get(self): - self.assertTrue(hasattr(kfet_config, 'subvention_cof')) + self.assertTrue(hasattr(kfet_config, "subvention_cof")) def test_subvention_cof(self): - reduction_cof = Decimal('20') - subvention_cof = Decimal('25') + reduction_cof = Decimal("20") + subvention_cof = Decimal("25") kfet_config.set(reduction_cof=reduction_cof) self.assertEqual(kfet_config.subvention_cof, subvention_cof) def test_set_decimal(self): """Test field of decimal type.""" - reduction_cof = Decimal('10') + reduction_cof = Decimal("10") # IUT kfet_config.set(reduction_cof=reduction_cof) # check @@ -37,9 +36,8 @@ class ConfigTest(TestCase): def test_set_modelinstance(self): """Test field of model instance type.""" - user = User.objects.create(username='foo_user') - account = Account.objects.create(trigramme='FOO', - cofprofile=user.profile) + user = User.objects.create(username="foo_user") + account = Account.objects.create(trigramme="FOO", cofprofile=user.profile) # IUT kfet_config.set(addcost_for=account) # check diff --git a/kfet/tests/test_forms.py b/kfet/tests/test_forms.py index e946d39d..37b05e74 100644 --- a/kfet/tests/test_forms.py +++ b/kfet/tests/test_forms.py @@ -11,14 +11,14 @@ from .utils import create_user class KPsulCheckoutFormTests(TestCase): - def setUp(self): self.now = timezone.now() user = create_user() self.c1 = Checkout.objects.create( - name='C1', balance=10, + name="C1", + balance=10, created_by=user.profile.account_kfet, valid_from=self.now, valid_to=self.now + datetime.timedelta(days=1), @@ -27,13 +27,12 @@ class KPsulCheckoutFormTests(TestCase): self.form = KPsulCheckoutForm() def test_checkout(self): - checkout_f = self.form.fields['checkout'] - self.assertListEqual(list(checkout_f.choices), [ - ('', '---------'), - (self.c1.pk, 'C1'), - ]) + checkout_f = self.form.fields["checkout"] + self.assertListEqual( + list(checkout_f.choices), [("", "---------"), (self.c1.pk, "C1")] + ) - @mock.patch('django.utils.timezone.now') + @mock.patch("django.utils.timezone.now") def test_checkout_valid(self, mock_now): """ Checkout are filtered using the current datetime. @@ -44,5 +43,5 @@ class KPsulCheckoutFormTests(TestCase): form = KPsulCheckoutForm() - checkout_f = form.fields['checkout'] - self.assertListEqual(list(checkout_f.choices), [('', '---------')]) + checkout_f = form.fields["checkout"] + self.assertListEqual(list(checkout_f.choices), [("", "---------")]) diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py index 727cac4e..7ce6605c 100644 --- a/kfet/tests/test_models.py +++ b/kfet/tests/test_models.py @@ -12,26 +12,24 @@ User = get_user_model() class AccountTests(TestCase): - def setUp(self): - self.account = Account(trigramme='000') - self.account.save({'username': 'user'}) + self.account = Account(trigramme="000") + self.account.save({"username": "user"}) def test_password(self): - self.account.change_pwd('anna') + self.account.change_pwd("anna") self.account.save() - self.assertEqual(Account.objects.get_by_password('anna'), self.account) + self.assertEqual(Account.objects.get_by_password("anna"), self.account) with self.assertRaises(Account.DoesNotExist): Account.objects.get_by_password(None) with self.assertRaises(Account.DoesNotExist): - Account.objects.get_by_password('bernard') + Account.objects.get_by_password("bernard") class CheckoutTests(TestCase): - def setUp(self): self.now = timezone.now() diff --git a/kfet/tests/test_statistic.py b/kfet/tests/test_statistic.py index 93de27a0..f0ed7f74 100644 --- a/kfet/tests/test_statistic.py +++ b/kfet/tests/test_statistic.py @@ -1,14 +1,13 @@ from unittest.mock import patch -from django.test import TestCase, Client -from django.contrib.auth.models import User, Permission +from django.contrib.auth.models import Permission, User +from django.test import Client, TestCase from kfet.models import Account, Article, ArticleCategory class TestStats(TestCase): - - @patch('gestioncof.signals.messages') + @patch("gestioncof.signals.messages") def test_user_stats(self, mock_messages): """ Checks that we can get the stat-related pages without any problem. @@ -28,8 +27,7 @@ class TestStats(TestCase): Account.objects.create(trigramme="BAR", cofprofile=user2.profile) article = Article.objects.create( - name="article", - category=ArticleCategory.objects.create(name="C") + name="article", category=ArticleCategory.objects.create(name="C") ) # Each user have its own client @@ -43,12 +41,17 @@ class TestStats(TestCase): user_urls = [ "/k-fet/accounts/FOO/stat/operations/list", "/k-fet/accounts/FOO/stat/operations?{}".format( - '&'.join(["scale=day", - "types=['purchase']", - "scale_args={'n_steps':+7,+'last':+True}", - "format=json"])), + "&".join( + [ + "scale=day", + "types=['purchase']", + "scale_args={'n_steps':+7,+'last':+True}", + "format=json", + ] + ) + ), "/k-fet/accounts/FOO/stat/balance/list", - "/k-fet/accounts/FOO/stat/balance?format=json" + "/k-fet/accounts/FOO/stat/balance?format=json", ] for url in user_urls: resp = client.get(url) @@ -60,7 +63,7 @@ class TestStats(TestCase): # receives a Redirect response articles_urls = [ "/k-fet/articles/{}/stat/sales/list".format(article.pk), - "/k-fet/articles/{}/stat/sales".format(article.pk) + "/k-fet/articles/{}/stat/sales".format(article.pk), ] for url in articles_urls: resp = client.get(url) diff --git a/kfet/tests/test_tests_utils.py b/kfet/tests/test_tests_utils.py index 8308bd5b..45ca2348 100644 --- a/kfet/tests/test_tests_utils.py +++ b/kfet/tests/test_tests_utils.py @@ -7,86 +7,79 @@ from gestioncof.models import CofProfile from ..models import Account from .testcases import TestCaseMixin -from .utils import ( - create_user, create_team, create_root, get_perms, user_add_perms, -) - +from .utils import create_root, create_team, create_user, get_perms, user_add_perms User = get_user_model() class UserHelpersTests(TestCaseMixin, TestCase): - def test_create_user(self): """create_user creates a basic user and its account.""" u = create_user() a = u.profile.account_kfet - self.assertInstanceExpected(u, { - 'get_full_name': 'first last', - 'username': 'user', - }) + self.assertInstanceExpected( + u, {"get_full_name": "first last", "username": "user"} + ) self.assertFalse(u.user_permissions.exists()) - self.assertEqual('000', a.trigramme) + self.assertEqual("000", a.trigramme) def test_create_team(self): u = create_team() a = u.profile.account_kfet - self.assertInstanceExpected(u, { - 'get_full_name': 'team member', - 'username': 'team', - }) - self.assertTrue(u.has_perm('kfet.is_team')) + self.assertInstanceExpected( + u, {"get_full_name": "team member", "username": "team"} + ) + self.assertTrue(u.has_perm("kfet.is_team")) - self.assertEqual('100', a.trigramme) + self.assertEqual("100", a.trigramme) def test_create_root(self): u = create_root() a = u.profile.account_kfet - self.assertInstanceExpected(u, { - 'get_full_name': 'super user', - 'username': 'root', - 'is_superuser': True, - 'is_staff': True, - }) + self.assertInstanceExpected( + u, + { + "get_full_name": "super user", + "username": "root", + "is_superuser": True, + "is_staff": True, + }, + ) - self.assertEqual('200', a.trigramme) + self.assertEqual("200", a.trigramme) class PermHelpersTest(TestCaseMixin, TestCase): - def setUp(self): cts = ContentType.objects.get_for_models(Account, CofProfile) self.perm1 = Permission.objects.create( - content_type=cts[Account], - codename='test_perm', - name='Perm for test', + content_type=cts[Account], codename="test_perm", name="Perm for test" ) self.perm2 = Permission.objects.create( content_type=cts[CofProfile], - codename='another_test_perm', - name='Another one', + codename="another_test_perm", + name="Another one", ) self.perm_team = Permission.objects.get( - content_type__app_label='kfet', - codename='is_team', + content_type__app_label="kfet", codename="is_team" ) def test_get_perms(self): - perms = get_perms('kfet.test_perm', 'gestioncof.another_test_perm') - self.assertDictEqual(perms, { - 'kfet.test_perm': self.perm1, - 'gestioncof.another_test_perm': self.perm2, - }) + perms = get_perms("kfet.test_perm", "gestioncof.another_test_perm") + self.assertDictEqual( + perms, + {"kfet.test_perm": self.perm1, "gestioncof.another_test_perm": self.perm2}, + ) def test_user_add_perms(self): - user = User.objects.create_user(username='user', password='user') + user = User.objects.create_user(username="user", password="user") user.user_permissions.add(self.perm1) - user_add_perms(user, ['kfet.is_team', 'gestioncof.another_test_perm']) + user_add_perms(user, ["kfet.is_team", "gestioncof.another_test_perm"]) self.assertQuerysetEqual( user.user_permissions.all(), diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 28599937..bd57b6f8 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -10,20 +10,33 @@ from django.utils import timezone from ..config import kfet_config from ..models import ( - Account, AccountNegative, Article, ArticleCategory, Checkout, - CheckoutStatement, Inventory, InventoryArticle, Operation, OperationGroup, - Order, OrderArticle, Supplier, SupplierArticle, Transfer, TransferGroup, + Account, + AccountNegative, + Article, + ArticleCategory, + Checkout, + CheckoutStatement, + Inventory, + InventoryArticle, + Operation, + OperationGroup, + Order, + OrderArticle, + Supplier, + SupplierArticle, + Transfer, + TransferGroup, ) from .testcases import ViewTestCaseMixin from .utils import create_team, create_user, get_perms, user_add_perms class AccountListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account' - url_expected = '/k-fet/accounts/' + url_name = "kfet.account" + url_expected = "/k-fet/accounts/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): r = self.client.get(self.url) @@ -31,58 +44,53 @@ class AccountListViewTests(ViewTestCaseMixin, TestCase): class AccountValidFreeTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.is_validandfree.ajax' - url_expected = '/k-fet/accounts/is_validandfree' + url_name = "kfet.account.is_validandfree.ajax" + url_expected = "/k-fet/accounts/is_validandfree" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok_isvalid_isfree(self): """Upper case trigramme not taken is valid and free.""" - r = self.client.get(self.url, {'trigramme': 'AAA'}) - self.assertDictEqual(json.loads(r.content.decode('utf-8')), { - 'is_valid': True, - 'is_free': True, - }) + r = self.client.get(self.url, {"trigramme": "AAA"}) + self.assertDictEqual( + json.loads(r.content.decode("utf-8")), {"is_valid": True, "is_free": True} + ) def test_ok_isvalid_notfree(self): """Already taken trigramme is not free, but valid.""" - r = self.client.get(self.url, {'trigramme': '000'}) - self.assertDictEqual(json.loads(r.content.decode('utf-8')), { - 'is_valid': True, - 'is_free': False, - }) + r = self.client.get(self.url, {"trigramme": "000"}) + self.assertDictEqual( + json.loads(r.content.decode("utf-8")), {"is_valid": True, "is_free": False} + ) def test_ok_notvalid_isfree(self): """Lower case if forbidden but free.""" - r = self.client.get(self.url, {'trigramme': 'aaa'}) - self.assertDictEqual(json.loads(r.content.decode('utf-8')), { - 'is_valid': False, - 'is_free': True, - }) + r = self.client.get(self.url, {"trigramme": "aaa"}) + self.assertDictEqual( + json.loads(r.content.decode("utf-8")), {"is_valid": False, "is_free": True} + ) class AccountCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.create' - url_expected = '/k-fet/accounts/new' + url_name = "kfet.account.create" + url_expected = "/k-fet/accounts/new" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] post_data = { - 'trigramme': 'AAA', - 'username': 'plopplopplop', - 'first_name': 'first', - 'last_name': 'last', - 'email': 'email@domain.net', + "trigramme": "AAA", + "username": "plopplopplop", + "first_name": "first", + "last_name": "last", + "email": "email@domain.net", } def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.add_account']), - } + return {"team1": create_team("team1", "101", perms=["kfet.add_account"])} def test_get_ok(self): r = self.client.get(self.url) @@ -90,18 +98,17 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.account.create')) + self.assertRedirects(r, reverse("kfet.account.create")) - account = Account.objects.get(trigramme='AAA') + account = Account.objects.get(trigramme="AAA") - self.assertInstanceExpected(account, { - 'username': 'plopplopplop', - 'first_name': 'first', - 'last_name': 'last', - }) + self.assertInstanceExpected( + account, + {"username": "plopplopplop", "first_name": "first", "last_name": "last"}, + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -109,132 +116,128 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase): class AccountCreateAjaxViewTests(ViewTestCaseMixin, TestCase): - urls_conf = [{ - 'name': 'kfet.account.create.fromuser', - 'kwargs': {'username': 'user'}, - 'expected': '/k-fet/accounts/new/user/user', - }, { - 'name': 'kfet.account.create.fromclipper', - 'kwargs': { - 'login_clipper': 'myclipper', - 'fullname': 'first last1 last2', + urls_conf = [ + { + "name": "kfet.account.create.fromuser", + "kwargs": {"username": "user"}, + "expected": "/k-fet/accounts/new/user/user", }, - 'expected': ( - '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2' - ), - }, { - 'name': 'kfet.account.create.empty', - 'expected': '/k-fet/accounts/new/empty', - }] + { + "name": "kfet.account.create.fromclipper", + "kwargs": {"login_clipper": "myclipper", "fullname": "first last1 last2"}, + "expected": ("/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2"), + }, + {"name": "kfet.account.create.empty", "expected": "/k-fet/accounts/new/empty"}, + ] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_fromuser(self): r = self.client.get(self.t_urls[0]) self.assertEqual(r.status_code, 200) - user = self.users['user'] + user = self.users["user"] - self.assertEqual(r.context['user_form'].instance, user) - self.assertEqual(r.context['cof_form'].instance, user.profile) - self.assertIn('account_form', r.context) + self.assertEqual(r.context["user_form"].instance, user) + self.assertEqual(r.context["cof_form"].instance, user.profile) + self.assertIn("account_form", r.context) def test_fromclipper(self): r = self.client.get(self.t_urls[1]) self.assertEqual(r.status_code, 200) - self.assertIn('user_form', r.context) - self.assertIn('cof_form', r.context) - self.assertIn('account_form', r.context) + self.assertIn("user_form", r.context) + self.assertIn("cof_form", r.context) + self.assertIn("account_form", r.context) def test_empty(self): r = self.client.get(self.t_urls[2]) self.assertEqual(r.status_code, 200) - self.assertIn('user_form', r.context) - self.assertIn('cof_form', r.context) - self.assertIn('account_form', r.context) + self.assertIn("user_form", r.context) + self.assertIn("cof_form", r.context) + self.assertIn("account_form", r.context) class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.create.autocomplete' - url_expected = '/k-fet/autocomplete/account_new' + url_name = "kfet.account.create.autocomplete" + url_expected = "/k-fet/autocomplete/account_new" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): - r = self.client.get(self.url, {'q': 'first'}) + r = self.client.get(self.url, {"q": "first"}) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.context['users_notcof']), 0) - self.assertEqual(len(r.context['users_cof']), 0) - self.assertSetEqual(set(r.context['kfet']), set([ - (self.accounts['user'], self.users['user']), - ])) + self.assertEqual(len(r.context["users_notcof"]), 0) + self.assertEqual(len(r.context["users_cof"]), 0) + self.assertSetEqual( + set(r.context["kfet"]), set([(self.accounts["user"], self.users["user"])]) + ) class AccountSearchViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.search.autocomplete' - url_expected = '/k-fet/autocomplete/account_search' + url_name = "kfet.account.search.autocomplete" + url_expected = "/k-fet/autocomplete/account_search" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): - r = self.client.get(self.url, {'q': 'first'}) + r = self.client.get(self.url, {"q": "first"}) self.assertEqual(r.status_code, 200) - self.assertSetEqual(set(r.context['accounts']), set([ - ('000', 'first last'), - ])) + self.assertSetEqual(set(r.context["accounts"]), set([("000", "first last")])) class AccountReadViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.read' - url_kwargs = {'trigramme': '001'} - url_expected = '/k-fet/accounts/001' + url_name = "kfet.account.read" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def get_users_extra(self): - return { - 'user1': create_user('user1', '001'), - } + return {"user1": create_user("user1", "001")} def setUp(self): super().setUp() - user1_acc = self.accounts['user1'] - team_acc = self.accounts['team'] + user1_acc = self.accounts["user1"] + team_acc = self.accounts["team"] # Dummy operations and operation groups checkout = Checkout.objects.create( - created_by=team_acc, name="checkout", + created_by=team_acc, + name="checkout", valid_from=timezone.now(), - valid_to=timezone.now() + timezone.timedelta(days=365) + valid_to=timezone.now() + timezone.timedelta(days=365), ) opeg_data = [ - (timezone.now(), Decimal('10')), - (timezone.now() - timezone.timedelta(days=3), Decimal('3')), + (timezone.now(), Decimal("10")), + (timezone.now() - timezone.timedelta(days=3), Decimal("3")), ] - OperationGroup.objects.bulk_create([ - OperationGroup( - on_acc=user1_acc, checkout=checkout, at=at, is_cof=False, - amount=amount - ) - for (at, amount) in opeg_data - ]) + OperationGroup.objects.bulk_create( + [ + OperationGroup( + on_acc=user1_acc, + checkout=checkout, + at=at, + is_cof=False, + amount=amount, + ) + for (at, amount) in opeg_data + ] + ) self.operation_groups = OperationGroup.objects.order_by("-amount") Operation.objects.create( group=self.operation_groups[0], type=Operation.PURCHASE, - amount=Decimal('10') + amount=Decimal("10"), ) Operation.objects.create( - group=self.operation_groups[1], - type=Operation.PURCHASE, - amount=Decimal('3') + group=self.operation_groups[1], type=Operation.PURCHASE, amount=Decimal("3") ) def test_ok(self): @@ -244,44 +247,42 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase): def test_ok_self(self): client = Client() - client.login(username='user1', password='user1') + client.login(username="user1", password="user1") r = client.get(self.url) self.assertEqual(r.status_code, 200) class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.update' - url_kwargs = {'trigramme': '001'} - url_expected = '/k-fet/accounts/001/edit' + url_name = "kfet.account.update" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/edit" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] post_data = { # User - 'first_name': 'The first', - 'last_name': 'The last', - 'email': '', + "first_name": "The first", + "last_name": "The last", + "email": "", # Group - 'groups[]': [], + "groups[]": [], # Account - 'trigramme': '051', - 'nickname': '', - 'promo': '', + "trigramme": "051", + "nickname": "", + "promo": "", # 'is_frozen': not checked # Account password - 'pwd1': '', - 'pwd2': '', + "pwd1": "", + "pwd2": "", } def get_users_extra(self): return { - 'user1': create_user('user1', '001'), - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_account', - ]), + "user1": create_user("user1", "001"), + "team1": create_team("team1", "101", perms=["kfet.change_account"]), } def test_get_ok(self): @@ -290,45 +291,40 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): def test_get_ok_self(self): client = Client() - client.login(username='user1', password='user1') + client.login(username="user1", password="user1") r = client.get(self.url) self.assertEqual(r.status_code, 200) def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.account.read', args=['051'])) + self.assertRedirects(r, reverse("kfet.account.read", args=["051"])) - self.accounts['user1'].refresh_from_db() - self.users['user1'].refresh_from_db() + self.accounts["user1"].refresh_from_db() + self.users["user1"].refresh_from_db() - self.assertInstanceExpected(self.accounts['user1'], { - 'first_name': 'The first', - 'last_name': 'The last', - 'trigramme': '051', - }) + self.assertInstanceExpected( + self.accounts["user1"], + {"first_name": "The first", "last_name": "The last", "trigramme": "051"}, + ) def test_post_ok_self(self): client = Client() - client.login(username='user1', password='user1') + client.login(username="user1", password="user1") - post_data = { - 'first_name': 'The first', - 'last_name': 'The last', - } + post_data = {"first_name": "The first", "last_name": "The last"} r = client.post(self.url, post_data) - self.assertRedirects(r, reverse('kfet.account.read', args=['001'])) + self.assertRedirects(r, reverse("kfet.account.read", args=["001"])) - self.accounts['user1'].refresh_from_db() - self.users['user1'].refresh_from_db() + self.accounts["user1"].refresh_from_db() + self.users["user1"].refresh_from_db() - self.assertInstanceExpected(self.accounts['user1'], { - 'first_name': 'The first', - 'last_name': 'The last', - }) + self.assertInstanceExpected( + self.accounts["user1"], {"first_name": "The first", "last_name": "The last"} + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -336,63 +332,54 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.group' - url_expected = '/k-fet/accounts/groups' + url_name = "kfet.account.group" + url_expected = "/k-fet/accounts/groups" - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), - } + return {"team1": create_team("team1", "101", perms=["kfet.manage_perms"])} def setUp(self): super().setUp() - self.group1 = Group.objects.create(name='K-Fêt - Group1') - self.group2 = Group.objects.create(name='K-Fêt - Group2') + self.group1 = Group.objects.create(name="K-Fêt - Group1") + self.group2 = Group.objects.create(name="K-Fêt - Group2") def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['groups'], - map(repr, [self.group1, self.group2]), - ordered=False, + r.context["groups"], map(repr, [self.group1, self.group2]), ordered=False ) class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.group.create' - url_expected = '/k-fet/accounts/groups/new' + url_name = "kfet.account.group.create" + url_expected = "/k-fet/accounts/groups/new" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), - } + return {"team1": create_team("team1", "101", perms=["kfet.manage_perms"])} @property def post_data(self): return { - 'name': 'The Group', - 'permissions': [ - str(self.perms['kfet.is_team'].pk), - str(self.perms['kfet.manage_perms'].pk), + "name": "The Group", + "permissions": [ + str(self.perms["kfet.is_team"].pk), + str(self.perms["kfet.manage_perms"].pk), ], } def setUp(self): super().setUp() - self.perms = get_perms( - 'kfet.is_team', - 'kfet.manage_perms', - ) + self.perms = get_perms("kfet.is_team", "kfet.manage_perms") def test_get_ok(self): r = self.client.get(self.url) @@ -400,58 +387,50 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): r = self.client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.account.group')) + self.assertRedirects(r, reverse("kfet.account.group")) - group = Group.objects.get(name='K-Fêt The Group') + group = Group.objects.get(name="K-Fêt The Group") self.assertQuerysetEqual( group.permissions.all(), - map(repr, [ - self.perms['kfet.is_team'], - self.perms['kfet.manage_perms'], - ]), + map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), ordered=False, ) class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.group.update' + url_name = "kfet.account.group.update" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] @property def url_kwargs(self): - return {'pk': self.group.pk} + return {"pk": self.group.pk} @property def url_expected(self): - return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk) + return "/k-fet/accounts/groups/{}/edit".format(self.group.pk) def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), - } + return {"team1": create_team("team1", "101", perms=["kfet.manage_perms"])} @property def post_data(self): return { - 'name': 'The Group', - 'permissions': [ - str(self.perms['kfet.is_team'].pk), - str(self.perms['kfet.manage_perms'].pk), + "name": "The Group", + "permissions": [ + str(self.perms["kfet.is_team"].pk), + str(self.perms["kfet.manage_perms"].pk), ], } def setUp(self): super().setUp() - self.perms = get_perms( - 'kfet.is_team', - 'kfet.manage_perms', - ) - self.group = Group.objects.create(name='K-Fêt - Group') + self.perms = get_perms("kfet.is_team", "kfet.manage_perms") + self.group = Group.objects.create(name="K-Fêt - Group") self.group.permissions.set(self.perms.values()) def test_get_ok(self): @@ -460,36 +439,31 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): r = self.client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.account.group')) + self.assertRedirects(r, reverse("kfet.account.group")) self.group.refresh_from_db() - self.assertEqual(self.group.name, 'K-Fêt The Group') + self.assertEqual(self.group.name, "K-Fêt The Group") self.assertQuerysetEqual( self.group.permissions.all(), - map(repr, [ - self.perms['kfet.is_team'], - self.perms['kfet.manage_perms'], - ]), + map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), ordered=False, ) class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.negative' - url_expected = '/k-fet/accounts/negatives' + url_name = "kfet.account.negative" + url_expected = "/k-fet/accounts/negatives" - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.view_negs']), - } + return {"team1": create_team("team1", "101", perms=["kfet.view_negs"])} def setUp(self): super().setUp() - account = self.accounts['user'] + account = self.accounts["user"] account.balance = -5 account.save() account.update_negative() @@ -498,82 +472,86 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['negatives'], - map(repr, [self.accounts['user'].negative]), + r.context["negatives"], + map(repr, [self.accounts["user"].negative]), ordered=False, ) class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.stat.operation.list' - url_kwargs = {'trigramme': '001'} - url_expected = '/k-fet/accounts/001/stat/operations/list' + url_name = "kfet.account.stat.operation.list" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/stat/operations/list" - auth_user = 'user1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "user1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return {'user1': create_user('user1', '001')} + return {"user1": create_user("user1", "001")} def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - content = json.loads(r.content.decode('utf-8')) + content = json.loads(r.content.decode("utf-8")) - base_url = reverse('kfet.account.stat.operation', args=['001']) + base_url = reverse("kfet.account.stat.operation", args=["001"]) - expected_stats = [{ - 'label': 'Derniers mois', - 'url': { - 'path': base_url, - 'query': { - 'scale_n_steps': ['7'], - 'scale_name': ['month'], - 'types': ["['purchase']"], - 'scale_last': ['True'], + expected_stats = [ + { + "label": "Derniers mois", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["7"], + "scale_name": ["month"], + "types": ["['purchase']"], + "scale_last": ["True"], + }, }, }, - }, { - 'label': 'Dernières semaines', - 'url': { - 'path': base_url, - 'query': { - 'scale_n_steps': ['7'], - 'scale_name': ['week'], - 'types': ["['purchase']"], - 'scale_last': ['True'], + { + "label": "Dernières semaines", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["7"], + "scale_name": ["week"], + "types": ["['purchase']"], + "scale_last": ["True"], + }, }, }, - }, { - 'label': 'Derniers jours', - 'url': { - 'path': base_url, - 'query': { - 'scale_n_steps': ['7'], - 'scale_name': ['day'], - 'types': ["['purchase']"], - 'scale_last': ['True'], + { + "label": "Derniers jours", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["7"], + "scale_name": ["day"], + "types": ["['purchase']"], + "scale_last": ["True"], + }, }, }, - }] + ] - for stat, expected in zip(content['stats'], expected_stats): - expected_url = expected.pop('url') - self.assertUrlsEqual(stat['url'], expected_url) + for stat, expected in zip(content["stats"], expected_stats): + expected_url = expected.pop("url") + self.assertUrlsEqual(stat["url"], expected_url) self.assertDictContainsSubset(expected, stat) class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.stat.operation' - url_kwargs = {'trigramme': '001'} - url_expected = '/k-fet/accounts/001/stat/operations' + url_name = "kfet.account.stat.operation" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/stat/operations" - auth_user = 'user1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "user1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return {'user1': create_user('user1', '001')} + return {"user1": create_user("user1", "001")} def test_ok(self): r = self.client.get(self.url) @@ -581,69 +559,60 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.stat.balance.list' - url_kwargs = {'trigramme': '001'} - url_expected = '/k-fet/accounts/001/stat/balance/list' + url_name = "kfet.account.stat.balance.list" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/stat/balance/list" - auth_user = 'user1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "user1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return {'user1': create_user('user1', '001')} + return {"user1": create_user("user1", "001")} def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - content = json.loads(r.content.decode('utf-8')) + content = json.loads(r.content.decode("utf-8")) - base_url = reverse('kfet.account.stat.balance', args=['001']) + base_url = reverse("kfet.account.stat.balance", args=["001"]) - expected_stats = [{ - 'label': 'Tout le temps', - 'url': base_url, - }, { - 'label': '1 an', - 'url': { - 'path': base_url, - 'query': {'last_days': ['365']}, + expected_stats = [ + {"label": "Tout le temps", "url": base_url}, + { + "label": "1 an", + "url": {"path": base_url, "query": {"last_days": ["365"]}}, }, - }, { - 'label': '6 mois', - 'url': { - 'path': base_url, - 'query': {'last_days': ['183']}, + { + "label": "6 mois", + "url": {"path": base_url, "query": {"last_days": ["183"]}}, }, - }, { - 'label': '3 mois', - 'url': { - 'path': base_url, - 'query': {'last_days': ['90']}, + { + "label": "3 mois", + "url": {"path": base_url, "query": {"last_days": ["90"]}}, }, - }, { - 'label': '30 jours', - 'url': { - 'path': base_url, - 'query': {'last_days': ['30']}, + { + "label": "30 jours", + "url": {"path": base_url, "query": {"last_days": ["30"]}}, }, - }] + ] - for stat, expected in zip(content['stats'], expected_stats): - expected_url = expected.pop('url') - self.assertUrlsEqual(stat['url'], expected_url) + for stat, expected in zip(content["stats"], expected_stats): + expected_url = expected.pop("url") + self.assertUrlsEqual(stat["url"], expected_url) self.assertDictContainsSubset(expected, stat) class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.stat.balance' - url_kwargs = {'trigramme': '001'} - url_expected = '/k-fet/accounts/001/stat/balance' + url_name = "kfet.account.stat.balance" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/stat/balance" - auth_user = 'user1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "user1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return {'user1': create_user('user1', '001')} + return {"user1": create_user("user1", "001")} def test_ok(self): r = self.client.get(self.url) @@ -651,23 +620,23 @@ class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): class CheckoutListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkout' - url_expected = '/k-fet/checkouts/' + url_name = "kfet.checkout" + url_expected = "/k-fet/checkouts/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() self.checkout1 = Checkout.objects.create( - name='Checkout 1', - created_by=self.accounts['team'], + name="Checkout 1", + created_by=self.accounts["team"], valid_from=self.now, valid_to=self.now + timedelta(days=5), ) self.checkout2 = Checkout.objects.create( - name='Checkout 2', - created_by=self.accounts['team'], + name="Checkout 2", + created_by=self.accounts["team"], valid_from=self.now + timedelta(days=10), valid_to=self.now + timedelta(days=15), ) @@ -676,33 +645,31 @@ class CheckoutListViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['checkouts'], + r.context["checkouts"], map(repr, [self.checkout1, self.checkout2]), ordered=False, ) class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkout.create' - url_expected = '/k-fet/checkouts/new' + url_name = "kfet.checkout.create" + url_expected = "/k-fet/checkouts/new" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] post_data = { - 'name': 'Checkout', - 'valid_from': '2017-10-08 17:45:00', - 'valid_to': '2017-11-08 16:00:00', - 'balance': '3.14', + "name": "Checkout", + "valid_from": "2017-10-08 17:45:00", + "valid_to": "2017-11-08 16:00:00", + "balance": "3.14", # 'is_protected': not checked } def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.add_checkout']), - } + return {"team1": create_team("team1", "101", perms=["kfet.add_checkout"])} def test_get_ok(self): r = self.client.get(self.url) @@ -710,20 +677,23 @@ class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - checkout = Checkout.objects.get(name='Checkout') + checkout = Checkout.objects.get(name="Checkout") self.assertRedirects(r, checkout.get_absolute_url()) - self.assertInstanceExpected(checkout, { - 'name': 'Checkout', - 'valid_from': timezone.make_aware(datetime(2017, 10, 8, 17, 45)), - 'valid_to': timezone.make_aware(datetime(2017, 11, 8, 16, 00)), - 'balance': Decimal('3.14'), - 'is_protected': False, - }) + self.assertInstanceExpected( + checkout, + { + "name": "Checkout", + "valid_from": timezone.make_aware(datetime(2017, 10, 8, 17, 45)), + "valid_to": timezone.make_aware(datetime(2017, 11, 8, 16, 00)), + "balance": Decimal("3.14"), + "is_protected": False, + }, + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -731,28 +701,29 @@ class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase): class CheckoutReadViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkout.read' + url_name = "kfet.checkout.read" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.checkout.pk} + return {"pk": self.checkout.pk} @property def url_expected(self): - return '/k-fet/checkouts/{}'.format(self.checkout.pk) + return "/k-fet/checkouts/{}".format(self.checkout.pk) def setUp(self): super().setUp() - with mock.patch('django.utils.timezone.now') as mock_now: + with mock.patch("django.utils.timezone.now") as mock_now: mock_now.return_value = self.now self.checkout = Checkout.objects.create( - name='Checkout', balance=Decimal('10'), - created_by=self.accounts['team'], + name="Checkout", + balance=Decimal("10"), + created_by=self.accounts["team"], valid_from=self.now, valid_to=self.now + timedelta(days=1), ) @@ -760,47 +731,43 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase): def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['checkout'], self.checkout) + self.assertEqual(r.context["checkout"], self.checkout) class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkout.update' + url_name = "kfet.checkout.update" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] post_data = { - 'name': 'Checkout updated', - 'valid_from': '2018-01-01 08:00:00', - 'valid_to': '2018-07-01 16:00:00', + "name": "Checkout updated", + "valid_from": "2018-01-01 08:00:00", + "valid_to": "2018-07-01 16:00:00", } @property def url_kwargs(self): - return {'pk': self.checkout.pk} + return {"pk": self.checkout.pk} @property def url_expected(self): - return '/k-fet/checkouts/{}/edit'.format(self.checkout.pk) + return "/k-fet/checkouts/{}/edit".format(self.checkout.pk) def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_checkout', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.change_checkout"])} def setUp(self): super().setUp() self.checkout = Checkout.objects.create( - name='Checkout', + name="Checkout", valid_from=self.now, valid_to=self.now + timedelta(days=5), - balance=Decimal('3.14'), + balance=Decimal("3.14"), is_protected=False, - created_by=self.accounts['team'], + created_by=self.accounts["team"], ) def test_get_ok(self): @@ -809,18 +776,21 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) self.assertRedirects(r, self.checkout.get_absolute_url()) self.checkout.refresh_from_db() - self.assertInstanceExpected(self.checkout, { - 'name': 'Checkout updated', - 'valid_from': timezone.make_aware(datetime(2018, 1, 1, 8, 0, 0)), - 'valid_to': timezone.make_aware(datetime(2018, 7, 1, 16, 0, 0)), - }) + self.assertInstanceExpected( + self.checkout, + { + "name": "Checkout updated", + "valid_from": timezone.make_aware(datetime(2018, 1, 1, 8, 0, 0)), + "valid_to": timezone.make_aware(datetime(2018, 7, 1, 16, 0, 0)), + }, + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -828,29 +798,29 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase): class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkoutstatement' - url_expected = '/k-fet/checkouts/statements/' + url_name = "kfet.checkoutstatement" + url_expected = "/k-fet/checkouts/statements/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() self.checkout1 = Checkout.objects.create( - created_by=self.accounts['team'], - name='Checkout 1', + created_by=self.accounts["team"], + name="Checkout 1", valid_from=self.now, valid_to=self.now + timedelta(days=5), ) self.checkout2 = Checkout.objects.create( - created_by=self.accounts['team'], - name='Checkout 2', + created_by=self.accounts["team"], + name="Checkout 2", valid_from=self.now + timedelta(days=10), valid_to=self.now + timedelta(days=15), ) self.statement1 = CheckoutStatement.objects.create( checkout=self.checkout1, - by=self.accounts['team'], + by=self.accounts["team"], balance_old=5, balance_new=0, amount_taken=5, @@ -860,63 +830,80 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - expected_statements = ( - list(self.checkout1.statements.all()) + - list(self.checkout2.statements.all()) + expected_statements = list(self.checkout1.statements.all()) + list( + self.checkout2.statements.all() ) self.assertQuerysetEqual( - r.context['checkoutstatements'], + r.context["checkoutstatements"], map(repr, expected_statements), ordered=False, ) class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkoutstatement.create' + url_name = "kfet.checkoutstatement.create" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] post_data = { # Let - 'balance_001': 0, 'balance_002': 0, 'balance_005': 0, - 'balance_01': 0, 'balance_02': 0, 'balance_05': 0, - 'balance_1': 1, 'balance_2': 0, 'balance_5': 0, - 'balance_10': 1, 'balance_20': 0, 'balance_50': 0, - 'balance_100': 1, 'balance_200': 0, 'balance_500': 0, + "balance_001": 0, + "balance_002": 0, + "balance_005": 0, + "balance_01": 0, + "balance_02": 0, + "balance_05": 0, + "balance_1": 1, + "balance_2": 0, + "balance_5": 0, + "balance_10": 1, + "balance_20": 0, + "balance_50": 0, + "balance_100": 1, + "balance_200": 0, + "balance_500": 0, # Taken - 'taken_001': 0, 'taken_002': 0, 'taken_005': 0, - 'taken_01': 0, 'taken_02': 0, 'taken_05': 0, - 'taken_1': 2, 'taken_2': 0, 'taken_5': 0, - 'taken_10': 2, 'taken_20': 0, 'taken_50': 0, - 'taken_100': 2, 'taken_200': 0, 'taken_500': 0, - 'taken_cheque': 0, + "taken_001": 0, + "taken_002": 0, + "taken_005": 0, + "taken_01": 0, + "taken_02": 0, + "taken_05": 0, + "taken_1": 2, + "taken_2": 0, + "taken_5": 0, + "taken_10": 2, + "taken_20": 0, + "taken_50": 0, + "taken_100": 2, + "taken_200": 0, + "taken_500": 0, + "taken_cheque": 0, # 'not_count': not checked } @property def url_kwargs(self): - return {'pk_checkout': self.checkout.pk} + return {"pk_checkout": self.checkout.pk} @property def url_expected(self): - return '/k-fet/checkouts/{}/statements/add'.format(self.checkout.pk) + return "/k-fet/checkouts/{}/statements/add".format(self.checkout.pk) def get_users_extra(self): return { - 'team1': create_team('team1', '001', perms=[ - 'kfet.add_checkoutstatement', - ]), + "team1": create_team("team1", "001", perms=["kfet.add_checkoutstatement"]) } def setUp(self): super().setUp() self.checkout = Checkout.objects.create( - name='Checkout', - created_by=self.accounts['team'], + name="Checkout", + created_by=self.accounts["team"], balance=5, valid_from=self.now, valid_to=self.now + timedelta(days=5), @@ -926,29 +913,32 @@ class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - @mock.patch('django.utils.timezone.now') + @mock.patch("django.utils.timezone.now") def test_post_ok(self, mock_now): self.now += timedelta(days=2) mock_now.return_value = self.now client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) self.assertRedirects(r, self.checkout.get_absolute_url()) statement = CheckoutStatement.objects.get(at=self.now) - self.assertInstanceExpected(statement, { - 'by': self.accounts['team1'], - 'checkout': self.checkout, - 'balance_old': Decimal('5'), - 'balance_new': Decimal('111'), - 'amount_taken': Decimal('222'), - 'amount_error': Decimal('328'), - 'at': self.now, - 'not_count': False, - }) + self.assertInstanceExpected( + statement, + { + "by": self.accounts["team1"], + "checkout": self.checkout, + "balance_old": Decimal("5"), + "balance_new": Decimal("111"), + "amount_taken": Decimal("222"), + "amount_error": Decimal("328"), + "at": self.now, + "not_count": False, + }, + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -956,59 +946,65 @@ class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase): class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.checkoutstatement.update' + url_name = "kfet.checkoutstatement.update" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] post_data = { - 'amount_taken': 3, - 'amount_error': 2, - 'balance_old': 8, - 'balance_new': 5, + "amount_taken": 3, + "amount_error": 2, + "balance_old": 8, + "balance_new": 5, # Taken - 'taken_001': 0, 'taken_002': 0, 'taken_005': 0, - 'taken_01': 0, 'taken_02': 0, 'taken_05': 0, - 'taken_1': 1, 'taken_2': 1, 'taken_5': 0, - 'taken_10': 0, 'taken_20': 0, 'taken_50': 0, - 'taken_100': 0, 'taken_200': 0, 'taken_500': 0, - 'taken_cheque': 0, + "taken_001": 0, + "taken_002": 0, + "taken_005": 0, + "taken_01": 0, + "taken_02": 0, + "taken_05": 0, + "taken_1": 1, + "taken_2": 1, + "taken_5": 0, + "taken_10": 0, + "taken_20": 0, + "taken_50": 0, + "taken_100": 0, + "taken_200": 0, + "taken_500": 0, + "taken_cheque": 0, } @property def url_kwargs(self): - return { - 'pk_checkout': self.checkout.pk, - 'pk': self.statement.pk, - } + return {"pk_checkout": self.checkout.pk, "pk": self.statement.pk} @property def url_expected(self): - return '/k-fet/checkouts/{pk_checkout}/statements/{pk}/edit'.format( - pk_checkout=self.checkout.pk, - pk=self.statement.pk, + return "/k-fet/checkouts/{pk_checkout}/statements/{pk}/edit".format( + pk_checkout=self.checkout.pk, pk=self.statement.pk ) def get_users_extra(self): return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_checkoutstatement', - ]), + "team1": create_team( + "team1", "101", perms=["kfet.change_checkoutstatement"] + ) } def setUp(self): super().setUp() self.checkout = Checkout.objects.create( - name='Checkout', - created_by=self.accounts['team'], + name="Checkout", + created_by=self.accounts["team"], balance=5, valid_from=self.now, valid_to=self.now + timedelta(days=5), ) self.statement = CheckoutStatement.objects.create( - by=self.accounts['team'], + by=self.accounts["team"], checkout=self.checkout, balance_new=5, balance_old=8, @@ -1020,27 +1016,30 @@ class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - @mock.patch('django.utils.timezone.now') + @mock.patch("django.utils.timezone.now") def test_post_ok(self, mock_now): self.now += timedelta(days=2) mock_now.return_value = self.now client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) self.assertRedirects(r, self.checkout.get_absolute_url()) self.statement.refresh_from_db() - self.assertInstanceExpected(self.statement, { - 'taken_1': 1, - 'taken_2': 1, - 'balance_new': 5, - 'balance_old': 8, - 'amount_error': 0, - 'amount_taken': 3, - }) + self.assertInstanceExpected( + self.statement, + { + "taken_1": 1, + "taken_2": 1, + "balance_new": 5, + "balance_old": 8, + "amount_error": 0, + "amount_taken": 3, + }, + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -1048,60 +1047,57 @@ class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase): class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.category' - url_expected = '/k-fet/categories/' + url_name = "kfet.category" + url_expected = "/k-fet/categories/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() - self.category1 = ArticleCategory.objects.create(name='Category 1') - self.category2 = ArticleCategory.objects.create(name='Category 2') + self.category1 = ArticleCategory.objects.create(name="Category 1") + self.category2 = ArticleCategory.objects.create(name="Category 2") def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['categories'], - map(repr, [self.category1, self.category2]), + r.context["categories"], map(repr, [self.category1, self.category2]) ) class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.category.update' + url_name = "kfet.category.update" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.category.pk} + return {"pk": self.category.pk} @property def url_expected(self): - return '/k-fet/categories/{}/edit'.format(self.category.pk) + return "/k-fet/categories/{}/edit".format(self.category.pk) def get_users_extra(self): return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_articlecategory', - ]), + "team1": create_team("team1", "101", perms=["kfet.change_articlecategory"]) } @property def post_data(self): return { - 'name': 'The Category', + "name": "The Category", # 'has_addcost': not checked } def setUp(self): super().setUp() - self.category = ArticleCategory.objects.create(name='Category') + self.category = ArticleCategory.objects.create(name="Category") def test_get_ok(self): r = self.client.get(self.url) @@ -1109,17 +1105,16 @@ class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.category')) + self.assertRedirects(r, reverse("kfet.category")) self.category.refresh_from_db() - self.assertInstanceExpected(self.category, { - 'name': 'The Category', - 'has_addcost': False, - }) + self.assertInstanceExpected( + self.category, {"name": "The Category", "has_addcost": False} + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -1127,59 +1122,50 @@ class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase): class ArticleListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.article' - url_expected = '/k-fet/articles/' + url_name = "kfet.article" + url_expected = "/k-fet/articles/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Category') - self.article1 = Article.objects.create( - name='Article 1', - category=category, - ) - self.article2 = Article.objects.create( - name='Article 2', - category=category, - ) + category = ArticleCategory.objects.create(name="Category") + self.article1 = Article.objects.create(name="Article 1", category=category) + self.article2 = Article.objects.create(name="Article 2", category=category) def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context['articles'], - map(repr, [self.article1, self.article2]), + r.context["articles"], map(repr, [self.article1, self.article2]) ) class ArticleCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.article.create' - url_expected = '/k-fet/articles/new' + url_name = "kfet.article.create" + url_expected = "/k-fet/articles/new" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.add_article']), - } + return {"team1": create_team("team1", "101", perms=["kfet.add_article"])} @property def post_data(self): return { - 'name': 'Article', - 'category': self.category.pk, - 'stock': 5, - 'price': '2.5', + "name": "Article", + "category": self.category.pk, + "stock": 5, + "price": "2.5", } def setUp(self): super().setUp() - self.category = ArticleCategory.objects.create(name='Category') + self.category = ArticleCategory.objects.create(name="Category") def test_get_ok(self): r = self.client.get(self.url) @@ -1187,18 +1173,17 @@ class ArticleCreateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - article = Article.objects.get(name='Article') + article = Article.objects.get(name="Article") self.assertRedirects(r, article.get_absolute_url()) - self.assertInstanceExpected(article, { - 'name': 'Article', - 'category': self.category, - }) + self.assertInstanceExpected( + article, {"name": "Article", "category": self.category} + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -1206,76 +1191,69 @@ class ArticleCreateViewTests(ViewTestCaseMixin, TestCase): class ArticleReadViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.article.read' + url_name = "kfet.article.read" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.article.pk} + return {"pk": self.article.pk} @property def url_expected(self): - return '/k-fet/articles/{}'.format(self.article.pk) + return "/k-fet/articles/{}".format(self.article.pk) def setUp(self): super().setUp() self.article = Article.objects.create( - name='Article', - category=ArticleCategory.objects.create(name='Category'), + name="Article", + category=ArticleCategory.objects.create(name="Category"), stock=5, - price=Decimal('2.5'), + price=Decimal("2.5"), ) def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['article'], self.article) + self.assertEqual(r.context["article"], self.article) class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.article.update' + url_name = "kfet.article.update" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.article.pk} + return {"pk": self.article.pk} @property def url_expected(self): - return '/k-fet/articles/{}/edit'.format(self.article.pk) + return "/k-fet/articles/{}/edit".format(self.article.pk) def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_article', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.change_article"])} @property def post_data(self): return { - 'name': 'The Article', - 'category': self.article.category.pk, - 'is_sold': '1', - 'price': '3.5', - 'box_type': 'carton', + "name": "The Article", + "category": self.article.category.pk, + "is_sold": "1", + "price": "3.5", + "box_type": "carton", # 'hidden': not checked } def setUp(self): super().setUp() - self.category = ArticleCategory.objects.create(name='Category') + self.category = ArticleCategory.objects.create(name="Category") self.article = Article.objects.create( - name='Article', - category=self.category, - stock=5, - price=Decimal('2.5'), + name="Article", category=self.category, stock=5, price=Decimal("2.5") ) def test_get_ok(self): @@ -1284,7 +1262,7 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) @@ -1292,10 +1270,9 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): self.article.refresh_from_db() - self.assertInstanceExpected(self.article, { - 'name': 'The Article', - 'price': Decimal('3.5'), - }) + self.assertInstanceExpected( + self.article, {"name": "The Article", "price": Decimal("3.5")} + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -1303,95 +1280,93 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.article.stat.sales.list' + url_name = "kfet.article.stat.sales.list" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.article.pk} + return {"pk": self.article.pk} @property def url_expected(self): - return '/k-fet/articles/{}/stat/sales/list'.format(self.article.pk) + return "/k-fet/articles/{}/stat/sales/list".format(self.article.pk) def setUp(self): super().setUp() self.article = Article.objects.create( - name='Article', - category=ArticleCategory.objects.create(name='Category'), + name="Article", category=ArticleCategory.objects.create(name="Category") ) def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - content = json.loads(r.content.decode('utf-8')) + content = json.loads(r.content.decode("utf-8")) - base_url = reverse('kfet.article.stat.sales', args=[self.article.pk]) + base_url = reverse("kfet.article.stat.sales", args=[self.article.pk]) expected_stats = [ { - 'label': 'Derniers mois', - 'url': { - 'path': base_url, - 'query': { - 'scale_n_steps': ['7'], - 'scale_name': ['month'], - 'scale_last': ['True'], + "label": "Derniers mois", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["7"], + "scale_name": ["month"], + "scale_last": ["True"], }, }, }, { - 'label': 'Dernières semaines', - 'url': { - 'path': base_url, - 'query': { - 'scale_n_steps': ['7'], - 'scale_name': ['week'], - 'scale_last': ['True'], + "label": "Dernières semaines", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["7"], + "scale_name": ["week"], + "scale_last": ["True"], }, }, }, { - 'label': 'Derniers jours', - 'url': { - 'path': base_url, - 'query': { - 'scale_n_steps': ['7'], - 'scale_name': ['day'], - 'scale_last': ['True'], + "label": "Derniers jours", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["7"], + "scale_name": ["day"], + "scale_last": ["True"], }, }, }, ] - for stat, expected in zip(content['stats'], expected_stats): - expected_url = expected.pop('url') - self.assertUrlsEqual(stat['url'], expected_url) + for stat, expected in zip(content["stats"], expected_stats): + expected_url = expected.pop("url") + self.assertUrlsEqual(stat["url"], expected_url) self.assertDictContainsSubset(expected, stat) class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.article.stat.sales' + url_name = "kfet.article.stat.sales" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.article.pk} + return {"pk": self.article.pk} @property def url_expected(self): - return '/k-fet/articles/{}/stat/sales'.format(self.article.pk) + return "/k-fet/articles/{}/stat/sales".format(self.article.pk) def setUp(self): super().setUp() self.article = Article.objects.create( - name='Article', - category=ArticleCategory.objects.create(name='Category'), + name="Article", category=ArticleCategory.objects.create(name="Category") ) def test_ok(self): @@ -1400,11 +1375,11 @@ class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): class KPsulViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.kpsul' - url_expected = '/k-fet/k-psul/' + url_name = "kfet.kpsul" + url_expected = "/k-fet/k-psul/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): r = self.client.get(self.url) @@ -1412,43 +1387,51 @@ class KPsulViewTests(ViewTestCaseMixin, TestCase): class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.kpsul.checkout_data' - url_expected = '/k-fet/k-psul/checkout_data' + url_name = "kfet.kpsul.checkout_data" + url_expected = "/k-fet/k-psul/checkout_data" - http_methods = ['POST'] + http_methods = ["POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() self.checkout = Checkout.objects.create( - name='Checkout', - balance=Decimal('10'), - created_by=self.accounts['team'], + name="Checkout", + balance=Decimal("10"), + created_by=self.accounts["team"], valid_from=self.now, valid_to=self.now + timedelta(days=5), ) def test_ok(self): - r = self.client.post(self.url, {'pk': self.checkout.pk}) + r = self.client.post(self.url, {"pk": self.checkout.pk}) self.assertEqual(r.status_code, 200) - content = json.loads(r.content.decode('utf-8')) + content = json.loads(r.content.decode("utf-8")) - expected = { - 'name': 'Checkout', - 'balance': '10.00', - } + expected = {"name": "Checkout", "balance": "10.00"} self.assertDictContainsSubset(expected, content) - self.assertSetEqual(set(content.keys()), set([ - 'balance', 'id', 'name', 'valid_from', 'valid_to', - 'last_statement_at', 'last_statement_balance', - 'last_statement_by_first_name', 'last_statement_by_last_name', - 'last_statement_by_trigramme', - ])) + self.assertSetEqual( + set(content.keys()), + set( + [ + "balance", + "id", + "name", + "valid_from", + "valid_to", + "last_statement_at", + "last_statement_balance", + "last_statement_by_first_name", + "last_statement_by_last_name", + "last_statement_by_trigramme", + ] + ), + ) class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): @@ -1488,13 +1471,14 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): For invalid requests, response errors should be tested. """ - url_name = 'kfet.kpsul.perform_operations' - url_expected = '/k-fet/k-psul/perform_operations' - http_methods = ['POST'] + url_name = "kfet.kpsul.perform_operations" + url_expected = "/k-fet/k-psul/perform_operations" - auth_user = 'team' - auth_forbidden = [None, 'user'] + http_methods = ["POST"] + + auth_user = "team" + auth_forbidden = [None, "user"] with_liq = True @@ -1518,7 +1502,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) # An Account, trigramme=000, balance=50 # Do not assume user is cof, nor not cof. - self.account = self.accounts['user'] + self.account = self.accounts["user"] self.account.balance = Decimal("50.00") self.account.save() @@ -1539,15 +1523,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): the response. """ - json_data = ( - json.loads(getattr(response, "content", b"{}").decode("utf-8")) - ) + json_data = json.loads(getattr(response, "content", b"{}").decode("utf-8")) try: self.assertEqual(response.status_code, 200) except AssertionError as exc: - msg = ( - "Expected response is 200, got {}. Errors: {}" - .format(response.status_code, json_data.get("errors")) + msg = "Expected response is 200, got {}. Errors: {}".format( + response.status_code, json_data.get("errors") ) raise AssertionError(msg) from exc return json_data @@ -1555,13 +1536,13 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): def get_base_post_data(self): return { # OperationGroup form - 'on_acc': str(self.account.pk), - 'checkout': str(self.checkout.pk), + "on_acc": str(self.account.pk), + "checkout": str(self.checkout.pk), # Operation formset - 'form-TOTAL_FORMS': '0', - 'form-INITIAL_FORMS': '0', - 'form-MIN_NUM_FORMS': '1', - 'form-MAX_NUM_FORMS': '1000', + "form-TOTAL_FORMS": "0", + "form-INITIAL_FORMS": "0", + "form-MIN_NUM_FORMS": "1", + "form-MAX_NUM_FORMS": "1000", } base_post_data = property(get_base_post_data) @@ -1575,40 +1556,42 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(json_data["errors"]["operation_group"], ["on_acc"]) def test_group_on_acc_expects_comment(self): - user_add_perms( - self.users["team"], ["kfet.perform_commented_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_commented_operations"]) self.account.trigramme = "#13" self.account.save() self.assertTrue(self.account.need_comment) - data = dict(self.base_post_data, **{ - "comment": "A comment to explain it", - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) def test_invalid_group_on_acc_expects_comment(self): - user_add_perms( - self.users["team"], ["kfet.perform_commented_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_commented_operations"]) self.account.trigramme = "#13" self.account.save() self.assertTrue(self.account.need_comment) - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -1620,14 +1603,17 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.save() self.assertTrue(self.account.need_comment) - data = dict(self.base_post_data, **{ - "comment": "A comment to explain it", - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) @@ -1638,20 +1624,21 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) def test_group_on_acc_frozen(self): - user_add_perms( - self.users["team"], ["kfet.override_frozen_protection"] - ) + user_add_perms(self.users["team"], ["kfet.override_frozen_protection"]) self.account.is_frozen = True self.account.save() - data = dict(self.base_post_data, **{ - "comment": "A comment to explain it", - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -1660,21 +1647,23 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.is_frozen = True self.account.save() - data = dict(self.base_post_data, **{ - "comment": "A comment to explain it", - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], - ["Forcer le gel d'un compte"], + json_data["errors"]["missing_perms"], ["Forcer le gel d'un compte"] ) def test_invalid_group_checkout(self): @@ -1701,13 +1690,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.cofprofile.is_cof = False self.account.cofprofile.save() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) # Check response status @@ -1715,39 +1707,48 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check object creations operation_group = OperationGroup.objects.get() - self.assertDictEqual(operation_group.__dict__, { - "_state": mock.ANY, - "at": mock.ANY, - "amount": Decimal("-5.00"), - "checkout_id": self.checkout.pk, - "comment": "", - "id": mock.ANY, - "is_cof": False, - "on_acc_id": self.account.pk, - "valid_by_id": None, - }) + self.assertDictEqual( + operation_group.__dict__, + { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("-5.00"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": None, + }, + ) operation = Operation.objects.get() - self.assertDictEqual(operation.__dict__, { - "_state": mock.ANY, - "addcost_amount": None, - "addcost_for_id": None, - "amount": Decimal("-5.00"), - "article_id": self.article.pk, - "article_nb": 2, - "canceled_at": None, - "canceled_by_id": None, - "group_id": operation_group.pk, - "id": mock.ANY, - "type": "purchase", - }) + self.assertDictEqual( + operation.__dict__, + { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-5.00"), + "article_id": self.article.pk, + "article_nb": 2, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "purchase", + }, + ) # Check response content - self.assertDictEqual(json_data, { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }) + self.assertDictEqual( + json_data, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, + ) # Check object updates self.account.refresh_from_db() @@ -1784,22 +1785,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, "group_id": operation_group.pk, "type": "purchase", - }, + } ], - }, - ], - "checkouts": [ - { - "id": self.checkout.pk, - "balance": Decimal("100.00"), - }, - ], - "articles": [ - { - "id": self.article.pk, - "stock": 18, - }, + } ], + "checkouts": [{"id": self.checkout.pk, "balance": Decimal("100.00")}], + "articles": [{"id": self.article.pk, "stock": 18}], }, ) @@ -1808,13 +1799,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.cofprofile.is_cof = True self.account.cofprofile.save() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -1833,14 +1827,17 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.article.stock, 18) def test_purchase_with_cash(self): - data = dict(self.base_post_data, **{ - "on_acc": str(self.accounts["liq"].pk), - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "on_acc": str(self.accounts["liq"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -1857,53 +1854,56 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.article.stock, 18) def test_invalid_purchase_expects_article(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": "", - "form-0-article_nb": "1", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": "", + "form-0-article_nb": "1", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"]["operations"], - [ - {"__all__": ["Un achat nécessite un article et une quantité"]}, - ], + [{"__all__": ["Un achat nécessite un article et une quantité"]}], ) def test_invalid_purchase_expects_article_nb(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"]["operations"], - [ - {"__all__": ["Un achat nécessite un article et une quantité"]}, - ], + [{"__all__": ["Un achat nécessite un article et une quantité"]}], ) - def test_invalid_purchase_expects_article_nb_greater_than_1( - self - ): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "-1", - }) + def test_invalid_purchase_expects_article_nb_greater_than_1(self): + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "-1", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -1912,26 +1912,26 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): json_data["errors"]["operations"], [ { - "__all__": [ - "Un achat nécessite un article et une quantité", - ], + "__all__": ["Un achat nécessite un article et une quantité"], "article_nb": [ - "Assurez-vous que cette valeur est supérieure ou " - "égale à 1.", + "Assurez-vous que cette valeur est supérieure ou " "égale à 1." ], - }, + } ], ) def test_invalid_operation_not_purchase_with_cash(self): - data = dict(self.base_post_data, **{ - "on_acc": str(self.accounts["liq"].pk), - "form-TOTAL_FORMS": "1", - "form-0-type": "deposit", - "form-0-amount": "10.00", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "on_acc": str(self.accounts["liq"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10.00", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -1940,50 +1940,62 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): def test_deposit(self): user_add_perms(self.users["team"], ["kfet.perform_deposit"]) - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "deposit", - "form-0-amount": "10.75", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) json_data = self._assertResponseOk(resp) operation_group = OperationGroup.objects.get() - self.assertDictEqual(operation_group.__dict__, { - "_state": mock.ANY, - "at": mock.ANY, - "amount": Decimal("10.75"), - "checkout_id": self.checkout.pk, - "comment": "", - "id": mock.ANY, - "is_cof": False, - "on_acc_id": self.account.pk, - "valid_by_id": self.accounts["team"].pk, - }) + self.assertDictEqual( + operation_group.__dict__, + { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("10.75"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": self.accounts["team"].pk, + }, + ) operation = Operation.objects.get() - self.assertDictEqual(operation.__dict__, { - "_state": mock.ANY, - "addcost_amount": None, - "addcost_for_id": None, - "amount": Decimal("10.75"), - "article_id": None, - "article_nb": None, - "canceled_at": None, - "canceled_by_id": None, - "group_id": operation_group.pk, - "id": mock.ANY, - "type": "deposit", - }) + self.assertDictEqual( + operation.__dict__, + { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("10.75"), + "article_id": None, + "article_nb": None, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "deposit", + }, + ) - self.assertDictEqual(json_data, { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }) + self.assertDictEqual( + json_data, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, + ) self.account.refresh_from_db() self.assertEqual(self.account.balance, Decimal("60.75")) @@ -2016,28 +2028,26 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, "group_id": operation_group.pk, "type": "deposit", - }, + } ], - }, - ], - "checkouts": [ - { - "id": self.checkout.pk, - "balance": Decimal("110.75"), - }, + } ], + "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}], "articles": [], }, ) def test_invalid_deposit_expects_amount(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "deposit", - "form-0-amount": "", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -2047,13 +2057,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) def test_invalid_deposit_too_many_params(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "deposit", - "form-0-amount": "10", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "3", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "3", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -2063,83 +2076,98 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) def test_invalid_deposit_expects_positive_amount(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "deposit", - "form-0-amount": "-10", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "-10", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["operations"], - [{"__all__": ["Charge non positive"]}] + json_data["errors"]["operations"], [{"__all__": ["Charge non positive"]}] ) def test_invalid_deposit_requires_perm(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "deposit", - "form-0-amount": "10.75", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "deposit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["missing_perms"], ["Effectuer une charge"] - ) + self.assertEqual(json_data["errors"]["missing_perms"], ["Effectuer une charge"]) def test_withdraw(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "withdraw", - "form-0-amount": "-10.75", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "-10.75", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) json_data = self._assertResponseOk(resp) operation_group = OperationGroup.objects.get() - self.assertDictEqual(operation_group.__dict__, { - "_state": mock.ANY, - "at": mock.ANY, - "amount": Decimal("-10.75"), - "checkout_id": self.checkout.pk, - "comment": "", - "id": mock.ANY, - "is_cof": False, - "on_acc_id": self.account.pk, - "valid_by_id": None, - }) + self.assertDictEqual( + operation_group.__dict__, + { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("-10.75"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": None, + }, + ) operation = Operation.objects.get() - self.assertDictEqual(operation.__dict__, { - "_state": mock.ANY, - "addcost_amount": None, - "addcost_for_id": None, - "amount": Decimal("-10.75"), - "article_id": None, - "article_nb": None, - "canceled_at": None, - "canceled_by_id": None, - "group_id": operation_group.pk, - "id": mock.ANY, - "type": "withdraw", - }) + self.assertDictEqual( + operation.__dict__, + { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-10.75"), + "article_id": None, + "article_nb": None, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "withdraw", + }, + ) - self.assertDictEqual(json_data, { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }) + self.assertDictEqual( + json_data, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, + ) self.account.refresh_from_db() self.assertEqual(self.account.balance, Decimal("39.25")) @@ -2172,28 +2200,26 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, "group_id": operation_group.pk, "type": "withdraw", - }, + } ], - }, - ], - "checkouts": [ - { - "id": self.checkout.pk, - "balance": Decimal("89.25"), - }, + } ], + "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}], "articles": [], }, ) def test_invalid_withdraw_expects_amount(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "withdraw", - "form-0-amount": "", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -2203,13 +2229,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) def test_invalid_withdraw_too_many_params(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "withdraw", - "form-0-amount": "-10", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "3", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "-10", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "3", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -2219,70 +2248,84 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) def test_invalid_withdraw_expects_negative_amount(self): - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "withdraw", - "form-0-amount": "10", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "withdraw", + "form-0-amount": "10", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["operations"], - [{"__all__": ["Retrait non négatif"]}] + json_data["errors"]["operations"], [{"__all__": ["Retrait non négatif"]}] ) def test_edit(self): user_add_perms(self.users["team"], ["kfet.edit_balance_account"]) - data = dict(self.base_post_data, **{ - "comment": "A comment to explain it", - "form-TOTAL_FORMS": "1", - "form-0-type": "edit", - "form-0-amount": "10.75", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "edit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) json_data = self._assertResponseOk(resp) operation_group = OperationGroup.objects.get() - self.assertDictEqual(operation_group.__dict__, { - "_state": mock.ANY, - "at": mock.ANY, - "amount": Decimal("10.75"), - "checkout_id": self.checkout.pk, - "comment": "A comment to explain it", - "id": mock.ANY, - "is_cof": False, - "on_acc_id": self.account.pk, - "valid_by_id": self.accounts["team"].pk, - }) + self.assertDictEqual( + operation_group.__dict__, + { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("10.75"), + "checkout_id": self.checkout.pk, + "comment": "A comment to explain it", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": self.accounts["team"].pk, + }, + ) operation = Operation.objects.get() - self.assertDictEqual(operation.__dict__, { - "_state": mock.ANY, - "addcost_amount": None, - "addcost_for_id": None, - "amount": Decimal("10.75"), - "article_id": None, - "article_nb": None, - "canceled_at": None, - "canceled_by_id": None, - "group_id": operation_group.pk, - "id": mock.ANY, - "type": "edit", - }) + self.assertDictEqual( + operation.__dict__, + { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("10.75"), + "article_id": None, + "article_nb": None, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "edit", + }, + ) - self.assertDictEqual(json_data, { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }) + self.assertDictEqual( + json_data, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, + ) self.account.refresh_from_db() self.assertEqual(self.account.balance, Decimal("60.75")) @@ -2315,48 +2358,48 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, "group_id": operation_group.pk, "type": "edit", - }, + } ], - }, - ], - "checkouts": [ - { - "id": self.checkout.pk, - "balance": Decimal("100.00"), - }, + } ], + "checkouts": [{"id": self.checkout.pk, "balance": Decimal("100.00")}], "articles": [], }, ) def test_invalid_edit_requires_perm(self): - data = dict(self.base_post_data, **{ - "comment": "A comment to explain it", - "form-TOTAL_FORMS": "1", - "form-0-type": "edit", - "form-0-amount": "10.75", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "edit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], - ["Modifier la balance d'un compte"], + json_data["errors"]["missing_perms"], ["Modifier la balance d'un compte"] ) def test_invalid_edit_expects_comment(self): user_add_perms(self.users["team"], ["kfet.edit_balance_account"]) - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "edit", - "form-0-amount": "10.75", - "form-0-article": "", - "form-0-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "edit", + "form-0-amount": "10.75", + "form-0-article": "", + "form-0-article_nb": "", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 400) @@ -2366,8 +2409,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): def _setup_addcost(self): self.register_user("addcost", create_user("addcost", "ADD")) kfet_config.set( - addcost_amount=Decimal("0.50"), - addcost_for=self.accounts["addcost"], + addcost_amount=Decimal("0.50"), addcost_for=self.accounts["addcost"] ) def test_addcost_user_is_not_cof(self): @@ -2375,13 +2417,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.cofprofile.save() self._setup_addcost() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2400,10 +2445,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data_ope = ( - self.kpsul_consumer_mock.group_send - .call_args[0][1]["opegroups"][0]["opes"][0] - ) + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2413,13 +2457,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.cofprofile.save() self._setup_addcost() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2438,10 +2485,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data_ope = ( - self.kpsul_consumer_mock.group_send - .call_args[0][1]["opegroups"][0]["opes"][0] - ) + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2450,14 +2496,17 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.cofprofile.save() self._setup_addcost() - data = dict(self.base_post_data, **{ - "on_acc": str(self.accounts["liq"].pk), - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "on_acc": str(self.accounts["liq"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2474,10 +2523,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("106.00")) - ws_data_ope = ( - self.kpsul_consumer_mock.group_send - .call_args[0][1]["opegroups"][0]["opes"][0] - ) + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2486,14 +2534,17 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].balance = Decimal("20.00") self.accounts["addcost"].save() - data = dict(self.base_post_data, **{ - "on_acc": str(self.accounts["addcost"].pk), - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "on_acc": str(self.accounts["addcost"].pk), + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2508,10 +2559,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].refresh_from_db() self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00")) - ws_data_ope = ( - self.kpsul_consumer_mock.group_send - .call_args[0][1]["opegroups"][0]["opes"][0] - ) + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -2520,13 +2570,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.article.category.has_addcost = False self.article.category.save() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2541,27 +2594,27 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].refresh_from_db() self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00")) - ws_data_ope = ( - self.kpsul_consumer_mock.group_send - .call_args[0][1]["opegroups"][0]["opes"][0] - ) + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) def test_negative_new(self): - user_add_perms( - self.users["team"], ["kfet.perform_negative_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) self.account.balance = Decimal("1.00") self.account.save() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2570,20 +2623,21 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.account.balance, Decimal("-4.00")) def test_negative_exists(self): - user_add_perms( - self.users["team"], ["kfet.perform_negative_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) self.account.balance = Decimal("-10.00") self.account.save() self.account.update_negative() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2597,17 +2651,20 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.save() self.account.update_negative() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "2", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "1", - "form-1-type": "deposit", - "form-1-amount": "5.00", - "form-1-article": "", - "form-1-article_nb": "", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "2", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "1", + "form-1-type": "deposit", + "form-1-amount": "5.00", + "form-1-article": "", + "form-1-article_nb": "", + } + ) resp = self.client.post(self.url, data) self._assertResponseOk(resp) @@ -2619,43 +2676,44 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.balance = Decimal("1.00") self.account.save() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"], - { - "missing_perms": ["Enregistrer des commandes en négatif"], - }, + {"missing_perms": ["Enregistrer des commandes en négatif"]}, ) def test_invalid_negative_exceeds_allowed_duration_from_config(self): - user_add_perms( - self.users["team"], ["kfet.perform_negative_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) kfet_config.set(overdraft_duration=timedelta(days=5)) self.account.balance = Decimal("1.00") self.account.save() self.account.negative = AccountNegative.objects.create( - account=self.account, - start=timezone.now() - timedelta(days=5, minutes=1), + account=self.account, start=timezone.now() - timedelta(days=5, minutes=1) ) - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) @@ -2663,9 +2721,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(json_data["errors"], {"negative": ["000"]}) def test_invalid_negative_exceeds_allowed_duration_from_account(self): - user_add_perms( - self.users["team"], ["kfet.perform_negative_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) kfet_config.set(overdraft_duration=timedelta(days=5)) self.account.balance = Decimal("1.00") self.account.save() @@ -2675,13 +2731,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): authz_overdraft_until=timezone.now() - timedelta(seconds=1), ) - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) @@ -2689,21 +2748,22 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(json_data["errors"], {"negative": ["000"]}) def test_invalid_negative_exceeds_amount_allowed_from_config(self): - user_add_perms( - self.users["team"], ["kfet.perform_negative_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) kfet_config.set(overdraft_amount=Decimal("-1.00")) self.account.balance = Decimal("1.00") self.account.save() self.account.update_negative() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) @@ -2711,9 +2771,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(json_data["errors"], {"negative": ["000"]}) def test_invalid_negative_exceeds_amount_allowed_from_account(self): - user_add_perms( - self.users["team"], ["kfet.perform_negative_operations"] - ) + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) kfet_config.set(overdraft_amount=Decimal("10.00")) self.account.balance = Decimal("1.00") self.account.save() @@ -2724,13 +2782,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): authz_overdraft_amount=Decimal("1.00"), ) - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "1", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) resp = self.client.post(self.url, data) self.assertEqual(resp.status_code, 403) @@ -2747,17 +2808,20 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.cofprofile.is_cof = False self.account.cofprofile.save() - data = dict(self.base_post_data, **{ - "form-TOTAL_FORMS": "2", - "form-0-type": "purchase", - "form-0-amount": "", - "form-0-article": str(self.article.pk), - "form-0-article_nb": "2", - "form-1-type": "purchase", - "form-1-amount": "", - "form-1-article": str(article2.pk), - "form-1-article_nb": "1", - }) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "2", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + "form-1-type": "purchase", + "form-1-amount": "", + "form-1-article": str(article2.pk), + "form-1-article_nb": "1", + } + ) resp = self.client.post(self.url, data) # Check response status @@ -2765,53 +2829,65 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check object creations operation_group = OperationGroup.objects.get() - self.assertDictEqual(operation_group.__dict__, { - "_state": mock.ANY, - "at": mock.ANY, - "amount": Decimal("-9.00"), - "checkout_id": self.checkout.pk, - "comment": "", - "id": mock.ANY, - "is_cof": False, - "on_acc_id": self.account.pk, - "valid_by_id": None, - }) + self.assertDictEqual( + operation_group.__dict__, + { + "_state": mock.ANY, + "at": mock.ANY, + "amount": Decimal("-9.00"), + "checkout_id": self.checkout.pk, + "comment": "", + "id": mock.ANY, + "is_cof": False, + "on_acc_id": self.account.pk, + "valid_by_id": None, + }, + ) operation_list = Operation.objects.all() self.assertEqual(len(operation_list), 2) - self.assertDictEqual(operation_list[0].__dict__, { - "_state": mock.ANY, - "addcost_amount": None, - "addcost_for_id": None, - "amount": Decimal("-5.00"), - "article_id": self.article.pk, - "article_nb": 2, - "canceled_at": None, - "canceled_by_id": None, - "group_id": operation_group.pk, - "id": mock.ANY, - "type": "purchase", - }) - self.assertDictEqual(operation_list[1].__dict__, { - "_state": mock.ANY, - "addcost_amount": None, - "addcost_for_id": None, - "amount": Decimal("-4.00"), - "article_id": article2.pk, - "article_nb": 1, - "canceled_at": None, - "canceled_by_id": None, - "group_id": operation_group.pk, - "id": mock.ANY, - "type": "purchase", - }) + self.assertDictEqual( + operation_list[0].__dict__, + { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-5.00"), + "article_id": self.article.pk, + "article_nb": 2, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "purchase", + }, + ) + self.assertDictEqual( + operation_list[1].__dict__, + { + "_state": mock.ANY, + "addcost_amount": None, + "addcost_for_id": None, + "amount": Decimal("-4.00"), + "article_id": article2.pk, + "article_nb": 1, + "canceled_at": None, + "canceled_by_id": None, + "group_id": operation_group.pk, + "id": mock.ANY, + "type": "purchase", + }, + ) # Check response content - self.assertDictEqual(json_data, { - "operationgroup": operation_group.pk, - "operations": [operation_list[0].pk, operation_list[1].pk], - "warnings": {}, - "errors": {}, - }) + self.assertDictEqual( + json_data, + { + "operationgroup": operation_group.pk, + "operations": [operation_list[0].pk, operation_list[1].pk], + "warnings": {}, + "errors": {}, + }, + ) # Check object updates self.account.refresh_from_db() @@ -2864,120 +2940,99 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "type": "purchase", }, ], - }, - ], - "checkouts": [ - { - "id": self.checkout.pk, - "balance": Decimal("100.00"), - }, + } ], + "checkouts": [{"id": self.checkout.pk, "balance": Decimal("100.00")}], "articles": [ - { - "id": self.article.pk, - "stock": 18, - }, - { - "id": article2.pk, - "stock": -6, - }, + {"id": self.article.pk, "stock": 18}, + {"id": article2.pk, "stock": -6}, ], }, ) class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.kpsul.cancel_operations' - url_expected = '/k-fet/k-psul/cancel_operations' + url_name = "kfet.kpsul.cancel_operations" + url_expected = "/k-fet/k-psul/cancel_operations" - http_methods = ['POST'] + http_methods = ["POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): pass class KPsulArticlesData(ViewTestCaseMixin, TestCase): - url_name = 'kfet.kpsul.articles_data' - url_expected = '/k-fet/k-psul/articles_data' + url_name = "kfet.kpsul.articles_data" + url_expected = "/k-fet/k-psul/articles_data" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Catégorie') - self.article1 = Article.objects.create( - category=category, - name='Article 1', - ) + category = ArticleCategory.objects.create(name="Catégorie") + self.article1 = Article.objects.create(category=category, name="Article 1") self.article2 = Article.objects.create( - category=category, - name='Article 2', - price=Decimal('2.5'), + category=category, name="Article 2", price=Decimal("2.5") ) def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - content = json.loads(r.content.decode('utf-8')) + content = json.loads(r.content.decode("utf-8")) - articles = content['articles'] + articles = content["articles"] - expected_list = [{ - 'category__name': 'Catégorie', - 'name': 'Article 1', - 'price': '0.00', - }, { - 'category__name': 'Catégorie', - 'name': 'Article 2', - 'price': '2.50', - }] + expected_list = [ + {"category__name": "Catégorie", "name": "Article 1", "price": "0.00"}, + {"category__name": "Catégorie", "name": "Article 2", "price": "2.50"}, + ] for expected, article in zip(expected_list, articles): self.assertDictContainsSubset(expected, article) - self.assertSetEqual(set(article.keys()), set([ - 'id', 'name', 'price', 'stock', - 'category_id', 'category__name', 'category__has_addcost', - ])) + self.assertSetEqual( + set(article.keys()), + set( + [ + "id", + "name", + "price", + "stock", + "category_id", + "category__name", + "category__has_addcost", + ] + ), + ) class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase): - url_name = 'kfet.kpsul.update_addcost' - url_expected = '/k-fet/k-psul/update_addcost' + url_name = "kfet.kpsul.update_addcost" + url_expected = "/k-fet/k-psul/update_addcost" - http_methods = ['POST'] + http_methods = ["POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] - post_data = { - 'trigramme': '000', - 'amount': '0.5', - } + post_data = {"trigramme": "000", "amount": "0.5"} def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.manage_addcosts', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.manage_addcosts"])} def test_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) self.assertEqual(r.status_code, 200) - self.assertEqual( - kfet_config.addcost_for, - Account.objects.get(trigramme='000'), - ) - self.assertEqual(kfet_config.addcost_amount, Decimal('0.5')) + self.assertEqual(kfet_config.addcost_for, Account.objects.get(trigramme="000")) + self.assertEqual(kfet_config.addcost_amount, Decimal("0.5")) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -2985,11 +3040,11 @@ class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase): class KPsulGetSettings(ViewTestCaseMixin, TestCase): - url_name = 'kfet.kpsul.get_settings' - url_expected = '/k-fet/k-psul/get_settings' + url_name = "kfet.kpsul.get_settings" + url_expected = "/k-fet/k-psul/get_settings" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): r = self.client.get(self.url) @@ -2997,10 +3052,10 @@ class KPsulGetSettings(ViewTestCaseMixin, TestCase): class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.history.json' - url_expected = '/k-fet/history.json' + url_name = "kfet.history.json" + url_expected = "/k-fet/history.json" - auth_user = 'user' + auth_user = "user" auth_forbidden = [None] def test_ok(self): @@ -3009,46 +3064,51 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.read.json' - url_expected = '/k-fet/accounts/read.json' + url_name = "kfet.account.read.json" + url_expected = "/k-fet/accounts/read.json" - http_methods = ['POST'] + http_methods = ["POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): - r = self.client.post(self.url, {'trigramme': '000'}) + r = self.client.post(self.url, {"trigramme": "000"}) self.assertEqual(r.status_code, 200) - content = json.loads(r.content.decode('utf-8')) + content = json.loads(r.content.decode("utf-8")) - expected = { - 'name': 'first last', - 'trigramme': '000', - 'balance': '0.00', - } + expected = {"name": "first last", "trigramme": "000", "balance": "0.00"} self.assertDictContainsSubset(expected, content) - self.assertSetEqual(set(content.keys()), set([ - 'balance', 'departement', 'email', 'id', 'is_cof', 'is_frozen', - 'name', 'nickname', 'promo', 'trigramme', - ])) + self.assertSetEqual( + set(content.keys()), + set( + [ + "balance", + "departement", + "email", + "id", + "is_cof", + "is_frozen", + "name", + "nickname", + "promo", + "trigramme", + ] + ), + ) class SettingsListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.settings' - url_expected = '/k-fet/settings/' + url_name = "kfet.settings" + url_expected = "/k-fet/settings/" - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.see_config', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.see_config"])} def test_ok(self): r = self.client.get(self.url) @@ -3056,31 +3116,27 @@ class SettingsListViewTests(ViewTestCaseMixin, TestCase): class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.settings.update' - url_expected = '/k-fet/settings/edit' + url_name = "kfet.settings.update" + url_expected = "/k-fet/settings/edit" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] @property def post_data(self): return { - 'kfet_reduction_cof': '25', - 'kfet_addcost_amount': '0.5', - 'kfet_addcost_for': self.accounts['user'].pk, - 'kfet_overdraft_duration': '2 00:00:00', - 'kfet_overdraft_amount': '25', - 'kfet_cancel_duration': '00:20:00', + "kfet_reduction_cof": "25", + "kfet_addcost_amount": "0.5", + "kfet_addcost_for": self.accounts["user"].pk, + "kfet_overdraft_duration": "2 00:00:00", + "kfet_overdraft_amount": "25", + "kfet_cancel_duration": "00:20:00", } def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_config', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.change_config"])} def test_get_ok(self): r = self.client.get(self.url) @@ -3089,19 +3145,15 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): r = self.client.post(self.url, self.post_data) # Redirect is skipped because client may lack permissions. - self.assertRedirects( - r, - reverse('kfet.settings'), - fetch_redirect_response=False, - ) + self.assertRedirects(r, reverse("kfet.settings"), fetch_redirect_response=False) expected_config = { - 'reduction_cof': Decimal('25'), - 'addcost_amount': Decimal('0.5'), - 'addcost_for': self.accounts['user'], - 'overdraft_duration': timedelta(days=2), - 'overdraft_amount': Decimal('25'), - 'cancel_duration': timedelta(minutes=20), + "reduction_cof": Decimal("25"), + "addcost_amount": Decimal("0.5"), + "addcost_for": self.accounts["user"], + "overdraft_duration": timedelta(days=2), + "overdraft_amount": Decimal("25"), + "cancel_duration": timedelta(minutes=20), } for key, expected in expected_config.items(): @@ -3109,11 +3161,11 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): class TransferListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.transfers' - url_expected = '/k-fet/transfers/' + url_name = "kfet.transfers" + url_expected = "/k-fet/transfers/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): r = self.client.get(self.url) @@ -3121,11 +3173,11 @@ class TransferListViewTests(ViewTestCaseMixin, TestCase): class TransferCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.transfers.create' - url_expected = '/k-fet/transfers/new' + url_name = "kfet.transfers.create" + url_expected = "/k-fet/transfers/new" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def test_ok(self): r = self.client.get(self.url) @@ -3133,195 +3185,180 @@ class TransferCreateViewTests(ViewTestCaseMixin, TestCase): class TransferPerformViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.transfers.perform' - url_expected = '/k-fet/transfers/perform' + url_name = "kfet.transfers.perform" + url_expected = "/k-fet/transfers/perform" - http_methods = ['POST'] + http_methods = ["POST"] - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): return { - 'team1': create_team('team1', '101', perms=[ - # Required - 'kfet.add_transfer', - # Convenience - 'kfet.perform_negative_operations', - ]), + "team1": create_team( + "team1", + "101", + perms=[ + # Required + "kfet.add_transfer", + # Convenience + "kfet.perform_negative_operations", + ], + ) } @property def post_data(self): return { # General - 'comment': '', + "comment": "", # Formset management - 'form-TOTAL_FORMS': '10', - 'form-INITIAL_FORMS': '0', - 'form-MIN_NUM_FORMS': '1', - 'form-MAX_NUM_FORMS': '1000', + "form-TOTAL_FORMS": "10", + "form-INITIAL_FORMS": "0", + "form-MIN_NUM_FORMS": "1", + "form-MAX_NUM_FORMS": "1000", # Transfer 1 - 'form-0-from_acc': str(self.accounts['user'].pk), - 'form-0-to_acc': str(self.accounts['team'].pk), - 'form-0-amount': '3.5', + "form-0-from_acc": str(self.accounts["user"].pk), + "form-0-to_acc": str(self.accounts["team"].pk), + "form-0-amount": "3.5", # Transfer 2 - 'form-1-from_acc': str(self.accounts['team'].pk), - 'form-1-to_acc': str(self.accounts['team1'].pk), - 'form-1-amount': '2.4', + "form-1-from_acc": str(self.accounts["team"].pk), + "form-1-to_acc": str(self.accounts["team1"].pk), + "form-1-amount": "2.4", } def test_ok(self): r = self.client.post(self.url, self.post_data) self.assertEqual(r.status_code, 200) - user = self.accounts['user'] + user = self.accounts["user"] user.refresh_from_db() - self.assertEqual(user.balance, Decimal('-3.5')) + self.assertEqual(user.balance, Decimal("-3.5")) - team = self.accounts['team'] + team = self.accounts["team"] team.refresh_from_db() - self.assertEqual(team.balance, Decimal('1.1')) + self.assertEqual(team.balance, Decimal("1.1")) - team1 = self.accounts['team1'] + team1 = self.accounts["team1"] team1.refresh_from_db() - self.assertEqual(team1.balance, Decimal('2.4')) + self.assertEqual(team1.balance, Decimal("2.4")) class TransferCancelViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.transfers.cancel' - url_expected = '/k-fet/transfers/cancel' + url_name = "kfet.transfers.cancel" + url_expected = "/k-fet/transfers/cancel" - http_methods = ['POST'] + http_methods = ["POST"] - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] + auth_user = "team1" + auth_forbidden = [None, "user", "team"] def get_users_extra(self): return { - 'team1': create_team('team1', '101', perms=[ - # Convenience - 'kfet.perform_negative_operations', - ]), + "team1": create_team( + "team1", + "101", + perms=[ + # Convenience + "kfet.perform_negative_operations" + ], + ) } @property def post_data(self): - return { - 'transfers[]': [self.transfer1.pk, self.transfer2.pk], - } + return {"transfers[]": [self.transfer1.pk, self.transfer2.pk]} def setUp(self): super().setUp() group = TransferGroup.objects.create() self.transfer1 = Transfer.objects.create( group=group, - from_acc=self.accounts['user'], - to_acc=self.accounts['team'], - amount='3.5', + from_acc=self.accounts["user"], + to_acc=self.accounts["team"], + amount="3.5", ) self.transfer2 = Transfer.objects.create( group=group, - from_acc=self.accounts['team'], - to_acc=self.accounts['root'], - amount='2.4', + from_acc=self.accounts["team"], + to_acc=self.accounts["root"], + amount="2.4", ) def test_ok(self): r = self.client.post(self.url, self.post_data) self.assertEqual(r.status_code, 200) - user = self.accounts['user'] + user = self.accounts["user"] user.refresh_from_db() - self.assertEqual(user.balance, Decimal('3.5')) + self.assertEqual(user.balance, Decimal("3.5")) - team = self.accounts['team'] + team = self.accounts["team"] team.refresh_from_db() - self.assertEqual(team.balance, Decimal('-1.1')) + self.assertEqual(team.balance, Decimal("-1.1")) - root = self.accounts['root'] + root = self.accounts["root"] root.refresh_from_db() - self.assertEqual(root.balance, Decimal('-2.4')) + self.assertEqual(root.balance, Decimal("-2.4")) class InventoryListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.inventory' - url_expected = '/k-fet/inventaires/' + url_name = "kfet.inventory" + url_expected = "/k-fet/inventaires/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() - self.inventory = Inventory.objects.create( - by=self.accounts['team'], - ) - category = ArticleCategory.objects.create(name='Category') - article = Article.objects.create( - name='Article', - category=category, - ) + self.inventory = Inventory.objects.create(by=self.accounts["team"]) + category = ArticleCategory.objects.create(name="Category") + article = Article.objects.create(name="Article", category=category) InventoryArticle.objects.create( - inventory=self.inventory, - article=article, - stock_old=5, - stock_new=0, + inventory=self.inventory, article=article, stock_old=5, stock_new=0 ) def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - inventories = r.context['inventories'] - self.assertQuerysetEqual( - inventories, - map(repr, [self.inventory]), - ) + inventories = r.context["inventories"] + self.assertQuerysetEqual(inventories, map(repr, [self.inventory])) class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.inventory.create' - url_expected = '/k-fet/inventaires/new' + url_name = "kfet.inventory.create" + url_expected = "/k-fet/inventaires/new" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.add_inventory', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.add_inventory"])} @property def post_data(self): return { # Formset management - 'form-TOTAL_FORMS': '2', - 'form-INITIAL_FORMS': '2', - 'form-MIN_NUM_FORMS': '0', - 'form-MAX_NUM_FORMS': '1000', + "form-TOTAL_FORMS": "2", + "form-INITIAL_FORMS": "2", + "form-MIN_NUM_FORMS": "0", + "form-MAX_NUM_FORMS": "1000", # Article 1 - 'form-0-article': str(self.article1.pk), - 'form-0-stock_new': '5', + "form-0-article": str(self.article1.pk), + "form-0-stock_new": "5", # Article 2 - 'form-1-article': str(self.article2.pk), - 'form-1-stock_new': '10', + "form-1-article": str(self.article2.pk), + "form-1-stock_new": "10", } def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Category') - self.article1 = Article.objects.create( - category=category, - name='Article 1', - ) - self.article2 = Article.objects.create( - category=category, - name='Article 2', - ) + category = ArticleCategory.objects.create(name="Category") + self.article1 = Article.objects.create(category=category, name="Article 1") + self.article2 = Article.objects.create(category=category, name="Article 2") def test_get_ok(self): r = self.client.get(self.url) @@ -3329,10 +3366,10 @@ class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.inventory')) + self.assertRedirects(r, reverse("kfet.inventory")) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -3340,34 +3377,26 @@ class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): class InventoryReadViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.inventory.read' + url_name = "kfet.inventory.read" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.inventory.pk} + return {"pk": self.inventory.pk} @property def url_expected(self): - return '/k-fet/inventaires/{}'.format(self.inventory.pk) + return "/k-fet/inventaires/{}".format(self.inventory.pk) def setUp(self): super().setUp() - self.inventory = Inventory.objects.create( - by=self.accounts['team'], - ) - category = ArticleCategory.objects.create(name='Category') - article = Article.objects.create( - name='Article', - category=category, - ) + self.inventory = Inventory.objects.create(by=self.accounts["team"]) + category = ArticleCategory.objects.create(name="Category") + article = Article.objects.create(name="Article", category=category) InventoryArticle.objects.create( - inventory=self.inventory, - article=article, - stock_old=5, - stock_new=0, + inventory=self.inventory, article=article, stock_old=5, stock_new=0 ) def test_ok(self): @@ -3376,65 +3405,58 @@ class InventoryReadViewTests(ViewTestCaseMixin, TestCase): class OrderListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.order' - url_expected = '/k-fet/orders/' + url_name = "kfet.order" + url_expected = "/k-fet/orders/" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Category') - article = Article.objects.create(name='Article', category=category) + category = ArticleCategory.objects.create(name="Category") + article = Article.objects.create(name="Article", category=category) - supplier = Supplier.objects.create(name='Supplier') + supplier = Supplier.objects.create(name="Supplier") SupplierArticle.objects.create(supplier=supplier, article=article) self.order = Order.objects.create(supplier=supplier) OrderArticle.objects.create( - order=self.order, - article=article, - quantity_ordered=24, + order=self.order, article=article, quantity_ordered=24 ) def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - orders = r.context['orders'] - self.assertQuerysetEqual( - orders, - map(repr, [self.order]), - ) + orders = r.context["orders"] + self.assertQuerysetEqual(orders, map(repr, [self.order])) class OrderReadViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.order.read' + url_name = "kfet.order.read" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.order.pk} + return {"pk": self.order.pk} @property def url_expected(self): - return '/k-fet/orders/{}'.format(self.order.pk) + return "/k-fet/orders/{}".format(self.order.pk) def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Category') - article = Article.objects.create(name='Article', category=category) + category = ArticleCategory.objects.create(name="Category") + article = Article.objects.create(name="Article", category=category) - supplier = Supplier.objects.create(name='Supplier') + supplier = Supplier.objects.create(name="Supplier") SupplierArticle.objects.create(supplier=supplier, article=article) self.order = Order.objects.create(supplier=supplier) OrderArticle.objects.create( - order=self.order, - article=article, - quantity_ordered=24, + order=self.order, article=article, quantity_ordered=24 ) def test_ok(self): @@ -3443,41 +3465,37 @@ class OrderReadViewTests(ViewTestCaseMixin, TestCase): class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.order.supplier.update' + url_name = "kfet.order.supplier.update" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.supplier.pk} + return {"pk": self.supplier.pk} @property def url_expected(self): - return '/k-fet/orders/suppliers/{}/edit'.format(self.supplier.pk) + return "/k-fet/orders/suppliers/{}/edit".format(self.supplier.pk) def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.change_supplier', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.change_supplier"])} @property def post_data(self): return { - 'name': 'The Supplier', - 'phone': '', - 'comment': '', - 'address': '', - 'email': '', + "name": "The Supplier", + "phone": "", + "comment": "", + "address": "", + "email": "", } def setUp(self): super().setUp() - self.supplier = Supplier.objects.create(name='Supplier') + self.supplier = Supplier.objects.create(name="Supplier") def test_get_ok(self): r = self.client.get(self.url) @@ -3485,13 +3503,13 @@ class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase): def test_post_ok(self): client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.order')) + self.assertRedirects(r, reverse("kfet.order")) self.supplier.refresh_from_db() - self.assertEqual(self.supplier.name, 'The Supplier') + self.assertEqual(self.supplier.name, "The Supplier") def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -3499,67 +3517,59 @@ class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase): class OrderCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.order.new' + url_name = "kfet.order.new" - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.supplier.pk} + return {"pk": self.supplier.pk} @property def url_expected(self): - return '/k-fet/orders/suppliers/{}/new-order'.format(self.supplier.pk) + return "/k-fet/orders/suppliers/{}/new-order".format(self.supplier.pk) def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.add_order']), - } + return {"team1": create_team("team1", "101", perms=["kfet.add_order"])} @property def post_data(self): return { # Formset management - 'form-TOTAL_FORMS': '1', - 'form-INITIAL_FORMS': '1', - 'form-MIN_NUM_FORMS': '0', - 'form-MAX_NUM_FORMS': '1000', + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-MIN_NUM_FORMS": "0", + "form-MAX_NUM_FORMS": "1000", # Article - 'form-0-article': self.article.pk, - 'form-0-quantity_ordered': '20', + "form-0-article": self.article.pk, + "form-0-quantity_ordered": "20", } def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Category') - self.article = Article.objects.create( - name='Article', - category=category, - ) + category = ArticleCategory.objects.create(name="Category") + self.article = Article.objects.create(name="Article", category=category) - self.supplier = Supplier.objects.create(name='Supplier') - SupplierArticle.objects.create( - supplier=self.supplier, - article=self.article, - ) + self.supplier = Supplier.objects.create(name="Supplier") + SupplierArticle.objects.create(supplier=self.supplier, article=self.article) def test_get_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - @mock.patch('django.utils.timezone.now') + @mock.patch("django.utils.timezone.now") def test_post_ok(self, mock_now): mock_now.return_value = self.now client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) order = Order.objects.get(at=self.now) - self.assertRedirects(r, reverse('kfet.order.read', args=[order.pk])) + self.assertRedirects(r, reverse("kfet.order.read", args=[order.pk])) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) @@ -3567,95 +3577,80 @@ class OrderCreateViewTests(ViewTestCaseMixin, TestCase): class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.order.to_inventory' + url_name = "kfet.order.to_inventory" - http_methods = ['GET', 'POST'] + http_methods = ["GET", "POST"] - auth_user = 'team' - auth_forbidden = [None, 'user'] + auth_user = "team" + auth_forbidden = [None, "user"] @property def url_kwargs(self): - return {'pk': self.order.pk} + return {"pk": self.order.pk} @property def url_expected(self): - return '/k-fet/orders/{}/to_inventory'.format(self.order.pk) + return "/k-fet/orders/{}/to_inventory".format(self.order.pk) def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=[ - 'kfet.order_to_inventory', - ]), - } + return {"team1": create_team("team1", "101", perms=["kfet.order_to_inventory"])} @property def post_data(self): return { # Formset mangaement - 'form-TOTAL_FORMS': '1', - 'form-INITIAL_FORMS': '1', - 'form-MIN_NUM_FORMS': '0', - 'form-MAX_NUM_FORMS': '1000', + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-MIN_NUM_FORMS": "0", + "form-MAX_NUM_FORMS": "1000", # Article 1 - 'form-0-article': self.article.pk, - 'form-0-quantity_received': '20', - 'form-0-price_HT': '', - 'form-0-TVA': '', - 'form-0-rights': '', + "form-0-article": self.article.pk, + "form-0-quantity_received": "20", + "form-0-price_HT": "", + "form-0-TVA": "", + "form-0-rights": "", } def setUp(self): super().setUp() - category = ArticleCategory.objects.create(name='Category') - self.article = Article.objects.create( - name='Article', - category=category, - ) + category = ArticleCategory.objects.create(name="Category") + self.article = Article.objects.create(name="Article", category=category) - supplier = Supplier.objects.create(name='Supplier') + supplier = Supplier.objects.create(name="Supplier") SupplierArticle.objects.create(supplier=supplier, article=self.article) self.order = Order.objects.create(supplier=supplier) OrderArticle.objects.create( - order=self.order, - article=self.article, - quantity_ordered=24, + order=self.order, article=self.article, quantity_ordered=24 ) def test_get_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - @mock.patch('django.utils.timezone.now') + @mock.patch("django.utils.timezone.now") def test_post_ok(self, mock_now): mock_now.return_value = self.now client = Client() - client.login(username='team1', password='team1') + client.login(username="team1", password="team1") r = client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.order')) + self.assertRedirects(r, reverse("kfet.order")) inventory = Inventory.objects.first() - self.assertInstanceExpected(inventory, { - 'by': self.accounts['team1'], - 'at': self.now, - 'order': self.order, - }) - self.assertQuerysetEqual( - inventory.articles.all(), - map(repr, [self.article]), + self.assertInstanceExpected( + inventory, + {"by": self.accounts["team1"], "at": self.now, "order": self.order}, ) + self.assertQuerysetEqual(inventory.articles.all(), map(repr, [self.article])) compte = InventoryArticle.objects.get(article=self.article) - self.assertInstanceExpected(compte, { - 'stock_old': 0, - 'stock_new': 20, - 'stock_error': 0, - }) + self.assertInstanceExpected( + compte, {"stock_old": 0, "stock_new": 20, "stock_error": 0} + ) def test_post_forbidden(self): r = self.client.post(self.url, self.post_data) diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index 3a69e9ca..36a4ab65 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -37,34 +37,32 @@ class TestCaseMixin: full_path = request.get_full_path() querystring = QueryDict(mutable=True) - querystring['next'] = full_path + querystring["next"] = full_path - login_url = '/login?' + querystring.urlencode(safe='/') + login_url = "/login?" + querystring.urlencode(safe="/") # We don't focus on what the login view does. # So don't fetch the redirect. - self.assertRedirects( - response, login_url, - fetch_redirect_response=False, - ) + self.assertRedirects(response, login_url, fetch_redirect_response=False) except AssertionError: raise AssertionError( "%(http_method)s request at %(path)s should be forbidden for " "%(username)s user.\n" "Response isn't 403, nor a redirect to login view. Instead, " - "response code is %(code)d." % { - 'http_method': request.method, - 'path': request.get_full_path(), - 'username': ( + "response code is %(code)d." + % { + "http_method": request.method, + "path": request.get_full_path(), + "username": ( "'{}'".format(request.user) if request.user.is_authenticated - else 'anonymous' + else "anonymous" ), - 'code': response.status_code, + "code": response.status_code, } ) - def assertForbiddenKfet(self, response, form_ctx='form'): + def assertForbiddenKfet(self, response, form_ctx="form"): """ Test that a response (retrieved with a Client) contains error due to lack of kfet permissions. @@ -83,7 +81,7 @@ class TestCaseMixin: form = response.context[form_ctx] self.assertIn("Permission refusée", form.non_field_errors()) except (AssertionError, AttributeError, KeyError): - messages = [str(msg) for msg in response.context['messages']] + messages = [str(msg) for msg in response.context["messages"]] self.assertIn("Permission refusée", messages) except AssertionError: request = response.wsgi_request @@ -91,15 +89,16 @@ class TestCaseMixin: "%(http_method)s request at %(path)s should raise an error " "for %(username)s user.\n" "Cannot find any errors in non-field errors of form " - "'%(form_ctx)s', nor in messages." % { - 'http_method': request.method, - 'path': request.get_full_path(), - 'username': ( + "'%(form_ctx)s', nor in messages." + % { + "http_method": request.method, + "path": request.get_full_path(), + "username": ( "'%s'" % request.user if request.user.is_authenticated - else 'anonymous' + else "anonymous" ), - 'form_ctx': form_ctx, + "form_ctx": form_ctx, } ) @@ -131,10 +130,9 @@ class TestCaseMixin: if type(expected) == dict: parsed = urlparse(actual) for part, expected_part in expected.items(): - if part == 'query': + if part == "query": self.assertDictEqual( - parse_qs(parsed.query), - expected.get('query', {}), + parse_qs(parsed.query), expected.get("query", {}) ) else: self.assertEqual(getattr(parsed, part), expected_part) @@ -215,10 +213,11 @@ class ViewTestCaseMixin(TestCaseMixin): can be given by defining an attribute '_data'. """ + url_name = None url_expected = None - http_methods = ['GET'] + http_methods = ["GET"] auth_user = None auth_forbidden = [] @@ -232,7 +231,7 @@ class ViewTestCaseMixin(TestCaseMixin): # 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 = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) @@ -268,14 +267,14 @@ class ViewTestCaseMixin(TestCaseMixin): # Format desc: username, password, trigramme users_base = { # user, user, 000 - 'user': create_user(), + "user": create_user(), # team, team, 100 - 'team': create_team(), + "team": create_team(), # root, root, 200 - 'root': create_root(), + "root": create_root(), } if self.with_liq: - users_base['liq'] = create_user('liq', 'LIQ') + users_base["liq"] = create_user("liq", "LIQ") return users_base @cached_property @@ -300,7 +299,7 @@ class ViewTestCaseMixin(TestCaseMixin): def register_user(self, label, user): self.users[label] = user - if hasattr(user.profile, 'account_kfet'): + if hasattr(user.profile, "account_kfet"): self.accounts[label] = user.profile.account_kfet def get_user(self, label): @@ -310,22 +309,25 @@ class ViewTestCaseMixin(TestCaseMixin): @property def urls_conf(self): - return [{ - 'name': self.url_name, - 'args': getattr(self, 'url_args', []), - 'kwargs': getattr(self, 'url_kwargs', {}), - 'expected': self.url_expected, - }] + return [ + { + "name": self.url_name, + "args": getattr(self, "url_args", []), + "kwargs": getattr(self, "url_kwargs", {}), + "expected": self.url_expected, + } + ] @property def t_urls(self): return [ reverse( - url_conf['name'], - args=url_conf.get('args', []), - kwargs=url_conf.get('kwargs', {}), + url_conf["name"], + args=url_conf.get("args", []), + kwargs=url_conf.get("kwargs", {}), ) - for url_conf in self.urls_conf] + for url_conf in self.urls_conf + ] @property def url(self): @@ -333,7 +335,7 @@ class ViewTestCaseMixin(TestCaseMixin): def test_urls(self): for url, conf in zip(self.t_urls, self.urls_conf): - self.assertEqual(url, conf['expected']) + self.assertEqual(url, conf["expected"]) def test_forbidden(self): for method in self.http_methods: @@ -348,7 +350,7 @@ class ViewTestCaseMixin(TestCaseMixin): client.login(username=user, password=user) send_request = getattr(client, method) - data = getattr(self, '{}_data'.format(method), {}) + data = getattr(self, "{}_data".format(method), {}) r = send_request(url, data) self.assertForbidden(r) diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py index f3222e14..f1b6933a 100644 --- a/kfet/tests/utils.py +++ b/kfet/tests/utils.py @@ -3,7 +3,6 @@ from django.contrib.auth.models import Permission from ..models import Account - User = get_user_model() @@ -23,27 +22,27 @@ def _create_user_and_account(user_attrs, account_attrs, perms=None): the account password is 'kfetpwd_'. """ - user_pwd = user_attrs.pop('password', user_attrs['username']) + user_pwd = user_attrs.pop("password", user_attrs["username"]) user = User.objects.create(**user_attrs) user.set_password(user_pwd) user.save() - account_attrs['cofprofile'] = user.profile - kfet_pwd = account_attrs.pop('password', 'kfetpwd_{}'.format(user_pwd)) + account_attrs["cofprofile"] = user.profile + kfet_pwd = account_attrs.pop("password", "kfetpwd_{}".format(user_pwd)) account = Account.objects.create(**account_attrs) if perms is not None: user = user_add_perms(user, perms) - if 'kfet.is_team' in perms: + if "kfet.is_team" in perms: account.change_pwd(kfet_pwd) account.save() return user -def create_user(username='user', trigramme='000', **kwargs): +def create_user(username="user", trigramme="000", **kwargs): """ Create a user without any permission and its kfet account. @@ -65,20 +64,20 @@ def create_user(username='user', trigramme='000', **kwargs): * trigramme: 000 """ - user_attrs = kwargs.setdefault('user_attrs', {}) + user_attrs = kwargs.setdefault("user_attrs", {}) - user_attrs.setdefault('username', username) - user_attrs.setdefault('first_name', 'first') - user_attrs.setdefault('last_name', 'last') - user_attrs.setdefault('email', 'mail@user.net') + user_attrs.setdefault("username", username) + user_attrs.setdefault("first_name", "first") + user_attrs.setdefault("last_name", "last") + user_attrs.setdefault("email", "mail@user.net") - account_attrs = kwargs.setdefault('account_attrs', {}) - account_attrs.setdefault('trigramme', trigramme) + account_attrs = kwargs.setdefault("account_attrs", {}) + account_attrs.setdefault("trigramme", trigramme) return _create_user_and_account(**kwargs) -def create_team(username='team', trigramme='100', **kwargs): +def create_team(username="team", trigramme="100", **kwargs): """ Create a user, member of the kfet team, and its kfet account. @@ -101,23 +100,23 @@ def create_team(username='team', trigramme='100', **kwargs): * kfet password: kfetpwd_team """ - user_attrs = kwargs.setdefault('user_attrs', {}) + user_attrs = kwargs.setdefault("user_attrs", {}) - user_attrs.setdefault('username', username) - user_attrs.setdefault('first_name', 'team') - user_attrs.setdefault('last_name', 'member') - user_attrs.setdefault('email', 'mail@team.net') + user_attrs.setdefault("username", username) + user_attrs.setdefault("first_name", "team") + user_attrs.setdefault("last_name", "member") + user_attrs.setdefault("email", "mail@team.net") - account_attrs = kwargs.setdefault('account_attrs', {}) - account_attrs.setdefault('trigramme', trigramme) + account_attrs = kwargs.setdefault("account_attrs", {}) + account_attrs.setdefault("trigramme", trigramme) - perms = kwargs.setdefault('perms', []) - perms.append('kfet.is_team') + perms = kwargs.setdefault("perms", []) + perms.append("kfet.is_team") return _create_user_and_account(**kwargs) -def create_root(username='root', trigramme='200', **kwargs): +def create_root(username="root", trigramme="200", **kwargs): """ Create a superuser and its kfet account. @@ -141,16 +140,16 @@ def create_root(username='root', trigramme='200', **kwargs): * kfet password: kfetpwd_root """ - user_attrs = kwargs.setdefault('user_attrs', {}) + user_attrs = kwargs.setdefault("user_attrs", {}) - user_attrs.setdefault('username', username) - user_attrs.setdefault('first_name', 'super') - user_attrs.setdefault('last_name', 'user') - user_attrs.setdefault('email', 'mail@root.net') - user_attrs['is_superuser'] = user_attrs['is_staff'] = True + user_attrs.setdefault("username", username) + user_attrs.setdefault("first_name", "super") + user_attrs.setdefault("last_name", "user") + user_attrs.setdefault("email", "mail@root.net") + user_attrs["is_superuser"] = user_attrs["is_staff"] = True - account_attrs = kwargs.setdefault('account_attrs', {}) - account_attrs.setdefault('trigramme', trigramme) + account_attrs = kwargs.setdefault("account_attrs", {}) + account_attrs.setdefault("trigramme", trigramme) return _create_user_and_account(**kwargs) @@ -159,10 +158,9 @@ def get_perms(*labels): """Return Permission instances from a list of '.'.""" perms = {} for label in set(labels): - app_label, codename = label.split('.', 1) + app_label, codename = label.split(".", 1) perms[label] = Permission.objects.get( - content_type__app_label=app_label, - codename=codename, + content_type__app_label=app_label, codename=codename ) return perms diff --git a/kfet/urls.py b/kfet/urls.py index 98d0bbf9..531e0cc9 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -4,238 +4,287 @@ from django.contrib.auth.decorators import permission_required from kfet import autocomplete, views from kfet.decorators import teamkfet_required - urlpatterns = [ - url(r'^login/generic$', views.login_generic, - name='kfet.login.generic'), - url(r'^history$', views.history, - name='kfet.history'), - + url(r"^login/generic$", views.login_generic, name="kfet.login.generic"), + url(r"^history$", views.history, name="kfet.history"), # ----- # Account urls # ----- - # Account - General - url(r'^accounts/$', views.account, - name='kfet.account'), - url(r'^accounts/is_validandfree$', views.account_is_validandfree_ajax, - name='kfet.account.is_validandfree.ajax'), - + url(r"^accounts/$", views.account, name="kfet.account"), + url( + r"^accounts/is_validandfree$", + views.account_is_validandfree_ajax, + name="kfet.account.is_validandfree.ajax", + ), # Account - Create - url(r'^accounts/new$', views.account_create, - name='kfet.account.create'), - url(r'^accounts/new/user/(?P.+)$', views.account_create_ajax, - name='kfet.account.create.fromuser'), - url(r'^accounts/new/clipper/(?P[\w-]+)/(?P.*)$', + url(r"^accounts/new$", views.account_create, name="kfet.account.create"), + url( + r"^accounts/new/user/(?P.+)$", views.account_create_ajax, - name='kfet.account.create.fromclipper'), - url(r'^accounts/new/empty$', views.account_create_ajax, - name='kfet.account.create.empty'), - url(r'^autocomplete/account_new$', autocomplete.account_create, - name='kfet.account.create.autocomplete'), - + name="kfet.account.create.fromuser", + ), + url( + r"^accounts/new/clipper/(?P[\w-]+)/(?P.*)$", + views.account_create_ajax, + name="kfet.account.create.fromclipper", + ), + url( + r"^accounts/new/empty$", + views.account_create_ajax, + name="kfet.account.create.empty", + ), + url( + r"^autocomplete/account_new$", + autocomplete.account_create, + name="kfet.account.create.autocomplete", + ), # Account - Search - url(r'^autocomplete/account_search$', autocomplete.account_search, - name='kfet.account.search.autocomplete'), - + url( + r"^autocomplete/account_search$", + autocomplete.account_search, + name="kfet.account.search.autocomplete", + ), # Account - Read - url(r'^accounts/(?P.{3})$', views.account_read, - name='kfet.account.read'), - + url( + r"^accounts/(?P.{3})$", views.account_read, name="kfet.account.read" + ), # Account - Update - url(r'^accounts/(?P.{3})/edit$', views.account_update, - name='kfet.account.update'), - + url( + r"^accounts/(?P.{3})/edit$", + views.account_update, + name="kfet.account.update", + ), # Account - Groups - url(r'^accounts/groups$', views.account_group, - name='kfet.account.group'), - url(r'^accounts/groups/new$', - permission_required('kfet.manage_perms') - (views.AccountGroupCreate.as_view()), - name='kfet.account.group.create'), - url(r'^accounts/groups/(?P\d+)/edit$', - permission_required('kfet.manage_perms') - (views.AccountGroupUpdate.as_view()), - name='kfet.account.group.update'), - - url(r'^accounts/negatives$', - permission_required('kfet.view_negs') - (views.AccountNegativeList.as_view()), - name='kfet.account.negative'), - + url(r"^accounts/groups$", views.account_group, name="kfet.account.group"), + url( + r"^accounts/groups/new$", + permission_required("kfet.manage_perms")(views.AccountGroupCreate.as_view()), + name="kfet.account.group.create", + ), + url( + r"^accounts/groups/(?P\d+)/edit$", + permission_required("kfet.manage_perms")(views.AccountGroupUpdate.as_view()), + name="kfet.account.group.update", + ), + url( + r"^accounts/negatives$", + permission_required("kfet.view_negs")(views.AccountNegativeList.as_view()), + name="kfet.account.negative", + ), # Account - Statistics - url(r'^accounts/(?P.{3})/stat/operations/list$', + url( + r"^accounts/(?P.{3})/stat/operations/list$", views.AccountStatOperationList.as_view(), - name='kfet.account.stat.operation.list'), - url(r'^accounts/(?P.{3})/stat/operations$', + name="kfet.account.stat.operation.list", + ), + url( + r"^accounts/(?P.{3})/stat/operations$", views.AccountStatOperation.as_view(), - name='kfet.account.stat.operation'), - - url(r'^accounts/(?P.{3})/stat/balance/list$', + name="kfet.account.stat.operation", + ), + url( + r"^accounts/(?P.{3})/stat/balance/list$", views.AccountStatBalanceList.as_view(), - name='kfet.account.stat.balance.list'), - url(r'^accounts/(?P.{3})/stat/balance$', + name="kfet.account.stat.balance.list", + ), + url( + r"^accounts/(?P.{3})/stat/balance$", views.AccountStatBalance.as_view(), - name='kfet.account.stat.balance'), - + name="kfet.account.stat.balance", + ), # ----- # Checkout urls # ----- - # Checkout - General - url('^checkouts/$', + url( + "^checkouts/$", teamkfet_required(views.CheckoutList.as_view()), - name='kfet.checkout'), + name="kfet.checkout", + ), # Checkout - Create - url('^checkouts/new$', + url( + "^checkouts/new$", teamkfet_required(views.CheckoutCreate.as_view()), - name='kfet.checkout.create'), + name="kfet.checkout.create", + ), # Checkout - Read - url('^checkouts/(?P\d+)$', + url( + "^checkouts/(?P\d+)$", teamkfet_required(views.CheckoutRead.as_view()), - name='kfet.checkout.read'), + name="kfet.checkout.read", + ), # Checkout - Update - url('^checkouts/(?P\d+)/edit$', + url( + "^checkouts/(?P\d+)/edit$", teamkfet_required(views.CheckoutUpdate.as_view()), - name='kfet.checkout.update'), - + name="kfet.checkout.update", + ), # ----- # Checkout Statement urls # ----- - # Checkout Statement - General - url('^checkouts/statements/$', + url( + "^checkouts/statements/$", teamkfet_required(views.CheckoutStatementList.as_view()), - name='kfet.checkoutstatement'), + name="kfet.checkoutstatement", + ), # Checkout Statement - Create - url('^checkouts/(?P\d+)/statements/add', + url( + "^checkouts/(?P\d+)/statements/add", teamkfet_required(views.CheckoutStatementCreate.as_view()), - name='kfet.checkoutstatement.create'), + name="kfet.checkoutstatement.create", + ), # Checkout Statement - Update - url('^checkouts/(?P\d+)/statements/(?P\d+)/edit', + url( + "^checkouts/(?P\d+)/statements/(?P\d+)/edit", teamkfet_required(views.CheckoutStatementUpdate.as_view()), - name='kfet.checkoutstatement.update'), - + name="kfet.checkoutstatement.update", + ), # ----- # Article urls # ----- - # Category - General - url('^categories/$', + url( + "^categories/$", teamkfet_required(views.CategoryList.as_view()), - name='kfet.category'), + name="kfet.category", + ), # Category - Update - url('^categories/(?P\d+)/edit$', + url( + "^categories/(?P\d+)/edit$", teamkfet_required(views.CategoryUpdate.as_view()), - name='kfet.category.update'), + name="kfet.category.update", + ), # Article - General - url('^articles/$', + url( + "^articles/$", teamkfet_required(views.ArticleList.as_view()), - name='kfet.article'), + name="kfet.article", + ), # Article - Create - url('^articles/new$', + url( + "^articles/new$", teamkfet_required(views.ArticleCreate.as_view()), - name='kfet.article.create'), + name="kfet.article.create", + ), # Article - Read - url('^articles/(?P\d+)$', + url( + "^articles/(?P\d+)$", teamkfet_required(views.ArticleRead.as_view()), - name='kfet.article.read'), + name="kfet.article.read", + ), # Article - Update - url('^articles/(?P\d+)/edit$', + url( + "^articles/(?P\d+)/edit$", teamkfet_required(views.ArticleUpdate.as_view()), - name='kfet.article.update'), + name="kfet.article.update", + ), # Article - Statistics - url(r'^articles/(?P\d+)/stat/sales/list$', + url( + r"^articles/(?P\d+)/stat/sales/list$", views.ArticleStatSalesList.as_view(), - name='kfet.article.stat.sales.list'), - url(r'^articles/(?P\d+)/stat/sales$', + name="kfet.article.stat.sales.list", + ), + url( + r"^articles/(?P\d+)/stat/sales$", views.ArticleStatSales.as_view(), - name='kfet.article.stat.sales'), - + name="kfet.article.stat.sales", + ), # ----- # K-Psul urls # ----- - - url('^k-psul/$', views.kpsul, name='kfet.kpsul'), - url('^k-psul/checkout_data$', views.kpsul_checkout_data, - name='kfet.kpsul.checkout_data'), - url('^k-psul/perform_operations$', views.kpsul_perform_operations, - name='kfet.kpsul.perform_operations'), - url('^k-psul/cancel_operations$', views.kpsul_cancel_operations, - name='kfet.kpsul.cancel_operations'), - url('^k-psul/articles_data', views.kpsul_articles_data, - name='kfet.kpsul.articles_data'), - url('^k-psul/update_addcost$', views.kpsul_update_addcost, - name='kfet.kpsul.update_addcost'), - url('^k-psul/get_settings$', views.kpsul_get_settings, - name='kfet.kpsul.get_settings'), - + url("^k-psul/$", views.kpsul, name="kfet.kpsul"), + url( + "^k-psul/checkout_data$", + views.kpsul_checkout_data, + name="kfet.kpsul.checkout_data", + ), + url( + "^k-psul/perform_operations$", + views.kpsul_perform_operations, + name="kfet.kpsul.perform_operations", + ), + url( + "^k-psul/cancel_operations$", + views.kpsul_cancel_operations, + name="kfet.kpsul.cancel_operations", + ), + url( + "^k-psul/articles_data", + views.kpsul_articles_data, + name="kfet.kpsul.articles_data", + ), + url( + "^k-psul/update_addcost$", + views.kpsul_update_addcost, + name="kfet.kpsul.update_addcost", + ), + url( + "^k-psul/get_settings$", + views.kpsul_get_settings, + name="kfet.kpsul.get_settings", + ), # ----- # JSON urls # ----- - - url(r'^history.json$', views.history_json, - name='kfet.history.json'), - url(r'^accounts/read.json$', views.account_read_json, - name='kfet.account.read.json'), - - + url(r"^history.json$", views.history_json, name="kfet.history.json"), + url( + r"^accounts/read.json$", views.account_read_json, name="kfet.account.read.json" + ), # ----- # Settings urls # ----- - - url(r'^settings/$', views.config_list, - name='kfet.settings'), - url(r'^settings/edit$', views.config_update, - name='kfet.settings.update'), - - + url(r"^settings/$", views.config_list, name="kfet.settings"), + url(r"^settings/edit$", views.config_update, name="kfet.settings.update"), # ----- # Transfers urls # ----- - - url(r'^transfers/$', views.transfers, - name='kfet.transfers'), - url(r'^transfers/new$', views.transfers_create, - name='kfet.transfers.create'), - url(r'^transfers/perform$', views.perform_transfers, - name='kfet.transfers.perform'), - url(r'^transfers/cancel$', views.cancel_transfers, - name='kfet.transfers.cancel'), - + url(r"^transfers/$", views.transfers, name="kfet.transfers"), + url(r"^transfers/new$", views.transfers_create, name="kfet.transfers.create"), + url(r"^transfers/perform$", views.perform_transfers, name="kfet.transfers.perform"), + url(r"^transfers/cancel$", views.cancel_transfers, name="kfet.transfers.cancel"), # ----- # Inventories urls # ----- - - url(r'^inventaires/$', + url( + r"^inventaires/$", teamkfet_required(views.InventoryList.as_view()), - name='kfet.inventory'), - url(r'^inventaires/new$', views.inventory_create, - name='kfet.inventory.create'), - url(r'^inventaires/(?P\d+)$', + name="kfet.inventory", + ), + url(r"^inventaires/new$", views.inventory_create, name="kfet.inventory.create"), + url( + r"^inventaires/(?P\d+)$", teamkfet_required(views.InventoryRead.as_view()), - name='kfet.inventory.read'), - + name="kfet.inventory.read", + ), # ----- # Order urls # ----- - - url(r'^orders/$', - teamkfet_required(views.OrderList.as_view()), - name='kfet.order'), - url(r'^orders/(?P\d+)$', + url(r"^orders/$", teamkfet_required(views.OrderList.as_view()), name="kfet.order"), + url( + r"^orders/(?P\d+)$", teamkfet_required(views.OrderRead.as_view()), - name='kfet.order.read'), - url(r'^orders/suppliers/(?P\d+)/edit$', + name="kfet.order.read", + ), + url( + r"^orders/suppliers/(?P\d+)/edit$", teamkfet_required(views.SupplierUpdate.as_view()), - name='kfet.order.supplier.update'), - url(r'^orders/suppliers/(?P\d+)/new-order$', views.order_create, - name='kfet.order.new'), - url(r'^orders/(?P\d+)/to_inventory$', views.order_to_inventory, - name='kfet.order.to_inventory'), + name="kfet.order.supplier.update", + ), + url( + r"^orders/suppliers/(?P\d+)/new-order$", + views.order_create, + name="kfet.order.new", + ), + url( + r"^orders/(?P\d+)/to_inventory$", + views.order_to_inventory, + name="kfet.order.to_inventory", + ), ] urlpatterns += [ # K-Fêt Open urls - url('^open/', include('kfet.open.urls')), + url("^open/", include("kfet.open.urls")) ] diff --git a/kfet/utils.py b/kfet/utils.py index 3d06bb0b..0c4f170a 100644 --- a/kfet/utils.py +++ b/kfet/utils.py @@ -1,11 +1,10 @@ -import math import json - -from django.core.cache import cache -from django.core.serializers.json import DjangoJSONEncoder +import math from channels.channel import Group from channels.generic.websockets import JsonWebsocketConsumer +from django.core.cache import cache +from django.core.serializers.json import DjangoJSONEncoder from .config import kfet_config @@ -16,8 +15,10 @@ def to_ukf(balance, is_cof=False): grant = (1 + subvention / 100) if is_cof else 1 return math.floor(balance * 10 * grant) + # Storage + class CachedMixin: """Object with cached properties. @@ -27,8 +28,9 @@ class CachedMixin: cache_prefix (str): Used to prefix keys in cache. """ + cached = {} - cache_prefix = '' + cache_prefix = "" def __init__(self, cache_prefix=None, *args, **kwargs): super().__init__(*args, **kwargs) @@ -36,12 +38,12 @@ class CachedMixin: self.cache_prefix = cache_prefix def cachekey(self, attr): - return '{}__{}'.format(self.cache_prefix, attr) + return "{}__{}".format(self.cache_prefix, attr) def __getattr__(self, attr): if attr in self.cached: return cache.get(self.cachekey(attr), self.cached.get(attr)) - elif hasattr(super(), '__getattr__'): + elif hasattr(super(), "__getattr__"): return super().__getattr__(attr) else: raise AttributeError("can't get attribute") @@ -49,19 +51,18 @@ class CachedMixin: def __setattr__(self, attr, value): if attr in self.cached: cache.set(self.cachekey(attr), value) - elif hasattr(super(), '__setattr__'): + elif hasattr(super(), "__setattr__"): super().__setattr__(attr, value) else: raise AttributeError("can't set attribute") def clear_cache(self): - cache.delete_many([ - self.cachekey(attr) for attr in self.cached.keys() - ]) + cache.delete_many([self.cachekey(attr) for attr in self.cached.keys()]) # Consumers + class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): """Custom Json Websocket Consumer. @@ -84,6 +85,7 @@ class PermConsumerMixin: message.user is appended as argument to each connection_groups method call. """ + http_user = True # Enable message.user perms_connect = [] @@ -107,7 +109,9 @@ class PermConsumerMixin: # We add user to connection_groups call. groups = self.connection_groups(user=message.user, **kwargs) for group in groups: - Group(group, channel_layer=message.channel_layer).discard(message.reply_channel) + Group(group, channel_layer=message.channel_layer).discard( + message.reply_channel + ) self.disconnect(message, **kwargs) def connection_groups(self, user, **kwargs): diff --git a/kfet/views.py b/kfet/views.py index f3e70dde..088f867e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,55 +1,92 @@ import ast -from urllib.parse import urlencode - -from django.shortcuts import render, get_object_or_404, redirect -from django.core.exceptions import PermissionDenied -from django.core.cache import cache -from django.views.generic import ListView, DetailView, TemplateView, FormView -from django.views.generic.detail import BaseDetailView -from django.views.generic.edit import CreateView, UpdateView -from django.core.urlresolvers import reverse, reverse_lazy -from django.contrib import messages -from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import User, Permission -from django.http import JsonResponse, Http404 -from django.forms import formset_factory -from django.db import transaction -from django.db.models import F, Sum, Prefetch, Count -from django.db.models.functions import Coalesce -from django.utils import timezone -from django.utils.decorators import method_decorator - -from gestioncof.models import CofProfile - -from kfet.config import kfet_config -from kfet.decorators import teamkfet_required -from kfet.models import ( - Account, Checkout, Article, AccountNegative, - CheckoutStatement, Supplier, SupplierArticle, Inventory, - InventoryArticle, Order, OrderArticle, Operation, OperationGroup, - TransferGroup, Transfer, ArticleCategory) -from kfet.forms import ( - AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm, - UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm, - AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm, - CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, - CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm, - KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm, - KPsulOperationFormSet, AddcostForm, FilterHistoryForm, - TransferFormSet, InventoryArticleForm, OrderArticleForm, - OrderArticleToInventoryForm, CategoryForm, KFetConfigForm - ) -from collections import defaultdict -from kfet import consumers -from datetime import timedelta -from decimal import Decimal import heapq import statistics -from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale +from collections import defaultdict +from datetime import timedelta +from decimal import Decimal +from urllib.parse import urlencode + +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.models import Permission, User +from django.contrib.messages.views import SuccessMessageMixin +from django.core.cache import cache +from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse, reverse_lazy +from django.db import transaction +from django.db.models import Count, F, Prefetch, Sum +from django.db.models.functions import Coalesce +from django.forms import formset_factory +from django.http import Http404, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.generic import DetailView, FormView, ListView, TemplateView +from django.views.generic.detail import BaseDetailView +from django.views.generic.edit import CreateView, UpdateView + +from gestioncof.models import CofProfile +from kfet import consumers +from kfet.config import kfet_config +from kfet.decorators import teamkfet_required +from kfet.forms import ( + AccountBalanceForm, + AccountForm, + AccountNegativeForm, + AccountNoTriForm, + AccountPwdForm, + AccountRestrictForm, + AccountTriForm, + AddcostForm, + ArticleForm, + ArticleRestrictForm, + CategoryForm, + CheckoutForm, + CheckoutRestrictForm, + CheckoutStatementCreateForm, + CheckoutStatementUpdateForm, + CofForm, + CofRestrictForm, + FilterHistoryForm, + InventoryArticleForm, + KFetConfigForm, + KPsulAccountForm, + KPsulCheckoutForm, + KPsulOperationFormSet, + KPsulOperationGroupForm, + OrderArticleForm, + OrderArticleToInventoryForm, + TransferFormSet, + UserForm, + UserGroupForm, + UserRestrictForm, + UserRestrictTeamForm, +) +from kfet.models import ( + Account, + AccountNegative, + Article, + ArticleCategory, + Checkout, + CheckoutStatement, + Inventory, + InventoryArticle, + Operation, + OperationGroup, + Order, + OrderArticle, + Supplier, + SupplierArticle, + Transfer, + TransferGroup, +) +from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest, tot_ventes from .auth.views import ( # noqa - account_group, login_generic, AccountGroupCreate, AccountGroupUpdate, + AccountGroupCreate, + AccountGroupUpdate, + account_group, + login_generic, ) @@ -57,22 +94,25 @@ def put_cleaned_data_in_dict(dict, form): for field in form.cleaned_data: dict[field] = form.cleaned_data[field] + # ----- # Account views # ----- # Account - General + @login_required @teamkfet_required def account(request): - accounts = Account.objects.select_related('cofprofile__user').order_by('trigramme') - return render(request, "kfet/account.html", { 'accounts' : accounts }) + accounts = Account.objects.select_related("cofprofile__user").order_by("trigramme") + return render(request, "kfet/account.html", {"accounts": accounts}) + @login_required @teamkfet_required def account_is_validandfree_ajax(request): - if not request.GET.get("trigramme", ''): + if not request.GET.get("trigramme", ""): raise Http404 trigramme = request.GET.get("trigramme") data = Account.is_validandfree(trigramme) @@ -81,6 +121,7 @@ def account_is_validandfree_ajax(request): # Account - Create + @login_required @teamkfet_required def account_create(request): @@ -90,21 +131,28 @@ def account_create(request): trigramme_form = AccountTriForm(request.POST) # Peuplement des forms - username = request.POST.get('username') - login_clipper = request.POST.get('login_clipper') + username = request.POST.get("username") + login_clipper = request.POST.get("login_clipper") forms = get_account_create_forms( - request, username=username, login_clipper=login_clipper) + request, username=username, login_clipper=login_clipper + ) - account_form = forms['account_form'] - cof_form = forms['cof_form'] - user_form = forms['user_form'] + account_form = forms["account_form"] + cof_form = forms["cof_form"] + user_form = forms["user_form"] - if all((user_form.is_valid(), cof_form.is_valid(), - trigramme_form.is_valid(), account_form.is_valid())): + if all( + ( + user_form.is_valid(), + cof_form.is_valid(), + trigramme_form.is_valid(), + account_form.is_valid(), + ) + ): # Checking permission - if not request.user.has_perm('kfet.add_account'): - messages.error(request, 'Permission refusée') + if not request.user.has_perm("kfet.add_account"): + messages.error(request, "Permission refusée") else: data = {} # Fill data for Account.save() @@ -112,35 +160,44 @@ def account_create(request): put_cleaned_data_in_dict(data, cof_form) try: - account = trigramme_form.save(data = data) + account = trigramme_form.save(data=data) account_form = AccountNoTriForm(request.POST, instance=account) account_form.save() - messages.success(request, 'Compte créé : %s' % account.trigramme) - return redirect('kfet.account.create') + messages.success(request, "Compte créé : %s" % account.trigramme) + return redirect("kfet.account.create") except Account.UserHasAccount as e: - messages.error(request, \ - "Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme) + messages.error( + request, + "Cet utilisateur a déjà un compte K-Fêt : %s" % e.trigramme, + ) else: - initial = { 'trigramme': request.GET.get('trigramme', '') } - trigramme_form = AccountTriForm(initial = initial) + initial = {"trigramme": request.GET.get("trigramme", "")} + trigramme_form = AccountTriForm(initial=initial) account_form = None cof_form = None user_form = None - return render(request, "kfet/account_create.html", { - 'trigramme_form': trigramme_form, - 'account_form': account_form, - 'cof_form': cof_form, - 'user_form': user_form, - }) + return render( + request, + "kfet/account_create.html", + { + "trigramme_form": trigramme_form, + "account_form": account_form, + "cof_form": cof_form, + "user_form": user_form, + }, + ) + def account_form_set_readonly_fields(user_form, cof_form): - user_form.fields['username'].widget.attrs['readonly'] = True - cof_form.fields['login_clipper'].widget.attrs['readonly'] = True - cof_form.fields['is_cof'].widget.attrs['disabled'] = True + user_form.fields["username"].widget.attrs["readonly"] = True + cof_form.fields["login_clipper"].widget.attrs["readonly"] = True + cof_form.fields["is_cof"].widget.attrs["disabled"] = True -def get_account_create_forms(request=None, username=None, login_clipper=None, - fullname=None): + +def get_account_create_forms( + request=None, username=None, login_clipper=None, fullname=None +): user = None clipper = False if login_clipper and (login_clipper == username or not username): @@ -158,26 +215,27 @@ def get_account_create_forms(request=None, username=None, login_clipper=None, # UserForm - Prefill user_initial = { - 'username' : login_clipper, - 'email' : "%s@clipper.ens.fr" % login_clipper} + "username": login_clipper, + "email": "%s@clipper.ens.fr" % login_clipper, + } if fullname: # Prefill du nom et prénom names = fullname.split() # Le premier, c'est le prénom - user_initial['first_name'] = names[0] + user_initial["first_name"] = names[0] if len(names) > 1: # Si d'autres noms -> tous dans le nom de famille - user_initial['last_name'] = " ".join(names[1:]) + user_initial["last_name"] = " ".join(names[1:]) # CofForm - Prefill - cof_initial = { 'login_clipper': login_clipper } + cof_initial = {"login_clipper": login_clipper} # Form créations if request: user_form = UserForm(request.POST, initial=user_initial) - cof_form = CofForm(request.POST, initial=cof_initial) + cof_form = CofForm(request.POST, initial=cof_initial) else: user_form = UserForm(initial=user_initial) - cof_form = CofForm(initial=cof_initial) + cof_form = CofForm(initial=cof_initial) # Protection (read-only) des champs username et login_clipper account_form_set_readonly_fields(user_form, cof_form) @@ -189,11 +247,11 @@ def get_account_create_forms(request=None, username=None, login_clipper=None, (cof, _) = CofProfile.objects.get_or_create(user=user) # UserForm + CofForm - Création à partir des instances existantes if request: - user_form = UserForm(request.POST, instance = user) - cof_form = CofForm(request.POST, instance = cof) + user_form = UserForm(request.POST, instance=user) + cof_form = CofForm(request.POST, instance=cof) else: user_form = UserForm(instance=user) - cof_form = CofForm(instance=cof) + cof_form = CofForm(instance=cof) # Protection (read-only) des champs username, login_clipper et is_cof account_form_set_readonly_fields(user_form, cof_form) except User.DoesNotExist: @@ -204,66 +262,64 @@ def get_account_create_forms(request=None, username=None, login_clipper=None, # connaît pas du tout, faut tout remplir if request: user_form = UserForm(request.POST) - cof_form = CofForm(request.POST) + cof_form = CofForm(request.POST) else: user_form = UserForm() - cof_form = CofForm() + cof_form = CofForm() # mais on laisse le username en écriture - cof_form.fields['login_clipper'].widget.attrs['readonly'] = True - cof_form.fields['is_cof'].widget.attrs['disabled'] = True + cof_form.fields["login_clipper"].widget.attrs["readonly"] = True + cof_form.fields["is_cof"].widget.attrs["disabled"] = True if request: account_form = AccountNoTriForm(request.POST) else: account_form = AccountNoTriForm() - return { - 'account_form': account_form, - 'cof_form': cof_form, - 'user_form': user_form, - } + return {"account_form": account_form, "cof_form": cof_form, "user_form": user_form} @login_required @teamkfet_required -def account_create_ajax(request, username=None, login_clipper=None, - fullname=None): +def account_create_ajax(request, username=None, login_clipper=None, fullname=None): forms = get_account_create_forms( - request=None, username=username, login_clipper=login_clipper, - fullname=fullname) - return render(request, "kfet/account_create_form.html", { - 'account_form' : forms['account_form'], - 'cof_form' : forms['cof_form'], - 'user_form' : forms['user_form'], - }) + request=None, username=username, login_clipper=login_clipper, fullname=fullname + ) + return render( + request, + "kfet/account_create_form.html", + { + "account_form": forms["account_form"], + "cof_form": forms["cof_form"], + "user_form": forms["user_form"], + }, + ) # Account - Read + @login_required def account_read(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) # Checking permissions if not account.readable or ( - not request.user.has_perm('kfet.is_team') and - request.user != account.user): + not request.user.has_perm("kfet.is_team") and request.user != account.user + ): raise PermissionDenied addcosts = ( - OperationGroup.objects - .filter(opes__addcost_for=account, - opes__canceled_at=None) - .extra({'date': "date(at)"}) - .values('date') - .annotate(sum_addcosts=Sum('opes__addcost_amount')) - .order_by('-date') + OperationGroup.objects.filter(opes__addcost_for=account, opes__canceled_at=None) + .extra({"date": "date(at)"}) + .values("date") + .annotate(sum_addcosts=Sum("opes__addcost_amount")) + .order_by("-date") + ) + + return render( + request, "kfet/account_read.html", {"account": account, "addcosts": addcosts} ) - return render(request, "kfet/account_read.html", { - 'account': account, - 'addcosts': addcosts, - }) # Account - Update @@ -273,21 +329,19 @@ def account_update(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) # Checking permissions - if not request.user.has_perm('kfet.is_team') \ - and request.user != account.user: + if not request.user.has_perm("kfet.is_team") and request.user != account.user: raise PermissionDenied - if request.user.has_perm('kfet.is_team'): + if request.user.has_perm("kfet.is_team"): user_form = UserRestrictTeamForm(instance=account.user) group_form = UserGroupForm(instance=account.user) account_form = AccountForm(instance=account) cof_form = CofRestrictForm(instance=account.cofprofile) pwd_form = AccountPwdForm() - if account.balance < 0 and not hasattr(account, 'negative'): - AccountNegative.objects.create(account=account, - start=timezone.now()) + if account.balance < 0 and not hasattr(account, "negative"): + AccountNegative.objects.create(account=account, start=timezone.now()) account.refresh_from_db() - if hasattr(account, 'negative'): + if hasattr(account, "negative"): negative_form = AccountNegativeForm(instance=account.negative) else: negative_form = None @@ -304,21 +358,23 @@ def account_update(request, trigramme): success = False missing_perm = True - if request.user.has_perm('kfet.is_team'): + if request.user.has_perm("kfet.is_team"): account_form = AccountForm(request.POST, instance=account) - cof_form = CofRestrictForm(request.POST, - instance=account.cofprofile) - user_form = UserRestrictTeamForm(request.POST, - instance=account.user) + cof_form = CofRestrictForm(request.POST, instance=account.cofprofile) + user_form = UserRestrictTeamForm(request.POST, instance=account.user) group_form = UserGroupForm(request.POST, instance=account.user) pwd_form = AccountPwdForm(request.POST) - if hasattr(account, 'negative'): - negative_form = AccountNegativeForm(request.POST, - instance=account.negative) + if hasattr(account, "negative"): + negative_form = AccountNegativeForm( + request.POST, instance=account.negative + ) - if (request.user.has_perm('kfet.change_account') - and account_form.is_valid() and cof_form.is_valid() - and user_form.is_valid()): + if ( + request.user.has_perm("kfet.change_account") + and account_form.is_valid() + and cof_form.is_valid() + and user_form.is_valid() + ): missing_perm = False data = {} # Fill data for Account.save() @@ -329,44 +385,48 @@ def account_update(request, trigramme): account_form.save(data=data) # Checking perm to update password - if (request.user.has_perm('kfet.change_account_password') - and pwd_form.is_valid()): - pwd = pwd_form.cleaned_data['pwd1'] + if ( + request.user.has_perm("kfet.change_account_password") + and pwd_form.is_valid() + ): + pwd = pwd_form.cleaned_data["pwd1"] account.change_pwd(pwd) account.save() - messages.success(request, 'Mot de passe mis à jour') + messages.success(request, "Mot de passe mis à jour") # Checking perm to manage perms - if (request.user.has_perm('kfet.manage_perms') - and group_form.is_valid()): + if request.user.has_perm("kfet.manage_perms") and group_form.is_valid(): group_form.save() # Checking perm to manage negative - if hasattr(account, 'negative'): + if hasattr(account, "negative"): balance_offset_old = 0 if account.negative.balance_offset: balance_offset_old = account.negative.balance_offset - if (hasattr(account, 'negative') - and request.user.has_perm('kfet.change_accountnegative') - and negative_form.is_valid()): - balance_offset_new = \ - negative_form.cleaned_data['balance_offset'] + if ( + hasattr(account, "negative") + and request.user.has_perm("kfet.change_accountnegative") + and negative_form.is_valid() + ): + balance_offset_new = negative_form.cleaned_data["balance_offset"] if not balance_offset_new: balance_offset_new = 0 - balance_offset_diff = (balance_offset_new - - balance_offset_old) + balance_offset_diff = balance_offset_new - balance_offset_old Account.objects.filter(pk=account.pk).update( - balance=F('balance') + balance_offset_diff) + balance=F("balance") + balance_offset_diff + ) negative_form.save() - if Account.objects.get(pk=account.pk).balance >= 0 \ - and not balance_offset_new: + if ( + Account.objects.get(pk=account.pk).balance >= 0 + and not balance_offset_new + ): AccountNegative.objects.get(account=account).delete() success = True messages.success( request, - 'Informations du compte %s mises à jour' - % account.trigramme) + "Informations du compte %s mises à jour" % account.trigramme, + ) # Modification de ses propres informations if request.user == account.user: @@ -380,75 +440,79 @@ def account_update(request, trigramme): user_form.save() account_form.save() success = True - messages.success(request, - 'Vos informations ont été mises à jour') + messages.success(request, "Vos informations ont été mises à jour") - if request.user.has_perm('kfet.is_team') \ - and pwd_form.is_valid(): - pwd = pwd_form.cleaned_data['pwd1'] + if request.user.has_perm("kfet.is_team") and pwd_form.is_valid(): + pwd = pwd_form.cleaned_data["pwd1"] account.change_pwd(pwd) account.save() - messages.success( - request, 'Votre mot de passe a été mis à jour') + messages.success(request, "Votre mot de passe a été mis à jour") if missing_perm: - messages.error(request, 'Permission refusée') + messages.error(request, "Permission refusée") if success: - return redirect('kfet.account.read', account.trigramme) + return redirect("kfet.account.read", account.trigramme) else: messages.error( - request, 'Informations non mises à jour. Corrigez les erreurs') + request, "Informations non mises à jour. Corrigez les erreurs" + ) - return render(request, "kfet/account_update.html", { - 'account': account, - 'account_form': account_form, - 'cof_form': cof_form, - 'user_form': user_form, - 'group_form': group_form, - 'negative_form': negative_form, - 'pwd_form': pwd_form, - }) + return render( + request, + "kfet/account_update.html", + { + "account": account, + "account_form": account_form, + "cof_form": cof_form, + "user_form": user_form, + "group_form": group_form, + "negative_form": negative_form, + "pwd_form": pwd_form, + }, + ) class AccountNegativeList(ListView): - queryset = ( - AccountNegative.objects - .select_related('account', 'account__cofprofile__user') - .exclude(account__trigramme='#13') - ) - template_name = 'kfet/account_negative.html' - context_object_name = 'negatives' + queryset = AccountNegative.objects.select_related( + "account", "account__cofprofile__user" + ).exclude(account__trigramme="#13") + template_name = "kfet/account_negative.html" + context_object_name = "negatives" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) real_balances = (neg.account.real_balance for neg in self.object_list) - context['negatives_sum'] = sum(real_balances) + context["negatives_sum"] = sum(real_balances) return context + # ----- # Checkout views # ----- # Checkout - General + class CheckoutList(ListView): - model = Checkout - template_name = 'kfet/checkout.html' - context_object_name = 'checkouts' + model = Checkout + template_name = "kfet/checkout.html" + context_object_name = "checkouts" + # Checkout - Create + class CheckoutCreate(SuccessMessageMixin, CreateView): - model = Checkout - template_name = 'kfet/checkout_create.html' - form_class = CheckoutForm - success_message = 'Nouvelle caisse : %(name)s' + model = Checkout + template_name = "kfet/checkout_create.html" + form_class = CheckoutForm + success_message = "Nouvelle caisse : %(name)s" # Surcharge de la validation def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.add_checkout'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.add_checkout"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Creating @@ -457,127 +521,161 @@ class CheckoutCreate(SuccessMessageMixin, CreateView): return super().form_valid(form) + # Checkout - Read + class CheckoutRead(DetailView): model = Checkout - template_name = 'kfet/checkout_read.html' - context_object_name = 'checkout' + template_name = "kfet/checkout_read.html" + context_object_name = "checkout" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['statements'] = context['checkout'].statements.order_by('-at') + context["statements"] = context["checkout"].statements.order_by("-at") return context + # Checkout - Update + class CheckoutUpdate(SuccessMessageMixin, UpdateView): - model = Checkout - template_name = 'kfet/checkout_update.html' - form_class = CheckoutRestrictForm - success_message = 'Informations mises à jour pour la caisse : %(name)s' + model = Checkout + template_name = "kfet/checkout_update.html" + form_class = CheckoutRestrictForm + success_message = "Informations mises à jour pour la caisse : %(name)s" # Surcharge de la validation def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_checkout'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.change_checkout"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating return super().form_valid(form) + # ----- # Checkout Statement views # ----- # Checkout Statement - General + class CheckoutStatementList(ListView): - model = CheckoutStatement - queryset = CheckoutStatement.objects.order_by('-at') - template_name = 'kfet/checkoutstatement.html' - context_object_name= 'checkoutstatements' + model = CheckoutStatement + queryset = CheckoutStatement.objects.order_by("-at") + template_name = "kfet/checkoutstatement.html" + context_object_name = "checkoutstatements" + # Checkout Statement - Create + def getAmountTaken(data): - return Decimal(data.taken_001 * 0.01 + data.taken_002 * 0.02 - + data.taken_005 * 0.05 + data.taken_01 * 0.1 - + data.taken_02 * 0.2 + data.taken_05 * 0.5 - + data.taken_1 * 1 + data.taken_2 * 2 - + data.taken_5 * 5 + data.taken_10 * 10 - + data.taken_20 * 20 + data.taken_50 * 50 - + data.taken_100 * 100 + data.taken_200 * 200 - + data.taken_500 * 500 + float(data.taken_cheque)) + return Decimal( + data.taken_001 * 0.01 + + data.taken_002 * 0.02 + + data.taken_005 * 0.05 + + data.taken_01 * 0.1 + + data.taken_02 * 0.2 + + data.taken_05 * 0.5 + + data.taken_1 * 1 + + data.taken_2 * 2 + + data.taken_5 * 5 + + data.taken_10 * 10 + + data.taken_20 * 20 + + data.taken_50 * 50 + + data.taken_100 * 100 + + data.taken_200 * 200 + + data.taken_500 * 500 + + float(data.taken_cheque) + ) + def getAmountBalance(data): - return Decimal(data['balance_001'] * 0.01 + data['balance_002'] * 0.02 - + data['balance_005'] * 0.05 + data['balance_01'] * 0.1 - + data['balance_02'] * 0.2 + data['balance_05'] * 0.5 - + data['balance_1'] * 1 + data['balance_2'] * 2 - + data['balance_5'] * 5 + data['balance_10'] * 10 - + data['balance_20'] * 20 + data['balance_50'] * 50 - + data['balance_100'] * 100 + data['balance_200'] * 200 - + data['balance_500'] * 500) + return Decimal( + data["balance_001"] * 0.01 + + data["balance_002"] * 0.02 + + data["balance_005"] * 0.05 + + data["balance_01"] * 0.1 + + data["balance_02"] * 0.2 + + data["balance_05"] * 0.5 + + data["balance_1"] * 1 + + data["balance_2"] * 2 + + data["balance_5"] * 5 + + data["balance_10"] * 10 + + data["balance_20"] * 20 + + data["balance_50"] * 50 + + data["balance_100"] * 100 + + data["balance_200"] * 200 + + data["balance_500"] * 500 + ) + class CheckoutStatementCreate(SuccessMessageMixin, CreateView): - model = CheckoutStatement - template_name = 'kfet/checkoutstatement_create.html' - form_class = CheckoutStatementCreateForm - success_message = 'Nouveau relevé : %(checkout)s - %(at)s' + model = CheckoutStatement + template_name = "kfet/checkoutstatement_create.html" + form_class = CheckoutStatementCreateForm + success_message = "Nouveau relevé : %(checkout)s - %(at)s" def get_success_url(self): - return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']}) + return reverse_lazy( + "kfet.checkout.read", kwargs={"pk": self.kwargs["pk_checkout"]} + ) def get_success_message(self, cleaned_data): return self.success_message % dict( - cleaned_data, - checkout = self.object.checkout.name, - at = self.object.at) + cleaned_data, checkout=self.object.checkout.name, at=self.object.at + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout']) - context['checkout'] = checkout + checkout = Checkout.objects.get(pk=self.kwargs["pk_checkout"]) + context["checkout"] = checkout return context def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.add_checkoutstatement'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.add_checkoutstatement"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Creating form.instance.amount_taken = getAmountTaken(form.instance) if not form.instance.not_count: form.instance.balance_new = getAmountBalance(form.cleaned_data) - form.instance.checkout_id = self.kwargs['pk_checkout'] + form.instance.checkout_id = self.kwargs["pk_checkout"] form.instance.by = self.request.user.profile.account_kfet return super().form_valid(form) + class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): model = CheckoutStatement - template_name = 'kfet/checkoutstatement_update.html' + template_name = "kfet/checkoutstatement_update.html" form_class = CheckoutStatementUpdateForm - success_message = 'Relevé modifié' + success_message = "Relevé modifié" def get_success_url(self): - return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']}) + return reverse_lazy( + "kfet.checkout.read", kwargs={"pk": self.kwargs["pk_checkout"]} + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout']) - context['checkout'] = checkout + checkout = Checkout.objects.get(pk=self.kwargs["pk_checkout"]) + context["checkout"] = checkout return context def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_checkoutstatement'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.change_checkoutstatement"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating form.instance.amount_taken = getAmountTaken(form.instance) return super().form_valid(form) + # ----- # Category views # ----- @@ -585,31 +683,30 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): # Category - General class CategoryList(ListView): - queryset = (ArticleCategory.objects - .prefetch_related('articles') - .order_by('name')) - template_name = 'kfet/category.html' - context_object_name = 'categories' + queryset = ArticleCategory.objects.prefetch_related("articles").order_by("name") + template_name = "kfet/category.html" + context_object_name = "categories" # Category - Update class CategoryUpdate(SuccessMessageMixin, UpdateView): model = ArticleCategory - template_name = 'kfet/category_update.html' + template_name = "kfet/category_update.html" form_class = CategoryForm - success_url = reverse_lazy('kfet.category') + success_url = reverse_lazy("kfet.category") success_message = "Informations mises à jour pour la catégorie : %(name)s" # Surcharge de la validation def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_articlecategory'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.change_articlecategory"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating return super().form_valid(form) + # ----- # Article views # ----- @@ -618,69 +715,65 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView): # Article - General class ArticleList(ListView): queryset = ( - Article.objects - .select_related('category') + Article.objects.select_related("category") .prefetch_related( Prefetch( - 'inventories', - queryset=Inventory.objects.order_by('-at'), - to_attr='inventory', + "inventories", + queryset=Inventory.objects.order_by("-at"), + to_attr="inventory", ) ) - .order_by('category__name', '-is_sold', 'name') + .order_by("category__name", "-is_sold", "name") ) - template_name = 'kfet/article.html' - context_object_name = 'articles' + template_name = "kfet/article.html" + context_object_name = "articles" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) articles = context[self.context_object_name] - context['nb_articles'] = len(articles) + context["nb_articles"] = len(articles) context[self.context_object_name] = articles.filter(is_sold=True) - context['not_sold_articles'] = articles.filter(is_sold=False) + context["not_sold_articles"] = articles.filter(is_sold=False) return context # Article - Create class ArticleCreate(SuccessMessageMixin, CreateView): model = Article - template_name = 'kfet/article_create.html' + template_name = "kfet/article_create.html" form_class = ArticleForm - success_message = 'Nouvel item : %(category)s - %(name)s' + success_message = "Nouvel item : %(category)s - %(name)s" # Surcharge de la validation def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.add_article'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.add_article"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Save ici pour save le manytomany suppliers article = form.save() # Save des suppliers déjà existant - for supplier in form.cleaned_data['suppliers']: - SupplierArticle.objects.create( - article=article, supplier=supplier) + for supplier in form.cleaned_data["suppliers"]: + SupplierArticle.objects.create(article=article, supplier=supplier) # Nouveau supplier - supplier_new = form.cleaned_data['supplier_new'].strip() + supplier_new = form.cleaned_data["supplier_new"].strip() if supplier_new: - supplier, created = Supplier.objects.get_or_create( - name=supplier_new) + supplier, created = Supplier.objects.get_or_create(name=supplier_new) if created: - SupplierArticle.objects.create( - article=article, supplier=supplier) + SupplierArticle.objects.create(article=article, supplier=supplier) # Inventaire avec stock initial inventory = Inventory() inventory.by = self.request.user.profile.account_kfet inventory.save() InventoryArticle.objects.create( - inventory=inventory, - article=article, - stock_old=article.stock, - stock_new=article.stock, - ) + inventory=inventory, + article=article, + stock_old=article.stock, + stock_new=article.stock, + ) # Creating return super().form_valid(form) @@ -689,60 +782,60 @@ class ArticleCreate(SuccessMessageMixin, CreateView): # Article - Read class ArticleRead(DetailView): model = Article - template_name = 'kfet/article_read.html' - context_object_name = 'article' + template_name = "kfet/article_read.html" + context_object_name = "article" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - inventoryarts = (InventoryArticle.objects - .filter(article=self.object) - .select_related('inventory') - .order_by('-inventory__at')) - context['inventoryarts'] = inventoryarts - supplierarts = (SupplierArticle.objects - .filter(article=self.object) - .select_related('supplier') - .order_by('-at')) - context['supplierarts'] = supplierarts + inventoryarts = ( + InventoryArticle.objects.filter(article=self.object) + .select_related("inventory") + .order_by("-inventory__at") + ) + context["inventoryarts"] = inventoryarts + supplierarts = ( + SupplierArticle.objects.filter(article=self.object) + .select_related("supplier") + .order_by("-at") + ) + context["supplierarts"] = supplierarts return context # Article - Update class ArticleUpdate(SuccessMessageMixin, UpdateView): model = Article - template_name = 'kfet/article_update.html' + template_name = "kfet/article_update.html" form_class = ArticleRestrictForm success_message = "Informations mises à jour pour l'article : %(name)s" # Surcharge de la validation def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_article'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.change_article"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Save ici pour save le manytomany suppliers article = form.save() # Save des suppliers déjà existant - for supplier in form.cleaned_data['suppliers']: + for supplier in form.cleaned_data["suppliers"]: if supplier not in article.suppliers.all(): - SupplierArticle.objects.create( - article=article, supplier=supplier) + SupplierArticle.objects.create(article=article, supplier=supplier) # On vire les suppliers désélectionnés for supplier in article.suppliers.all(): - if supplier not in form.cleaned_data['suppliers']: + if supplier not in form.cleaned_data["suppliers"]: SupplierArticle.objects.filter( - article=article, supplier=supplier).delete() + article=article, supplier=supplier + ).delete() # Nouveau supplier - supplier_new = form.cleaned_data['supplier_new'].strip() + supplier_new = form.cleaned_data["supplier_new"].strip() if supplier_new: - supplier, created = Supplier.objects.get_or_create( - name=supplier_new) + supplier, created = Supplier.objects.get_or_create(name=supplier_new) if created: - SupplierArticle.objects.create( - article=article, supplier=supplier) + SupplierArticle.objects.create(article=article, supplier=supplier) # Updating return super().form_valid(form) @@ -752,66 +845,82 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): # K-Psul # ----- + @teamkfet_required def kpsul(request): data = {} - data['operationgroup_form'] = KPsulOperationGroupForm() - data['trigramme_form'] = KPsulAccountForm() - data['checkout_form'] = KPsulCheckoutForm() - data['operation_formset'] = KPsulOperationFormSet( - queryset=Operation.objects.none(), - ) - return render(request, 'kfet/kpsul.html', data) + data["operationgroup_form"] = KPsulOperationGroupForm() + data["trigramme_form"] = KPsulAccountForm() + data["checkout_form"] = KPsulCheckoutForm() + data["operation_formset"] = KPsulOperationFormSet(queryset=Operation.objects.none()) + return render(request, "kfet/kpsul.html", data) @teamkfet_required def kpsul_get_settings(request): addcost_for = kfet_config.addcost_for data = { - 'subvention_cof': kfet_config.subvention_cof, - 'addcost_for': addcost_for and addcost_for.trigramme or '', - 'addcost_amount': kfet_config.addcost_amount, + "subvention_cof": kfet_config.subvention_cof, + "addcost_for": addcost_for and addcost_for.trigramme or "", + "addcost_amount": kfet_config.addcost_amount, } return JsonResponse(data) @teamkfet_required def account_read_json(request): - trigramme = request.POST.get('trigramme', '') - account = get_object_or_404(Account, trigramme=trigramme) - data = { 'id': account.pk, 'name': account.name, 'email': account.email, - 'is_cof': account.is_cof, 'promo': account.promo, - 'balance': account.balance, 'is_frozen': account.is_frozen, - 'departement': account.departement, 'nickname': account.nickname, - 'trigramme': account.trigramme } + trigramme = request.POST.get("trigramme", "") + account = get_object_or_404(Account, trigramme=trigramme) + data = { + "id": account.pk, + "name": account.name, + "email": account.email, + "is_cof": account.is_cof, + "promo": account.promo, + "balance": account.balance, + "is_frozen": account.is_frozen, + "departement": account.departement, + "nickname": account.nickname, + "trigramme": account.trigramme, + } return JsonResponse(data) @teamkfet_required def kpsul_checkout_data(request): - pk = request.POST.get('pk', 0) + pk = request.POST.get("pk", 0) if not pk: pk = 0 data = ( - Checkout.objects - .annotate( - last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'), - last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'), - last_statement_by_trigramme=F('statements__by__trigramme'), - last_statement_balance=F('statements__balance_new'), - last_statement_at=F('statements__at')) + Checkout.objects.annotate( + last_statement_by_first_name=F( + "statements__by__cofprofile__user__first_name" + ), + last_statement_by_last_name=F( + "statements__by__cofprofile__user__last_name" + ), + last_statement_by_trigramme=F("statements__by__trigramme"), + last_statement_balance=F("statements__balance_new"), + last_statement_at=F("statements__at"), + ) .select_related( - 'statements' - 'statements__by', - 'statements__by__cofprofile__user') + "statements" "statements__by", "statements__by__cofprofile__user" + ) .filter(pk=pk) - .order_by('statements__at') + .order_by("statements__at") .values( - 'id', 'name', 'balance', 'valid_from', 'valid_to', - 'last_statement_balance', 'last_statement_at', - 'last_statement_by_trigramme', 'last_statement_by_last_name', - 'last_statement_by_first_name') + "id", + "name", + "balance", + "valid_from", + "valid_to", + "last_statement_balance", + "last_statement_at", + "last_statement_by_trigramme", + "last_statement_by_last_name", + "last_statement_by_first_name", + ) .last() ) if data is None: @@ -824,43 +933,34 @@ def kpsul_update_addcost(request): addcost_form = AddcostForm(request.POST) if not addcost_form.is_valid(): - data = {'errors': {'addcost': list(addcost_form.errors)}} + data = {"errors": {"addcost": list(addcost_form.errors)}} return JsonResponse(data, status=400) - required_perms = ['kfet.manage_addcosts'] + required_perms = ["kfet.manage_addcosts"] if not request.user.has_perms(required_perms): data = { - 'errors': { - 'missing_perms': get_missing_perms(required_perms, - request.user) - } + "errors": {"missing_perms": get_missing_perms(required_perms, request.user)} } return JsonResponse(data, status=403) - trigramme = addcost_form.cleaned_data['trigramme'] + trigramme = addcost_form.cleaned_data["trigramme"] account = trigramme and Account.objects.get(trigramme=trigramme) or None - amount = addcost_form.cleaned_data['amount'] + amount = addcost_form.cleaned_data["amount"] - kfet_config.set(addcost_for=account, - addcost_amount=amount) + kfet_config.set(addcost_for=account, addcost_amount=amount) - data = { - 'addcost': { - 'for': account and account.trigramme or None, - 'amount': amount, - } - } - consumers.KPsul.group_send('kfet.kpsul', data) + data = {"addcost": {"for": account and account.trigramme or None, "amount": amount}} + consumers.KPsul.group_send("kfet.kpsul", data) return JsonResponse(data) def get_missing_perms(required_perms, user): - missing_perms_codenames = [(perm.split('.'))[1] - for perm in required_perms - if not user.has_perm(perm)] + missing_perms_codenames = [ + (perm.split("."))[1] for perm in required_perms if not user.has_perm(perm) + ] missing_perms = list( - Permission.objects - .filter(codename__in=missing_perms_codenames) - .values_list('name', flat=True) + Permission.objects.filter(codename__in=missing_perms_codenames).values_list( + "name", flat=True + ) ) return missing_perms @@ -868,21 +968,20 @@ def get_missing_perms(required_perms, user): @teamkfet_required def kpsul_perform_operations(request): # Initializing response data - data = {'operationgroup': 0, 'operations': [], - 'warnings': {}, 'errors': {}} + data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}} # Checking operationgroup operationgroup_form = KPsulOperationGroupForm(request.POST) if not operationgroup_form.is_valid(): - data['errors']['operation_group'] = list(operationgroup_form.errors) + data["errors"]["operation_group"] = list(operationgroup_form.errors) # Checking operation_formset operation_formset = KPsulOperationFormSet(request.POST) if not operation_formset.is_valid(): - data['errors']['operations'] = list(operation_formset.errors) + data["errors"]["operations"] = list(operation_formset.errors) # Returning BAD REQUEST if errors - if data['errors']: + if data["errors"]: return JsonResponse(data, status=400) # Pre-saving (no commit) @@ -901,19 +1000,19 @@ def kpsul_perform_operations(request): to_addcost_for_balance = 0 # For balance of addcost_for to_checkout_balance = 0 # For balance of selected checkout to_articles_stocks = defaultdict(lambda: 0) # For stocks articles - is_addcost = all((addcost_for, addcost_amount, - addcost_for != operationgroup.on_acc)) + is_addcost = all( + (addcost_for, addcost_amount, addcost_for != operationgroup.on_acc) + ) need_comment = operationgroup.on_acc.need_comment # Filling data of each operations # + operationgroup + calculating other stuffs for operation in operations: if operation.type == Operation.PURCHASE: - operation.amount = - operation.article.price * operation.article_nb + operation.amount = -operation.article.price * operation.article_nb if is_addcost & operation.article.category.has_addcost: operation.addcost_for = addcost_for - operation.addcost_amount = addcost_amount \ - * operation.article_nb + operation.addcost_amount = addcost_amount * operation.article_nb operation.amount -= operation.addcost_amount to_addcost_for_balance += operation.addcost_amount if operationgroup.on_acc.is_cash: @@ -925,38 +1024,37 @@ def kpsul_perform_operations(request): to_articles_stocks[operation.article] -= operation.article_nb else: if operationgroup.on_acc.is_cash: - data['errors']['account'] = 'LIQ' + data["errors"]["account"] = "LIQ" if operation.type != Operation.EDIT: to_checkout_balance += operation.amount operationgroup.amount += operation.amount if operation.type == Operation.DEPOSIT: - required_perms.add('kfet.perform_deposit') + required_perms.add("kfet.perform_deposit") if operation.type == Operation.EDIT: - required_perms.add('kfet.edit_balance_account') + required_perms.add("kfet.edit_balance_account") need_comment = True if operationgroup.on_acc.is_cof: to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor - (perms, stop) = (operationgroup.on_acc - .perms_to_perform_operation( - amount=operationgroup.amount) - ) + (perms, stop) = operationgroup.on_acc.perms_to_perform_operation( + amount=operationgroup.amount + ) required_perms |= perms if need_comment: operationgroup.comment = operationgroup.comment.strip() if not operationgroup.comment: - data['errors']['need_comment'] = True + data["errors"]["need_comment"] = True - if data['errors']: + if data["errors"]: return JsonResponse(data, status=400) if stop or not request.user.has_perms(required_perms): missing_perms = get_missing_perms(required_perms, request.user) if missing_perms: - data['errors']['missing_perms'] = missing_perms + data["errors"]["missing_perms"] = missing_perms if stop: - data['errors']['negative'] = [operationgroup.on_acc.trigramme] + data["errors"]["negative"] = [operationgroup.on_acc.trigramme] return JsonResponse(data, status=403) # If 1 perm is required, filling who perform the operations @@ -972,9 +1070,9 @@ def kpsul_perform_operations(request): on_acc = operationgroup.on_acc if not on_acc.is_cash: ( - Account.objects - .filter(pk=on_acc.pk) - .update(balance=F('balance') + operationgroup.amount) + Account.objects.filter(pk=on_acc.pk).update( + balance=F("balance") + operationgroup.amount + ) ) on_acc.refresh_from_db() on_acc.update_negative() @@ -982,106 +1080,117 @@ def kpsul_perform_operations(request): # Updating checkout's balance if to_checkout_balance: Checkout.objects.filter(pk=operationgroup.checkout.pk).update( - balance=F('balance') + to_checkout_balance) + balance=F("balance") + to_checkout_balance + ) # Saving addcost_for with new balance if there is one if is_addcost and to_addcost_for_balance: Account.objects.filter(pk=addcost_for.pk).update( - balance=F('balance') + to_addcost_for_balance) + balance=F("balance") + to_addcost_for_balance + ) # Saving operation group operationgroup.save() - data['operationgroup'] = operationgroup.pk + data["operationgroup"] = operationgroup.pk # Filling operationgroup id for each operations and saving for operation in operations: operation.group = operationgroup operation.save() - data['operations'].append(operation.pk) + data["operations"].append(operation.pk) # Updating articles stock for article in to_articles_stocks: Article.objects.filter(pk=article.pk).update( - stock=F('stock') + to_articles_stocks[article]) + stock=F("stock") + to_articles_stocks[article] + ) # Websocket data websocket_data = {} - websocket_data['opegroups'] = [{ - 'add': True, - 'id': operationgroup.pk, - 'amount': operationgroup.amount, - 'checkout__name': operationgroup.checkout.name, - 'at': operationgroup.at, - 'is_cof': operationgroup.is_cof, - 'comment': operationgroup.comment, - 'valid_by__trigramme': (operationgroup.valid_by and - operationgroup.valid_by.trigramme or None), - 'on_acc__trigramme': operationgroup.on_acc.trigramme, - 'opes': [], - }] + websocket_data["opegroups"] = [ + { + "add": True, + "id": operationgroup.pk, + "amount": operationgroup.amount, + "checkout__name": operationgroup.checkout.name, + "at": operationgroup.at, + "is_cof": operationgroup.is_cof, + "comment": operationgroup.comment, + "valid_by__trigramme": ( + operationgroup.valid_by and operationgroup.valid_by.trigramme or None + ), + "on_acc__trigramme": operationgroup.on_acc.trigramme, + "opes": [], + } + ] for operation in operations: ope_data = { - 'id': operation.pk, 'type': operation.type, - 'amount': operation.amount, - 'addcost_amount': operation.addcost_amount, - 'addcost_for__trigramme': ( - operation.addcost_for and addcost_for.trigramme or None), - 'article__name': ( - operation.article and operation.article.name or None), - 'article_nb': operation.article_nb, - 'group_id': operationgroup.pk, - 'canceled_by__trigramme': None, 'canceled_at': None, + "id": operation.pk, + "type": operation.type, + "amount": operation.amount, + "addcost_amount": operation.addcost_amount, + "addcost_for__trigramme": ( + operation.addcost_for and addcost_for.trigramme or None + ), + "article__name": (operation.article and operation.article.name or None), + "article_nb": operation.article_nb, + "group_id": operationgroup.pk, + "canceled_by__trigramme": None, + "canceled_at": None, } - websocket_data['opegroups'][0]['opes'].append(ope_data) + websocket_data["opegroups"][0]["opes"].append(ope_data) # Need refresh from db cause we used update on queryset operationgroup.checkout.refresh_from_db() - websocket_data['checkouts'] = [{ - 'id': operationgroup.checkout.pk, - 'balance': operationgroup.checkout.balance, - }] - websocket_data['articles'] = [] + websocket_data["checkouts"] = [ + {"id": operationgroup.checkout.pk, "balance": operationgroup.checkout.balance} + ] + websocket_data["articles"] = [] # Need refresh from db cause we used update on querysets articles_pk = [article.pk for article in to_articles_stocks] - articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk) + articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) for article in articles: - websocket_data['articles'].append({ - 'id': article['id'], - 'stock': article['stock'] - }) - consumers.KPsul.group_send('kfet.kpsul', websocket_data) + websocket_data["articles"].append( + {"id": article["id"], "stock": article["stock"]} + ) + consumers.KPsul.group_send("kfet.kpsul", websocket_data) return JsonResponse(data) @teamkfet_required def kpsul_cancel_operations(request): # Pour la réponse - data = { 'canceled': [], 'warnings': {}, 'errors': {}} + data = {"canceled": [], "warnings": {}, "errors": {}} # Checking if BAD REQUEST (opes_pk not int or not existing) try: # Set pour virer les doublons - opes_post = set(map(int, filter(None, request.POST.getlist('operations[]', [])))) + opes_post = set( + map(int, filter(None, request.POST.getlist("operations[]", []))) + ) except ValueError: return JsonResponse(data, status=400) - opes_all = ( - Operation.objects - .select_related('group', 'group__on_acc', 'group__on_acc__negative') - .filter(pk__in=opes_post)) - opes_pk = [ ope.pk for ope in opes_all ] - opes_notexisting = [ ope for ope in opes_post if ope not in opes_pk ] + opes_all = Operation.objects.select_related( + "group", "group__on_acc", "group__on_acc__negative" + ).filter(pk__in=opes_post) + opes_pk = [ope.pk for ope in opes_all] + opes_notexisting = [ope for ope in opes_post if ope not in opes_pk] if opes_notexisting: - data['errors']['opes_notexisting'] = opes_notexisting + data["errors"]["opes_notexisting"] = opes_notexisting return JsonResponse(data, status=400) - opes_already_canceled = [] # Déjà annulée - opes = [] # Pas déjà annulée + opes_already_canceled = [] # Déjà annulée + opes = [] # Pas déjà annulée required_perms = set() - stop_all = False + stop_all = False cancel_duration = kfet_config.cancel_duration - to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes - to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé - to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses - to_articles_stocks = defaultdict(lambda:0) # ------ sur les stocks d'articles + to_accounts_balances = defaultdict( + lambda: 0 + ) # Modifs à faire sur les balances des comptes + to_groups_amounts = defaultdict( + lambda: 0 + ) # ------ sur les montants des groupes d'opé + to_checkouts_balances = defaultdict(lambda: 0) # ------ sur les balances de caisses + to_articles_stocks = defaultdict(lambda: 0) # ------ sur les stocks d'articles for ope in opes_all: if ope.canceled_at: # Opération déjà annulée, va pour un warning en Response @@ -1090,7 +1199,7 @@ def kpsul_cancel_operations(request): opes.append(ope.pk) # Si opé il y a plus de CANCEL_DURATION, permission requise if ope.group.at + cancel_duration < timezone.now(): - required_perms.add('kfet.cancel_old_operations') + required_perms.add("kfet.cancel_old_operations") # Calcul de toutes modifs à faire en cas de validation @@ -1112,14 +1221,15 @@ def kpsul_cancel_operations(request): # par `.save()`, amount_error est recalculé automatiquement, # ce qui n'est pas le cas en faisant un update sur queryset # TODO ? : Maj les balance_old de relevés pour modifier l'erreur - last_statement = (CheckoutStatement.objects - .filter(checkout=ope.group.checkout) - .order_by('at') - .last()) + last_statement = ( + CheckoutStatement.objects.filter(checkout=ope.group.checkout) + .order_by("at") + .last() + ) if not last_statement or last_statement.at < ope.group.at: if ope.is_checkout: if ope.group.on_acc.is_cash: - to_checkouts_balances[ope.group.checkout] -= - ope.amount + to_checkouts_balances[ope.group.checkout] -= -ope.amount else: to_checkouts_balances[ope.group.checkout] -= ope.amount @@ -1131,23 +1241,25 @@ def kpsul_cancel_operations(request): # Note : si InventoryArticle est maj par .save(), stock_error # est recalculé automatiquement if ope.article and ope.article_nb: - last_stock = (InventoryArticle.objects - .select_related('inventory') + last_stock = ( + InventoryArticle.objects.select_related("inventory") .filter(article=ope.article) - .order_by('inventory__at') - .last()) + .order_by("inventory__at") + .last() + ) if not last_stock or last_stock.inventory.at < ope.group.at: to_articles_stocks[ope.article] += ope.article_nb if not opes: - data['warnings']['already_canceled'] = opes_already_canceled + data["warnings"]["already_canceled"] = opes_already_canceled return JsonResponse(data) negative_accounts = [] # Checking permissions or stop for account in to_accounts_balances: (perms, stop) = account.perms_to_perform_operation( - amount = to_accounts_balances[account]) + amount=to_accounts_balances[account] + ) required_perms |= perms stop_all = stop_all or stop if stop: @@ -1156,22 +1268,25 @@ def kpsul_cancel_operations(request): if stop_all or not request.user.has_perms(required_perms): missing_perms = get_missing_perms(required_perms, request.user) if missing_perms: - data['errors']['missing_perms'] = missing_perms + data["errors"]["missing_perms"] = missing_perms if stop_all: - data['errors']['negative'] = negative_accounts + data["errors"]["negative"] = negative_accounts return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None canceled_at = timezone.now() with transaction.atomic(): - (Operation.objects.filter(pk__in=opes) - .update(canceled_by=canceled_by, canceled_at=canceled_at)) + ( + Operation.objects.filter(pk__in=opes).update( + canceled_by=canceled_by, canceled_at=canceled_at + ) + ) for account in to_accounts_balances: ( - Account.objects - .filter(pk=account.pk) - .update(balance=F('balance') + to_accounts_balances[account]) + Account.objects.filter(pk=account.pk).update( + balance=F("balance") + to_accounts_balances[account] + ) ) if not account.is_cash: # Should always be true, but we want to be sure @@ -1179,76 +1294,86 @@ def kpsul_cancel_operations(request): account.update_negative() for checkout in to_checkouts_balances: Checkout.objects.filter(pk=checkout.pk).update( - balance = F('balance') + to_checkouts_balances[checkout]) + balance=F("balance") + to_checkouts_balances[checkout] + ) for group in to_groups_amounts: OperationGroup.objects.filter(pk=group.pk).update( - amount = F('amount') + to_groups_amounts[group]) + amount=F("amount") + to_groups_amounts[group] + ) for article in to_articles_stocks: Article.objects.filter(pk=article.pk).update( - stock = F('stock') + to_articles_stocks[article]) + stock=F("stock") + to_articles_stocks[article] + ) # Websocket data - websocket_data = { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] } + websocket_data = {"opegroups": [], "opes": [], "checkouts": [], "articles": []} # Need refresh from db cause we used update on querysets - opegroups_pk = [ opegroup.pk for opegroup in to_groups_amounts ] - opegroups = (OperationGroup.objects - .values('id','amount','is_cof').filter(pk__in=opegroups_pk)) + opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts] + opegroups = OperationGroup.objects.values("id", "amount", "is_cof").filter( + pk__in=opegroups_pk + ) for opegroup in opegroups: - websocket_data['opegroups'].append({ - 'cancellation': True, - 'id': opegroup['id'], - 'amount': opegroup['amount'], - 'is_cof': opegroup['is_cof'], - }) + websocket_data["opegroups"].append( + { + "cancellation": True, + "id": opegroup["id"], + "amount": opegroup["amount"], + "is_cof": opegroup["is_cof"], + } + ) canceled_by__trigramme = canceled_by and canceled_by.trigramme or None for ope in opes: - websocket_data['opes'].append({ - 'cancellation': True, - 'id': ope, - 'canceled_by__trigramme': canceled_by__trigramme, - 'canceled_at': canceled_at, - }) + websocket_data["opes"].append( + { + "cancellation": True, + "id": ope, + "canceled_by__trigramme": canceled_by__trigramme, + "canceled_at": canceled_at, + } + ) # Need refresh from db cause we used update on querysets - checkouts_pk = [ checkout.pk for checkout in to_checkouts_balances] - checkouts = (Checkout.objects - .values('id', 'balance').filter(pk__in=checkouts_pk)) + checkouts_pk = [checkout.pk for checkout in to_checkouts_balances] + checkouts = Checkout.objects.values("id", "balance").filter(pk__in=checkouts_pk) for checkout in checkouts: - websocket_data['checkouts'].append({ - 'id': checkout['id'], - 'balance': checkout['balance']}) + websocket_data["checkouts"].append( + {"id": checkout["id"], "balance": checkout["balance"]} + ) # Need refresh from db cause we used update on querysets - articles_pk = [ article.pk for articles in to_articles_stocks] - articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk) + articles_pk = [article.pk for articles in to_articles_stocks] + articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) for article in articles: - websocket_data['articles'].append({ - 'id': article['id'], - 'stock': article['stock']}) - consumers.KPsul.group_send('kfet.kpsul', websocket_data) + websocket_data["articles"].append( + {"id": article["id"], "stock": article["stock"]} + ) + consumers.KPsul.group_send("kfet.kpsul", websocket_data) - data['canceled'] = opes + data["canceled"] = opes if opes_already_canceled: - data['warnings']['already_canceled'] = opes_already_canceled + data["warnings"]["already_canceled"] = opes_already_canceled return JsonResponse(data) + @login_required def history_json(request): # Récupération des paramètres - from_date = request.POST.get('from', None) - to_date = request.POST.get('to', None) - limit = request.POST.get('limit', None); - checkouts = request.POST.getlist('checkouts[]', None) - accounts = request.POST.getlist('accounts[]', None) + from_date = request.POST.get("from", None) + to_date = request.POST.get("to", None) + limit = request.POST.get("limit", None) + checkouts = request.POST.getlist("checkouts[]", None) + accounts = request.POST.getlist("accounts[]", None) # Construction de la requête (sur les opérations) pour le prefetch queryset_prefetch = Operation.objects.select_related( - 'article', 'canceled_by', 'addcost_for') + "article", "canceled_by", "addcost_for" + ) # Construction de la requête principale opegroups = ( - OperationGroup.objects - .prefetch_related(Prefetch('opes', queryset=queryset_prefetch)) - .select_related('on_acc', 'valid_by') - .order_by('at') + OperationGroup.objects.prefetch_related( + Prefetch("opes", queryset=queryset_prefetch) + ) + .select_related("on_acc", "valid_by") + .order_by("at") ) # Application des filtres if from_date: @@ -1260,65 +1385,68 @@ def history_json(request): if accounts: opegroups = opegroups.filter(on_acc_id__in=accounts) # Un non-membre de l'équipe n'a que accès à son historique - if not request.user.has_perm('kfet.is_team'): + if not request.user.has_perm("kfet.is_team"): opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet) if limit: opegroups = opegroups[:limit] - # Construction de la réponse opegroups_list = [] for opegroup in opegroups: opegroup_dict = { - 'id' : opegroup.id, - 'amount' : opegroup.amount, - 'at' : opegroup.at, - 'checkout_id': opegroup.checkout_id, - 'is_cof' : opegroup.is_cof, - 'comment' : opegroup.comment, - 'opes' : [], - 'on_acc__trigramme': - opegroup.on_acc and opegroup.on_acc.trigramme or None, + "id": opegroup.id, + "amount": opegroup.amount, + "at": opegroup.at, + "checkout_id": opegroup.checkout_id, + "is_cof": opegroup.is_cof, + "comment": opegroup.comment, + "opes": [], + "on_acc__trigramme": opegroup.on_acc and opegroup.on_acc.trigramme or None, } - if request.user.has_perm('kfet.is_team'): - opegroup_dict['valid_by__trigramme'] = ( - opegroup.valid_by and opegroup.valid_by.trigramme or None) + if request.user.has_perm("kfet.is_team"): + opegroup_dict["valid_by__trigramme"] = ( + opegroup.valid_by and opegroup.valid_by.trigramme or None + ) for ope in opegroup.opes.all(): ope_dict = { - 'id' : ope.id, - 'type' : ope.type, - 'amount' : ope.amount, - 'article_nb' : ope.article_nb, - 'addcost_amount': ope.addcost_amount, - 'canceled_at' : ope.canceled_at, - 'article__name': - ope.article and ope.article.name or None, - 'addcost_for__trigramme': - ope.addcost_for and ope.addcost_for.trigramme or None, + "id": ope.id, + "type": ope.type, + "amount": ope.amount, + "article_nb": ope.article_nb, + "addcost_amount": ope.addcost_amount, + "canceled_at": ope.canceled_at, + "article__name": ope.article and ope.article.name or None, + "addcost_for__trigramme": ope.addcost_for + and ope.addcost_for.trigramme + or None, } - if request.user.has_perm('kfet.is_team'): - ope_dict['canceled_by__trigramme'] = ( - ope.canceled_by and ope.canceled_by.trigramme or None) - opegroup_dict['opes'].append(ope_dict) + if request.user.has_perm("kfet.is_team"): + ope_dict["canceled_by__trigramme"] = ( + ope.canceled_by and ope.canceled_by.trigramme or None + ) + opegroup_dict["opes"].append(ope_dict) opegroups_list.append(opegroup_dict) - return JsonResponse({ 'opegroups': opegroups_list }) + return JsonResponse({"opegroups": opegroups_list}) + @teamkfet_required def kpsul_articles_data(request): - articles = ( - Article.objects - .values('id', 'name', 'price', 'stock', 'category_id', - 'category__name', 'category__has_addcost') - .filter(is_sold=True)) - return JsonResponse({ 'articles': list(articles) }) + articles = Article.objects.values( + "id", + "name", + "price", + "stock", + "category_id", + "category__name", + "category__has_addcost", + ).filter(is_sold=True) + return JsonResponse({"articles": list(articles)}) @teamkfet_required def history(request): - data = { - 'filter_form': FilterHistoryForm(), - } - return render(request, 'kfet/history.html', data) + data = {"filter_form": FilterHistoryForm()} + return render(request, "kfet/history.html", data) # ----- @@ -1327,77 +1455,73 @@ def history(request): class SettingsList(TemplateView): - template_name = 'kfet/settings.html' + template_name = "kfet/settings.html" -config_list = permission_required('kfet.see_config')(SettingsList.as_view()) +config_list = permission_required("kfet.see_config")(SettingsList.as_view()) class SettingsUpdate(SuccessMessageMixin, FormView): form_class = KFetConfigForm - template_name = 'kfet/settings_update.html' - success_message = 'Paramètres mis à jour' - success_url = reverse_lazy('kfet.settings') + template_name = "kfet/settings_update.html" + success_message = "Paramètres mis à jour" + success_url = reverse_lazy("kfet.settings") def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_config'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.change_config"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) form.save() return super().form_valid(form) -config_update = ( - permission_required('kfet.change_config')(SettingsUpdate.as_view()) -) +config_update = permission_required("kfet.change_config")(SettingsUpdate.as_view()) # ----- # Transfer views # ----- + @teamkfet_required def transfers(request): transfers_pre = Prefetch( - 'transfers', - queryset=( - Transfer.objects - .select_related('from_acc', 'to_acc') - ), + "transfers", queryset=(Transfer.objects.select_related("from_acc", "to_acc")) ) transfergroups = ( - TransferGroup.objects - .select_related('valid_by') + TransferGroup.objects.select_related("valid_by") .prefetch_related(transfers_pre) - .order_by('-at') + .order_by("-at") ) - return render(request, 'kfet/transfers.html', { - 'transfergroups': transfergroups, - }) + return render(request, "kfet/transfers.html", {"transfergroups": transfergroups}) @teamkfet_required def transfers_create(request): transfer_formset = TransferFormSet(queryset=Transfer.objects.none()) - return render(request, 'kfet/transfers_create.html', - { 'transfer_formset': transfer_formset }) + return render( + request, "kfet/transfers_create.html", {"transfer_formset": transfer_formset} + ) + @teamkfet_required def perform_transfers(request): - data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 } + data = {"errors": {}, "transfers": [], "transfergroup": 0} # Checking transfer_formset transfer_formset = TransferFormSet(request.POST) if not transfer_formset.is_valid(): - return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400) + return JsonResponse({"errors": list(transfer_formset.errors)}, status=400) - transfers = transfer_formset.save(commit = False) + transfers = transfer_formset.save(commit=False) # Initializing vars - required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers - to_accounts_balances = defaultdict(lambda:0) # For balances of accounts + required_perms = set( + ["kfet.add_transfer"] + ) # Required perms to perform all transfers + to_accounts_balances = defaultdict(lambda: 0) # For balances of accounts for transfer in transfers: to_accounts_balances[transfer.from_acc] -= transfer.amount @@ -1409,7 +1533,8 @@ def perform_transfers(request): # Checking if ok on all accounts for account in to_accounts_balances: (perms, stop) = account.perms_to_perform_operation( - amount = to_accounts_balances[account]) + amount=to_accounts_balances[account] + ) required_perms |= perms stop_all = stop_all or stop if stop: @@ -1418,9 +1543,9 @@ def perform_transfers(request): if stop_all or not request.user.has_perms(required_perms): missing_perms = get_missing_perms(required_perms, request.user) if missing_perms: - data['errors']['missing_perms'] = missing_perms + data["errors"]["missing_perms"] = missing_perms if stop_all: - data['errors']['negative'] = negative_accounts + data["errors"]["negative"] = negative_accounts return JsonResponse(data, status=403) # Creating transfer group @@ -1428,69 +1553,72 @@ def perform_transfers(request): if required_perms: transfergroup.valid_by = request.user.profile.account_kfet - comment = request.POST.get('comment', '') + comment = request.POST.get("comment", "") transfergroup.comment = comment.strip() with transaction.atomic(): # Updating balances accounts for account in to_accounts_balances: Account.objects.filter(pk=account.pk).update( - balance = F('balance') + to_accounts_balances[account]) + balance=F("balance") + to_accounts_balances[account] + ) account.refresh_from_db() if account.balance < 0: - if hasattr(account, 'negative'): + if hasattr(account, "negative"): if not account.negative.start: account.negative.start = timezone.now() account.negative.save() else: - negative = AccountNegative( - account = account, start = timezone.now()) + negative = AccountNegative(account=account, start=timezone.now()) negative.save() - elif (hasattr(account, 'negative') - and not account.negative.balance_offset): + elif hasattr(account, "negative") and not account.negative.balance_offset: account.negative.delete() # Saving transfer group transfergroup.save() - data['transfergroup'] = transfergroup.pk + data["transfergroup"] = transfergroup.pk # Saving all transfers with group for transfer in transfers: transfer.group = transfergroup transfer.save() - data['transfers'].append(transfer.pk) + data["transfers"].append(transfer.pk) return JsonResponse(data) + @teamkfet_required def cancel_transfers(request): # Pour la réponse - data = { 'canceled': [], 'warnings': {}, 'errors': {}} + data = {"canceled": [], "warnings": {}, "errors": {}} # Checking if BAD REQUEST (transfers_pk not int or not existing) try: # Set pour virer les doublons - transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', [])))) + transfers_post = set( + map(int, filter(None, request.POST.getlist("transfers[]", []))) + ) except ValueError: return JsonResponse(data, status=400) - transfers_all = ( - Transfer.objects - .select_related('group', 'from_acc', 'from_acc__negative', - 'to_acc', 'to_acc__negative') - .filter(pk__in=transfers_post)) - transfers_pk = [ transfer.pk for transfer in transfers_all ] - transfers_notexisting = [ transfer for transfer in transfers_post - if transfer not in transfers_pk ] + transfers_all = Transfer.objects.select_related( + "group", "from_acc", "from_acc__negative", "to_acc", "to_acc__negative" + ).filter(pk__in=transfers_post) + transfers_pk = [transfer.pk for transfer in transfers_all] + transfers_notexisting = [ + transfer for transfer in transfers_post if transfer not in transfers_pk + ] if transfers_notexisting: - data['errors']['transfers_notexisting'] = transfers_notexisting + data["errors"]["transfers_notexisting"] = transfers_notexisting return JsonResponse(data, status=400) - transfers_already_canceled = [] # Déjà annulée - transfers = [] # Pas déjà annulée + transfers_already_canceled = [] # Déjà annulée + transfers = [] # Pas déjà annulée required_perms = set() - stop_all = False + stop_all = False cancel_duration = kfet_config.cancel_duration - to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes + to_accounts_balances = defaultdict( + lambda: 0 + ) # Modifs à faire sur les balances des comptes for transfer in transfers_all: if transfer.canceled_at: # Transfert déjà annulé, va pour un warning en Response @@ -1499,7 +1627,7 @@ def cancel_transfers(request): transfers.append(transfer.pk) # Si transfer il y a plus de CANCEL_DURATION, permission requise if transfer.group.at + cancel_duration < timezone.now(): - required_perms.add('kfet.cancel_old_operations') + required_perms.add("kfet.cancel_old_operations") # Calcul de toutes modifs à faire en cas de validation @@ -1508,14 +1636,15 @@ def cancel_transfers(request): to_accounts_balances[transfer.to_acc] += -transfer.amount if not transfers: - data['warnings']['already_canceled'] = transfers_already_canceled + data["warnings"]["already_canceled"] = transfers_already_canceled return JsonResponse(data) negative_accounts = [] # Checking permissions or stop for account in to_accounts_balances: (perms, stop) = account.perms_to_perform_operation( - amount = to_accounts_balances[account]) + amount=to_accounts_balances[account] + ) required_perms |= perms stop_all = stop_all or stop if stop: @@ -1524,76 +1653,79 @@ def cancel_transfers(request): if stop_all or not request.user.has_perms(required_perms): missing_perms = get_missing_perms(required_perms, request.user) if missing_perms: - data['errors']['missing_perms'] = missing_perms + data["errors"]["missing_perms"] = missing_perms if stop_all: - data['errors']['negative'] = negative_accounts + data["errors"]["negative"] = negative_accounts return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None canceled_at = timezone.now() with transaction.atomic(): - (Transfer.objects.filter(pk__in=transfers) - .update(canceled_by=canceled_by, canceled_at=canceled_at)) + ( + Transfer.objects.filter(pk__in=transfers).update( + canceled_by=canceled_by, canceled_at=canceled_at + ) + ) for account in to_accounts_balances: Account.objects.filter(pk=account.pk).update( - balance = F('balance') + to_accounts_balances[account]) + balance=F("balance") + to_accounts_balances[account] + ) account.refresh_from_db() if account.balance < 0: - if hasattr(account, 'negative'): + if hasattr(account, "negative"): if not account.negative.start: account.negative.start = timezone.now() account.negative.save() else: - negative = AccountNegative( - account = account, start = timezone.now()) + negative = AccountNegative(account=account, start=timezone.now()) negative.save() - elif (hasattr(account, 'negative') - and not account.negative.balance_offset): + elif hasattr(account, "negative") and not account.negative.balance_offset: account.negative.delete() - data['canceled'] = transfers + data["canceled"] = transfers if transfers_already_canceled: - data['warnings']['already_canceled'] = transfers_already_canceled + data["warnings"]["already_canceled"] = transfers_already_canceled return JsonResponse(data) + class InventoryList(ListView): - queryset = (Inventory.objects - .select_related('by', 'order') - .annotate(nb_articles=Count('articles')) - .order_by('-at')) - template_name = 'kfet/inventory.html' - context_object_name = 'inventories' + queryset = ( + Inventory.objects.select_related("by", "order") + .annotate(nb_articles=Count("articles")) + .order_by("-at") + ) + template_name = "kfet/inventory.html" + context_object_name = "inventories" + @teamkfet_required def inventory_create(request): - articles = (Article.objects - .select_related('category') - .order_by('category__name', 'name') - ) + articles = Article.objects.select_related("category").order_by( + "category__name", "name" + ) initial = [] for article in articles: - initial.append({ - 'article' : article.pk, - 'stock_old': article.stock, - 'name' : article.name, - 'category' : article.category_id, - 'category__name': article.category.name, - 'box_capacity': article.box_capacity or 0, - }) - - cls_formset = formset_factory( - form = InventoryArticleForm, - extra = 0, + initial.append( + { + "article": article.pk, + "stock_old": article.stock, + "name": article.name, + "category": article.category_id, + "category__name": article.category.name, + "box_capacity": article.box_capacity or 0, + } ) + cls_formset = formset_factory(form=InventoryArticleForm, extra=0) + if request.POST: formset = cls_formset(request.POST, initial=initial) - if not request.user.has_perm('kfet.add_inventory'): - messages.error(request, 'Permission refusée') + if not request.user.has_perm("kfet.add_inventory"): + messages.error(request, "Permission refusée") elif formset.is_valid(): with transaction.atomic(): @@ -1602,60 +1734,63 @@ def inventory_create(request): inventory.by = request.user.profile.account_kfet saved = False for form in formset: - if form.cleaned_data['stock_new'] is not None: + if form.cleaned_data["stock_new"] is not None: if not saved: inventory.save() saved = True - article = articles.get(pk=form.cleaned_data['article'].pk) + article = articles.get(pk=form.cleaned_data["article"].pk) stock_old = article.stock - stock_new = form.cleaned_data['stock_new'] + stock_new = form.cleaned_data["stock_new"] InventoryArticle.objects.create( - inventory = inventory, - article = article, - stock_old = stock_old, - stock_new = stock_new) + inventory=inventory, + article=article, + stock_old=stock_old, + stock_new=stock_new, + ) article.stock = stock_new article.save() if saved: - messages.success(request, 'Inventaire créé') - return redirect('kfet.inventory') - messages.warning(request, 'Bah alors ? On a rien compté ?') + messages.success(request, "Inventaire créé") + return redirect("kfet.inventory") + messages.warning(request, "Bah alors ? On a rien compté ?") else: - messages.error(request, 'Pas marché') + messages.error(request, "Pas marché") else: - formset = cls_formset(initial = initial) + formset = cls_formset(initial=initial) + + return render(request, "kfet/inventory_create.html", {"formset": formset}) - return render(request, 'kfet/inventory_create.html', { - 'formset': formset, - }) class InventoryRead(DetailView): model = Inventory - template_name = 'kfet/inventory_read.html' - context_object_name = 'inventory' + template_name = "kfet/inventory_read.html" + context_object_name = "inventory" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - inventoryarticles = (InventoryArticle.objects - .select_related('article', 'article__category') - .filter(inventory = self.object) - .order_by('article__category__name', 'article__name')) - context['inventoryarts'] = inventoryarticles + inventoryarticles = ( + InventoryArticle.objects.select_related("article", "article__category") + .filter(inventory=self.object) + .order_by("article__category__name", "article__name") + ) + context["inventoryarts"] = inventoryarticles return context + # ----- # Order views # ----- + class OrderList(ListView): - queryset = Order.objects.select_related('supplier', 'inventory') - template_name = 'kfet/order.html' - context_object_name = 'orders' + queryset = Order.objects.select_related("supplier", "inventory") + template_name = "kfet/order.html" + context_object_name = "orders" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['suppliers'] = Supplier.objects.order_by('name') + context["suppliers"] = Supplier.objects.order_by("name") return context @@ -1664,30 +1799,25 @@ def order_create(request, pk): supplier = get_object_or_404(Supplier, pk=pk) articles = ( - Article.objects - .filter(suppliers=supplier.pk) + Article.objects.filter(suppliers=supplier.pk) .distinct() - .select_related('category') - .order_by('category__name', 'name') + .select_related("category") + .order_by("category__name", "name") ) # Force hit to cache articles = list(articles) sales_q = ( - Operation.objects - .select_related('group') + Operation.objects.select_related("group") .filter(article__in=articles, canceled_at=None) - .values('article') - .annotate(nb=Sum('article_nb')) + .values("article") + .annotate(nb=Sum("article_nb")) ) scale = WeekScale(last=True, n_steps=5, std_chunk=False) - chunks = scale.chunkify_qs(sales_q, field='group__at') + chunks = scale.chunkify_qs(sales_q, field="group__at") - sales = [ - {d['article']: d['nb'] for d in chunk} - for chunk in chunks - ] + sales = [{d["article"]: d["nb"] for d in chunk} for chunk in chunks] initial = [] @@ -1716,103 +1846,103 @@ def order_create(request, pk): c_rec = 5 else: c_rec = round(c_rec_temp) - initial.append({ - 'article': article.pk, - 'name': article.name, - 'category': article.category_id, - 'category__name': article.category.name, - 'stock': article.stock, - 'box_capacity': article.box_capacity, - 'v_all': v_all, - 'v_moy': round(v_moy), - 'v_et': round(v_et), - 'v_prev': round(v_prev), - 'c_rec': article.box_capacity and c_rec or round(c_rec_tot), - }) + initial.append( + { + "article": article.pk, + "name": article.name, + "category": article.category_id, + "category__name": article.category.name, + "stock": article.stock, + "box_capacity": article.box_capacity, + "v_all": v_all, + "v_moy": round(v_moy), + "v_et": round(v_et), + "v_prev": round(v_prev), + "c_rec": article.box_capacity and c_rec or round(c_rec_tot), + } + ) - cls_formset = formset_factory( - form=OrderArticleForm, - extra=0, - ) + cls_formset = formset_factory(form=OrderArticleForm, extra=0) if request.POST: formset = cls_formset(request.POST, initial=initial) - if not request.user.has_perm('kfet.add_order'): - messages.error(request, 'Permission refusée') + if not request.user.has_perm("kfet.add_order"): + messages.error(request, "Permission refusée") elif formset.is_valid(): order = Order() order.supplier = supplier saved = False for form in formset: - if form.cleaned_data['quantity_ordered'] is not None: + if form.cleaned_data["quantity_ordered"] is not None: if not saved: order.save() saved = True - article = form.cleaned_data['article'] - q_ordered = form.cleaned_data['quantity_ordered'] + article = form.cleaned_data["article"] + q_ordered = form.cleaned_data["quantity_ordered"] if article.box_capacity: q_ordered *= article.box_capacity OrderArticle.objects.create( - order=order, - article=article, - quantity_ordered=q_ordered, + order=order, article=article, quantity_ordered=q_ordered ) if saved: - messages.success(request, 'Commande créée') - return redirect('kfet.order.read', order.pk) - messages.warning(request, 'Rien commandé => Pas de commande') + messages.success(request, "Commande créée") + return redirect("kfet.order.read", order.pk) + messages.warning(request, "Rien commandé => Pas de commande") else: - messages.error(request, 'Corrigez les erreurs') + messages.error(request, "Corrigez les erreurs") else: formset = cls_formset(initial=initial) scale.label_fmt = "S-{rev_i}" - return render(request, 'kfet/order_create.html', { - 'supplier': supplier, - 'formset': formset, - 'scale': scale, - }) + return render( + request, + "kfet/order_create.html", + {"supplier": supplier, "formset": formset, "scale": scale}, + ) class OrderRead(DetailView): model = Order - template_name = 'kfet/order_read.html' - context_object_name = 'order' + template_name = "kfet/order_read.html" + context_object_name = "order" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - orderarticles = (OrderArticle.objects - .select_related('article', 'article__category') - .filter(order=self.object) - .order_by('article__category__name', 'article__name') - ) - context['orderarts'] = orderarticles - mail = ("Bonjour,\n\nNous voudrions pour le ##DATE## à la K-Fêt de " - "l'ENS Ulm :") + orderarticles = ( + OrderArticle.objects.select_related("article", "article__category") + .filter(order=self.object) + .order_by("article__category__name", "article__name") + ) + context["orderarts"] = orderarticles + mail = ( + "Bonjour,\n\nNous voudrions pour le ##DATE## à la K-Fêt de " "l'ENS Ulm :" + ) category = 0 for orderarticle in orderarticles: if category != orderarticle.article.category: category = orderarticle.article.category - mail += '\n' + mail += "\n" nb = orderarticle.quantity_ordered - box = '' + box = "" if orderarticle.article.box_capacity: nb /= orderarticle.article.box_capacity if nb >= 2: - box = ' %ss de' % orderarticle.article.box_type + box = " %ss de" % orderarticle.article.box_type else: - box = ' %s de' % orderarticle.article.box_type + box = " %s de" % orderarticle.article.box_type name = orderarticle.article.name.capitalize() mail += "\n- %s%s %s" % (round(nb), box, name) - mail += ("\n\nMerci d'appeler le numéro suivant lorsque les livreurs " - "sont là : ##TELEPHONE##\nCordialement,\n##PRENOM## ##NOM## " - ", pour la K-Fêt de l'ENS Ulm") + mail += ( + "\n\nMerci d'appeler le numéro suivant lorsque les livreurs " + "sont là : ##TELEPHONE##\nCordialement,\n##PRENOM## ##NOM## " + ", pour la K-Fêt de l'ENS Ulm" + ) - context['mail'] = mail + context["mail"] = mail return context @@ -1820,69 +1950,70 @@ class OrderRead(DetailView): def order_to_inventory(request, pk): order = get_object_or_404(Order, pk=pk) - if hasattr(order, 'inventory'): + if hasattr(order, "inventory"): raise Http404 supplier_prefetch = Prefetch( - 'article__supplierarticle_set', + "article__supplierarticle_set", queryset=( - SupplierArticle.objects - .filter(supplier=order.supplier) - .order_by('-at') + SupplierArticle.objects.filter(supplier=order.supplier).order_by("-at") ), - to_attr='supplier', + to_attr="supplier", ) order_articles = ( - OrderArticle.objects - .filter(order=order.pk) - .select_related('article', 'article__category') - .prefetch_related( - supplier_prefetch, - ) - .order_by('article__category__name', 'article__name') + OrderArticle.objects.filter(order=order.pk) + .select_related("article", "article__category") + .prefetch_related(supplier_prefetch) + .order_by("article__category__name", "article__name") ) initial = [] for order_article in order_articles: article = order_article.article - initial.append({ - 'article': article.pk, - 'name': article.name, - 'category': article.category_id, - 'category__name': article.category.name, - 'quantity_ordered': order_article.quantity_ordered, - 'quantity_received': order_article.quantity_ordered, - 'price_HT': article.supplier[0].price_HT, - 'TVA': article.supplier[0].TVA, - 'rights': article.supplier[0].rights, - }) + initial.append( + { + "article": article.pk, + "name": article.name, + "category": article.category_id, + "category__name": article.category.name, + "quantity_ordered": order_article.quantity_ordered, + "quantity_received": order_article.quantity_ordered, + "price_HT": article.supplier[0].price_HT, + "TVA": article.supplier[0].TVA, + "rights": article.supplier[0].rights, + } + ) cls_formset = formset_factory(OrderArticleToInventoryForm, extra=0) - if request.method == 'POST': + if request.method == "POST": formset = cls_formset(request.POST, initial=initial) - if not request.user.has_perm('kfet.order_to_inventory'): - messages.error(request, 'Permission refusée') + if not request.user.has_perm("kfet.order_to_inventory"): + messages.error(request, "Permission refusée") elif formset.is_valid(): with transaction.atomic(): inventory = Inventory.objects.create( - order=order, by=request.user.profile.account_kfet, + order=order, by=request.user.profile.account_kfet ) new_supplierarticle = [] new_inventoryarticle = [] for form in formset: - q_received = form.cleaned_data['quantity_received'] - article = form.cleaned_data['article'] + q_received = form.cleaned_data["quantity_received"] + article = form.cleaned_data["article"] - price_HT = form.cleaned_data['price_HT'] - TVA = form.cleaned_data['TVA'] - rights = form.cleaned_data['rights'] + price_HT = form.cleaned_data["price_HT"] + TVA = form.cleaned_data["TVA"] + rights = form.cleaned_data["rights"] - if any((form.initial['price_HT'] != price_HT, - form.initial['TVA'] != TVA, - form.initial['rights'] != rights)): + if any( + ( + form.initial["price_HT"] != price_HT, + form.initial["TVA"] != TVA, + form.initial["rights"] != rights, + ) + ): new_supplierarticle.append( SupplierArticle( supplier=order.supplier, @@ -1893,9 +2024,9 @@ def order_to_inventory(request, pk): ) ) ( - OrderArticle.objects - .filter(order=order, article=article) - .update(quantity_received=q_received) + OrderArticle.objects.filter( + order=order, article=article + ).update(quantity_received=q_received) ) new_inventoryarticle.append( InventoryArticle( @@ -1912,29 +2043,29 @@ def order_to_inventory(request, pk): SupplierArticle.objects.bulk_create(new_supplierarticle) InventoryArticle.objects.bulk_create(new_inventoryarticle) messages.success(request, "C'est tout bon !") - return redirect('kfet.order') + return redirect("kfet.order") else: messages.error(request, "Corrigez les erreurs") else: formset = cls_formset(initial=initial) - return render(request, 'kfet/order_to_inventory.html', { - 'formset': formset, - 'order': order, - }) + return render( + request, "kfet/order_to_inventory.html", {"formset": formset, "order": order} + ) + class SupplierUpdate(SuccessMessageMixin, UpdateView): - model = Supplier - template_name = 'kfet/supplier_form.html' - fields = ['name', 'address', 'email', 'phone', 'comment'] - success_url = reverse_lazy('kfet.order') - sucess_message = 'Données fournisseur mis à jour' + model = Supplier + template_name = "kfet/supplier_form.html" + fields = ["name", "address", "email", "phone", "comment"] + success_url = reverse_lazy("kfet.order") + sucess_message = "Données fournisseur mis à jour" # Surcharge de la validation def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_supplier'): - form.add_error(None, 'Permission refusée') + if not self.request.user.has_perm("kfet.change_supplier"): + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating return super().form_valid(form) @@ -1952,14 +2083,12 @@ class JSONResponseMixin(object): """ A mixin that can be used to render a JSON response. """ + def render_to_json_response(self, context, **response_kwargs): """ Returns a JSON response, transforming 'context' to make the payload. """ - return JsonResponse( - self.get_data(context), - **response_kwargs - ) + return JsonResponse(self.get_data(context), **response_kwargs) def get_data(self, context): """ @@ -1980,7 +2109,6 @@ class JSONDetailView(JSONResponseMixin, BaseDetailView): class PkUrlMixin(object): - def get_object(self, *args, **kwargs): get_by = self.kwargs.get(self.pk_url_kwarg) return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by}) @@ -1993,7 +2121,8 @@ class SingleResumeStat(JSONDetailView): url to retrieve data, label, ... """ - id_prefix = '' + + id_prefix = "" nb_default = 0 stats = [] @@ -2004,27 +2133,28 @@ class SingleResumeStat(JSONDetailView): object_id = self.object.id context = {} stats = [] - prefix = '{}_{}'.format(self.id_prefix, object_id) + prefix = "{}_{}".format(self.id_prefix, object_id) for i, stat_def in enumerate(self.stats): url_pk = getattr(self.object, self.pk_url_kwarg) - url_params_d = stat_def.get('url_params', {}) + url_params_d = stat_def.get("url_params", {}) if len(url_params_d) > 0: - url_params = '?{}'.format(urlencode(url_params_d)) + url_params = "?{}".format(urlencode(url_params_d)) else: - url_params = '' - stats.append({ - 'label': stat_def['label'], - 'btn': 'btn_{}_{}'.format(prefix, i), - 'url': '{url}{params}'.format( - url=reverse(self.url_stat, args=[url_pk]), - params=url_params, - ), - }) - context['id_prefix'] = prefix - context['content_id'] = "content_%s" % prefix - context['stats'] = stats - context['default_stat'] = self.nb_default - context['object_id'] = object_id + url_params = "" + stats.append( + { + "label": stat_def["label"], + "btn": "btn_{}_{}".format(prefix, i), + "url": "{url}{params}".format( + url=reverse(self.url_stat, args=[url_pk]), params=url_params + ), + } + ) + context["id_prefix"] = prefix + context["content_id"] = "content_%s" % prefix + context["stats"] = stats + context["default_stat"] = self.nb_default + context["object_id"] = object_id return context @@ -2036,31 +2166,18 @@ ID_PREFIX_ACC_BALANCE = "balance_acc" class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): """Manifest for balance stats of an account.""" + model = Account - context_object_name = 'account' - pk_url_kwarg = 'trigramme' - url_stat = 'kfet.account.stat.balance' + context_object_name = "account" + pk_url_kwarg = "trigramme" + url_stat = "kfet.account.stat.balance" id_prefix = ID_PREFIX_ACC_BALANCE stats = [ - { - 'label': 'Tout le temps', - }, - { - 'label': '1 an', - 'url_params': {'last_days': 365}, - }, - { - 'label': '6 mois', - 'url_params': {'last_days': 183}, - }, - { - 'label': '3 mois', - 'url_params': {'last_days': 90}, - }, - { - 'label': '30 jours', - 'url_params': {'last_days': 30}, - }, + {"label": "Tout le temps"}, + {"label": "1 an", "url_params": {"last_days": 365}}, + {"label": "6 mois", "url_params": {"last_days": 183}}, + {"label": "3 mois", "url_params": {"last_days": 90}}, + {"label": "30 jours", "url_params": {"last_days": 30}}, ] nb_default = 0 @@ -2081,9 +2198,10 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): Operations and Transfers are taken into account. """ + model = Account - pk_url_kwarg = 'trigramme' - context_object_name = 'account' + pk_url_kwarg = "trigramme" + context_object_name = "account" def get_changes_list(self, last_days=None, begin_date=None, end_date=None): account = self.object @@ -2096,11 +2214,7 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): # prepare querysets # TODO: retirer les opgroup dont tous les op sont annulées opegroups = OperationGroup.objects.filter(on_acc=account) - transfers = ( - Transfer.objects - .filter(canceled_at=None) - .select_related('group') - ) + transfers = Transfer.objects.filter(canceled_at=None).select_related("group") recv_transfers = transfers.filter(to_acc=account) sent_transfers = transfers.filter(from_acc=account) @@ -2127,69 +2241,62 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): actions = [] - actions.append({ - 'at': (begin_date or account.created_at).isoformat(), - 'amount': 0, - 'balance': 0, - }) - actions.append({ - 'at': (end_date or timezone.now()).isoformat(), - 'amount': 0, - 'balance': 0, - }) + actions.append( + { + "at": (begin_date or account.created_at).isoformat(), + "amount": 0, + "balance": 0, + } + ) + actions.append( + {"at": (end_date or timezone.now()).isoformat(), "amount": 0, "balance": 0} + ) - actions += [ - { - 'at': ope_grp.at.isoformat(), - 'amount': ope_grp.amount, - 'balance': 0, - } for ope_grp in opegroups - ] + [ - { - 'at': tr.group.at.isoformat(), - 'amount': tr.amount, - 'balance': 0, - } for tr in recv_transfers - ] + [ - { - 'at': tr.group.at.isoformat(), - 'amount': -tr.amount, - 'balance': 0, - } for tr in sent_transfers - ] + actions += ( + [ + {"at": ope_grp.at.isoformat(), "amount": ope_grp.amount, "balance": 0} + for ope_grp in opegroups + ] + + [ + {"at": tr.group.at.isoformat(), "amount": tr.amount, "balance": 0} + for tr in recv_transfers + ] + + [ + {"at": tr.group.at.isoformat(), "amount": -tr.amount, "balance": 0} + for tr in sent_transfers + ] + ) # Maintenant on trie la liste des actions par ordre du plus récent # an plus ancien et on met à jour la balance if len(actions) > 1: - actions = sorted(actions, key=lambda k: k['at'], reverse=True) - actions[0]['balance'] = account.balance - for i in range(len(actions)-1): - actions[i+1]['balance'] = \ - actions[i]['balance'] - actions[i+1]['amount'] + actions = sorted(actions, key=lambda k: k["at"], reverse=True) + actions[0]["balance"] = account.balance + for i in range(len(actions) - 1): + actions[i + 1]["balance"] = ( + actions[i]["balance"] - actions[i + 1]["amount"] + ) return actions def get_context_data(self, *args, **kwargs): context = {} - last_days = self.request.GET.get('last_days', None) + last_days = self.request.GET.get("last_days", None) if last_days is not None: last_days = int(last_days) - begin_date = self.request.GET.get('begin_date', None) - end_date = self.request.GET.get('end_date', None) + begin_date = self.request.GET.get("begin_date", None) + end_date = self.request.GET.get("end_date", None) changes = self.get_changes_list( - last_days=last_days, - begin_date=begin_date, end_date=end_date, + last_days=last_days, begin_date=begin_date, end_date=end_date ) - context['charts'] = [{ - "color": "rgb(200, 20, 60)", - "label": "Balance", - "values": changes, - }] - context['is_time_chart'] = True + context["charts"] = [ + {"color": "rgb(200, 20, 60)", "label": "Balance", "values": changes} + ] + context["is_time_chart"] = True if len(changes) > 0: - context['min_date'] = changes[-1]['at'] - context['max_date'] = changes[0]['at'] + context["min_date"] = changes[-1]["at"] + context["max_date"] = changes[0]["at"] # TODO: offset return context @@ -2215,13 +2322,14 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" class AccountStatOperationList(PkUrlMixin, SingleResumeStat): """Manifest for operations stats of an account.""" + model = Account - context_object_name = 'account' - pk_url_kwarg = 'trigramme' + context_object_name = "account" + pk_url_kwarg = "trigramme" id_prefix = ID_PREFIX_ACC_LAST nb_default = 2 stats = last_stats_manifest(types=[Operation.PURCHASE]) - url_stat = 'kfet.account.stat.operation' + url_stat = "kfet.account.stat.operation" def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) @@ -2236,9 +2344,10 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): """Datasets of operations of an account.""" + model = Account - pk_url_kwarg = 'trigramme' - context_object_name = 'account' + pk_url_kwarg = "trigramme" + context_object_name = "account" id_prefix = "" def get_operations(self, scale, types=None): @@ -2247,26 +2356,25 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): # puis on choisi pour chaques intervalle les opérations # effectuées dans ces intervalles de temps all_operations = ( - Operation.objects - .filter(group__on_acc=self.object, - canceled_at=None) - .values('article_nb', 'group__at') - .order_by('group__at') + Operation.objects.filter(group__on_acc=self.object, canceled_at=None) + .values("article_nb", "group__at") + .order_by("group__at") ) if types is not None: all_operations = all_operations.filter(type__in=types) chunks = scale.get_by_chunks( - all_operations, field_db='group__at', - field_callback=(lambda d: d['group__at']), + all_operations, + field_db="group__at", + field_callback=(lambda d: d["group__at"]), ) return chunks def get_context_data(self, *args, **kwargs): old_ctx = super().get_context_data(*args, **kwargs) - context = {'labels': old_ctx['labels']} + context = {"labels": old_ctx["labels"]} scale = self.scale - types = self.request.GET.get('types', None) + types = self.request.GET.get("types", None) if types is not None: types = ast.literal_eval(types) @@ -2274,12 +2382,16 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): # On compte les opérations nb_ventes = [] for chunk in operations: - ventes = sum(ope['article_nb'] for ope in chunk) + ventes = sum(ope["article_nb"] for ope in chunk) nb_ventes.append(ventes) - context['charts'] = [{"color": "rgb(200, 20, 60)", - "label": "NB items achetés", - "values": nb_ventes}] + context["charts"] = [ + { + "color": "rgb(200, 20, 60)", + "label": "NB items achetés", + "values": nb_ventes, + } + ] return context def get_object(self, *args, **kwargs): @@ -2304,11 +2416,12 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art" class ArticleStatSalesList(SingleResumeStat): """Manifest for sales stats of an article.""" + model = Article - context_object_name = 'article' + context_object_name = "article" id_prefix = ID_PREFIX_ART_LAST nb_default = 2 - url_stat = 'kfet.article.stat.sales' + url_stat = "kfet.article.stat.sales" stats = last_stats_manifest() @method_decorator(teamkfet_required) @@ -2318,34 +2431,30 @@ class ArticleStatSalesList(SingleResumeStat): class ArticleStatSales(ScaleMixin, JSONDetailView): """Datasets of sales of an article.""" + model = Article - context_object_name = 'article' + context_object_name = "article" def get_context_data(self, *args, **kwargs): old_ctx = super().get_context_data(*args, **kwargs) - context = {'labels': old_ctx['labels']} + context = {"labels": old_ctx["labels"]} scale = self.scale all_purchases = ( - Operation.objects - .filter( - type=Operation.PURCHASE, - article=self.object, - canceled_at=None, + Operation.objects.filter( + type=Operation.PURCHASE, article=self.object, canceled_at=None ) - .values('group__at', 'article_nb') - .order_by('group__at') + .values("group__at", "article_nb") + .order_by("group__at") ) - liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ') - liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ') + liq_only = all_purchases.filter(group__on_acc__trigramme="LIQ") + liq_exclude = all_purchases.exclude(group__on_acc__trigramme="LIQ") chunks_liq = scale.get_by_chunks( - liq_only, field_db='group__at', - field_callback=lambda d: d['group__at'], + liq_only, field_db="group__at", field_callback=lambda d: d["group__at"] ) chunks_no_liq = scale.get_by_chunks( - liq_exclude, field_db='group__at', - field_callback=lambda d: d['group__at'], + liq_exclude, field_db="group__at", field_callback=lambda d: d["group__at"] ) # On compte les opérations @@ -2353,21 +2462,25 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): nb_accounts = [] nb_liq = [] for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq): - sum_accounts = sum(ope['article_nb'] for ope in chunk_no_liq) - sum_liq = sum(ope['article_nb'] for ope in chunk_liq) + sum_accounts = sum(ope["article_nb"] for ope in chunk_no_liq) + sum_liq = sum(ope["article_nb"] for ope in chunk_liq) nb_ventes.append(sum_accounts + sum_liq) nb_accounts.append(sum_accounts) nb_liq.append(sum_liq) - context['charts'] = [{"color": "rgb(200, 20, 60)", - "label": "Toutes consommations", - "values": nb_ventes}, - {"color": "rgb(54, 162, 235)", - "label": "LIQ", - "values": nb_liq}, - {"color": "rgb(255, 205, 86)", - "label": "Comptes K-Fêt", - "values": nb_accounts}] + context["charts"] = [ + { + "color": "rgb(200, 20, 60)", + "label": "Toutes consommations", + "values": nb_ventes, + }, + {"color": "rgb(54, 162, 235)", "label": "LIQ", "values": nb_liq}, + { + "color": "rgb(255, 205, 86)", + "label": "Comptes K-Fêt", + "values": nb_accounts, + }, + ] return context @method_decorator(teamkfet_required) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 91bd9d38..19122322 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -2,6 +2,7 @@ import csv from unittest import mock from urllib.parse import parse_qs, urlparse +import icalendar from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.http import QueryDict @@ -9,13 +10,10 @@ from django.test import Client from django.utils import timezone from django.utils.functional import cached_property -import icalendar - User = get_user_model() class TestCaseMixin: - def assertForbidden(self, response): """ Test that the response (retrieved with a Client) is a denial of access. @@ -40,31 +38,30 @@ class TestCaseMixin: full_path = request.get_full_path() querystring = QueryDict(mutable=True) - querystring['next'] = full_path + querystring["next"] = full_path - login_url = '{}?{}'.format( - reverse('cof-login'), querystring.urlencode(safe='/')) + login_url = "{}?{}".format( + reverse("cof-login"), querystring.urlencode(safe="/") + ) # We don't focus on what the login view does. # So don't fetch the redirect. - self.assertRedirects( - response, login_url, - fetch_redirect_response=False, - ) + self.assertRedirects(response, login_url, fetch_redirect_response=False) except AssertionError: raise AssertionError( "%(http_method)s request at %(path)s should be forbidden for " "%(username)s user.\n" "Response isn't 403, nor a redirect to login view. Instead, " - "response code is %(code)d." % { - 'http_method': request.method, - 'path': request.get_full_path(), - 'username': ( + "response code is %(code)d." + % { + "http_method": request.method, + "path": request.get_full_path(), + "username": ( "'{}'".format(request.user) if request.user.is_authenticated() - else 'anonymous' + else "anonymous" ), - 'code': response.status_code, + "code": response.status_code, } ) @@ -85,10 +82,9 @@ class TestCaseMixin: if type(expected) == dict: parsed = urlparse(actual) for part, expected_part in expected.items(): - if part == 'query': + if part == "query": self.assertDictEqual( - parse_qs(parsed.query), - expected.get('query', {}), + parse_qs(parsed.query), expected.get("query", {}) ) else: self.assertEqual(getattr(parsed, part), expected_part) @@ -105,22 +101,17 @@ class TestCaseMixin: for k, v in kwargs.items(): setattr(self, k, Elt(v)) - results_as_ldap = [ - Entry(uid=uid, cn=name) for uid, name in results - ] + results_as_ldap = [Entry(uid=uid, cn=name) for uid, name in results] mock_connection = mock.MagicMock() mock_connection.entries = results_as_ldap # Connection is used as a context manager. mock_context_manager = mock.MagicMock() - mock_context_manager.return_value.__enter__.return_value = ( - mock_connection - ) + mock_context_manager.return_value.__enter__.return_value = mock_connection patcher = mock.patch( - 'gestioncof.autocomplete.Connection', - new=mock_context_manager, + "gestioncof.autocomplete.Connection", new=mock_context_manager ) patcher.start() self.addCleanup(patcher.stop) @@ -128,8 +119,8 @@ class TestCaseMixin: return mock_connection def load_from_csv_response(self, r): - decoded = r.content.decode('utf-8') - return list(csv.reader(decoded.split('\n')[:-1])) + decoded = r.content.decode("utf-8") + return list(csv.reader(decoded.split("\n")[:-1])) def _test_event_equal(self, event, exp): for k, v_desc in exp.items(): @@ -155,7 +146,7 @@ class TestCaseMixin: cal = icalendar.Calendar.from_ical(ical_content) - for ev in cal.walk('vevent'): + for ev in cal.walk("vevent"): found, i_found = self._find_event(ev, remaining) if found: remaining.pop(i_found) @@ -235,10 +226,11 @@ class ViewTestCaseMixin(TestCaseMixin): can be given by defining an attribute '_data'. """ + url_name = None url_expected = None - http_methods = ['GET'] + http_methods = ["GET"] auth_user = None auth_forbidden = [] @@ -250,7 +242,7 @@ class ViewTestCaseMixin(TestCaseMixin): # 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 = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) @@ -268,10 +260,7 @@ class ViewTestCaseMixin(TestCaseMixin): if self.auth_user: # The wrapper is a sanity check. self.assertTrue( - self.client.login( - username=self.auth_user, - password=self.auth_user, - ) + self.client.login(username=self.auth_user, password=self.auth_user) ) def tearDown(self): @@ -289,8 +278,8 @@ class ViewTestCaseMixin(TestCaseMixin): """ return { - 'user': User.objects.create_user('user', '', 'user'), - 'root': User.objects.create_superuser('root', '', 'root'), + "user": User.objects.create_user("user", "", "user"), + "root": User.objects.create_superuser("root", "", "root"), } @cached_property @@ -323,22 +312,25 @@ class ViewTestCaseMixin(TestCaseMixin): @property def urls_conf(self): - return [{ - 'name': self.url_name, - 'args': getattr(self, 'url_args', []), - 'kwargs': getattr(self, 'url_kwargs', {}), - 'expected': self.url_expected, - }] + return [ + { + "name": self.url_name, + "args": getattr(self, "url_args", []), + "kwargs": getattr(self, "url_kwargs", {}), + "expected": self.url_expected, + } + ] @property def t_urls(self): return [ reverse( - url_conf['name'], - args=url_conf.get('args', []), - kwargs=url_conf.get('kwargs', {}), + url_conf["name"], + args=url_conf.get("args", []), + kwargs=url_conf.get("kwargs", {}), ) - for url_conf in self.urls_conf] + for url_conf in self.urls_conf + ] @property def url(self): @@ -346,7 +338,7 @@ class ViewTestCaseMixin(TestCaseMixin): def test_urls(self): for url, conf in zip(self.t_urls, self.urls_conf): - self.assertEqual(url, conf['expected']) + self.assertEqual(url, conf["expected"]) def test_forbidden(self): for method in self.http_methods: @@ -361,7 +353,7 @@ class ViewTestCaseMixin(TestCaseMixin): client.login(username=user, password=user) send_request = getattr(client, method) - data = getattr(self, '{}_data'.format(method), {}) + data = getattr(self, "{}_data".format(method), {}) r = send_request(url, data) self.assertForbidden(r) diff --git a/utils/views/autocomplete.py b/utils/views/autocomplete.py index ca50c63b..c5d51343 100644 --- a/utils/views/autocomplete.py +++ b/utils/views/autocomplete.py @@ -1,6 +1,5 @@ -from django.db.models import Q - from dal import autocomplete +from django.db.models import Q class Select2QuerySetView(autocomplete.Select2QuerySetView): @@ -18,7 +17,7 @@ class Select2QuerySetView(autocomplete.Select2QuerySetView): for word in words: for field in self.search_fields: - filter_q |= Q(**{'{}__icontains'.format(field): word}) + filter_q |= Q(**{"{}__icontains".format(field): word}) return filter_q From 402b5443937a35a94deb4b138c257d42973a2a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 12:47:19 +0200 Subject: [PATCH 106/122] core -- Fix flake8 errors --- bda/algorithm.py | 2 -- bda/management/commands/manage_reventes.py | 4 +++- bda/management/commands/sendrappels.py | 5 ++++- bda/models.py | 2 +- bda/tests/test_revente.py | 2 +- gestioncof/apps.py | 2 +- gestioncof/decorators.py | 4 ++-- gestioncof/shared.py | 2 -- kfet/admin.py | 3 --- kfet/cms/apps.py | 2 +- kfet/forms.py | 2 +- kfet/models.py | 3 +-- kfet/views.py | 6 +----- 13 files changed, 16 insertions(+), 23 deletions(-) delete mode 100644 kfet/admin.py diff --git a/bda/algorithm.py b/bda/algorithm.py index 830ef119..add09335 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,7 +1,5 @@ import random -from django.db.models import Max - class Algorithm(object): diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 52d25252..bd25a28e 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -9,7 +9,9 @@ from bda.models import SpectacleRevente class Command(BaseCommand): - help = "Envoie les mails de notification et effectue " "les tirages au sort des reventes" + help = ( + "Envoie les mails de notification et effectue les tirages au sort des reventes" + ) leave_locale_alone = True def handle(self, *args, **options): diff --git a/bda/management/commands/sendrappels.py b/bda/management/commands/sendrappels.py index 33f85330..65026736 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -11,7 +11,10 @@ from bda.models import Spectacle class Command(BaseCommand): - help = "Envoie les mails de rappel des spectacles dont la date " "approche.\nNe renvoie pas les mails déjà envoyés." + help = ( + "Envoie les mails de rappel des spectacles dont la date approche.\n" + "Ne renvoie pas les mails déjà envoyés." + ) leave_locale_alone = True def handle(self, *args, **options): diff --git a/bda/models.py b/bda/models.py index bd4ea4cb..9ac38a41 100644 --- a/bda/models.py +++ b/bda/models.py @@ -98,7 +98,7 @@ class Spectacle(models.Model): """ try: return self.image.url - except: + except Exception: return None def send_rappel(self): diff --git a/bda/tests/test_revente.py b/bda/tests/test_revente.py index b0d69dc7..202e9494 100644 --- a/bda/tests/test_revente.py +++ b/bda/tests/test_revente.py @@ -1,7 +1,7 @@ from datetime import timedelta from django.contrib.auth.models import User -from django.test import Client, TestCase +from django.test import TestCase from django.utils import timezone from bda.models import ( diff --git a/gestioncof/apps.py b/gestioncof/apps.py index b132366a..88e2fbfc 100644 --- a/gestioncof/apps.py +++ b/gestioncof/apps.py @@ -6,7 +6,7 @@ class GestioncofConfig(AppConfig): verbose_name = "Gestion des adhérents du COF" def ready(self): - from . import signals + from . import signals # noqa self.register_config() diff --git a/gestioncof/decorators.py b/gestioncof/decorators.py index fe5a7ccc..ef811730 100644 --- a/gestioncof/decorators.py +++ b/gestioncof/decorators.py @@ -5,7 +5,7 @@ def is_cof(user): try: profile = user.profile return profile.is_cof - except: + except Exception: return False @@ -16,7 +16,7 @@ def is_buro(user): try: profile = user.profile return profile.is_buro - except: + except Exception: return False diff --git a/gestioncof/shared.py b/gestioncof/shared.py index 87e19842..1a5bd32e 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -2,8 +2,6 @@ from django.conf import settings from django.contrib.sites.models import Site from django_cas_ng.backends import CASBackend -from gestioncof.models import CofProfile - class COFCASBackend(CASBackend): def clean_username(self, username): diff --git a/kfet/admin.py b/kfet/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/kfet/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/kfet/cms/apps.py b/kfet/cms/apps.py index d4928ac6..d675cbb5 100644 --- a/kfet/cms/apps.py +++ b/kfet/cms/apps.py @@ -7,4 +7,4 @@ class KFetCMSAppConfig(AppConfig): verbose_name = "CMS K-Fêt" def ready(self): - from . import hooks + from . import hooks # noqa diff --git a/kfet/forms.py b/kfet/forms.py index b253fd67..e314d80c 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -219,7 +219,7 @@ class CheckoutStatementCreateForm(forms.ModelForm): or self.cleaned_data["balance_500"] is None ): raise ValidationError( - "Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp (et t'as pas idée de comment c'est long de vérifier que t'as mis des valeurs de partout...)" + "Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp." ) super().clean() diff --git a/kfet/models.py b/kfet/models.py index 6b16505e..5a8ea858 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -8,7 +8,6 @@ from django.db import models, transaction from django.db.models import F from django.urls import reverse from django.utils import timezone -from django.utils.six.moves import reduce from django.utils.translation import ugettext_lazy as _ from gestioncof.models import CofProfile @@ -164,7 +163,7 @@ class Account(models.Model): pattern = re.compile("^[^a-z]{3}$") data["is_valid"] = pattern.match(trigramme) and True or False try: - account = Account.objects.get(trigramme=trigramme) + Account.objects.get(trigramme=trigramme) except Account.DoesNotExist: data["is_free"] = True return data diff --git a/kfet/views.py b/kfet/views.py index 088f867e..a2a69930 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2,7 +2,6 @@ import ast import heapq import statistics from collections import defaultdict -from datetime import timedelta from decimal import Decimal from urllib.parse import urlencode @@ -10,12 +9,10 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import Permission, User from django.contrib.messages.views import SuccessMessageMixin -from django.core.cache import cache from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse, reverse_lazy from django.db import transaction from django.db.models import Count, F, Prefetch, Sum -from django.db.models.functions import Coalesce from django.forms import formset_factory from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render @@ -30,7 +27,6 @@ from kfet import consumers from kfet.config import kfet_config from kfet.decorators import teamkfet_required from kfet.forms import ( - AccountBalanceForm, AccountForm, AccountNegativeForm, AccountNoTriForm, @@ -80,7 +76,7 @@ from kfet.models import ( Transfer, TransferGroup, ) -from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest, tot_ventes +from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest from .auth.views import ( # noqa AccountGroupCreate, From 61fbf0bc805bdc34a138e6038147bbad5bbf13aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 15:45:32 +0200 Subject: [PATCH 107/122] typo --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8fcd1144..95d9305d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,7 @@ test: paths: - vendor/ # For GitLab CI to get coverage from build. - # Keep this disabled for now, at it may kill GitLab... + # Keep this disabled for now, as it may kill GitLab... # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' linters: From 9bc3355a2164e825ff0063fb46dcdf04e84c1d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 15:50:49 +0200 Subject: [PATCH 108/122] pre-commit hook: fix shellcheck's SC2086 & SC2181 --- .pre-commit.sh | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.pre-commit.sh b/.pre-commit.sh index e621b126..30f09eb9 100755 --- a/.pre-commit.sh +++ b/.pre-commit.sh @@ -17,18 +17,18 @@ STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".p printf "> black ... " if type black &>/dev/null; then - if [ -z $STAGED_PYTHON_FILES ]; then + if [ -z "$STAGED_PYTHON_FILES" ]; then printf "OK\n" else BLACK_OUTPUT="/tmp/gc-black-output.log" touch $BLACK_OUTPUT - black --check $STAGED_PYTHON_FILES &>$BLACK_OUTPUT - printf "OK\n" - if [ $? -ne 0 ]; then - black $STAGED_PYTHON_FILES &>$BLACK_OUTPUT + if ! black --check "$STAGED_PYTHON_FILES" &>$BLACK_OUTPUT; then + black "$STAGED_PYTHON_FILES" &>$BLACK_OUTPUT tail -1 $BLACK_OUTPUT formatter_updated=1 + else + printf "OK\n" fi fi else @@ -41,18 +41,18 @@ fi printf "> isort ... " if type isort &>/dev/null; then - if [ -z $STAGED_PYTHON_FILES ]; then + if [ -z "$STAGED_PYTHON_FILES" ]; then printf "OK\n" else ISORT_OUTPUT="/tmp/gc-isort-output.log" touch $ISORT_OUTPUT - isort --check-only $STAGED_PYTHON_FILES &>$ISORT_OUTPUT - printf "OK\n" - if [ $? -ne 0 ]; then - isort $STAGED_PYTHON_FILES &>$ISORT_OUTPUT + if ! isort --check-only "$STAGED_PYTHON_FILES" &>$ISORT_OUTPUT; then + isort "$STAGED_PYTHON_FILES" &>$ISORT_OUTPUT printf "Reformatted.\n" formatter_updated=1 + else + printf "OK\n" fi fi else @@ -65,19 +65,18 @@ fi printf "> flake8 ... " if type flake8 &>/dev/null; then - if [ -z $STAGED_PYTHON_FILES ]; then + if [ -z "$STAGED_PYTHON_FILES" ]; then printf "OK\n" else FLAKE8_OUTPUT="/tmp/gc-flake8-output.log" touch $FLAKE8_OUTPUT - flake8 $STAGED_PYTHON_FILES &>$FLAKE8_OUTPUT - if [ $? -eq 0 ]; then - printf "OK\n" - else + if ! flake8 "$STAGED_PYTHON_FILES" &>$FLAKE8_OUTPUT; then printf "FAIL\n" cat $FLAKE8_OUTPUT checker_dirty=1 + else + printf "OK\n" fi fi else From aac9b41442a06a9f91a1176b1ef4ba4807b3eab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 17:39:23 +0200 Subject: [PATCH 109/122] bda.tests -- Silence syncmails in setup --- bda/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 8bd5b462..4f906f82 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -51,7 +51,7 @@ class BdATestHelpers: def require_custommails(self): from django.core.management import call_command - call_command("syncmails") + call_command("syncmails", verbosity=0) def check_restricted_access( self, url, validate_user=user_is_cof, redirect_url=None From 2c0ab1e55edb72c41c606f039415ffc906c0c1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 23:57:33 +0200 Subject: [PATCH 110/122] use xargs to prevent globbing in pre-commit.sh --- .pre-commit.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.pre-commit.sh b/.pre-commit.sh index 30f09eb9..0e0e3c1a 100755 --- a/.pre-commit.sh +++ b/.pre-commit.sh @@ -12,6 +12,7 @@ checker_dirty=0 # 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 ... " @@ -23,8 +24,8 @@ if type black &>/dev/null; then BLACK_OUTPUT="/tmp/gc-black-output.log" touch $BLACK_OUTPUT - if ! black --check "$STAGED_PYTHON_FILES" &>$BLACK_OUTPUT; then - black "$STAGED_PYTHON_FILES" &>$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 @@ -47,8 +48,8 @@ if type isort &>/dev/null; then ISORT_OUTPUT="/tmp/gc-isort-output.log" touch $ISORT_OUTPUT - if ! isort --check-only "$STAGED_PYTHON_FILES" &>$ISORT_OUTPUT; then - isort "$STAGED_PYTHON_FILES" &>$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 @@ -71,7 +72,7 @@ if type flake8 &>/dev/null; then FLAKE8_OUTPUT="/tmp/gc-flake8-output.log" touch $FLAKE8_OUTPUT - if ! flake8 "$STAGED_PYTHON_FILES" &>$FLAKE8_OUTPUT; then + if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then printf "FAIL\n" cat $FLAKE8_OUTPUT checker_dirty=1 From 9da9649a45a775899cb13774e156157419a3a67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Oct 2018 00:55:54 +0200 Subject: [PATCH 111/122] Use the syncmail command as defined in custommail --- bda/tests/test_views.py | 10 ++- gestioncof/management/commands/syncmails.py | 89 --------------------- gestioncof/tests/test_views.py | 7 +- 3 files changed, 13 insertions(+), 93 deletions(-) delete mode 100644 gestioncof/management/commands/syncmails.py diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 4f906f82..6bfa3257 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -1,9 +1,12 @@ import json +import os from datetime import timedelta from unittest import mock from urllib.parse import urlencode +from django.conf import settings from django.contrib.auth.models import User +from django.core.management import call_command from django.test import Client, TestCase from django.utils import timezone @@ -49,9 +52,10 @@ class BdATestHelpers: ] def require_custommails(self): - from django.core.management import call_command - - call_command("syncmails", verbosity=0) + data_file = os.path.join( + settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json" + ) + call_command("syncmails", data_file, verbosity=0) def check_restricted_access( self, url, validate_user=user_is_cof, redirect_url=None diff --git a/gestioncof/management/commands/syncmails.py b/gestioncof/management/commands/syncmails.py deleted file mode 100644 index 0dd15d34..00000000 --- a/gestioncof/management/commands/syncmails.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Import des mails de GestioCOF dans la base de donnée -""" - -import json -import os - -from custommail.models import CustomMail, Type, Variable -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): - help = ( - "Va chercher les données mails de GestioCOF stocké au format json " - "dans /gestioncof/management/data/custommails.json. Le format des " - "données est celui donné par la commande :" - " `python manage.py dumpdata custommail --natural-foreign` " - "La bonne façon de mettre à jour ce fichier est donc de le " - "charger à l'aide de syncmails, le faire les modifications à " - "l'aide de l'interface administration et/ou du shell puis de le " - "remplacer par le nouveau résultat de la commande précédente." - ) - - def handle(self, *args, **options): - load_from_file(log=self.stdout.write) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index d65247c1..43ea148c 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -1,8 +1,10 @@ import csv +import os import uuid from datetime import timedelta from custommail.models import CustomMail +from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.messages.api import get_messages @@ -32,7 +34,10 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase): auth_forbidden = [None, "user", "member"] def requires_mails(self): - call_command("syncmails", verbosity=0) + data_file = os.path.join( + settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json" + ) + call_command("syncmails", data_file, verbosity=0) def test_get(self): r = self.client.get(self.url) From e478beee5cf39e1f454c731d0166bcb5bf3db0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Oct 2018 12:13:03 +0200 Subject: [PATCH 112/122] core.ci -- Narrow services to jobs that need them --- .gitlab-ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 95d9305d..0a0e501f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,5 @@ image: "python:3.5" -services: - - postgres:latest - - redis:latest - variables: # GestioCOF settings DJANGO_SETTINGS_MODULE: "cof.settings.prod" @@ -37,6 +33,9 @@ test: - coverage run manage.py test after_script: - coverage report + services: + - postgres:latest + - redis:latest cache: key: test paths: From b795a06b9c307777d5c7d65e6233d432a349a565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Oct 2018 12:33:26 +0200 Subject: [PATCH 113/122] core.ci -- Use postgres version of production server --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0a0e501f..35637457 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,7 +34,7 @@ test: after_script: - coverage report services: - - postgres:latest + - postgres:9.6 - redis:latest cache: key: test From 95ba7798d7e109fd27a00c0b86ba5bc78177be72 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 4 Nov 2018 23:07:57 +0100 Subject: [PATCH 114/122] Ignore VSCode files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2f3d166c..825eed63 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ media/ # PyCharm .idea .cache + +# VSCode +.vscode/ \ No newline at end of file From 00a1e79af61f829df3b8c3a1af8d6016b446a8d4 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 00:54:44 +0100 Subject: [PATCH 115/122] Fix CAS support in python-cas 1.3+ cas.eleves.ens.fr has /serviceValidate, not /p3/serviceValidate, and is thus *probably* a V2 CAS server. python-cas was broken and using /serviceValidate for V3 while it should have been /p3/serviceValidate, see https://github.com/python-cas/python-cas/commit/c3ac4b6c769fcdf735cc48f9d51707041bc699b2 --- cof/settings/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index ebc7fb2a..4c853a16 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -186,7 +186,7 @@ LOGIN_URL = "cof-login" LOGIN_REDIRECT_URL = "home" CAS_SERVER_URL = "https://cas.eleves.ens.fr/" -CAS_VERSION = "3" +CAS_VERSION = "2" CAS_LOGIN_MSG = None CAS_IGNORE_REFERER = True CAS_REDIRECT_URL = "/" From 7124821f7c44882d038eb86a01e854957122b3d3 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 21:54:08 +0100 Subject: [PATCH 116/122] =?UTF-8?q?Permet=20la=20suppression=20d'un=20voeu?= =?UTF-8?q?=20ajout=C3=A9=20mais=20non=20enregistr=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voir #203. Cette solution est horrible, tout comme le code dans lequel elle se trouve. Déso pas déso. --- bda/templates/bda/inscription-tirage.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bda/templates/bda/inscription-tirage.html b/bda/templates/bda/inscription-tirage.html index 3fd81378..9a39df6f 100644 --- a/bda/templates/bda/inscription-tirage.html +++ b/bda/templates/bda/inscription-tirage.html @@ -52,6 +52,11 @@ var django = { } else { deleteInput.attr("checked", true); } + } else { + // Reset the default values + var selects = $(form).find("select"); + $(selects[0]).val(""); + $(selects[1]).val("1"); } // callback }); From e09fa2b84742a6a3b113eac667ef93c0e703dfaa Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 22:16:43 +0100 Subject: [PATCH 117/122] Affiche un message d'erreur lors de l'enregistrement des voeux BDA Voir #203 --- bda/views.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bda/views.py b/bda/views.py index 050d6851..b67737bc 100644 --- a/bda/views.py +++ b/bda/views.py @@ -192,6 +192,7 @@ def inscription(request, tirage_id): success = False stateerror = False + form_invalid = False if request.method == "POST": # use *this* queryset dbstate = _hash_queryset(participant.choixspectacle_set.all()) @@ -204,6 +205,8 @@ def inscription(request, tirage_id): formset.save() success = True formset = BdaFormSet(instance=participant) + else: + form_invalid = True else: formset = BdaFormSet(instance=participant) # use *this* queryset @@ -217,15 +220,21 @@ def inscription(request, tirage_id): # Messages if success: messages.success( - request, "Votre inscription a été mise à jour avec " "succès !" + request, "Votre inscription a été mise à jour avec succès !" ) - if stateerror: + elif stateerror: messages.error( request, "Impossible d'enregistrer vos modifications " ": vous avez apporté d'autres modifications " "entre temps.", ) + elif form_invalid: + messages.error( + request, + "Une erreur s'est produite lors de l'enregistrement de vos vœux. " + "Avez-vous demandé plusieurs fois le même spectacle ?", + ) return render( request, "bda/inscription-tirage.html", From 511981e762acc8fba60f232b3f043685698ffd9d Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 22:22:49 +0100 Subject: [PATCH 118/122] Simplifie la Logique --- bda/views.py | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/bda/views.py b/bda/views.py index b67737bc..49d509eb 100644 --- a/bda/views.py +++ b/bda/views.py @@ -190,23 +190,31 @@ def inscription(request, tirage_id): formset=InscriptionInlineFormSet, ) - success = False - stateerror = False - form_invalid = False if request.method == "POST": # use *this* queryset dbstate = _hash_queryset(participant.choixspectacle_set.all()) if "dbstate" in request.POST and dbstate != request.POST["dbstate"]: - stateerror = True formset = BdaFormSet(instance=participant) + messages.error( + request, + "Impossible d'enregistrer vos modifications " + ": vous avez apporté d'autres modifications " + "entre temps.", + ) else: formset = BdaFormSet(request.POST, instance=participant) if formset.is_valid(): formset.save() - success = True formset = BdaFormSet(instance=participant) + messages.success( + request, "Votre inscription a été mise à jour avec succès !" + ) else: - form_invalid = True + messages.error( + request, + "Une erreur s'est produite lors de l'enregistrement de vos vœux. " + "Avez-vous demandé plusieurs fois le même spectacle ?", + ) else: formset = BdaFormSet(instance=participant) # use *this* queryset @@ -217,24 +225,6 @@ def inscription(request, tirage_id): total_price += choice.spectacle.price if choice.double: total_price += choice.spectacle.price - # Messages - if success: - messages.success( - request, "Votre inscription a été mise à jour avec succès !" - ) - elif stateerror: - messages.error( - request, - "Impossible d'enregistrer vos modifications " - ": vous avez apporté d'autres modifications " - "entre temps.", - ) - elif form_invalid: - messages.error( - request, - "Une erreur s'est produite lors de l'enregistrement de vos vœux. " - "Avez-vous demandé plusieurs fois le même spectacle ?", - ) return render( request, "bda/inscription-tirage.html", From b80927efa3c154dbcddeb7a2002081e8abdca595 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 22:44:09 +0100 Subject: [PATCH 119/122] Message d'erreur qui ne parle pas de object Voeu --- bda/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bda/views.py b/bda/views.py index 49d509eb..7f14ee37 100644 --- a/bda/views.py +++ b/bda/views.py @@ -11,6 +11,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core import serializers from django.core.urlresolvers import reverse +from django.core.exceptions import NON_FIELD_ERRORS from django.db import transaction from django.db.models import Count, Prefetch from django.forms.models import inlineformset_factory @@ -188,6 +189,12 @@ def inscription(request, tirage_id): ChoixSpectacle, fields=("spectacle", "double_choice", "priority"), formset=InscriptionInlineFormSet, + can_order=True, + error_messages={ + NON_FIELD_ERRORS: { + 'unique_together': "Vous avez déjà demandé ce voeu plus haut !", + }, + } ) if request.method == "POST": From d7f4d32c9229dc924cafb209de2de8a5fd695759 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 22:46:02 +0100 Subject: [PATCH 120/122] Oh mon dieu! --- bda/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bda/views.py b/bda/views.py index 7f14ee37..81669492 100644 --- a/bda/views.py +++ b/bda/views.py @@ -189,7 +189,6 @@ def inscription(request, tirage_id): ChoixSpectacle, fields=("spectacle", "double_choice", "priority"), formset=InscriptionInlineFormSet, - can_order=True, error_messages={ NON_FIELD_ERRORS: { 'unique_together': "Vous avez déjà demandé ce voeu plus haut !", From 042ce80b7863987ae3101112d4115a64ffe2c3cc Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 22:52:20 +0100 Subject: [PATCH 121/122] Run black --- bda/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bda/views.py b/bda/views.py index 81669492..93f37583 100644 --- a/bda/views.py +++ b/bda/views.py @@ -191,9 +191,9 @@ def inscription(request, tirage_id): formset=InscriptionInlineFormSet, error_messages={ NON_FIELD_ERRORS: { - 'unique_together': "Vous avez déjà demandé ce voeu plus haut !", - }, - } + "unique_together": "Vous avez déjà demandé ce voeu plus haut !" + } + }, ) if request.method == "POST": From 7f2f25cb710d1cbc5c0b417651a4ca02df172c62 Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 12 Nov 2018 23:04:37 +0100 Subject: [PATCH 122/122] Isort --- bda/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/views.py b/bda/views.py index 93f37583..9c4c54f7 100644 --- a/bda/views.py +++ b/bda/views.py @@ -10,8 +10,8 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core import serializers -from django.core.urlresolvers import reverse from django.core.exceptions import NON_FIELD_ERRORS +from django.core.urlresolvers import reverse from django.db import transaction from django.db.models import Count, Prefetch from django.forms.models import inlineformset_factory