diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b0c1f4c6..ce3bd041 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,13 +43,21 @@ variables: # Keep this disabled for now, as it may kill GitLab... # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' +kfettest: + stage: test + extends: .test_template + variables: + DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" + script: + - coverage run manage.py test kfet + coftest: stage: test extends: .test_template variables: DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" script: - - coverage run manage.py test gestioncof bda kfet petitscours shared --parallel + - coverage run manage.py test gestioncof bda petitscours shared --parallel bdstest: stage: test diff --git a/CHANGELOG.md b/CHANGELOG.md index 137b6860..42ab598f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ adhérents ni des cotisations. ## TODO Prod +- Lancer `python manage.py update_translation_fields` après la migration +- Mettre à jour les units systemd `daphne.service` et `worker.service` + - Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés ## Version ??? - ??/??/???? @@ -31,6 +34,8 @@ adhérents ni des cotisations. - Fixe un problème de rendu causé par l'agrandissement du menu +- Mise à jour vers Channels 3.x et Django 3.2 + ## Version 0.12 - 17/06/2022 ### K-Fêt diff --git a/bda/migrations/0019_auto_20220630_1245.py b/bda/migrations/0019_auto_20220630_1245.py new file mode 100644 index 00000000..12b7149d --- /dev/null +++ b/bda/migrations/0019_auto_20220630_1245.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2022-06-30 10:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bda", "0018_auto_20201021_1818"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="choixspectacle", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="choixspectacle", + constraint=models.UniqueConstraint( + fields=("participant", "spectacle"), name="unique_participation" + ), + ), + ] diff --git a/bda/models.py b/bda/models.py index 578f235c..9bc2ce3c 100644 --- a/bda/models.py +++ b/bda/models.py @@ -253,7 +253,11 @@ class ChoixSpectacle(models.Model): class Meta: ordering = ("priority",) - unique_together = (("participant", "spectacle"),) + constraints = [ + models.UniqueConstraint( + fields=["participant", "spectacle"], name="unique_participation" + ) + ] verbose_name = "voeu" verbose_name_plural = "voeux" diff --git a/bda/templates/bda-attrib.html b/bda/templates/bda-attrib.html index fac0de67..057cacb4 100644 --- a/bda/templates/bda-attrib.html +++ b/bda/templates/bda-attrib.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block extra_head %} diff --git a/bda/templates/bda/etat-places.html b/bda/templates/bda/etat-places.html index 401cc856..d1af0667 100644 --- a/bda/templates/bda/etat-places.html +++ b/bda/templates/bda/etat-places.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %}

État des inscriptions BdA

diff --git a/bda/templates/bda/inscription-tirage.html b/bda/templates/bda/inscription-tirage.html index 3f8091df..1eecd7af 100644 --- a/bda/templates/bda/inscription-tirage.html +++ b/bda/templates/bda/inscription-tirage.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block extra_head %} diff --git a/bda/templates/bda/participants.html b/bda/templates/bda/participants.html index 4ab2d1f7..c99e5182 100644 --- a/bda/templates/bda/participants.html +++ b/bda/templates/bda/participants.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %}

{{ spectacle }}

diff --git a/bda/templates/bda/revente/confirm-shotgun.html b/bda/templates/bda/revente/confirm-shotgun.html index d7614c25..bf8dccba 100644 --- a/bda/templates/bda/revente/confirm-shotgun.html +++ b/bda/templates/bda/revente/confirm-shotgun.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {%block realcontent %} diff --git a/bda/templates/bda/revente/confirmed.html b/bda/templates/bda/revente/confirmed.html index 780330bd..6f8ee583 100644 --- a/bda/templates/bda/revente/confirmed.html +++ b/bda/templates/bda/revente/confirmed.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %}

Inscription à une revente

diff --git a/bda/templates/bda/revente/mail-success.html b/bda/templates/bda/revente/mail-success.html index 5e970eb7..6340a451 100644 --- a/bda/templates/bda/revente/mail-success.html +++ b/bda/templates/bda/revente/mail-success.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %} diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index cd09f997..c42e0203 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %} diff --git a/bda/templates/bda/revente/subscribe.html b/bda/templates/bda/revente/subscribe.html index e0a7176c..c91fff15 100644 --- a/bda/templates/bda/revente/subscribe.html +++ b/bda/templates/bda/revente/subscribe.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles%} +{% load static %} {% block realcontent %}

Inscriptions pour BdA-Revente

