From 0dc622687b689fa1b671a9a7021ac1bc16c943f8 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 16 Jan 2025 17:21:16 +0100 Subject: [PATCH 01/10] fix(cof_settings): Set ASGI_APPLICATION --- gestioasso/settings_cof.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gestioasso/settings_cof.py b/gestioasso/settings_cof.py index 4d24caf2..95f4136b 100644 --- a/gestioasso/settings_cof.py +++ b/gestioasso/settings_cof.py @@ -201,6 +201,8 @@ CHANNEL_LAYERS = credentials.get_json( }, ) +ASGI_APPLICATION = "gestioasso.routing.application" + CORS_ORIGIN_WHITELIST = credentials.get("CORS_ORIGIN_WHITELIST", []) From 35c0f02ec545c7a9d773552b9d1537f985e6d203 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 16 Jan 2025 18:16:42 +0100 Subject: [PATCH 02/10] fix(settings_cof): Update variables --- gestioasso/settings_cof.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gestioasso/settings_cof.py b/gestioasso/settings_cof.py index 95f4136b..cc54dcf1 100644 --- a/gestioasso/settings_cof.py +++ b/gestioasso/settings_cof.py @@ -203,7 +203,8 @@ CHANNEL_LAYERS = credentials.get_json( ASGI_APPLICATION = "gestioasso.routing.application" -CORS_ORIGIN_WHITELIST = credentials.get("CORS_ORIGIN_WHITELIST", []) +CORS_ALLOWED_ORIGINS = credentials.get("CORS_ALLOWED_ORIGINS", []) +CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS SITE_ID = 1 From 9ec07f426c08328d8b34d20a0abed97a0793e27b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 16 Jan 2025 18:18:36 +0100 Subject: [PATCH 03/10] fix(settings_cof): Origins must have a scheme --- gestioasso/settings_cof.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioasso/settings_cof.py b/gestioasso/settings_cof.py index cc54dcf1..6bccefc2 100644 --- a/gestioasso/settings_cof.py +++ b/gestioasso/settings_cof.py @@ -204,7 +204,7 @@ CHANNEL_LAYERS = credentials.get_json( ASGI_APPLICATION = "gestioasso.routing.application" CORS_ALLOWED_ORIGINS = credentials.get("CORS_ALLOWED_ORIGINS", []) -CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS +CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] SITE_ID = 1 From e4e6dfe7c6991d8e309c416bfce42ad48b8a8aa3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 16 Jan 2025 18:47:11 +0100 Subject: [PATCH 04/10] fix(kfet/open): Wrap sync function for async use --- kfet/open/consumers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kfet/open/consumers.py b/kfet/open/consumers.py index 1b190212..4b21bef2 100644 --- a/kfet/open/consumers.py +++ b/kfet/open/consumers.py @@ -1,3 +1,5 @@ +from asgiref.sync import sync_to_async + from ..decorators import kfet_is_team from ..utils import DjangoJsonWebsocketConsumer, PermConsumerMixin from .open import kfet_open @@ -19,7 +21,7 @@ class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer): """Send current status on connect.""" await super().connect() - group = "team" if kfet_is_team(self.user) else "base" + group = "team" if await sync_to_async(kfet_is_team)(self.user) else "base" await self.channel_layer.group_add(f"kfet.open.{group}", self.channel_name) From 01bf885ede5f2f5693ca55b1fee0ed13ac4b6127 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 17 Jan 2025 08:42:07 +0100 Subject: [PATCH 05/10] fix(gestioncof/cms): The tag ifnotequal has been deprecated for 5 years and has now been removed --- gestioncof/cms/templates/cofcms/cof_actu_index_page.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index 4508a66c..a1ea29ed 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -22,10 +22,10 @@
{% if actus.has_previous %} - {% trans "Actualités plus récentes" %} + {% trans "Actualités plus récentes" %} {% endif %} {% if actus.has_next %} - {% trans "Actualités plus anciennes" %} + {% trans "Actualités plus anciennes" %} {% endif %} {% for actu in page.actus %} @@ -44,10 +44,10 @@ {% endfor %} {% if actus.has_previous %} - {% trans "Actualités plus récentes" %} + {% trans "Actualités plus récentes" %} {% endif %} {% if actus.has_next %} - {% trans "Actualités plus anciennes" %} + {% trans "Actualités plus anciennes" %} {% endif %}
{% endblock %} From 58cad7c769b3b4ed9ae6c52cb3ed12a83f95c87d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 17 Jan 2025 10:20:46 +0100 Subject: [PATCH 06/10] feat(kfet): Add a summary of the account balances --- kfet/templates/kfet/account.html | 22 ++++++++++++++++++++++ kfet/views.py | 11 ++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/kfet/templates/kfet/account.html b/kfet/templates/kfet/account.html index 9b63c1da..682eafde 100644 --- a/kfet/templates/kfet/account.html +++ b/kfet/templates/kfet/account.html @@ -31,6 +31,28 @@ {% endif %} + + + + {% endblock %} {% block main %} diff --git a/kfet/views.py b/kfet/views.py index 83d1a9e2..2f9ae6f9 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -184,7 +184,16 @@ class DemandeSoireeView(FormView): @teamkfet_required def account(request): accounts = Account.objects.select_related("cofprofile__user").order_by("trigramme") - return render(request, "kfet/account.html", {"accounts": accounts}) + positive_accounts = Account.objects.filter(balance__gte=0) + negative_accounts = Account.objects.filter(balance__lt=0) + + return render(request, "kfet/account.html", { + "accounts": accounts, + "positive_count": positive_accounts.count(), + "positives_sum": sum(acc.balance for acc in positive_accounts), + "negative_count": negative_accounts.count(), + "negatives_sum": sum(acc.balance for acc in negative_accounts), + }) @login_required From c83ee4e3955c479ca707eba4180be90f887ca0bf Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 20 Jan 2025 20:14:35 +0100 Subject: [PATCH 07/10] fix(kfet/accounts): Exclude #13 from the statistics --- kfet/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 2f9ae6f9..a91e884e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -184,8 +184,8 @@ class DemandeSoireeView(FormView): @teamkfet_required def account(request): accounts = Account.objects.select_related("cofprofile__user").order_by("trigramme") - positive_accounts = Account.objects.filter(balance__gte=0) - negative_accounts = Account.objects.filter(balance__lt=0) + positive_accounts = Account.objects.filter(balance__gte=0).exclude(trigramme="#13") + negative_accounts = Account.objects.filter(balance__lt=0).exclude(trigramme="#13") return render(request, "kfet/account.html", { "accounts": accounts, From ee61ed97d2f8282937a2dc77c571986cb484b3cc Mon Sep 17 00:00:00 2001 From: catvayor Date: Thu, 23 Jan 2025 19:27:56 +0100 Subject: [PATCH 08/10] feat(kfet): block self deposit --- kfet/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kfet/views.py b/kfet/views.py index a91e884e..1f27fa78 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1180,6 +1180,13 @@ def kpsul_perform_operations(request): operationgroup.amount += operation.amount if operation.type == Operation.DEPOSIT: required_perms.add("kfet.perform_deposit") + if request.user.profile.account_kfet == on_acc: + data["errors"].append( + { + "code": "auto_deposit", + "message": ("Impossible de charger son propre trigramme"), + } + ) if operation.type == Operation.EDIT: required_perms.add("kfet.edit_balance_account") need_comment = True From 4f8813bc8132310d27923bd54414237971dd7d2e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 23 Jan 2025 22:52:22 +0100 Subject: [PATCH 09/10] fix(kfet/utils): Add sync_to_async --- kfet/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kfet/utils.py b/kfet/utils.py index 540f260c..e7ec91c2 100644 --- a/kfet/utils.py +++ b/kfet/utils.py @@ -1,6 +1,7 @@ import json import math +from asgiref.sync import sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.core.cache import cache from django.core.serializers.json import DjangoJSONEncoder @@ -95,7 +96,7 @@ class PermConsumerMixin: """Check permissions on connection.""" self.user = self.scope["user"] - if self.user.has_perms(self.perms_connect): + if await sync_to_async(self.user.has_perms)(self.perms_connect): await super().connect() else: await self.close() From 57eaa85e0f91a2e5c5b1ab912f8657ce1ec363e9 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 18 Feb 2025 21:57:02 +0100 Subject: [PATCH 10/10] feat(shared): Add a view for exporting members to a mailing-list --- bds/urls.py | 7 ++++++ gestioasso/settings/local.py | 3 +++ gestioasso/settings_bds.py | 3 +++ gestioasso/settings_cof.py | 3 +++ gestioncof/urls.py | 14 +++++++++++ shared/views.py | 45 ++++++++++++++++++++++++++++++++++-- 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/bds/urls.py b/bds/urls.py index 95065773..68d780df 100644 --- a/bds/urls.py +++ b/bds/urls.py @@ -1,6 +1,7 @@ from django.urls import path from bds import views +from shared.views import SympaListView app_name = "bds" urlpatterns = [ @@ -21,4 +22,10 @@ urlpatterns = [ name="members.expired", ), path("members/reset", views.ResetMembershipView.as_view(), name="members.reset"), + # Sympa export view + path( + "sympa/members/", + SympaListView.as_view(filters={"bds__is_member": True}), + name="export.sympa", + ), ] diff --git a/gestioasso/settings/local.py b/gestioasso/settings/local.py index 2cba6a10..c6a22e64 100644 --- a/gestioasso/settings/local.py +++ b/gestioasso/settings/local.py @@ -27,6 +27,9 @@ ALLOWED_HOSTS = [] DEBUG = True EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +SYMPA_PASSWORD = b"sympa" +SYMPA_USERNAME = b"sympa" + if TESTING: PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] diff --git a/gestioasso/settings_bds.py b/gestioasso/settings_bds.py index 231ae101..e640b222 100644 --- a/gestioasso/settings_bds.py +++ b/gestioasso/settings_bds.py @@ -26,6 +26,9 @@ EMAIL_HOST = credentials.get("EMAIL_HOST") DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +SYMPA_PASSWORD = credentials["SYMPA_PASSWORD"].encode() +SYMPA_USERNAME = credentials["SYMPA_USERNAME"].encode() + ## # Installed Apps configuration diff --git a/gestioasso/settings_cof.py b/gestioasso/settings_cof.py index 6bccefc2..893aaec4 100644 --- a/gestioasso/settings_cof.py +++ b/gestioasso/settings_cof.py @@ -30,6 +30,9 @@ LDAP_SERVER_URL = credentials.get("LDAP_SERVER_URL") DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +SYMPA_PASSWORD = credentials["SYMPA_PASSWORD"].encode() +SYMPA_USERNAME = credentials["SYMPA_USERNAME"].encode() + ## # Installed Apps configuration diff --git a/gestioncof/urls.py b/gestioncof/urls.py index d0ba75c7..624f8e22 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -4,6 +4,16 @@ from django.views.generic.base import TemplateView from django_cas_ng import views as django_cas_views from gestioncof import csv_views, views +from shared.views import SympaListView + +sympa_patterns = [ + path( + f"{mailing}/", + SympaListView.as_view(filters={f"profile__mailing_{mailing}": True}), + name=f"sympa.{mailing}", + ) + for mailing in ["bda", "bda_revente", "cof", "unernestaparis"] +] export_patterns = [ path("members", views.export_members, name="export.members"), @@ -162,4 +172,8 @@ urlpatterns = [ # Clubs # ----- path("clubs/", include(clubs_patterns)), + # ----- + # Sympa export + # ----- + path("sympa/", include(sympa_patterns)), ] diff --git a/shared/views.py b/shared/views.py index a1ffb185..fbff4ec0 100644 --- a/shared/views.py +++ b/shared/views.py @@ -1,12 +1,53 @@ +import base64 from collections import namedtuple +from typing import Any from dal import autocomplete +from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured -from django.http import Http404 -from django.views.generic import TemplateView +from django.http import Http404, HttpResponse +from django.views.generic import TemplateView, View from shared.autocomplete import ModelSearch +User = get_user_model() + + +class SympaListView(View): + realm = "sympa" + + username = settings.SYMPA_USERNAME + password = settings.SYMPA_PASSWORD + + filters: dict[str, Any] = {} + + def dispatch(self, request, *args, **kwargs): + if "HTTP_AUTHORIZATION" in request.META: + auth = request.META["HTTP_AUTHORIZATION"].split() + + if len(auth) == 2 and auth[0].lower() == "basic": + name, passwd = base64.b64decode(auth[1]).split(b":") + + if name == self.username and passwd == self.password: + return self.render_to_response(request, *args, **kwargs) + + return HttpResponse( + status=401, headers={"WWW-Authenticate": f'Basic realm="{self.realm}"'} + ) + + def render_to_response(self, request, *args, **kwargs): + """ + Renders a list of emails in a text response. + """ + + users = User.objects.filter(**self.filters) + + return HttpResponse( + b"\n".join(u.email.encode("utf-8") for u in users if u.email), + content_type="text/plain", + ) + class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView): """Compatibility layer between ModelSearch and Select2QuerySetView."""