diff --git a/bda/templates/bda/revente/tirages.html b/bda/templates/bda/revente/tirages.html index 4d9ac126..6ef55e03 100644 --- a/bda/templates/bda/revente/tirages.html +++ b/bda/templates/bda/revente/tirages.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %} diff --git a/bda/templates/spectacle_list.html b/bda/templates/spectacle_list.html index 4539d730..1ffd7cc3 100644 --- a/bda/templates/spectacle_list.html +++ b/bda/templates/spectacle_list.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block extra_head %} diff --git a/bda/urls.py b/bda/urls.py index 5b452362..726c4057 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -1,74 +1,80 @@ -from django.conf.urls import url +from django.urls import re_path from bda import views from bda.views import SpectacleListView from gestioncof.decorators import buro_required urlpatterns = [ - url( + re_path( 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, name="bda-tirage"), - url( + re_path(r"^places/(?P\d+)$", views.places, name="bda-places-attribuees"), + re_path( + r"^etat-places/(?P\d+)$", views.etat_places, name="bda-etat-places" + ), + re_path(r"^tirage/(?P\d+)$", views.tirage, name="bda-tirage"), + re_path( r"^spectacles/(?P\d+)$", buro_required(SpectacleListView.as_view()), name="bda-liste-spectacles", ), - url( + re_path( r"^spectacles/(?P\d+)/(?P\d+)$", views.spectacle, name="bda-spectacle", ), - url( + re_path( r"^spectacles/unpaid/(?P\d+)$", views.UnpaidParticipants.as_view(), name="bda-unpaid", ), - url( + re_path( r"^spectacles/autocomplete$", views.spectacle_autocomplete, name="bda-spectacle-autocomplete", ), - url( + re_path( r"^participants/autocomplete$", views.participant_autocomplete, name="bda-participant-autocomplete", ), # Urls BdA-Revente - url( + re_path( r"^revente/(?P\d+)/manage$", views.revente_manage, name="bda-revente-manage", ), - url( + re_path( r"^revente/(?P\d+)/subscribe$", views.revente_subscribe, name="bda-revente-subscribe", ), - url( + re_path( r"^revente/(?P\d+)/tirages$", views.revente_tirages, name="bda-revente-tirages", ), - url( + re_path( r"^revente/(?P\d+)/buy$", views.revente_buy, name="bda-revente-buy", ), - url( + re_path( r"^revente/(?P\d+)/confirm$", views.revente_confirm, name="bda-revente-confirm", ), - url( + re_path( 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"^catalogue/(?P[a-z]+)$", views.catalogue, name="bda-catalogue"), + re_path( + r"^mails-rappel/(?P\d+)$", views.send_rappel, name="bda-rappels" + ), + re_path( + r"^catalogue/(?P[a-z]+)$", views.catalogue, name="bda-catalogue" + ), ] diff --git a/bds/__init__.py b/bds/__init__.py index 5c287005..e69de29b 100644 --- a/bds/__init__.py +++ b/bds/__init__.py @@ -1 +0,0 @@ -default_app_config = "bds.apps.BdsConfig" diff --git a/bds/apps.py b/bds/apps.py index 5c0fa0fd..740d3559 100644 --- a/bds/apps.py +++ b/bds/apps.py @@ -1,5 +1,4 @@ -from django import apps as global_apps -from django.apps import AppConfig +from django.apps import AppConfig, apps as global_apps from django.db.models import Q from django.db.models.signals import post_migrate diff --git a/bds/templates/bds/base.html b/bds/templates/bds/base.html index f456f6dc..74759e88 100644 --- a/bds/templates/bds/base.html +++ b/bds/templates/bds/base.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} {% load bulma_utils %} diff --git a/bds/tests/test_views.py b/bds/tests/test_views.py index ef6139f4..1cfe6199 100644 --- a/bds/tests/test_views.py +++ b/bds/tests/test_views.py @@ -1,3 +1,4 @@ +from datetime import date from unittest import mock from django.conf import settings @@ -6,6 +7,8 @@ from django.contrib.auth.models import Permission from django.test import Client, TestCase from django.urls import reverse, reverse_lazy +from bds.models import BDSProfile + User = get_user_model() @@ -24,19 +27,19 @@ def login_url(next=None): class TestHomeView(TestCase): @mock.patch("gestioncof.signals.messages") - def test_get(self, mock_messages): + def test_get(self, _): user = User.objects.create_user(username="random_user") give_bds_buro_permissions(user) self.client.force_login( user, backend="django.contrib.auth.backends.ModelBackend" ) resp = self.client.get(reverse("bds:home")) - self.assertEquals(resp.status_code, 200) + self.assertEqual(resp.status_code, 200) class TestRegistrationView(TestCase): @mock.patch("gestioncof.signals.messages") - def test_get_autocomplete(self, mock_messages): + def test_get_autocomplete(self, _): user = User.objects.create_user(username="toto") url = reverse("bds:autocomplete") + "?q=foo" client = Client() @@ -48,15 +51,15 @@ class TestRegistrationView(TestCase): # Logged-in but unprivileged GET client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") resp = client.get(url) - self.assertEquals(resp.status_code, 403) + self.assertEqual(resp.status_code, 403) # Burô user GET give_bds_buro_permissions(user) resp = client.get(url) - self.assertEquals(resp.status_code, 200) + self.assertEqual(resp.status_code, 200) @mock.patch("gestioncof.signals.messages") - def test_get(self, mock_messages): + def test_get(self, _): user = User.objects.create_user(username="toto") url = reverse("bds:user.update", args=(user.id,)) client = Client() @@ -65,6 +68,66 @@ class TestRegistrationView(TestCase): resp = client.get(url) self.assertRedirects(resp, login_url(next=url)) + # Logged-in but unprivileged GET + client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") + resp = client.get(url) + self.assertEqual(resp.status_code, 403) + + # Burô user GET + give_bds_buro_permissions(user) + resp = client.get(url) + self.assertEqual(resp.status_code, 200) + + +class TestResetMemberships(TestCase): + @classmethod + def setUpTestData(cls): + cls.admin = User.objects.create_user(username="bds_admin") + + # Create users and profiles + cls.u_1 = User.objects.create_user(username="bds-01") + cls.p_1 = BDSProfile.objects.create(user=cls.u_1) + + cls.u_2 = User.objects.create_user(username="bds-02") + cls.p_2 = BDSProfile.objects.create(user=cls.u_2) + + cls.u_3 = User.objects.create_user(username="bds-03") + cls.p_3 = BDSProfile.objects.create(user=cls.u_3) + + cls.u_4 = User.objects.create_user(username="bds-04") + cls.p_4 = BDSProfile.objects.create(user=cls.u_4) + + @mock.patch("gestioncof.signals.messages") + def setUp(self, _): + give_bds_buro_permissions(self.admin) + # bds-01 est membre à l'année + self.p_1.is_member = True + self.p_1.mails_bds = True + self.p_1.cotisation_period = "ANN" + self.p_1.save() + + # bds-02 est membre au S1 + self.p_2.is_member = True + self.p_2.mails_bds = True + self.p_2.cotisation_period = "SE1" + self.p_2.save() + + # bds-03 est membre au S2 + self.p_3.is_member = True + self.p_3.mails_bds = True + self.p_3.cotisation_period = "SE2" + self.p_3.save() + + @mock.patch("gestioncof.signals.messages") + def test_get_expired(self, _): + user = User.objects.create_user(username="toto") + url = reverse("bds:members.expired") + client = Client() + + # Anonymous GET + resp = client.get(url) + self.assertRedirects(resp, login_url(next=url)) + # Logged-in but unprivileged GET client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") resp = client.get(url) @@ -74,3 +137,114 @@ class TestRegistrationView(TestCase): give_bds_buro_permissions(user) resp = client.get(url) self.assertEquals(resp.status_code, 200) + + self.assertQuerysetEqual( + resp.context_data["object_list"], BDSProfile.expired_members() + ) + + @mock.patch("gestioncof.signals.messages") + def test_get_reset(self, _): + user = User.objects.create_user(username="tata") + url = reverse("bds:members.reset") + client = Client() + + # Anonymous GET + resp = client.get(url) + self.assertRedirects(resp, login_url(next=url)) + + # Logged-in but unprivileged GET + client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") + resp = client.get(url) + self.assertEquals(resp.status_code, 403) + + # Burô user GET + give_bds_buro_permissions(user) + resp = client.get(url) + self.assertRedirects(resp, reverse("bds:members.expired")) + + def test_expired_memberships(self): + # In september, there is no expired members + with mock.patch("bds.models.timezone.now", return_value=date(2022, 9, 1)): + self.assertQuerysetEqual(BDSProfile.expired_members(), []) + + # In march, only bds-02's membership is expired + with mock.patch("django.utils.timezone.now", return_value=date(2023, 3, 1)): + self.assertQuerysetEqual(BDSProfile.expired_members(), [self.p_2]) + + # During summer, all memberships are expired + with mock.patch("django.utils.timezone.now", return_value=date(2023, 7, 1)): + self.assertQuerysetEqual( + BDSProfile.expired_members(), + [self.p_1, self.p_2, self.p_3], + ordered=False, + ) + + @mock.patch("gestioncof.signals.messages") + def test_reset_memberships(self, _): + url = reverse("bds:members.reset") + + # Reset during S1 does nothig + with mock.patch("bds.models.timezone.now", return_value=date(2022, 9, 1)): + self.client.force_login( + self.admin, backend="django.contrib.auth.backends.ModelBackend" + ) + resp = self.client.get(url) + + self.assertRedirects(resp, reverse("bds:members.expired")) + + self.assertQuerysetEqual(BDSProfile.expired_members(), []) + + self.assertQuerysetEqual( + BDSProfile.objects.filter(is_member=True), + [self.p_1, self.p_2, self.p_3], + ordered=False, + ) + + # Reset in march + with mock.patch("django.utils.timezone.now", return_value=date(2023, 3, 1)): + self.client.force_login( + self.admin, backend="django.contrib.auth.backends.ModelBackend" + ) + resp = self.client.get(url) + + self.assertRedirects(resp, reverse("bds:members.expired")) + + # After a reset we have no expired memberships + self.assertQuerysetEqual(BDSProfile.expired_members(), []) + + # Test reset attributes + self.p_2.refresh_from_db() + self.assertEqual( + [self.p_2.is_member, self.p_2.mails_bds, self.p_2.cotisation_period], + [False, False, "NO"], + ) + + # Reset during summer + with mock.patch("django.utils.timezone.now", return_value=date(2023, 7, 1)): + # bds-02's membership wa already reset + self.assertQuerysetEqual( + BDSProfile.expired_members(), [self.p_1, self.p_3], ordered=False + ) + + self.client.force_login( + self.admin, backend="django.contrib.auth.backends.ModelBackend" + ) + resp = self.client.get(url) + + self.assertRedirects(resp, reverse("bds:members.expired")) + + # After a reset we have no expired memberships + self.assertQuerysetEqual(BDSProfile.expired_members(), []) + + # Test reset attributes + self.p_1.refresh_from_db() + self.assertEqual( + [self.p_1.is_member, self.p_1.mails_bds, self.p_1.cotisation_period], + [False, False, "NO"], + ) + + self.p_3.refresh_from_db() + self.assertEqual( + [self.p_3.is_member, self.p_3.mails_bds, self.p_3.cotisation_period], + [False, False, "NO"], + ) diff --git a/events/migrations/0005_auto_20220630_1239.py b/events/migrations/0005_auto_20220630_1239.py new file mode 100644 index 00000000..d2624da2 --- /dev/null +++ b/events/migrations/0005_auto_20220630_1239.py @@ -0,0 +1,63 @@ +# Generated by Django 3.2.13 on 2022-06-30 10:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0004_unique_constraints"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="extrafield", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="extrafieldcontent", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="option", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="optionchoice", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="registration", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="extrafield", + constraint=models.UniqueConstraint( + fields=("event", "name"), name="unique_extra_field" + ), + ), + migrations.AddConstraint( + model_name="extrafieldcontent", + constraint=models.UniqueConstraint( + fields=("field", "registration"), name="unique_extra_field_content" + ), + ), + migrations.AddConstraint( + model_name="option", + constraint=models.UniqueConstraint( + fields=("event", "name"), name="unique_event_option" + ), + ), + migrations.AddConstraint( + model_name="optionchoice", + constraint=models.UniqueConstraint( + fields=("option", "choice"), name="unique_option_choice" + ), + ), + migrations.AddConstraint( + model_name="registration", + constraint=models.UniqueConstraint( + fields=("event", "user"), name="unique_registration" + ), + ), + ] diff --git a/events/models.py b/events/models.py index 7b536c86..a421e8a3 100644 --- a/events/models.py +++ b/events/models.py @@ -72,9 +72,13 @@ class Option(models.Model): multi_choices = models.BooleanField(_("choix multiples"), default=False) class Meta: + constraints = [ + models.UniqueConstraint( + fields=["event", "name"], name="unique_event_option" + ) + ] verbose_name = _("option d'événement") verbose_name_plural = _("options d'événement") - unique_together = [["event", "name"]] def __str__(self): return self.name @@ -87,9 +91,13 @@ class OptionChoice(models.Model): choice = models.CharField(_("choix"), max_length=200) class Meta: + constraints = [ + models.UniqueConstraint( + fields=["option", "choice"], name="unique_option_choice" + ) + ] verbose_name = _("choix d'option d'événement") verbose_name_plural = _("choix d'option d'événement") - unique_together = [["option", "choice"]] def __str__(self): return self.choice @@ -118,7 +126,9 @@ class ExtraField(models.Model): field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE) class Meta: - unique_together = [["event", "name"]] + constraints = [ + models.UniqueConstraint(fields=["event", "name"], name="unique_extra_field") + ] class ExtraFieldContent(models.Model): @@ -137,9 +147,13 @@ class ExtraFieldContent(models.Model): ) class Meta: + constraints = [ + models.UniqueConstraint( + fields=["field", "registration"], name="unique_extra_field_content" + ) + ] verbose_name = _("contenu d'un champ événement supplémentaire") verbose_name_plural = _("contenus d'un champ événement supplémentaire") - unique_together = [["field", "registration"]] def __str__(self): max_length = 50 @@ -163,9 +177,13 @@ class Registration(models.Model): ) class Meta: + constraints = [ + models.UniqueConstraint( + fields=["event", "user"], name="unique_registration" + ) + ] verbose_name = _("inscription à un événement") verbose_name_plural = _("inscriptions à un événement") - unique_together = [["event", "user"]] def __str__(self): return "inscription de {} à {}".format(self.user, self.event) diff --git a/gestioasso/asgi.py b/gestioasso/asgi.py index 773acaa0..728a3433 100644 --- a/gestioasso/asgi.py +++ b/gestioasso/asgi.py @@ -1,8 +1,15 @@ +""" +ASGI entrypoint. Configures Django and then runs the application +defined in the ASGI_APPLICATION setting. +""" + import os -from channels.asgi import get_channel_layer +import django +from channels.routing import get_default_application if "DJANGO_SETTINGS_MODULE" not in os.environ: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local") -channel_layer = get_channel_layer() +django.setup() +application = get_default_application() diff --git a/gestioasso/routing.py b/gestioasso/routing.py index 3c2e5718..2b42648a 100644 --- a/gestioasso/routing.py +++ b/gestioasso/routing.py @@ -1,3 +1,20 @@ -from channels.routing import include +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application +from django.urls import path -routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")] +from kfet.routing import KFRouter + +application = ProtocolTypeRouter( + { + # WebSocket chat handler + "websocket": AuthMiddlewareStack( + URLRouter( + [ + path("ws/k-fet", KFRouter), + ] + ) + ), + "http": get_asgi_application(), + } +) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index ebfef337..05a7b2c9 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -85,7 +85,6 @@ MIDDLEWARE = ( + MIDDLEWARE + [ "djconfig.middleware.DjConfigMiddleware", - "wagtail.core.middleware.SiteMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware", ] ) @@ -109,6 +108,8 @@ MEDIA_URL = "/gestion/media/" CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") +ASGI_APPLICATION = "gestioasso.routing.application" + # --- # Auth-related stuff # --- @@ -147,7 +148,7 @@ CACHES = { CHANNEL_LAYERS = { "default": { - "BACKEND": "asgi_redis.RedisChannelLayer", + "BACKEND": "shared.channels.ChannelLayer", "CONFIG": { "hosts": [ ( @@ -160,11 +161,9 @@ CHANNEL_LAYERS = { ) ] }, - "ROUTING": "gestioasso.routing.routing", } } - # --- # reCAPTCHA settings # https://github.com/praekelt/django-recaptcha diff --git a/gestioasso/settings/common.py b/gestioasso/settings/common.py index cabe7000..13f2e5b1 100644 --- a/gestioasso/settings/common.py +++ b/gestioasso/settings/common.py @@ -101,7 +101,7 @@ TEMPLATES = [ DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", "NAME": DBNAME, "USER": DBUSER, "PASSWORD": DBPASSWD, @@ -111,6 +111,7 @@ DATABASES = { SITE_ID = 1 +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # --- # Internationalization diff --git a/gestioasso/settings/local.py b/gestioasso/settings/local.py index 5c8c2734..2cba6a10 100644 --- a/gestioasso/settings/local.py +++ b/gestioasso/settings/local.py @@ -47,8 +47,7 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache" # Use the default in memory asgi backend for local development CHANNEL_LAYERS = { "default": { - "BACKEND": "asgiref.inmemory.ChannelLayer", - "ROUTING": "gestioasso.routing.routing", + "BACKEND": "channels.layers.InMemoryChannelLayer", } } diff --git a/gestioncof/__init__.py b/gestioncof/__init__.py index 3bb260b9..e69de29b 100644 --- a/gestioncof/__init__.py +++ b/gestioncof/__init__.py @@ -1 +0,0 @@ -default_app_config = "gestioncof.apps.GestioncofConfig" diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 89e4160d..bb90cf98 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import Group, Permission, User from django.db.models import Q from django.urls import reverse from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from gestioncof.models import ( Club, diff --git a/gestioncof/cms/__init__.py b/gestioncof/cms/__init__.py index 043b644d..e69de29b 100644 --- a/gestioncof/cms/__init__.py +++ b/gestioncof/cms/__init__.py @@ -1 +0,0 @@ -default_app_config = "gestioncof.cms.apps.COFCMSAppConfig" diff --git a/gestioncof/cms/templatetags/cofcms_tags.py b/gestioncof/cms/templatetags/cofcms_tags.py index f9e62aed..36774232 100644 --- a/gestioncof/cms/templatetags/cofcms_tags.py +++ b/gestioncof/cms/templatetags/cofcms_tags.py @@ -2,7 +2,7 @@ from datetime import date, timedelta from django import template from django.utils import formats, timezone -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from ..models import COFActuPage, COFRootPage diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 2a57f970..1d482e7f 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.forms import AuthenticationForm from django.forms.formsets import BaseFormSet, formset_factory from django.forms.widgets import CheckboxSelectMultiple, RadioSelect -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from djconfig.forms import ConfigForm from bda.models import Spectacle @@ -276,7 +276,9 @@ class RegistrationProfileForm(forms.ModelForm): self.fields["mailing_bda_revente"].initial = True self.fields["mailing_unernestaparis"].initial = True - self.fields.keyOrder = [ + class Meta: + model = CofProfile + fields = [ "login_clipper", "phone", "occupation", @@ -290,22 +292,6 @@ class RegistrationProfileForm(forms.ModelForm): "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", - ) - STATUS_CHOICES = ( ("no", "Non"), diff --git a/gestioncof/migrations/0019_auto_20220630_1241.py b/gestioncof/migrations/0019_auto_20220630_1241.py new file mode 100644 index 00000000..eec1cfe3 --- /dev/null +++ b/gestioncof/migrations/0019_auto_20220630_1241.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.13 on 2022-06-30 10:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("gestioncof", "0018_petitscours_email"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="eventregistration", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="petitcoursability", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="surveyanswer", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="eventregistration", + constraint=models.UniqueConstraint( + fields=("user", "event"), name="unique_event_registration" + ), + ), + migrations.AddConstraint( + model_name="petitcoursability", + constraint=models.UniqueConstraint( + fields=("user", "niveau", "matiere"), name="unique_competence_level" + ), + ), + migrations.AddConstraint( + model_name="surveyanswer", + constraint=models.UniqueConstraint( + fields=("user", "survey"), name="unique_survey_answer" + ), + ), + ] diff --git a/gestioncof/models.py b/gestioncof/models.py index c2c71660..e8f6fc57 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import User from django.db import models from django.db.models.signals import post_delete, post_save from django.dispatch import receiver -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from bda.models import Spectacle from shared.utils import choices_length @@ -193,8 +193,12 @@ class EventRegistration(models.Model): paid = models.BooleanField("A payé", default=False) class Meta: + constraints = [ + models.UniqueConstraint( + fields=["user", "event"], name="unique_event_registration" + ) + ] verbose_name = "Inscription" - unique_together = ("user", "event") def __str__(self): return "Inscription de {} à {}".format(self.user, self.event.title) @@ -246,8 +250,12 @@ class SurveyAnswer(models.Model): answers = models.ManyToManyField(SurveyQuestionAnswer, related_name="selected_by") class Meta: + constraints = [ + models.UniqueConstraint( + fields=["user", "survey"], name="unique_survey_answer" + ) + ] verbose_name = "Réponses" - unique_together = ("user", "survey") def __str__(self): return "Réponse de %s sondage %s" % ( diff --git a/gestioncof/signals.py b/gestioncof/signals.py index cf4b1f16..deed6ce9 100644 --- a/gestioncof/signals.py +++ b/gestioncof/signals.py @@ -1,7 +1,7 @@ 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.utils.translation import gettext_lazy as _ from django_cas_ng.signals import cas_user_authenticated diff --git a/gestioncof/templates/base.html b/gestioncof/templates/base.html index d313ee9d..7020e3c5 100644 --- a/gestioncof/templates/base.html +++ b/gestioncof/templates/base.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} diff --git a/gestioncof/templates/registration.html b/gestioncof/templates/registration.html index 2ef997e1..9807afde 100644 --- a/gestioncof/templates/registration.html +++ b/gestioncof/templates/registration.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block page_size %}col-sm-8{% endblock %} diff --git a/gestioncof/templates/tristate_js.html b/gestioncof/templates/tristate_js.html index af906ebe..6b5312a8 100644 --- a/gestioncof/templates/tristate_js.html +++ b/gestioncof/templates/tristate_js.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index 100e0825..9deeefae 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -1,5 +1,5 @@ {% extends 'kfet/base_col_2.html' %} -{% load l10n staticfiles widget_tweaks bootstrap %} +{% load l10n static widget_tweaks bootstrap %} {% block extra_head %} diff --git a/kfet/templates/kfet/home.html b/kfet/templates/kfet/home.html index e5175dc3..8704bbe9 100644 --- a/kfet/templates/kfet/home.html +++ b/kfet/templates/kfet/home.html @@ -1,5 +1,5 @@ {% extends "kfet/base_col_1.html" %} -{% load staticfiles %} +{% load static %} {% load kfet_tags %} {% block title %}Accueil{% endblock %} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index a89c4072..b67e298d 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -1,5 +1,5 @@ {% extends 'kfet/base.html' %} -{% load staticfiles %} +{% load static %} {% block extra_head %} diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html index f8bef33d..549727e0 100644 --- a/kfet/templates/kfet/transfers.html +++ b/kfet/templates/kfet/transfers.html @@ -1,6 +1,5 @@ {% extends 'kfet/base_col_2.html' %} -{% load staticfiles %} -{% load l10n staticfiles widget_tweaks %} +{% load l10n static widget_tweaks %} {% block title %}Transferts{% endblock %} {% block header-title %}Transferts{% endblock %} diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index fc429d97..3a85264d 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -1,5 +1,5 @@ {% extends "kfet/base_col_1.html" %} -{% load staticfiles %} +{% load static %} {% block extra_head %} diff --git a/kfet/tests/test_tests_utils.py b/kfet/tests/test_tests_utils.py index 49661e23..2c42ff79 100644 --- a/kfet/tests/test_tests_utils.py +++ b/kfet/tests/test_tests_utils.py @@ -94,6 +94,7 @@ class PermHelpersTest(TestCaseMixin, TestCase): self.assertQuerysetEqual( user.user_permissions.all(), map(repr, [self.perm1, self.perm2, self.perm_team]), + transform=repr, ordered=False, ) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 7a7eddcb..d09ff3e8 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta from decimal import Decimal from unittest import mock +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer from django.contrib.auth.models import User from django.test import Client, TestCase from django.urls import reverse @@ -518,6 +520,7 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( group.permissions.all(), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), + transform=repr, ordered=False, ) @@ -571,6 +574,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( self.group.permissions.all(), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), + transform=repr, ordered=False, ) @@ -598,6 +602,7 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context["negatives"], map(repr, [self.accounts["user"].negative]), + transform=repr, ordered=False, ) @@ -698,7 +703,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): for stat, expected in zip(content["stats"], expected_stats): expected_url = expected.pop("url") self.assertUrlsEqual(stat["url"], expected_url) - self.assertDictContainsSubset(expected, stat) + self.assertEqual(stat, {**stat, **expected}) class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): @@ -807,7 +812,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): for stat, expected in zip(content["stats"], expected_stats): expected_url = expected.pop("url") self.assertUrlsEqual(stat["url"], expected_url) - self.assertDictContainsSubset(expected, stat) + self.assertEqual(stat, {**stat, **expected}) class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): @@ -875,6 +880,7 @@ class CheckoutListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context["checkouts"], map(repr, [self.checkout1, self.checkout2]), + transform=repr, ordered=False, ) @@ -1065,6 +1071,7 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context["checkoutstatements"], map(repr, expected_statements), + transform=repr, ordered=False, ) @@ -1291,7 +1298,9 @@ class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase): 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]), + transform=repr, ) @@ -1366,7 +1375,9 @@ class ArticleListViewTests(ViewTestCaseMixin, TestCase): 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]), + transform=repr, ) @@ -1636,7 +1647,7 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): for stat, expected in zip(content["stats"], expected_stats): expected_url = expected.pop("url") self.assertUrlsEqual(stat["url"], expected_url) - self.assertDictContainsSubset(expected, stat) + self.assertEqual(stat, {**stat, **expected}) class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): @@ -1705,7 +1716,7 @@ class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase): expected = {"name": "Checkout", "balance": "10.00"} - self.assertDictContainsSubset(expected, content) + self.assertEqual(content, {**content, **expected}) self.assertSetEqual( set(content.keys()), @@ -1808,10 +1819,13 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): 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) + # Create a channel to listen to KPsul's messages + channel_layer = get_channel_layer() + self.channel = async_to_sync(channel_layer.new_channel)() + + async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel) + + self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel) # Reset cache of kfet config kfet_config._conf_init = False @@ -2043,9 +2057,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.article.stock, 18) # Check websocket data - self.kpsul_consumer_mock.group_send.assert_called_once_with( - "kfet.kpsul", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "groups": [ { "add": True, @@ -2307,9 +2324,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): 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", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "groups": [ { "add": True, @@ -2478,9 +2498,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): 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", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "groups": [ { "add": True, @@ -2635,9 +2658,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): 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", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "groups": [ { "add": True, @@ -2750,9 +2776,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]["groups"][0][ - "entries" - ][0] + ws_data = self.receive_msg() + ws_data_ope = ws_data["groups"][0]["entries"][0] + self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2790,9 +2816,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]["groups"][0][ - "entries" - ][0] + ws_data = self.receive_msg() + ws_data_ope = ws_data["groups"][0]["entries"][0] + self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2828,9 +2854,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]["groups"][0][ - "entries" - ][0] + ws_data = self.receive_msg() + ws_data_ope = ws_data["groups"][0]["entries"][0] + self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2864,9 +2890,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]["groups"][0][ - "entries" - ][0] + ws_data = self.receive_msg() + ws_data_ope = ws_data["groups"][0]["entries"][0] + self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -2899,9 +2925,9 @@ 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]["groups"][0][ - "entries" - ][0] + ws_data = self.receive_msg() + ws_data_ope = ws_data["groups"][0]["entries"][0] + self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -3123,9 +3149,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(article2.stock, -6) # Check websocket data - self.kpsul_consumer_mock.group_send.assert_called_once_with( - "kfet.kpsul", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "groups": [ { "add": True, @@ -3218,10 +3247,14 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): 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) + # Create a channel to listen to KPsul's messages + channel_layer = get_channel_layer() + + self.channel = async_to_sync(channel_layer.new_channel)() + + async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel) + + self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel) def _assertResponseOk(self, response): """ @@ -3271,7 +3304,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): on_acc=self.account, checkout=self.checkout, content=[ - {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2} + { + "type": Operation.PURCHASE, + "article": self.article, + "article_nb": 2, + } ], ) operation = group.opes.get() @@ -3345,9 +3382,15 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - self.kpsul_consumer_mock.group_send.assert_called_with( - "kfet.kpsul", - {"checkouts": [], "articles": [{"id": self.article.pk, "stock": 22}]}, + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, + { + "type": "kpsul", + "checkouts": [], + "articles": [{"id": self.article.pk, "stock": 22}], + }, ) def test_purchase_with_addcost(self): @@ -3407,11 +3450,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("95.00")) - ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][ - "checkouts" - ] + ws_data = self.receive_msg() + self.assertListEqual( - ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("95.00")}] + ws_data["checkouts"], + [{"id": self.checkout.pk, "balance": Decimal("95.00")}], ) def test_purchase_cash_with_addcost(self): @@ -3447,11 +3490,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): addcost_account.refresh_from_db() self.assertEqual(addcost_account.balance, Decimal("9.00")) - ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][ - "checkouts" - ] + ws_data = self.receive_msg() + self.assertListEqual( - ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("94.00")}] + ws_data["checkouts"], + [{"id": self.checkout.pk, "balance": Decimal("94.00")}], ) @mock.patch("django.utils.timezone.now") @@ -3533,9 +3576,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("89.25")) - self.kpsul_consumer_mock.group_send.assert_called_with( - "kfet.kpsul", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}], "articles": [], }, @@ -3620,9 +3666,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("110.75")) - self.kpsul_consumer_mock.group_send.assert_called_with( - "kfet.kpsul", + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, { + "type": "kpsul", "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}], "articles": [], }, @@ -3707,9 +3756,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - self.kpsul_consumer_mock.group_send.assert_called_with( - "kfet.kpsul", - {"checkouts": [], "articles": []}, + ws_data = self.receive_msg() + + self.assertDictEqual( + ws_data, + {"type": "kpsul", "checkouts": [], "articles": []}, ) @mock.patch("django.utils.timezone.now") @@ -4049,7 +4100,7 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase): ] for expected, article in zip(expected_list, articles): - self.assertDictContainsSubset(expected, article) + self.assertEqual(article, {**article, **expected}) self.assertSetEqual( set(article.keys()), set( @@ -4149,7 +4200,7 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): content = json.loads(r.content.decode("utf-8")) expected = {"name": "first last", "trigramme": "000", "balance": "0.00"} - self.assertDictContainsSubset(expected, content) + self.assertEqual(content, {**content, **expected}) self.assertSetEqual( set(content.keys()), @@ -4393,7 +4444,9 @@ class InventoryListViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) inventories = r.context["inventories"] - self.assertQuerysetEqual(inventories, map(repr, [self.inventory])) + self.assertQuerysetEqual( + inventories, map(repr, [self.inventory]), transform=repr + ) class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): @@ -4580,7 +4633,7 @@ class OrderListViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) orders = r.context["orders"] - self.assertQuerysetEqual(orders, map(repr, [self.order])) + self.assertQuerysetEqual(orders, map(repr, [self.order]), transform=repr) class OrderReadViewTests(ViewTestCaseMixin, TestCase): @@ -4795,7 +4848,9 @@ class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase): inventory, {"by": self.accounts["team1"], "at": self.now, "order": self.order}, ) - self.assertQuerysetEqual(inventory.articles.all(), map(repr, [self.article])) + self.assertQuerysetEqual( + inventory.articles.all(), map(repr, [self.article]), transform=repr + ) compte = InventoryArticle.objects.get(article=self.article) diff --git a/kfet/utils.py b/kfet/utils.py index 0c4f170a..540f260c 100644 --- a/kfet/utils.py +++ b/kfet/utils.py @@ -1,8 +1,7 @@ import json import math -from channels.channel import Group -from channels.generic.websockets import JsonWebsocketConsumer +from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.core.cache import cache from django.core.serializers.json import DjangoJSONEncoder @@ -63,7 +62,7 @@ class CachedMixin: # Consumers -class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): +class DjangoJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): """Custom Json Websocket Consumer. Encode to JSON with DjangoJSONEncoder. @@ -71,7 +70,10 @@ class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): """ @classmethod - def encode_json(cls, content): + async def encode_json(cls, content): + # Remove the type value, only used by Channels to choose the group to send to + content.pop("type") + return json.dumps(content, cls=DjangoJSONEncoder) @@ -89,31 +91,11 @@ class PermConsumerMixin: http_user = True # Enable message.user perms_connect = [] - def connect(self, message, **kwargs): + async def connect(self): """Check permissions on connection.""" - if message.user.has_perms(self.perms_connect): - super().connect(message, **kwargs) + self.user = self.scope["user"] + + if self.user.has_perms(self.perms_connect): + await super().connect() else: - self.close() - - def raw_connect(self, message, **kwargs): - # Same as original raw_connect method of JsonWebsocketConsumer - # 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).add(message.reply_channel) - self.connect(message, **kwargs) - - def raw_disconnect(self, message, **kwargs): - # Same as original raw_connect method of JsonWebsocketConsumer - # 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 - ) - self.disconnect(message, **kwargs) - - def connection_groups(self, user, **kwargs): - """`message.user` is available as `user` arg. Original behavior.""" - return super().connection_groups(user=user, **kwargs) + await self.close() diff --git a/kfet/views.py b/kfet/views.py index 569e42a5..bd25f3fe 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -31,10 +31,11 @@ from django.views.generic.detail import BaseDetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView from gestioncof.models import CofProfile -from kfet import KFET_DELETED_TRIGRAMME, consumers +from kfet import KFET_DELETED_TRIGRAMME from kfet.auth.decorators import kfet_password_auth from kfet.autocomplete import kfet_account_only_autocomplete, kfet_autocomplete from kfet.config import kfet_config +from kfet.consumers import KPsul from kfet.decorators import teamkfet_required from kfet.forms import ( AccountForm, @@ -984,8 +985,13 @@ def kpsul_update_addcost(request): 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}, + "type": "kpsul", + } + + KPsul.group_send("kfet.kpsul", data) + return JsonResponse(data) @@ -1169,7 +1175,7 @@ def kpsul_perform_operations(request): ) # Websocket data - websocket_data = {} + websocket_data = {"type": "kpsul"} websocket_data["groups"] = [ { "add": True, @@ -1216,7 +1222,9 @@ def kpsul_perform_operations(request): websocket_data["articles"].append( {"id": article["id"], "stock": article["stock"]} ) - consumers.KPsul.group_send("kfet.kpsul", websocket_data) + + KPsul.group_send("kfet.kpsul", websocket_data) + return JsonResponse(data) @@ -1411,7 +1419,7 @@ def cancel_operations(request): articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) # Websocket data - websocket_data = {"checkouts": [], "articles": []} + websocket_data = {"checkouts": [], "articles": [], "type": "kpsul"} for checkout in checkouts: websocket_data["checkouts"].append( {"id": checkout["id"], "balance": checkout["balance"]} @@ -1420,7 +1428,8 @@ def cancel_operations(request): websocket_data["articles"].append( {"id": article["id"], "stock": article["stock"]} ) - consumers.KPsul.group_send("kfet.kpsul", websocket_data) + + KPsul.group_send("kfet.kpsul", websocket_data) data["canceled"] = list(opes) data["opegroups_to_update"] = list(opegroups) diff --git a/manage.py b/manage.py index 913e4f6e..00e46405 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings") from django.core.management import execute_from_command_line diff --git a/petitscours/models.py b/petitscours/models.py index 8e5d4884..0be81449 100644 --- a/petitscours/models.py +++ b/petitscours/models.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import User from django.db import models from django.db.models import Min from django.utils.functional import cached_property -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from shared.utils import choices_length @@ -44,9 +44,13 @@ class PetitCoursAbility(models.Model): class Meta: app_label = "gestioncof" + constraints = [ + models.UniqueConstraint( + fields=["user", "niveau", "matiere"], name="unique_competence_level" + ) + ] verbose_name = "Compétence petits cours" verbose_name_plural = "Compétences des petits cours" - unique_together = ("user", "niveau", "matiere") def __str__(self): return "{:s} - {!s} - {:s}".format( diff --git a/petitscours/templates/petitscours/demande_detail.html b/petitscours/templates/petitscours/demande_detail.html index d7f9ca8b..8711fcda 100644 --- a/petitscours/templates/petitscours/demande_detail.html +++ b/petitscours/templates/petitscours/demande_detail.html @@ -1,5 +1,5 @@ {% extends "petitscours/base_title.html" %} -{% load staticfiles %} +{% load static %} {% block page_size %}col-sm-8{% endblock %} diff --git a/petitscours/templates/petitscours/demande_list.html b/petitscours/templates/petitscours/demande_list.html index e4c3c782..04132d57 100644 --- a/petitscours/templates/petitscours/demande_list.html +++ b/petitscours/templates/petitscours/demande_list.html @@ -1,5 +1,5 @@ {% extends "petitscours/base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %}

Demandes de petits cours

diff --git a/petitscours/templates/petitscours/details_demande_infos.html b/petitscours/templates/petitscours/details_demande_infos.html index 39cee1d3..42f37d56 100644 --- a/petitscours/templates/petitscours/details_demande_infos.html +++ b/petitscours/templates/petitscours/details_demande_infos.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} diff --git a/petitscours/templates/petitscours/inscription.html b/petitscours/templates/petitscours/inscription.html index 9512e0b3..eaf10524 100644 --- a/petitscours/templates/petitscours/inscription.html +++ b/petitscours/templates/petitscours/inscription.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load staticfiles %} +{% load static %} {% block extra_head %} diff --git a/petitscours/templates/petitscours/traitement_demande_autre_niveau.html b/petitscours/templates/petitscours/traitement_demande_autre_niveau.html index cb3ec379..c10c8aaf 100644 --- a/petitscours/templates/petitscours/traitement_demande_autre_niveau.html +++ b/petitscours/templates/petitscours/traitement_demande_autre_niveau.html @@ -1,5 +1,5 @@ {% extends "petitscours/base_title.html" %} -{% load staticfiles %} +{% load static %} {% block realcontent %}

diff --git a/provisioning/nginx/gestiocof.conf b/provisioning/nginx/gestiocof.conf index f623bcf9..7d7567c6 100644 --- a/provisioning/nginx/gestiocof.conf +++ b/provisioning/nginx/gestiocof.conf @@ -15,8 +15,8 @@ server { rewrite ^/gestion$ http://localhost:8080/gestion/ redirect; # Les pages statiques sont servies à part. - location /gestion/static { try_files $uri $uri/ =404; } - location /gestion/media { try_files $uri $uri/ =404; } + location /static { try_files $uri $uri/ =404; } + location /media { try_files $uri $uri/ =404; } # On proxy-pass les requêtes vers les pages dynamiques à daphne location / { diff --git a/provisioning/systemd/daphne.service b/provisioning/systemd/daphne.service index 31b31c16..bae9f3ca 100644 --- a/provisioning/systemd/daphne.service +++ b/provisioning/systemd/daphne.service @@ -11,7 +11,7 @@ WorkingDirectory=/vagrant Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" ExecStart=/home/vagrant/venv/bin/daphne \ -u /srv/gestiocof/gestiocof.sock \ - cof.asgi:channel_layer + gestioasso.asgi:application [Install] WantedBy=multi-user.target diff --git a/provisioning/systemd/worker.service b/provisioning/systemd/worker.service index a9ea733f..0d97e9a4 100644 --- a/provisioning/systemd/worker.service +++ b/provisioning/systemd/worker.service @@ -10,7 +10,10 @@ Group=vagrant TimeoutSec=300 WorkingDirectory=/vagrant Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" -ExecStart=/home/vagrant/venv/bin/python manage.py runworker +ExecStart=/home/vagrant/venv/bin/python manage.py runworker \ + 'kfet.open.team' \ + 'kfet.open.base' \ + 'kpsul' [Install] WantedBy=multi-user.target diff --git a/requirements-devel.txt b/requirements-devel.txt index 8dc49eb1..637123a5 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -3,6 +3,6 @@ django-debug-toolbar ipython # Tools -black +black==22.3.0 flake8 isort diff --git a/requirements-prod.txt b/requirements-prod.txt index 6d6fd334..b4a99d6b 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -1,16 +1,15 @@ -r requirements.txt # Postgresql bindings -psycopg2<2.8 +psycopg2==2.9.* # Redis -django-redis-cache==2.1.* -redis~=2.10.6 -asgi-redis==1.4.* +django-redis-cache==3.0.* +redis==3.5.* +channels-redis==3.4.* # ASGI protocol and HTTP server -asgiref~=1.1.2 -daphne==1.3.0 +daphne==3.0.* # ldap bindings python-ldap diff --git a/requirements.txt b/requirements.txt index 8baaa5ed..6caad72f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,19 @@ configparser==3.5.0 -Django==2.2.* -django-autocomplete-light==3.3.* -django-cas-ng==3.6.* -django-djconfig==0.8.0 -django-hCaptcha==0.1.0 +Django==3.2.* +django-autocomplete-light==3.9.4 +django-cas-ng==4.3.* +django-djconfig==0.10.0 +django-hCaptcha==0.2.0 icalendar Pillow django-bootstrap-form==3.3 statistics==1.0.3.5 django-widget-tweaks==1.4.1 -channels==1.1.* +channels==3.0.* python-dateutil -wagtail==2.7.* +wagtail==2.13.* wagtailmenus==3.* -wagtail-modeltranslation==0.10.* -django-cors-headers==2.2.0 +wagtail-modeltranslation==0.11.* +django-cors-headers==3.13.* django-js-reverse -authens==0.1b0 +authens==0.1b4 diff --git a/setup.cfg b/setup.cfg index 8aa73856..9b1c72d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,8 +3,8 @@ source = bda bds clubs - cof events + gestioasso gestioncof kfet petitscours diff --git a/shared/channels.py b/shared/channels.py new file mode 100644 index 00000000..ae8c1248 --- /dev/null +++ b/shared/channels.py @@ -0,0 +1,45 @@ +import datetime +import random +from decimal import Decimal + +import msgpack +from channels_redis.core import RedisChannelLayer + + +def encode_kf(obj): + if isinstance(obj, Decimal): + return {"__decimal__": True, "as_str": str(obj)} + elif isinstance(obj, datetime.datetime): + return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")} + return obj + + +def decode_kf(obj): + if "__decimal__" in obj: + obj = Decimal(obj["as_str"]) + elif "__datetime__" in obj: + obj = datetime.datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f") + return obj + + +class ChannelLayer(RedisChannelLayer): + def serialize(self, message): + """Serializes to a byte string.""" + value = msgpack.packb(message, default=encode_kf, use_bin_type=True) + + if self.crypter: + value = self.crypter.encrypt(value) + + # As we use an sorted set to expire messages + # we need to guarantee uniqueness, with 12 bytes. + random_prefix = random.getrandbits(8 * 12).to_bytes(12, "big") + return random_prefix + value + + def deserialize(self, message): + """Deserializes from a byte string.""" + # Removes the random prefix + message = message[12:] + + if self.crypter: + message = self.crypter.decrypt(message, self.expiry + 10) + return msgpack.unpackb(message, object_hook=decode_kf, raw=False) diff --git a/shell.nix b/shell.nix index be49fa83..0476c269 100644 --- a/shell.nix +++ b/shell.nix @@ -1,22 +1,51 @@ +{ pkgs ? (import ) { }, lib ? pkgs.lib }: + let + mkRequirements = file: + builtins.concatStringsSep "\n" + (builtins.filter + (s: !(lib.hasPrefix "-r" s || lib.hasPrefix "#" s || s == "")) + (lib.splitString "\n" (builtins.readFile file))); + + pypiDataRev = "2505eb53d85cd727c87611ee4aa35152821a12b2"; + pypiDataSha256 = "0nhl0rzlp4fgzxb15pmnq14d0rzcwhvwn40vx7fnk41z9gwxcp4c"; + + pypiData = builtins.fetchTarball { + name = "pypi-deps-db-src"; + url = "https://github.com/DavHau/pypi-deps-db/tarball/${pypiDataRev}"; + sha256 = "${pypiDataSha256}"; + }; + mach-nix = import (builtins.fetchGit { url = "https://github.com/DavHau/mach-nix"; ref = "refs/tags/3.5.0"; }) - { }; + { + inherit pypiData; - requirements = builtins.readFile ./requirements.txt; + python = "python39"; + }; - requirements-dev = '' - django-debug-toolbar - ipython - black - isort - flake8 - ''; + requirements = mkRequirements ./requirements.txt; + + requirements-prod = mkRequirements ./requirements-prod.txt; + + requirements-dev = mkRequirements ./requirements-devel.txt; + + pyEnv = mach-nix.mkPython { + requirements = builtins.concatStringsSep "\n" [ + requirements + requirements-dev + requirements-prod + ]; + }; in -mach-nix.mkPythonShell { - requirements = requirements + requirements-dev; +pkgs.mkShell { + buildInputs = [ pyEnv ]; + + shellHook = '' + export DJANGO_SETTINGS_MODULE=gestioasso.settings.local + ''; }

Date {{ demande.created }}
Nom/prénom {{ demande.name }}