From 639ea923682c1c980076931da77eb3f8c0bbb4ad Mon Sep 17 00:00:00 2001 From: Julien Malka Date: Wed, 21 Aug 2024 19:33:49 +0200 Subject: [PATCH 001/172] feat: init profiles generation --- src/app/urls.py | 9 +++++ src/app/utils.py | 10 ++++++ src/app/views.py | 18 ++++++++++ src/shared/templates/home.html | 6 +++- src/shared/templates/iosprofile.xml | 51 +++++++++++++++++++++++++++++ src/shared/templates/profiles.html | 8 +++++ 6 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/app/utils.py create mode 100644 src/app/views.py create mode 100644 src/shared/templates/iosprofile.xml create mode 100644 src/shared/templates/profiles.html diff --git a/src/app/urls.py b/src/app/urls.py index 19fb416..f4a3984 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -17,12 +17,21 @@ Including another URLconf from django.conf import settings from django.conf.urls.static import static from django.contrib import admin +from django.contrib.auth.decorators import login_required from django.urls import include, path from django.views.generic import TemplateView +from . import views + urlpatterns = [ path("", TemplateView.as_view(template_name="home.html"), name="index"), path("login", TemplateView.as_view(template_name="login.html"), name="login"), + path( + "profiles.html", + login_required(TemplateView.as_view(template_name="profiles.html")), + name="profiles", + ), + path("profiles/ios.xml", views.profile_ios, name="profiles-ios"), path("", include("dgsi.urls")), path("accounts/", include("allauth.urls")), ] diff --git a/src/app/utils.py b/src/app/utils.py new file mode 100644 index 0000000..d2cedb7 --- /dev/null +++ b/src/app/utils.py @@ -0,0 +1,10 @@ +import json + +import kanidm + + +async def get_radius_credential(username): + config = kanidm.KanidmClientConfig(uri="https://sso.dgnum.eu") + client = kanidm.KanidmClient(config=config) + token = await client.get_radius_token(username) + return json.loads(token.content)["secret"] diff --git a/src/app/views.py b/src/app/views.py new file mode 100644 index 0000000..df52ecf --- /dev/null +++ b/src/app/views.py @@ -0,0 +1,18 @@ +from asgiref.sync import async_to_sync, sync_to_async +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from . import utils + + +@sync_to_async +@login_required(login_url="/login/") +@async_to_sync +async def profile_ios(request): + radius_credential = await utils.get_radius_credential(request.user.username) + return render( + request, + "iosprofile.xml", + {"radius_credential": radius_credential}, + content_type="text/xml", + ) diff --git a/src/shared/templates/home.html b/src/shared/templates/home.html index f865627..58c992c 100644 --- a/src/shared/templates/home.html +++ b/src/shared/templates/home.html @@ -1,3 +1,7 @@ {% extends "base.html" %} -{% block content %}{% endblock %} +{% block content %} + +{% endblock %} diff --git a/src/shared/templates/iosprofile.xml b/src/shared/templates/iosprofile.xml new file mode 100644 index 0000000..ba8d514 --- /dev/null +++ b/src/shared/templates/iosprofile.xml @@ -0,0 +1,51 @@ + + + + + PayloadContent + + + AutoJoin + + CaptiveBypass + + EncryptionType + WPA2 + HIDDEN_NETWORK + + IsHotspot + + Password + {{ radius_credential }} + PayloadDescription + Configures Wi-Fi settings + PayloadDisplayName + DGNum WiFi + PayloadIdentifier + com.apple.wifi.managed.5A2AE473-F6B7-4D60-9778-B25D26317C41 + PayloadType + com.apple.wifi.managed + PayloadUUID + 5A2AE473-F6B7-4D60-9778-B25D26317C41 + PayloadVersion + 1 + ProxyType + None + SSID_STR + DGNum + + + PayloadDisplayName + DGNum WiFi + PayloadIdentifier + WiFi-PSK-Sample.D5B78A3C-CDA8-471F-984C-06F977EF870C + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + 444C9683-221C-49AF-997D-2B6B84710DAA + PayloadVersion + 1 + + diff --git a/src/shared/templates/profiles.html b/src/shared/templates/profiles.html new file mode 100644 index 0000000..2046f2c --- /dev/null +++ b/src/shared/templates/profiles.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
+ iOS + Android +
+{% endblock %} From 772ca45292e9ffcb7990aa3c3dff46f616ab8836 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 11 Sep 2024 21:07:11 +0200 Subject: [PATCH 002/172] feat(pkgs): Update pykanidm --- pkgs/pykanidm/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/pykanidm/default.nix b/pkgs/pykanidm/default.nix index f49d8b2..8f03681 100644 --- a/pkgs/pykanidm/default.nix +++ b/pkgs/pykanidm/default.nix @@ -11,14 +11,14 @@ buildPythonPackage rec { pname = "kanidm"; - version = "1.1.0-rc.16"; + version = "1.2.3"; pyproject = true; src = fetchFromGitHub { owner = "kanidm"; repo = "kanidm"; rev = "v${version}"; - hash = "sha256-NH9V5KKI9LAtJ2/WuWtUJUzkjVMfO7Q5NQkK7Ys2olU="; + hash = "sha256-J02IbAY5lyoMaq6wJiHizqeFBd5hB6id2YMPxlPsASM="; }; sourceRoot = "source/pykanidm"; From 8599992dd755e95c61ce551625d4e310fd0fccda Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 11 Sep 2024 21:08:08 +0200 Subject: [PATCH 003/172] feat(dgsi): Add a basic profile view --- .credentials/KANIDM_URI | 1 + src/dgsi/urls.py | 10 ++++++- src/dgsi/views.py | 34 +++++++++++++++++++++++ src/shared/kanidm.py | 8 ++++++ src/shared/templates/_hero.html | 2 +- src/shared/templates/account/profile.html | 25 +++++++++++++++++ 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 .credentials/KANIDM_URI create mode 100644 src/shared/kanidm.py create mode 100644 src/shared/templates/account/profile.html diff --git a/.credentials/KANIDM_URI b/.credentials/KANIDM_URI new file mode 100644 index 0000000..cd6933f --- /dev/null +++ b/.credentials/KANIDM_URI @@ -0,0 +1 @@ +https://sso.dgnum.eu diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 637600f..deb3429 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -1 +1,9 @@ -urlpatterns = [] +from django.urls import path + +from . import views + +app_name = "dgsi" + +urlpatterns = [ + path("profile", views.ProfileView.as_view(), name="dgn-profile"), +] diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 60f00ef..2277922 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1 +1,35 @@ # Create your views here. +import json +from typing import Optional + +from asgiref.sync import async_to_sync +from django.contrib.auth import get_user_model +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import QuerySet +from django.views.generic import DetailView + +from shared.kanidm import client + +User = get_user_model() + + +class ProfileView(LoginRequiredMixin, DetailView): + model = User + template_name = "account/profile.html" + + def get_object(self, queryset: Optional[QuerySet] = None): + assert isinstance(self.request.user, User) + + return self.request.user + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + username = self.request.user.get_username() + + ctx["person"] = async_to_sync(client.person_account_get)(username) + + content: str = async_to_sync(client.get_radius_token)(username).content + + ctx["radius_secret"] = json.loads(content).get("secret") + return ctx diff --git a/src/shared/kanidm.py b/src/shared/kanidm.py new file mode 100644 index 0000000..32a77e1 --- /dev/null +++ b/src/shared/kanidm.py @@ -0,0 +1,8 @@ +from kanidm import KanidmClient +from loadcredential import Credentials + +credentials = Credentials(env_prefix="DGSI_") + +client = KanidmClient( + uri=credentials["KANIDM_URI"], token=credentials["KANIDM_AUTH_TOKEN"] +) diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index 7b5391b..37b4a70 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -8,7 +8,7 @@

Dossier Général des Services Informagiques

-

Système d'information de la DGNum

+

Système d'information de la DGNum

{% if user.is_authenticated %} diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html new file mode 100644 index 0000000..fdbd01c --- /dev/null +++ b/src/shared/templates/account/profile.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

+ Profil de {{ person.displayname }} + {{ person.name }} +

+
+ +

Identifiant unique :

+ + {{ person.uuid }} +
+ +

Token RADIUS :

+ + {{ radius_secret }} +
+ +

Membre des groupes suivants :

+ + {% for group in person.memberof %} + {{ group }}
+ {% endfor %} +{% endblock content %} From 7581bf59dfc08d7c9cad45686d8ae8f0475d5ce6 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 13 Sep 2024 16:55:44 +0200 Subject: [PATCH 004/172] feat(models): Add a Profile model This simplifies the management of the views data --- src/dgsi/migrations/0001_initial.py | 37 +++++++++++++++++++++++ src/dgsi/models.py | 30 ++++++++++++++++++ src/dgsi/views.py | 37 ++++++----------------- src/shared/templates/account/profile.html | 10 +++--- 4 files changed, 82 insertions(+), 32 deletions(-) create mode 100644 src/dgsi/migrations/0001_initial.py diff --git a/src/dgsi/migrations/0001_initial.py b/src/dgsi/migrations/0001_initial.py new file mode 100644 index 0000000..1a56d02 --- /dev/null +++ b/src/dgsi/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.12 on 2024-09-13 14:30 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Profile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 6b20219..6d3491f 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -1 +1,31 @@ # Create your models here. + +from dataclasses import dataclass +from functools import cached_property +from typing import Optional + +from asgiref.sync import async_to_sync +from django.contrib.auth.models import User +from django.db import models +from kanidm.models.person import Person + +from shared.kanidm import client + + +@dataclass +class KanidmProfile: + person: Person + secret: Optional[str] + + +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + + @cached_property + def kanidm_profile(self): + person = async_to_sync(client.person_account_get)(self.user.username) + data = async_to_sync(client.get_radius_token)(self.user.username).data + + secret = data.get("secret") if data is not None else None + + return KanidmProfile(person, secret) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 2277922..786a36b 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,35 +1,18 @@ -# Create your views here. -import json -from typing import Optional - -from asgiref.sync import async_to_sync -from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin -from django.db.models import QuerySet -from django.views.generic import DetailView - -from shared.kanidm import client - -User = get_user_model() +from django.contrib.auth.models import User +from django.views.generic import TemplateView -class ProfileView(LoginRequiredMixin, DetailView): +class ProfileView(LoginRequiredMixin, TemplateView): model = User template_name = "account/profile.html" - def get_object(self, queryset: Optional[QuerySet] = None): + def get_context_data(self, **kwargs): assert isinstance(self.request.user, User) - return self.request.user - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - - username = self.request.user.get_username() - - ctx["person"] = async_to_sync(client.person_account_get)(username) - - content: str = async_to_sync(client.get_radius_token)(username).content - - ctx["radius_secret"] = json.loads(content).get("secret") - return ctx + return super().get_context_data( + # Pyright throws a fit as it doesn't detect the reverse relation + # giving a user its profile + profile=self.request.user.profile.kanidm_profile, # pyright: ignore + **kwargs + ) diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index fdbd01c..cb3bf13 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -2,24 +2,24 @@ {% block content %}

- Profil de {{ person.displayname }} - {{ person.name }} + Profil de {{ profile.person.displayname }} + {{ profile.person.name }}


Identifiant unique :

- {{ person.uuid }} + {{ profile.person.uuid }}

Token RADIUS :

- {{ radius_secret }} + {{ profile.secret }}

Membre des groupes suivants :

- {% for group in person.memberof %} + {% for group in profile.person.memberof %} {{ group }}
{% endfor %} {% endblock content %} From 7491fdb376b32fb69966400e6343faf2fffce993 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 13 Sep 2024 17:09:21 +0200 Subject: [PATCH 005/172] chore(shell): Switch back to django-stubs It is more up to date, and no longer requires mypy --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index a339fb9..8ce494c 100644 --- a/default.nix +++ b/default.nix @@ -43,7 +43,7 @@ in ps.django-allauth ps.django-compressor ps.django-debug-toolbar - ps.django-types + ps.django-stubs ps.loadcredential ] ++ (builtins.map (p: ps.callPackage ./pkgs/${p} { }) [ From aed32e0725c9a50626ff4176a97f187710aa128d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 13 Sep 2024 17:32:45 +0200 Subject: [PATCH 006/172] feat(profile): Change the url so that the redirection is automatic after connexion --- src/dgsi/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index deb3429..cc2e0d0 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -5,5 +5,5 @@ from . import views app_name = "dgsi" urlpatterns = [ - path("profile", views.ProfileView.as_view(), name="dgn-profile"), + path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), ] From 855664e4101a859d61c318abc25ac99a3a41cdc4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 13 Sep 2024 21:25:26 +0200 Subject: [PATCH 007/172] feat(templates): Tweak _hero.html This makes the title fit on one line --- src/shared/templates/_hero.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index 37b4a70..e482ce2 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -3,8 +3,8 @@
-
-
+
+

Dossier Général des Services Informagiques

From c02b29a6f5405f908d5950554381bfe5a8cf4124 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 14:44:47 +0200 Subject: [PATCH 008/172] feat(shell): Use python312 --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 8ce494c..b4d0d99 100644 --- a/default.nix +++ b/default.nix @@ -35,7 +35,7 @@ in pkgs.dart-sass # Python dependencies - (pkgs.python3.withPackages ( + (pkgs.python312.withPackages ( ps: [ ps.daphne From f56961c7bbf6132826159e9622504687b6f2cce3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 15:44:25 +0200 Subject: [PATCH 009/172] feat(User): Switch to a custom model, simplifying the logic --- src/app/settings.py | 1 + src/dgsi/admin.py | 7 +- src/dgsi/migrations/0001_initial.py | 112 ++++++++++++++++++++-- src/dgsi/models.py | 29 +++--- src/dgsi/views.py | 12 --- src/shared/templates/account/profile.html | 11 ++- 6 files changed, 133 insertions(+), 39 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index af2393f..14c4af8 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -129,6 +129,7 @@ SOCIALACCOUNT_PROVIDERS = { } AUTH_PASSWORD_VALIDATORS = [] +AUTH_USER_MODEL = "dgsi.User" ### diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 846f6b4..5279d05 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -1 +1,6 @@ -# Register your models here. +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + +from dgsi.models import User + +admin.site.register(User, UserAdmin) diff --git a/src/dgsi/migrations/0001_initial.py b/src/dgsi/migrations/0001_initial.py index 1a56d02..a9dfd9c 100644 --- a/src/dgsi/migrations/0001_initial.py +++ b/src/dgsi/migrations/0001_initial.py @@ -1,7 +1,8 @@ -# Generated by Django 4.2.12 on 2024-09-13 14:30 +# Generated by Django 4.2.12 on 2024-09-14 13:35 -import django.db.models.deletion -from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone from django.db import migrations, models @@ -9,12 +10,12 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name="Profile", + name="User", fields=[ ( "id", @@ -25,13 +26,106 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("password", models.CharField(max_length=128, verbose_name="password")), ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", ), ), ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], ), ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 6d3491f..b60cd1d 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -1,31 +1,36 @@ -# Create your models here. - from dataclasses import dataclass from functools import cached_property from typing import Optional from asgiref.sync import async_to_sync -from django.contrib.auth.models import User -from django.db import models +from django.contrib.auth.models import AbstractUser from kanidm.models.person import Person from shared.kanidm import client +ADMIN_GROUP = "idm_admins@sso.dgnum.eu" + @dataclass class KanidmProfile: person: Person - secret: Optional[str] + radius_secret: Optional[str] -class Profile(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) +class User(AbstractUser): + """ + Custom User class, to have a direct link to the Kanidm data. + """ @cached_property - def kanidm_profile(self): - person = async_to_sync(client.person_account_get)(self.user.username) - data = async_to_sync(client.get_radius_token)(self.user.username).data + def kanidm(self) -> KanidmProfile: + radius_data = async_to_sync(client.get_radius_token)(self.username).data - secret = data.get("secret") if data is not None else None + return KanidmProfile( + person=async_to_sync(client.person_account_get)(self.username), + radius_secret=radius_data and radius_data.get("secret"), + ) - return KanidmProfile(person, secret) + @property + def is_admin(self) -> bool: + return ADMIN_GROUP in self.kanidm.person.memberof diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 786a36b..75ebf1f 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,18 +1,6 @@ from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.models import User from django.views.generic import TemplateView class ProfileView(LoginRequiredMixin, TemplateView): - model = User template_name = "account/profile.html" - - def get_context_data(self, **kwargs): - assert isinstance(self.request.user, User) - - return super().get_context_data( - # Pyright throws a fit as it doesn't detect the reverse relation - # giving a user its profile - profile=self.request.user.profile.kanidm_profile, # pyright: ignore - **kwargs - ) diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index cb3bf13..90e20ae 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -1,25 +1,26 @@ {% extends "base.html" %} +{% load i18n %} {% block content %}

- Profil de {{ profile.person.displayname }} - {{ profile.person.name }} + Profil de {{ user.kanidm.person.displayname }} + {{ user.kanidm.person.name }}


Identifiant unique :

- {{ profile.person.uuid }} + {{ user.kanidm.person.uuid }}

Token RADIUS :

- {{ profile.secret }} + {{ user.kanidm.radius_secret }}

Membre des groupes suivants :

- {% for group in profile.person.memberof %} + {% for group in user.kanidm.person.memberof %} {{ group }}
{% endfor %} {% endblock content %} From 3b2937b6d11664f90089640afac9250e11244982 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 15:44:52 +0200 Subject: [PATCH 010/172] feat(dgsi.mixins): Introduce StaffRequiredMixin --- src/dgsi/mixins.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/dgsi/mixins.py diff --git a/src/dgsi/mixins.py b/src/dgsi/mixins.py new file mode 100644 index 0000000..dcb6d70 --- /dev/null +++ b/src/dgsi/mixins.py @@ -0,0 +1,16 @@ +from django.contrib.auth.mixins import UserPassesTestMixin +from django.http import HttpRequest + +from dgsi.models import User + + +class StaffRequiredMixin(UserPassesTestMixin): + request: HttpRequest + + def test_func(self) -> bool | None: + if self.request.user.is_authenticated: + return False + + assert isinstance(self.request.user, User) + + return self.request.user.is_admin From 06351ca22e85fb8fcf61a060585d3e8f3bdfa24f Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 15:45:56 +0200 Subject: [PATCH 011/172] chore(.gitignore): Don't consider backups of the local db --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9054e66..48ab904 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .pre-commit-config.yaml db.sqlite3 +db.sqlite3.* __pycache__/ .static/* From 2642a64116f2c92709f26c6b08921ee082682d75 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 15:46:11 +0200 Subject: [PATCH 012/172] feat(shell): Add ipython --- default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/default.nix b/default.nix index b4d0d99..7f377cd 100644 --- a/default.nix +++ b/default.nix @@ -45,6 +45,7 @@ in ps.django-debug-toolbar ps.django-stubs ps.loadcredential + ps.ipython ] ++ (builtins.map (p: ps.callPackage ./pkgs/${p} { }) [ "django-browser-reload" From 2817054e7e49309247bcbf62751f6ddaf6b8297b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 15:53:10 +0200 Subject: [PATCH 013/172] feat(dgsi.views): Add a view to create users --- src/dgsi/urls.py | 1 + src/dgsi/views.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index cc2e0d0..38c0e0c 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -6,4 +6,5 @@ app_name = "dgsi" urlpatterns = [ path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), + path("accounts/create/", views.CreateUserView.as_view(), name="dgn-create_user"), ] diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 75ebf1f..5515f8f 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,6 +1,13 @@ from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import TemplateView +from django.views.generic import CreateView, TemplateView + +from dgsi.mixins import StaffRequiredMixin +from dgsi.models import User class ProfileView(LoginRequiredMixin, TemplateView): template_name = "account/profile.html" + + +class CreateUserView(StaffRequiredMixin, CreateView): + model = User From abc71eb52d251daa22e36dee8825b54bf383a2f7 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 14 Sep 2024 16:35:57 +0200 Subject: [PATCH 014/172] feat(accounts): Simplify the template and disable signup --- src/app/settings.py | 4 ++ src/shared/account.py | 10 ++++ .../templates/allauth/layouts/base.html | 50 ------------------- 3 files changed, 14 insertions(+), 50 deletions(-) create mode 100644 src/shared/account.py diff --git a/src/app/settings.py b/src/app/settings.py index 14c4af8..0328933 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -110,6 +110,10 @@ AUTHENTICATION_BACKENDS = [ "allauth.account.auth_backends.AuthenticationBackend", ] +ACCOUNT_ADAPTER = "shared.account.AccountAdapter" +ACCOUNT_CHANGE_EMAIL = True +ACCOUNT_EMAIL_NOTIFICATIONS = True + SOCIALACCOUNT_ONLY = True SOCIALACCOUNT_PROVIDERS = { "openid_connect": { diff --git a/src/shared/account.py b/src/shared/account.py new file mode 100644 index 0000000..544fcd4 --- /dev/null +++ b/src/shared/account.py @@ -0,0 +1,10 @@ +from allauth.account.adapter import DefaultAccountAdapter + + +class AccountAdapter(DefaultAccountAdapter): + """ + Overrides the Account Adapter. + """ + + def is_open_for_signup(self, request): + return False diff --git a/src/shared/templates/allauth/layouts/base.html b/src/shared/templates/allauth/layouts/base.html index 3746ddc..eb6082e 100644 --- a/src/shared/templates/allauth/layouts/base.html +++ b/src/shared/templates/allauth/layouts/base.html @@ -16,56 +16,6 @@ {% include "_hero.html" %}
- - -
-
{% block content %} {% endblock content %} From 8baad602c6c5c1189aa9557985937af647f50dab Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 16 Sep 2024 17:56:53 +0200 Subject: [PATCH 015/172] feat(pkgs): Add django-allauth-cas --- pkgs/django-allauth-cas/01-setup.patch | 12 +++++++ pkgs/django-allauth-cas/default.nix | 46 ++++++++++++++++++++++++++ pkgs/python-cas/default.nix | 43 ++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 pkgs/django-allauth-cas/01-setup.patch create mode 100644 pkgs/django-allauth-cas/default.nix create mode 100644 pkgs/python-cas/default.nix diff --git a/pkgs/django-allauth-cas/01-setup.patch b/pkgs/django-allauth-cas/01-setup.patch new file mode 100644 index 0000000..92897d2 --- /dev/null +++ b/pkgs/django-allauth-cas/01-setup.patch @@ -0,0 +1,12 @@ +diff --git a/setup.py b/setup.py +index fb06ec0..506677f 100644 +--- a/setup.py ++++ b/setup.py +@@ -52,7 +52,6 @@ setup( + install_requires=[ + "django-allauth", + "python-cas", +- "six", + ], + extras_require={ + "docs": ["sphinx"], diff --git a/pkgs/django-allauth-cas/default.nix b/pkgs/django-allauth-cas/default.nix new file mode 100644 index 0000000..8721e21 --- /dev/null +++ b/pkgs/django-allauth-cas/default.nix @@ -0,0 +1,46 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + wheel, + django-allauth, + python-cas, +}: + +buildPythonPackage rec { + pname = "django-allauth-cas"; + version = "unstable-2024-01-25"; + pyproject = true; + + src = fetchFromGitHub { + owner = "jlucasp25"; + repo = "django-allauth-cas"; + rev = "77e02f3796cd564a9a0c48b5b568b14d4d4c5687"; + hash = "sha256-y/IquXl/4+9MJmsgbWtPun3tBbRJ4kJFzWo5c+5WeHk="; + }; + + patches = [ ./01-setup.patch ]; + + build-system = [ + setuptools + wheel + ]; + + dependencies = [ + django-allauth + python-cas + ]; + + pythonImportsCheck = [ + "allauth_cas" + ]; + + meta = { + description = "CAS support for django-allauth"; + homepage = "https://github.com/jlucasp25/django-allauth-cas"; + changelog = "https://github.com/jlucasp25/django-allauth-cas/blob/${src.rev}/CHANGELOG.rst"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ ]; + }; +} diff --git a/pkgs/python-cas/default.nix b/pkgs/python-cas/default.nix new file mode 100644 index 0000000..c8c2ac7 --- /dev/null +++ b/pkgs/python-cas/default.nix @@ -0,0 +1,43 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + wheel, + lxml, + requests, + six, +}: + +buildPythonPackage rec { + pname = "python-cas"; + version = "1.6.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "python-cas"; + repo = "python-cas"; + rev = "v${version}"; + hash = "sha256-0lpjG/Sma0tJGtahiFE1CjvTyswrBUp+F6f1S65b+lk="; + }; + + nativeBuildInputs = [ + setuptools + wheel + ]; + + propagatedBuildInputs = [ + lxml + requests + six + ]; + + pythonImportsCheck = [ "cas" ]; + + meta = with lib; { + description = "Python CAS (Central Authentication Service) client library support CAS 1.0/2.0/3.0"; + homepage = "https://github.com/python-cas/python-cas"; + license = licenses.mit; + maintainers = with maintainers; [ ]; + }; +} From 99764928c61a43c1d78f32ddc2637d86273c6137 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 16 Sep 2024 17:57:26 +0200 Subject: [PATCH 016/172] feat(nix): Rework the python environment --- default.nix | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/default.nix b/default.nix index 7f377cd..c818152 100644 --- a/default.nix +++ b/default.nix @@ -24,6 +24,14 @@ let commitizen.enable = true; }; }; + + python = pkgs.python312.override { + packageOverrides = + self: _: + pkgs.lib.genAttrs (builtins.attrNames (builtins.readDir ./pkgs)) ( + p: self.callPackage ./pkgs/${p} { } + ); + }; in { @@ -35,26 +43,23 @@ in pkgs.dart-sass # Python dependencies - (pkgs.python312.withPackages ( - ps: - [ - ps.daphne - ps.django - ps.django-allauth - ps.django-compressor - ps.django-debug-toolbar - ps.django-stubs - ps.loadcredential - ps.ipython - ] - ++ (builtins.map (p: ps.callPackage ./pkgs/${p} { }) [ - "django-browser-reload" - "django-bulma-forms" - "django-sass-processor" - "django-sass-processor-dart-sass" - "pykanidm" - ]) - )) + (python.withPackages (ps: [ + ps.daphne + ps.django + ps.django-allauth + ps.django-allauth-cas + ps.django-browser-reload + ps.django-bulma-forms + ps.django-compressor + ps.django-debug-toolbar + ps.django-sass-processor + ps.django-sass-processor-dart-sass + ps.django-stubs + ps.ipython + ps.loadcredential + ps.pykanidm + ps.python-cas + ])) ] ++ check.enabledPackages; env = { From 82123717cc0830cc56b2c67d5c95561a3fd0ceec Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 16 Sep 2024 19:07:02 +0200 Subject: [PATCH 017/172] feat(app): Remove daphne --- default.nix | 1 - src/app/settings.py | 1 - 2 files changed, 2 deletions(-) diff --git a/default.nix b/default.nix index c818152..60a6190 100644 --- a/default.nix +++ b/default.nix @@ -44,7 +44,6 @@ in # Python dependencies (python.withPackages (ps: [ - ps.daphne ps.django ps.django-allauth ps.django-allauth-cas diff --git a/src/app/settings.py b/src/app/settings.py index 0328933..08e8d03 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -24,7 +24,6 @@ ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) # List the installed applications INSTALLED_APPS = [ - "daphne", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", From fd5e036f4970748300baeb5251edc756b8c5f7db Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 16 Sep 2024 19:07:19 +0200 Subject: [PATCH 018/172] feat(profile): Add more informations --- src/shared/templates/account/profile.html | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index 90e20ae..4627f5b 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -8,19 +8,30 @@
-

Identifiant unique :

+ {% if user.kanidm.radius_secret %} +

Mot de passe WiFi :

- {{ user.kanidm.person.uuid }} + {{ user.kanidm.radius_secret }} +
+ {% endif %} + +

Adresse e-mail :

+ {{ user.email }}
-

Token RADIUS :

+

Informations techniques

+
- {{ user.kanidm.radius_secret }} +

Identifiant unique :

+ + {{ user.kanidm.person.uuid }}

Membre des groupes suivants :

+
{% for group in user.kanidm.person.memberof %} - {{ group }}
+ {{ group }} {% endfor %} +
{% endblock content %} From 0272edc266752a0afa8a416732b8ac833440eb0d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 17 Sep 2024 10:36:37 +0200 Subject: [PATCH 019/172] feat(profile): Make the password selectable on click --- src/shared/templates/account/profile.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index 4627f5b..997e977 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -11,7 +11,13 @@ {% if user.kanidm.radius_secret %}

Mot de passe WiFi :

- {{ user.kanidm.radius_secret }} +
{% endif %} From 590ac25b8c73ab4846bffb5118f0df8cb77e5ac0 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 17 Sep 2024 17:12:20 +0200 Subject: [PATCH 020/172] feat(home): Add profile link --- src/shared/templates/home.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/shared/templates/home.html b/src/shared/templates/home.html index f865627..45f2c21 100644 --- a/src/shared/templates/home.html +++ b/src/shared/templates/home.html @@ -1,3 +1,8 @@ {% extends "base.html" %} +{% load i18n %} -{% block content %}{% endblock %} +{% block content %} + {% if user.is_authenticated %} + {% trans "Mon profil" %} + {% endif %} +{% endblock content %} From 763c2dfcbbe43fe7f3c3489e96ba24a580813737 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 17 Sep 2024 17:12:45 +0200 Subject: [PATCH 021/172] feat(profile): Add translations and tweak template --- src/shared/templates/account/profile.html | 30 +++++++++++++++++------ src/shared/templates/base.html | 2 ++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index 997e977..39d5a29 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -1,15 +1,23 @@ {% extends "base.html" %} {% load i18n %} +{% block extra_head %} + +{% endblock extra_head %} + {% block content %}

- Profil de {{ user.kanidm.person.displayname }} + {% blocktrans with displayname=user.kanidm.person.displayname %}Profil de {{ displayname }}{% endblocktrans %} {{ user.kanidm.person.name }}


{% if user.kanidm.radius_secret %} -

Mot de passe WiFi :

+

{% trans "Mot de passe WiFi :" %}

{% endif %} -

Adresse e-mail :

+

{% trans "Adresse e-mail :" %}

{{ user.email }}
-

Informations techniques

+

{% trans "Informations techniques" %}


-

Identifiant unique :

+

{% trans "Identifiant unique :" %}

- {{ user.kanidm.person.uuid }} +
-

Membre des groupes suivants :

+

{% trans "Membre des groupes suivants :" %}

-
+
{% for group in user.kanidm.person.memberof %} {{ group }} {% endfor %} diff --git a/src/shared/templates/base.html b/src/shared/templates/base.html index 3432ddc..8f5d395 100644 --- a/src/shared/templates/base.html +++ b/src/shared/templates/base.html @@ -7,6 +7,8 @@ DGNum + {% block extra_head %}{% endblock extra_head %} + {% include "_links.html" %} From 1732249a2ddc79907a1a5f2405dd3146de9639c9 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 18 Sep 2024 22:14:40 +0200 Subject: [PATCH 022/172] feat(pkgs): Add django-allauth and django-allauth-cas --- pkgs/django-allauth-cas/02-registry.patch | 39 +++++++++ pkgs/django-allauth-cas/default.nix | 5 +- pkgs/django-allauth/default.nix | 96 +++++++++++++++++++++++ src/app/settings.py | 2 + src/shared/cas/__init__.py | 0 src/shared/cas/provider.py | 11 +++ src/shared/cas/urls.py | 5 ++ src/shared/cas/views.py | 14 ++++ 8 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 pkgs/django-allauth-cas/02-registry.patch create mode 100644 pkgs/django-allauth/default.nix create mode 100644 src/shared/cas/__init__.py create mode 100644 src/shared/cas/provider.py create mode 100644 src/shared/cas/urls.py create mode 100644 src/shared/cas/views.py diff --git a/pkgs/django-allauth-cas/02-registry.patch b/pkgs/django-allauth-cas/02-registry.patch new file mode 100644 index 0000000..cfd905f --- /dev/null +++ b/pkgs/django-allauth-cas/02-registry.patch @@ -0,0 +1,39 @@ +diff --git a/allauth_cas/signals.py b/allauth_cas/signals.py +index 36c9b24..530c26e 100644 +--- a/allauth_cas/signals.py ++++ b/allauth_cas/signals.py +@@ -1,4 +1,4 @@ +-from allauth.account.adapter import get_adapter ++from allauth.socialaccount.adapter import get_adapter + from allauth.account.utils import get_next_redirect_url + from allauth.socialaccount import providers + from django.contrib.auth.signals import user_logged_out +@@ -14,7 +14,7 @@ def cas_account_logout(sender, request, **kwargs): + if not provider_id: + return + +- provider = providers.registry.by_id(provider_id, request) ++ provider = get_adapter(request).get_provider(request, provider_id) + + if not provider.message_suggest_caslogout_on_logout(request): + return +diff --git a/allauth_cas/views.py b/allauth_cas/views.py +index d08e354..9e81e53 100644 +--- a/allauth_cas/views.py ++++ b/allauth_cas/views.py +@@ -1,5 +1,5 @@ + import cas +-from allauth.account.adapter import get_adapter ++from allauth.socialaccount.adapter import get_adapter + from allauth.account.utils import get_next_redirect_url + from allauth.socialaccount import providers + from allauth.socialaccount.helpers import ( +@@ -56,7 +56,7 @@ class CASAdapter: + """ + Returns a provider instance for the current request. + """ +- return providers.registry.by_id(self.provider_id, self.request) ++ return get_adapter(self.request).get_provider(self.request, self.provider_id) + + def complete_login(self, request, response): + """ diff --git a/pkgs/django-allauth-cas/default.nix b/pkgs/django-allauth-cas/default.nix index 8721e21..7914852 100644 --- a/pkgs/django-allauth-cas/default.nix +++ b/pkgs/django-allauth-cas/default.nix @@ -20,7 +20,10 @@ buildPythonPackage rec { hash = "sha256-y/IquXl/4+9MJmsgbWtPun3tBbRJ4kJFzWo5c+5WeHk="; }; - patches = [ ./01-setup.patch ]; + patches = [ + ./01-setup.patch + ./02-registry.patch + ]; build-system = [ setuptools diff --git a/pkgs/django-allauth/default.nix b/pkgs/django-allauth/default.nix new file mode 100644 index 0000000..509c3b1 --- /dev/null +++ b/pkgs/django-allauth/default.nix @@ -0,0 +1,96 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + pythonOlder, + # build-system + setuptools, + # dependencies + django, + python3-openid, + requests, + requests-oauthlib, + pyjwt, + # optional-dependencies + python3-saml, + qrcode, + fido2, + # tests + pillow, + pytestCheckHook, + pytest-django, + # passthru tests + dj-rest-auth, +}: + +buildPythonPackage rec { + pname = "django-allauth"; + version = "64.2.1"; + pyproject = true; + + disabled = pythonOlder "3.7"; + + src = fetchFromGitHub { + owner = "pennersr"; + repo = "django-allauth"; + rev = "refs/tags/${version}"; + hash = "sha256-JKjM+zqrXidxpbi+fo6wbvdXlw2oDYH51EsvQ5yp3R8="; + }; + + nativeBuildInputs = [ + setuptools + ]; + + propagatedBuildInputs = [ + django + ]; + + passthru.optional-dependencies = { + mfa = [ + qrcode + fido2 + ]; + openid = [ + python3-openid + ]; + saml = [ + python3-saml + ]; + socialaccount = [ + pyjwt + requests + requests-oauthlib + ] ++ pyjwt.optional-dependencies.crypto; + steam = [ + python3-openid + ]; + }; + + pythonImportsCheck = [ + "allauth" + ]; + + nativeCheckInputs = [ + pillow + pytestCheckHook + pytest-django + ] ++ lib.flatten (builtins.attrValues passthru.optional-dependencies); + + disabledTests = [ + # Tests require network access + "test_login" + ]; + + passthru.tests = { + inherit dj-rest-auth; + }; + + meta = with lib; { + changelog = "https://github.com/pennersr/django-allauth/blob/${version}/ChangeLog.rst"; + description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication"; + downloadPage = "https://github.com/pennersr/django-allauth"; + homepage = "https://www.intenct.nl/projects/django-allauth"; + license = licenses.mit; + maintainers = with maintainers; [ derdennisop ]; + }; +} diff --git a/src/app/settings.py b/src/app/settings.py index 08e8d03..abca0ea 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -37,6 +37,8 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.openid_connect", + "allauth_cas", + "shared.cas", # Main app "dgsi", ] diff --git a/src/shared/cas/__init__.py b/src/shared/cas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/cas/provider.py b/src/shared/cas/provider.py new file mode 100644 index 0000000..fea3fa4 --- /dev/null +++ b/src/shared/cas/provider.py @@ -0,0 +1,11 @@ +from allauth.socialaccount.providers.base import ProviderAccount +from allauth_cas.providers import CASProvider as Provider + + +class CASProvider(Provider): + id = "cas" # Choose an identifier for your provider + name = "CAS ENS" # Verbose name of your provider + account_class = ProviderAccount + + +provider_classes = [CASProvider] diff --git a/src/shared/cas/urls.py b/src/shared/cas/urls.py new file mode 100644 index 0000000..34c1ea1 --- /dev/null +++ b/src/shared/cas/urls.py @@ -0,0 +1,5 @@ +from allauth_cas.urls import default_urlpatterns + +from .provider import CASProvider + +urlpatterns = default_urlpatterns(CASProvider) diff --git a/src/shared/cas/views.py b/src/shared/cas/views.py new file mode 100644 index 0000000..51bfd00 --- /dev/null +++ b/src/shared/cas/views.py @@ -0,0 +1,14 @@ +from allauth_cas.views import CASAdapter as Adapter +from allauth_cas.views import CASCallbackView, CASLoginView + +from .provider import CASProvider + + +class CASAdapter(Adapter): + provider_id = CASProvider.id + url = "https://cas.eleves.ens.fr" + version = 3 + + +login = CASLoginView.adapter_view(CASAdapter) +callback = CASCallbackView.adapter_view(CASAdapter) From 1d2f4a5866846f7dabc2d0b58816df73b17779f2 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 18 Sep 2024 22:19:33 +0200 Subject: [PATCH 023/172] feat(account): Use a custom adpater --- src/app/settings.py | 20 +++++++++++++++----- src/shared/account.py | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index abca0ea..59c3418 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -111,11 +111,6 @@ AUTHENTICATION_BACKENDS = [ "allauth.account.auth_backends.AuthenticationBackend", ] -ACCOUNT_ADAPTER = "shared.account.AccountAdapter" -ACCOUNT_CHANGE_EMAIL = True -ACCOUNT_EMAIL_NOTIFICATIONS = True - -SOCIALACCOUNT_ONLY = True SOCIALACCOUNT_PROVIDERS = { "openid_connect": { "OAUTH_PKCE_ENABLED": True, @@ -127,12 +122,25 @@ SOCIALACCOUNT_PROVIDERS = { "secret": credentials["KANIDM_SECRET"], "settings": { "server_url": f"https://sso.dgnum.eu/oauth2/openid/{credentials['KANIDM_CLIENT']}", + "color": "primary", }, } ], }, + "cas": { + "APP": { + "provider_id": "ens_cas", + "name": "CAS ENS", + "settings": {"color": "danger"}, + }, + }, } +SOCIALACCOUNT_ONLY = True +SOCIALACCOUNT_ADAPTER = "shared.account.SharedAccountAdapter" +ACCOUNT_EMAIL_VERIFICATION = "none" +ACCOUNT_AUTHENTICATION_METHOD = "username" + AUTH_PASSWORD_VALIDATORS = [] AUTH_USER_MODEL = "dgsi.User" @@ -206,6 +214,8 @@ if DEBUG: "django_browser_reload.middleware.BrowserReloadMiddleware", ] + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + INTERNAL_IPS = ["127.0.0.1"] DEBUG_TOOLBAR_CONFIG = {"INSERT_BEFORE": ""} diff --git a/src/shared/account.py b/src/shared/account.py index 544fcd4..601cccb 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -1,10 +1,35 @@ -from allauth.account.adapter import DefaultAccountAdapter +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.socialaccount.models import SocialLogin + +from dgsi.models import User -class AccountAdapter(DefaultAccountAdapter): +class SharedAccountAdapter(DefaultSocialAccountAdapter): """ - Overrides the Account Adapter. + Overrides the Account Adapter, to allow a simpler connection via CAS. """ - def is_open_for_signup(self, request): - return False + def pre_social_login(self, request, sociallogin): + match sociallogin.account.provider: + # TODO: Add a correspondance table between ENS logins and ours + case "ens_cas": + # In this case, the username is located in extra_data["uid"] + username = sociallogin.account.extra_data["uid"] + case "kanidm": + username = sociallogin.account.extra_data["preferred_username"] + case _p: + raise KeyError(f"No sociallogin '{_p}' is supposed to exist.") + + try: + # Connect an existing user if the login already exists, even if it + # with another social method + user = User.objects.get(username=username) + sociallogin.connect(request, user) + except User.DoesNotExist: + pass + + def populate_user(self, request, sociallogin, data): + return super().populate_user(request, sociallogin, data) + + def save_user(self, request, sociallogin: SocialLogin, form=None): + return super().save_user(request, sociallogin, form) From 97535a6bc0baa15b469236a92f60270ca267945b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 18 Sep 2024 22:20:05 +0200 Subject: [PATCH 024/172] fix(models): Don't crash when no kanidm profile exists --- src/dgsi/models.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index b60cd1d..70c2b4d 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -4,6 +4,7 @@ from typing import Optional from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser +from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person from shared.kanidm import client @@ -23,14 +24,19 @@ class User(AbstractUser): """ @cached_property - def kanidm(self) -> KanidmProfile: - radius_data = async_to_sync(client.get_radius_token)(self.username).data + def kanidm(self) -> Optional[KanidmProfile]: + try: + radius_data = async_to_sync(client.get_radius_token)(self.username).data - return KanidmProfile( - person=async_to_sync(client.person_account_get)(self.username), - radius_secret=radius_data and radius_data.get("secret"), - ) + return KanidmProfile( + person=async_to_sync(client.person_account_get)(self.username), + radius_secret=radius_data and radius_data.get("secret"), + ) + except NoMatchingEntries: + return None @property def is_admin(self) -> bool: - return ADMIN_GROUP in self.kanidm.person.memberof + return (self.kanidm is not None) and ( + ADMIN_GROUP in self.kanidm.person.memberof + ) From 35d6a7fa8ceadb45a79371a5308725d2be9a64c3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 18 Sep 2024 22:20:17 +0200 Subject: [PATCH 025/172] feat(templates): Tweak --- src/shared/templates/_hero.html | 2 +- .../templates/allauth/elements/provider.html | 2 +- src/shared/templates/login.html | 2 +- .../socialaccount/snippets/provider_list.html | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 src/shared/templates/socialaccount/snippets/provider_list.html diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index e482ce2..4fd16c8 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -21,7 +21,7 @@ {% else %} - + {% trans "Connexion" %} diff --git a/src/shared/templates/allauth/elements/provider.html b/src/shared/templates/allauth/elements/provider.html index 36bc3f7..89b8094 100644 --- a/src/shared/templates/allauth/elements/provider.html +++ b/src/shared/templates/allauth/elements/provider.html @@ -1 +1 @@ -{{ attrs.name }} +{{ attrs.name }} diff --git a/src/shared/templates/login.html b/src/shared/templates/login.html index 03e6ca0..d6a5160 100644 --- a/src/shared/templates/login.html +++ b/src/shared/templates/login.html @@ -15,7 +15,7 @@ - diff --git a/src/shared/templates/socialaccount/snippets/provider_list.html b/src/shared/templates/socialaccount/snippets/provider_list.html new file mode 100644 index 0000000..87a3119 --- /dev/null +++ b/src/shared/templates/socialaccount/snippets/provider_list.html @@ -0,0 +1,18 @@ +{% load allauth socialaccount %} +{% get_providers as socialaccount_providers %} +{% if socialaccount_providers %} + {% element provider_list %} + {% for provider in socialaccount_providers %} + {% if provider.id == "openid" %} + {% for brand in provider.get_brands %} + {% provider_login_url provider openid=brand.openid_url process=process as href %} + {% element provider name=brand.name provider_id=provider.id href=href color=provider.app.settings.color %} + {% endelement %} + {% endfor %} + {% endif %} + {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} + {% element provider name=provider.name provider_id=provider.id href=href color=provider.app.settings.color %} + {% endelement %} + {% endfor %} + {% endelement %} +{% endif %} From c3f0e703847a4886341ce745357c17562f8bf59d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 18 Sep 2024 22:32:44 +0200 Subject: [PATCH 026/172] feat(profile): Use the local name, add a notification when no dgn account exists --- src/dgsi/views.py | 9 +++++++++ src/shared/templates/account/profile.html | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 5515f8f..cb3c4f7 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -8,6 +8,15 @@ from dgsi.models import User class ProfileView(LoginRequiredMixin, TemplateView): template_name = "account/profile.html" + def get_context_data(self, **kwargs): + u = self.request.user + assert isinstance(u, User) + + return super().get_context_data( + displayname=f"{u.first_name} {u.last_name}", + **kwargs, + ) + class CreateUserView(StaffRequiredMixin, CreateView): model = User diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index 39d5a29..14a90dc 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -11,8 +11,8 @@ {% block content %}

- {% blocktrans with displayname=user.kanidm.person.displayname %}Profil de {{ displayname }}{% endblocktrans %} - {{ user.kanidm.person.name }} + {% blocktrans %}Profil de {{ displayname }}{% endblocktrans %} + {{ user.username }}


@@ -33,6 +33,7 @@ {{ user.email }}
+ {% if user.kanidm %}

{% trans "Informations techniques" %}


@@ -54,4 +55,7 @@ {{ group }} {% endfor %}
+ {% else %} +
{% trans "Pas de compte DGNum répertorié." %}
+ {% endif %} {% endblock content %} From 5af8e2fd24601ac579bfec521df03e970e0e06dc Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 18 Sep 2024 23:00:36 +0200 Subject: [PATCH 027/172] feat(account): Prevent connections from some categories of users --- src/shared/account.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/shared/account.py b/src/shared/account.py index 601cccb..0210a79 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -1,5 +1,7 @@ +from allauth.core.exceptions import ImmediateHttpResponse from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.models import SocialLogin +from django.http import HttpResponseRedirect from dgsi.models import User @@ -15,6 +17,16 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): case "ens_cas": # In this case, the username is located in extra_data["uid"] username = sociallogin.account.extra_data["uid"] + + # Validate that the user is a regular one + home = sociallogin.account.extra_data["homeDirectory"].split("/") + + if (home[1] != "users") or ( + home[2] + in ["absint", "algo", "grecc", "guests", "spi", "spi1", "staffs"] + ): + raise ImmediateHttpResponse(HttpResponseRedirect("/")) + case "kanidm": username = sociallogin.account.extra_data["preferred_username"] case _p: From cd8859f610aa88b6cbe392c4b8bc8a60bd66e94d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 19 Sep 2024 09:14:42 +0200 Subject: [PATCH 028/172] feat(account): Add a template with a message when a CAS account is forbidden --- src/dgsi/urls.py | 5 +++++ src/shared/account.py | 5 ++++- src/shared/templates/account/forbidden_category.html | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/shared/templates/account/forbidden_category.html diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 38c0e0c..2555ae0 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -7,4 +7,9 @@ app_name = "dgsi" urlpatterns = [ path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), path("accounts/create/", views.CreateUserView.as_view(), name="dgn-create_user"), + path( + "accounts/forbidden/", + views.TemplateView.as_view(template_name="account/forbidden_category.html"), + name="dgn-forbidden_account", + ), ] diff --git a/src/shared/account.py b/src/shared/account.py index 0210a79..31eb5ad 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -2,6 +2,7 @@ from allauth.core.exceptions import ImmediateHttpResponse from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.models import SocialLogin from django.http import HttpResponseRedirect +from django.urls import reverse from dgsi.models import User @@ -25,7 +26,9 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): home[2] in ["absint", "algo", "grecc", "guests", "spi", "spi1", "staffs"] ): - raise ImmediateHttpResponse(HttpResponseRedirect("/")) + raise ImmediateHttpResponse( + HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account")) + ) case "kanidm": username = sociallogin.account.extra_data["preferred_username"] diff --git a/src/shared/templates/account/forbidden_category.html b/src/shared/templates/account/forbidden_category.html new file mode 100644 index 0000000..1892cce --- /dev/null +++ b/src/shared/templates/account/forbidden_category.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+{% endblock content %} From 36ccc6be24a7df9acf1505df86f43c98ca10e5cb Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 19 Sep 2024 11:37:45 +0200 Subject: [PATCH 029/172] feat(account): Add a view to create accounts TODO: include the reset mechanism for the password --- src/dgsi/forms.py | 35 +++++++++++++++++++ src/dgsi/urls.py | 6 +++- src/dgsi/views.py | 31 ++++++++++++++-- .../templates/account/create_kanidm.html | 14 ++++++++ src/shared/templates/home.html | 5 ++- 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/dgsi/forms.py create mode 100644 src/shared/templates/account/create_kanidm.html diff --git a/src/dgsi/forms.py b/src/dgsi/forms.py new file mode 100644 index 0000000..2bc31a7 --- /dev/null +++ b/src/dgsi/forms.py @@ -0,0 +1,35 @@ +from asgiref.sync import async_to_sync +from django.core.exceptions import ValidationError +from django.forms import BooleanField, CharField, EmailField, forms +from django.utils.translation import gettext_lazy as _ + +from shared.kanidm import client + + +@async_to_sync +async def name_validator(value: str) -> None: + try: + await client.person_account_get(value) + except ValueError: + return + + raise ValidationError(_("Identifiant déjà présent dans la base de données.")) + + +class CreateKanidmAccountForm(forms.Form): + # TODO: Add a field for the clipper login information for the local mapping + name = CharField( + label=_("Identifiant"), + help_text=_("De préférence identique au login ENS de la personne concernée"), + validators=[name_validator], + ) + displayname = CharField(label=_("Nom d'usage")) + mail = EmailField( + label=_("Adresse e-mail"), + help_text=_("De préférence l'adresse '@ens.psl.eu'"), + ) + active = BooleanField( + label=_("Membre actif"), + help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members"), + required=False, + ) diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 2555ae0..09cd361 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -6,7 +6,11 @@ app_name = "dgsi" urlpatterns = [ path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), - path("accounts/create/", views.CreateUserView.as_view(), name="dgn-create_user"), + path( + "accounts/create-kanidm/", + views.CreateKanidmAccountView.as_view(), + name="dgn-create_user", + ), path( "accounts/forbidden/", views.TemplateView.as_view(template_name="account/forbidden_category.html"), diff --git a/src/dgsi/views.py b/src/dgsi/views.py index cb3c4f7..f1e5d95 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,8 +1,12 @@ +from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import CreateView, TemplateView +from django.urls import reverse_lazy +from django.views.generic import FormView, TemplateView +from dgsi.forms import CreateKanidmAccountForm from dgsi.mixins import StaffRequiredMixin from dgsi.models import User +from shared.kanidm import client class ProfileView(LoginRequiredMixin, TemplateView): @@ -18,5 +22,26 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) -class CreateUserView(StaffRequiredMixin, CreateView): - model = User +class CreateKanidmAccountView(StaffRequiredMixin, FormView): + form_class = CreateKanidmAccountForm + template_name = "account/create_kanidm.html" + + success_url = reverse_lazy("dgsi:dgn-create_user") + + @async_to_sync + async def form_valid(self, form): + d = form.cleaned_data + + # Create the base account + await client.person_account_create(d["name"], d["displayname"]) + + # Update the information + await client.person_account_update(d["name"], mail=d["mail"]) + + # If necessary, add the user to the active members group + if d["active"]: + await client.group_add_members("dgnum_members", [d["name"]]) + + # TODO: Generate a reset token, and update it + + return super().form_valid(form) diff --git a/src/shared/templates/account/create_kanidm.html b/src/shared/templates/account/create_kanidm.html new file mode 100644 index 0000000..2ccb838 --- /dev/null +++ b/src/shared/templates/account/create_kanidm.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +

{% trans "Création de compte Kanidm" %}

+
+ +
+ {% csrf_token %} + {% include "bulma/form.html" with form=form %} + + +
+{% endblock content %} diff --git a/src/shared/templates/home.html b/src/shared/templates/home.html index 45f2c21..a0b0968 100644 --- a/src/shared/templates/home.html +++ b/src/shared/templates/home.html @@ -3,6 +3,9 @@ {% block content %} {% if user.is_authenticated %} - {% trans "Mon profil" %} + {% trans "Mon profil" %} + {% endif %} + {% if user.is_admin %} + {% trans "Créer un nouveau compte" %} {% endif %} {% endblock content %} From ea3c5cf6fd94b959414b1a382b1ad56d5f4eb2b5 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 19 Sep 2024 11:40:16 +0200 Subject: [PATCH 030/172] fix(mixins): Use the correct test function --- src/dgsi/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dgsi/mixins.py b/src/dgsi/mixins.py index dcb6d70..90deae0 100644 --- a/src/dgsi/mixins.py +++ b/src/dgsi/mixins.py @@ -7,8 +7,8 @@ from dgsi.models import User class StaffRequiredMixin(UserPassesTestMixin): request: HttpRequest - def test_func(self) -> bool | None: - if self.request.user.is_authenticated: + def test_func(self) -> bool: + if not self.request.user.is_authenticated: return False assert isinstance(self.request.user, User) From 283815d55504353d0e6931bdbf35c0a10229a4a9 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 19 Sep 2024 11:59:24 +0200 Subject: [PATCH 031/172] fix(create_user): Kanidm expects a list of email addresses --- src/dgsi/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index f1e5d95..86f1651 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -36,7 +36,7 @@ class CreateKanidmAccountView(StaffRequiredMixin, FormView): await client.person_account_create(d["name"], d["displayname"]) # Update the information - await client.person_account_update(d["name"], mail=d["mail"]) + await client.person_account_update(d["name"], mail=[d["mail"]]) # If necessary, add the user to the active members group if d["active"]: From 7c11b0de4bbf20394929258861dfc987ddcc1f6e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 19 Sep 2024 13:03:09 +0200 Subject: [PATCH 032/172] feat(create_user): Send an email with the credentials reset link --- src/dgsi/views.py | 26 ++++++++++++++++++- .../templates/mail/credentials_reset.txt | 17 ++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/shared/templates/mail/credentials_reset.txt diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 86f1651..742da24 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,5 +1,7 @@ from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.mail import EmailMessage +from django.template.loader import render_to_string from django.urls import reverse_lazy from django.views.generic import FormView, TemplateView @@ -30,6 +32,8 @@ class CreateKanidmAccountView(StaffRequiredMixin, FormView): @async_to_sync async def form_valid(self, form): + ttl = 86400 # 24h + d = form.cleaned_data # Create the base account @@ -42,6 +46,26 @@ class CreateKanidmAccountView(StaffRequiredMixin, FormView): if d["active"]: await client.group_add_members("dgnum_members", [d["name"]]) - # TODO: Generate a reset token, and update it + # FIXME: Will maybe change when kanidm gets its shit together and switches to POST + r = await client.call_get( + f"/v1/person/{d['name']}/_credential/_update_intent/{ttl}" + ) + + assert r.data is not None + + token: str = r.data["token"] + link = f"https://sso.dgnum.eu/ui/reset?token={token}" + + # Send an email to the new user with the given email address + EmailMessage( + subject="Réinitialisation de mot de passe DGNum -- DGNum password reset", + body=render_to_string( + "mail/credentials_reset.txt", + context={"link": link}, + ), + from_email="To Be Determined ", + to=[d["mail"]], + headers={"Reply-To": "contact@dgnum.eu"}, + ).send() return super().form_valid(form) diff --git a/src/shared/templates/mail/credentials_reset.txt b/src/shared/templates/mail/credentials_reset.txt new file mode 100644 index 0000000..f20a603 --- /dev/null +++ b/src/shared/templates/mail/credentials_reset.txt @@ -0,0 +1,17 @@ +Bonjour, + +Une demande de réinitialisation de votre mot de passe DGNum a été effectuée. + +Pour mettre à jour vos moyens de connexion, merci de vous rendre à l'adresse : {{ link }} + +-- + +Hello, + +A request to reset your DGNum password has been made. + +To update your login details, please go to: {{ link }} + + +Bien cordialement, +La Délégation Générale Numérique From e31731f8e6ca1e787064a2ba58eb72ec5b0e1079 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 21 Sep 2024 23:01:21 +0200 Subject: [PATCH 033/172] feat(apps): Install django-unfold --- default.nix | 1 + pkgs/django-unfold/default.nix | 40 ++++++++++++++++++++++++++++++++++ src/app/settings.py | 1 + src/app/urls.py | 2 +- 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 pkgs/django-unfold/default.nix diff --git a/default.nix b/default.nix index 60a6190..33d30c0 100644 --- a/default.nix +++ b/default.nix @@ -54,6 +54,7 @@ in ps.django-sass-processor ps.django-sass-processor-dart-sass ps.django-stubs + ps.django-unfold ps.ipython ps.loadcredential ps.pykanidm diff --git a/pkgs/django-unfold/default.nix b/pkgs/django-unfold/default.nix new file mode 100644 index 0000000..a6d1889 --- /dev/null +++ b/pkgs/django-unfold/default.nix @@ -0,0 +1,40 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + poetry-core, + django, +}: + +buildPythonPackage rec { + pname = "django-unfold"; + version = "0.39.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "unfoldadmin"; + repo = "django-unfold"; + rev = version; + hash = "sha256-CmmlTx2eLcANc6ANy25ii1KVebkmUEJmDCe+/RwakAg="; + }; + + build-system = [ + poetry-core + ]; + + dependencies = [ + django + ]; + + pythonImportsCheck = [ + "unfold" + ]; + + meta = { + description = "Modern Django admin theme for seamless interface development"; + homepage = "https://github.com/unfoldadmin/django-unfold"; + changelog = "https://github.com/unfoldadmin/django-unfold/blob/${src.rev}/CHANGELOG.md"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ ]; + }; +} diff --git a/src/app/settings.py b/src/app/settings.py index 59c3418..dd6ea60 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -24,6 +24,7 @@ ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) # List the installed applications INSTALLED_APPS = [ + "unfold", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/src/app/urls.py b/src/app/urls.py index 19fb416..7a3942a 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -25,11 +25,11 @@ urlpatterns = [ path("login", TemplateView.as_view(template_name="login.html"), name="login"), path("", include("dgsi.urls")), path("accounts/", include("allauth.urls")), + path("admin/", admin.site.urls), ] if settings.DEBUG: urlpatterns += [ - path("admin/", admin.site.urls), path("__reload__/", include("django_browser_reload.urls")), path("__debug__/", include("debug_toolbar.urls")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) From 5ec6a482ccad8175ea457a55f62a088cf53f706b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 21 Sep 2024 23:01:42 +0200 Subject: [PATCH 034/172] feat(accounts): Copy distant attributes --- src/shared/account.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared/account.py b/src/shared/account.py index 31eb5ad..863d1c7 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -41,7 +41,13 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): user = User.objects.get(username=username) sociallogin.connect(request, user) except User.DoesNotExist: - pass + return + + # We now know that a user exists, copy the distant attributes + user.is_staff = user.is_admin + user.is_superuser = user.is_admin + + user.save() def populate_user(self, request, sociallogin, data): return super().populate_user(request, sociallogin, data) From 01597ffb7b5dec58112a56321d50ee448815662e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 21 Sep 2024 23:01:59 +0200 Subject: [PATCH 035/172] feat(home): Add a link to the admin panel --- src/shared/templates/home.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/templates/home.html b/src/shared/templates/home.html index a0b0968..6cb248e 100644 --- a/src/shared/templates/home.html +++ b/src/shared/templates/home.html @@ -7,5 +7,6 @@ {% endif %} {% if user.is_admin %} {% trans "Créer un nouveau compte" %} + {% trans "Interface admin" %} {% endif %} {% endblock content %} From ef80f9389bc74103966a5760a2e62fa5b986ec7e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 22 Sep 2024 20:12:52 +0200 Subject: [PATCH 036/172] feat(messages): Add support for dismissable notifications --- src/app/settings.py | 13 +++++++++++++ src/dgsi/views.py | 5 ++++- src/shared/static/bulma/bulma.scss | 13 +++++++++++++ src/shared/static/js/dgsi.js | 11 +++++++++++ src/shared/templates/_links.html | 3 +++ src/shared/templates/base.html | 12 +++++++++++- 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/shared/static/js/dgsi.js diff --git a/src/app/settings.py b/src/app/settings.py index dd6ea60..879214d 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -4,6 +4,7 @@ Django settings for the DGSI project. from pathlib import Path +from django.contrib.messages import constants as messages from loadcredential import Credentials credentials = Credentials(env_prefix="DGSI_") @@ -201,6 +202,18 @@ SASS_PROCESSOR_ENABLED = True DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +## +# Messages configuration + +MESSAGE_TAGS = { + messages.DEBUG: "is-dark", + messages.INFO: "is-primary", + messages.SUCCESS: "is-success", + messages.WARNING: "is-warning", + messages.ERROR: "is-danger", +} + + ### # Extend settings when running in dev mode diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 742da24..b4e73be 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,8 +1,10 @@ from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin from django.core.mail import EmailMessage from django.template.loader import render_to_string from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, TemplateView from dgsi.forms import CreateKanidmAccountForm @@ -24,9 +26,10 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) -class CreateKanidmAccountView(StaffRequiredMixin, FormView): +class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView): form_class = CreateKanidmAccountForm template_name = "account/create_kanidm.html" + success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.") success_url = reverse_lazy("dgsi:dgn-create_user") diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 3321e66..8bcb9a5 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -16,3 +16,16 @@ body { flex-direction: column; justify-content: space-between; } + +#notifications { + margin-left: -0.75rem; + margin-right: -0.75rem; + position: sticky; + display: block; + inset: 1.5rem; + z-index: 500; +} + +.notification { + margin-bottom: var(--bulma-block-spacing); +} diff --git a/src/shared/static/js/dgsi.js b/src/shared/static/js/dgsi.js new file mode 100644 index 0000000..61f1234 --- /dev/null +++ b/src/shared/static/js/dgsi.js @@ -0,0 +1,11 @@ +document.addEventListener("DOMContentLoaded", () => { + (document.querySelectorAll(".notification .delete") || []).forEach( + ($delete) => { + const $notification = $delete.parentNode; + const dismiss = () => $notification.parentNode.removeChild($notification); + + $delete.addEventListener("click", dismiss); + setTimeout(dismiss, 15000); + }, + ); +}); diff --git a/src/shared/templates/_links.html b/src/shared/templates/_links.html index 14ee324..84065b7 100644 --- a/src/shared/templates/_links.html +++ b/src/shared/templates/_links.html @@ -10,3 +10,6 @@ + + + diff --git a/src/shared/templates/base.html b/src/shared/templates/base.html index 8f5d395..e535d9a 100644 --- a/src/shared/templates/base.html +++ b/src/shared/templates/base.html @@ -7,7 +7,8 @@ DGNum - {% block extra_head %}{% endblock extra_head %} + {% block extra_head %} + {% endblock extra_head %} {% include "_links.html" %} @@ -16,6 +17,15 @@ {% include "_hero.html" %}
+
+ {% for message in messages %} +
+ + {{ message|safe }} +
+ {% endfor %} +
+ {% block content %} {% endblock content %}
From ef9877ea60a8dd0d36f2ff583bc916b275206803 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 22 Sep 2024 20:13:15 +0200 Subject: [PATCH 037/172] chore(nix): Remove eslint hook, as it does not really work --- default.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/default.nix b/default.nix index 33d30c0..e83ff83 100644 --- a/default.nix +++ b/default.nix @@ -8,9 +8,6 @@ let src = ./.; hooks = { - # JS hooks - eslint.enable = true; - # Python hooks ruff.enable = true; black.enable = true; From 26fdcbe8efaf33bb0c242c3c67eb9a7a88d3892e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 22 Sep 2024 20:13:55 +0200 Subject: [PATCH 038/172] feat(home):feat(templates): Move style to the sass bundle --- src/shared/static/bulma/bulma.scss | 22 ++++++- .../templates/account/create_kanidm.html | 3 +- .../templates/account/forbidden_category.html | 1 + src/shared/templates/account/profile.html | 61 ++++++++----------- src/shared/templates/home.html | 18 +++--- .../socialaccount/snippets/provider_list.html | 29 ++++----- 6 files changed, 76 insertions(+), 58 deletions(-) diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 8bcb9a5..526e83d 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -7,7 +7,7 @@ $dark: rgb(46, 46, 46); @use "sass" with ( $primary: $blue, $link: rgb(72, 95, 199), - $dark: $dark, + $dark: $dark ); body { @@ -17,6 +17,26 @@ body { justify-content: space-between; } +.bt-link { + display: flex; + width: 100%; + margin-bottom: calc(0.5 * var(--bulma-block-spacing)); + font-size: 1.25rem; + + // Dark color for text + --bulma-color-l: var(--bulma-dark-l); + --bulma-color-l-delta: 0%; + color: hsl( + var(--bulma-dark-h), + var(--bulma-dark-s), + calc(var(--bulma-color-l) + var(--bulma-color-l-delta)) + ); +} + +.grid.groups { + --bulma-grid-column-min: 24rem; +} + #notifications { margin-left: -0.75rem; margin-right: -0.75rem; diff --git a/src/shared/templates/account/create_kanidm.html b/src/shared/templates/account/create_kanidm.html index 2ccb838..c17aa91 100644 --- a/src/shared/templates/account/create_kanidm.html +++ b/src/shared/templates/account/create_kanidm.html @@ -1,11 +1,12 @@ {% extends "base.html" %} + {% load i18n %} {% block content %}

{% trans "Création de compte Kanidm" %}


-
+ {% csrf_token %} {% include "bulma/form.html" with form=form %} diff --git a/src/shared/templates/account/forbidden_category.html b/src/shared/templates/account/forbidden_category.html index 1892cce..c296c89 100644 --- a/src/shared/templates/account/forbidden_category.html +++ b/src/shared/templates/account/forbidden_category.html @@ -1,4 +1,5 @@ {% extends "base.html" %} + {% load i18n %} {% block content %} diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index 14a90dc..2c8dc79 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -1,13 +1,6 @@ {% extends "base.html" %} -{% load i18n %} -{% block extra_head %} - -{% endblock extra_head %} +{% load i18n %} {% block content %}

@@ -17,16 +10,14 @@
{% if user.kanidm.radius_secret %} -

{% trans "Mot de passe WiFi :" %}

+

{% trans "Mot de passe WiFi :" %}

- -
+ +
{% endif %}

{% trans "Adresse e-mail :" %}

@@ -34,28 +25,28 @@
{% if user.kanidm %} -

{% trans "Informations techniques" %}

-
+

{% trans "Informations techniques" %}

+
-

{% trans "Identifiant unique :" %}

+

{% trans "Identifiant unique :" %}

- -
+ +
-

{% trans "Membre des groupes suivants :" %}

+

{% trans "Membre des groupes suivants :" %}

-
- {% for group in user.kanidm.person.memberof %} - {{ group }} - {% endfor %} -
+
+ {% for group in user.kanidm.person.memberof %} + {{ group }} + {% endfor %} +
{% else %} -
{% trans "Pas de compte DGNum répertorié." %}
+
+ {% trans "Pas de compte DGNum répertorié." %} +
{% endif %} {% endblock content %} diff --git a/src/shared/templates/home.html b/src/shared/templates/home.html index 6cb248e..ffe3eb9 100644 --- a/src/shared/templates/home.html +++ b/src/shared/templates/home.html @@ -1,12 +1,16 @@ {% extends "base.html" %} + {% load i18n %} {% block content %} - {% if user.is_authenticated %} - {% trans "Mon profil" %} - {% endif %} - {% if user.is_admin %} - {% trans "Créer un nouveau compte" %} - {% trans "Interface admin" %} - {% endif %} +
+ {% if user.is_authenticated %} + {% trans "Mon profil" %} + {% endif %} + {% if user.is_admin %} + {% trans "Créer un nouveau compte Kanidm" %} + {% trans "Interface admin" %} + {% endif %} +
{% endblock content %} diff --git a/src/shared/templates/socialaccount/snippets/provider_list.html b/src/shared/templates/socialaccount/snippets/provider_list.html index 87a3119..3eb191e 100644 --- a/src/shared/templates/socialaccount/snippets/provider_list.html +++ b/src/shared/templates/socialaccount/snippets/provider_list.html @@ -1,18 +1,19 @@ {% load allauth socialaccount %} + {% get_providers as socialaccount_providers %} {% if socialaccount_providers %} - {% element provider_list %} - {% for provider in socialaccount_providers %} - {% if provider.id == "openid" %} - {% for brand in provider.get_brands %} - {% provider_login_url provider openid=brand.openid_url process=process as href %} - {% element provider name=brand.name provider_id=provider.id href=href color=provider.app.settings.color %} - {% endelement %} - {% endfor %} - {% endif %} - {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} - {% element provider name=provider.name provider_id=provider.id href=href color=provider.app.settings.color %} - {% endelement %} - {% endfor %} - {% endelement %} + {% element provider_list %} + {% for provider in socialaccount_providers %} + {% if provider.id == "openid" %} + {% for brand in provider.get_brands %} + {% provider_login_url provider openid=brand.openid_url process=process as href %} + {% element provider name=brand.name provider_id=provider.id href=href color=provider.app.settings.color %} + {% endelement %} + {% endfor %} + {% endif %} + {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} + {% element provider name=provider.name provider_id=provider.id href=href color=provider.app.settings.color %} +{% endelement %} +{% endfor %} +{% endelement %} {% endif %} From 0c54fd29abc21a973580785452308bd58ae3ac43 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 23 Sep 2024 16:53:40 +0200 Subject: [PATCH 039/172] fix(profile): Tweak CSS so that the group names don't overflow We do not really lose any information as all groups end with `@sso.dgnum.eu` --- src/shared/static/bulma/bulma.scss | 5 +++++ src/shared/templates/account/profile.html | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 526e83d..a465188 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -37,6 +37,11 @@ body { --bulma-grid-column-min: 24rem; } +.groups > .button > span { + overflow-x: hidden; + text-overflow: ellipsis; +} + #notifications { margin-left: -0.75rem; margin-right: -0.75rem; diff --git a/src/shared/templates/account/profile.html b/src/shared/templates/account/profile.html index 2c8dc79..eed8365 100644 --- a/src/shared/templates/account/profile.html +++ b/src/shared/templates/account/profile.html @@ -41,7 +41,9 @@
{% for group in user.kanidm.person.memberof %} - {{ group }} +
+ {{ group }} +
{% endfor %}
{% else %} From 39623a88029ce943068d099e6dbe6b0e3ec2a536 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 23 Sep 2024 18:16:02 +0200 Subject: [PATCH 040/172] feat(admin): Update the registration of the User model --- src/dgsi/admin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 5279d05..7bf0b54 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -1,6 +1,9 @@ from django.contrib import admin -from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from dgsi.models import User +from unfold.admin import ModelAdmin -admin.site.register(User, UserAdmin) +@admin.register(User) +class UserAdmin(BaseUserAdmin, ModelAdmin): + pass From cfa14a6aae970cd09b2aaef0b3b0831a2a5b4c0e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 23 Sep 2024 18:16:21 +0200 Subject: [PATCH 041/172] fix(shell): Run linters at pre-push --- default.nix | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/default.nix b/default.nix index e83ff83..3facc5c 100644 --- a/default.nix +++ b/default.nix @@ -9,13 +9,25 @@ let hooks = { # Python hooks - ruff.enable = true; - black.enable = true; - isort.enable = true; + black = { + enable = true; + stages = [ "pre-push" ]; + }; + + isort = { + enable = true; + stages = [ "pre-push" ]; + }; # Nix Hooks - statix.enable = true; - deadnix.enable = true; + statix = { + enable = true; + stages = [ "pre-push" ]; + }; + deadnix = { + enable = true; + stages = [ "pre-push" ]; + }; # Misc Hooks commitizen.enable = true; From 9b7f4f17e88697836a8922b9a22538739ad28375 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 23 Sep 2024 18:17:51 +0200 Subject: [PATCH 042/172] feat(dgsi): Add a Service model This will allow to list our services, whith links directly in the app and perform ToS verification --- src/dgsi/admin.py | 10 +++++++-- src/dgsi/migrations/0002_service.py | 33 +++++++++++++++++++++++++++++ src/dgsi/models.py | 10 +++++++++ src/dgsi/urls.py | 1 + src/dgsi/views.py | 12 +++++++++-- 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/dgsi/migrations/0002_service.py diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 7bf0b54..cbccfc7 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -1,9 +1,15 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin - -from dgsi.models import User from unfold.admin import ModelAdmin +from dgsi.models import Service, User + + @admin.register(User) class UserAdmin(BaseUserAdmin, ModelAdmin): pass + + +@admin.register(Service) +class AdminClass(ModelAdmin): + compressed_fields = True diff --git a/src/dgsi/migrations/0002_service.py b/src/dgsi/migrations/0002_service.py new file mode 100644 index 0000000..454008a --- /dev/null +++ b/src/dgsi/migrations/0002_service.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.12 on 2024-09-23 15:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("dgsi", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Service", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Nom du service proposé" + ), + ), + ("url", models.URLField(verbose_name="Adresse du service")), + ], + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 70c2b4d..f484630 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -4,6 +4,8 @@ from typing import Optional from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser +from django.db import models +from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person @@ -12,6 +14,14 @@ from shared.kanidm import client ADMIN_GROUP = "idm_admins@sso.dgnum.eu" +class Service(models.Model): + name = models.CharField(_("Nom du service proposé"), max_length=255) + url = models.URLField(_("Adresse du service")) + + def __str__(self) -> str: + return f"{self.name} [{self.url}]" + + @dataclass class KanidmProfile: person: Person diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 09cd361..7c2f866 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -16,4 +16,5 @@ urlpatterns = [ views.TemplateView.as_view(template_name="account/forbidden_category.html"), name="dgn-forbidden_account", ), + path("services/", views.ServiceListView.as_view(), name="dgn-services"), ] diff --git a/src/dgsi/views.py b/src/dgsi/views.py index b4e73be..54231bf 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -5,11 +5,11 @@ from django.core.mail import EmailMessage from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import FormView, TemplateView +from django.views.generic import FormView, ListView, TemplateView from dgsi.forms import CreateKanidmAccountForm from dgsi.mixins import StaffRequiredMixin -from dgsi.models import User +from dgsi.models import Service, User from shared.kanidm import client @@ -26,6 +26,14 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) +class ServiceListView(LoginRequiredMixin, ListView): + model = Service + + +## +# INFO: Below are views related to the administration of DGSI + + class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView): form_class = CreateKanidmAccountForm template_name = "account/create_kanidm.html" From 8c3cba0af8af8c1081f74a5687011f611c0255b0 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 23 Sep 2024 21:40:35 +0200 Subject: [PATCH 043/172] feat(kanidm): Update the required group to be an admin --- src/dgsi/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index f484630..4c4b670 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -11,7 +11,7 @@ from kanidm.models.person import Person from shared.kanidm import client -ADMIN_GROUP = "idm_admins@sso.dgnum.eu" +ADMIN_GROUP = "dgnum_admins@sso.dgnum.eu" class Service(models.Model): From cab836955872a8455ddf7567367b0180b4e7b119 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 23 Sep 2024 23:59:29 +0200 Subject: [PATCH 044/172] feat(services): Add an icon, and a transitive redirect view --- src/dgsi/migrations/0003_service_icon.py | 19 +++++++++++++++++++ src/dgsi/models.py | 1 + src/dgsi/templates/dgsi/service_list.html | 18 ++++++++++++++++++ src/dgsi/urls.py | 5 +++++ src/dgsi/views.py | 11 ++++++++++- src/shared/static/bulma/bulma.scss | 8 ++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/dgsi/migrations/0003_service_icon.py create mode 100644 src/dgsi/templates/dgsi/service_list.html diff --git a/src/dgsi/migrations/0003_service_icon.py b/src/dgsi/migrations/0003_service_icon.py new file mode 100644 index 0000000..25666b9 --- /dev/null +++ b/src/dgsi/migrations/0003_service_icon.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.12 on 2024-09-23 21:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("dgsi", "0002_service"), + ] + + operations = [ + migrations.AddField( + model_name="service", + name="icon", + field=models.CharField( + blank=True, max_length=255, verbose_name="Icône du service" + ), + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 4c4b670..404ff51 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -17,6 +17,7 @@ ADMIN_GROUP = "dgnum_admins@sso.dgnum.eu" class Service(models.Model): name = models.CharField(_("Nom du service proposé"), max_length=255) url = models.URLField(_("Adresse du service")) + icon = models.CharField(_("Icône du service"), max_length=255, blank=True) def __str__(self) -> str: return f"{self.name} [{self.url}]" diff --git a/src/dgsi/templates/dgsi/service_list.html b/src/dgsi/templates/dgsi/service_list.html new file mode 100644 index 0000000..2c7b2bb --- /dev/null +++ b/src/dgsi/templates/dgsi/service_list.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Services accessibles via la DGNum" %}

+
+ + +{% endblock content %} diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 7c2f866..848df88 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -17,4 +17,9 @@ urlpatterns = [ name="dgn-forbidden_account", ), path("services/", views.ServiceListView.as_view(), name="dgn-services"), + path( + "services/redirect/", + views.ServiceRedirectView.as_view(), + name="dgn-services_redirect", + ), ] diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 54231bf..a9b2e7a 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,3 +1,4 @@ +from typing import Any from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin @@ -5,7 +6,8 @@ from django.core.mail import EmailMessage from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import FormView, ListView, TemplateView +from django.views.generic import FormView, ListView, RedirectView, TemplateView +from django.views.generic.detail import SingleObjectMixin from dgsi.forms import CreateKanidmAccountForm from dgsi.mixins import StaffRequiredMixin @@ -30,6 +32,13 @@ class ServiceListView(LoginRequiredMixin, ListView): model = Service +class ServiceRedirectView(LoginRequiredMixin, SingleObjectMixin, RedirectView): + model = Service + + def get_redirect_url(self, *args: Any, **kwargs: Any) -> str: + return self.get_object().url + + ## # INFO: Below are views related to the administration of DGSI diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index a465188..fd9e8a7 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -33,6 +33,14 @@ body { ); } +.bt-links { + justify-content: space-evenly; +} + +.bt-links > .button { + width: 47.5%; +} + .grid.groups { --bulma-grid-column-min: 24rem; } From 87e13b357ec15882f53f10013fd03199afb7c350 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 10:06:51 +0200 Subject: [PATCH 045/172] chore(services): Add comments --- src/dgsi/models.py | 3 +++ src/dgsi/views.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 404ff51..9b15ff6 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -19,6 +19,9 @@ class Service(models.Model): url = models.URLField(_("Adresse du service")) icon = models.CharField(_("Icône du service"), max_length=255, blank=True) + # TODO: Add a group field, to show only if the user really has access, + # when this field is null, then it is open bar + def __str__(self) -> str: return f"{self.name} [{self.url}]" diff --git a/src/dgsi/views.py b/src/dgsi/views.py index a9b2e7a..42add5a 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,4 +1,5 @@ from typing import Any + from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin @@ -31,6 +32,8 @@ class ProfileView(LoginRequiredMixin, TemplateView): class ServiceListView(LoginRequiredMixin, ListView): model = Service + # TODO: Only show available websites + class ServiceRedirectView(LoginRequiredMixin, SingleObjectMixin, RedirectView): model = Service From e8ce6f343b932af9e197fa506fb7b0c6d1451f5e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 11:28:44 +0200 Subject: [PATCH 046/172] feat(unfold): Change the name --- src/app/settings.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/settings.py b/src/app/settings.py index 879214d..41fb61e 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -5,6 +5,7 @@ Django settings for the DGSI project. from pathlib import Path from django.contrib.messages import constants as messages +from django.utils.translation import gettext_lazy as _ from loadcredential import Credentials credentials = Credentials(env_prefix="DGSI_") @@ -214,6 +215,14 @@ MESSAGE_TAGS = { } +### +# Unfold Interface configuration + +UNFOLD = { + "SITE_HEADER": _("Administration de DGSI"), +} + + ### # Extend settings when running in dev mode From e10d33176b69376e1d9878c20f9fa07d9e05039d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 11:29:47 +0200 Subject: [PATCH 047/172] chore(templates): Rename templates --- .../templates/dgsi/create_kanidm_account.html} | 0 .../templates/account => dgsi/templates/dgsi}/profile.html | 0 src/dgsi/urls.py | 2 +- src/dgsi/views.py | 4 ++-- .../templates/{account => accounts}/forbidden_category.html | 3 +++ 5 files changed, 6 insertions(+), 3 deletions(-) rename src/{shared/templates/account/create_kanidm.html => dgsi/templates/dgsi/create_kanidm_account.html} (100%) rename src/{shared/templates/account => dgsi/templates/dgsi}/profile.html (100%) rename src/shared/templates/{account => accounts}/forbidden_category.html (86%) diff --git a/src/shared/templates/account/create_kanidm.html b/src/dgsi/templates/dgsi/create_kanidm_account.html similarity index 100% rename from src/shared/templates/account/create_kanidm.html rename to src/dgsi/templates/dgsi/create_kanidm_account.html diff --git a/src/shared/templates/account/profile.html b/src/dgsi/templates/dgsi/profile.html similarity index 100% rename from src/shared/templates/account/profile.html rename to src/dgsi/templates/dgsi/profile.html diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 848df88..107b945 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -13,7 +13,7 @@ urlpatterns = [ ), path( "accounts/forbidden/", - views.TemplateView.as_view(template_name="account/forbidden_category.html"), + views.TemplateView.as_view(template_name="accounts/forbidden_category.html"), name="dgn-forbidden_account", ), path("services/", views.ServiceListView.as_view(), name="dgn-services"), diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 42add5a..5b7d3c1 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -17,7 +17,7 @@ from shared.kanidm import client class ProfileView(LoginRequiredMixin, TemplateView): - template_name = "account/profile.html" + template_name = "dgsi/profile.html" def get_context_data(self, **kwargs): u = self.request.user @@ -48,7 +48,7 @@ class ServiceRedirectView(LoginRequiredMixin, SingleObjectMixin, RedirectView): class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView): form_class = CreateKanidmAccountForm - template_name = "account/create_kanidm.html" + template_name = "dgsi/create_kanidm_account.html" success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.") success_url = reverse_lazy("dgsi:dgn-create_user") diff --git a/src/shared/templates/account/forbidden_category.html b/src/shared/templates/accounts/forbidden_category.html similarity index 86% rename from src/shared/templates/account/forbidden_category.html rename to src/shared/templates/accounts/forbidden_category.html index c296c89..887aa24 100644 --- a/src/shared/templates/account/forbidden_category.html +++ b/src/shared/templates/accounts/forbidden_category.html @@ -3,6 +3,9 @@ {% load i18n %} {% block content %} +

{% trans "Connexion impossible" %}

+
+
{% blocktrans %}Votre catégorie de compte ENS ne permet pas de vous identifier auprès de la DGNum.
Si vous pensez qu'il s'agit une erreur, merci de contacter la DGNum à l'adresse : contact@dgnum.eu{% endblocktrans %}
From b9f165c1e6797910594ef7cbfaafcf77e2af5e8d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 11:31:05 +0200 Subject: [PATCH 048/172] chore(index): Move the view inside dgsi --- src/app/urls.py | 1 - src/dgsi/urls.py | 4 ++++ src/dgsi/views.py | 4 ++++ src/shared/templates/_hero.html | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/urls.py b/src/app/urls.py index 7a3942a..5d0ec79 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -21,7 +21,6 @@ from django.urls import include, path from django.views.generic import TemplateView urlpatterns = [ - path("", TemplateView.as_view(template_name="home.html"), name="index"), path("login", TemplateView.as_view(template_name="login.html"), name="login"), path("", include("dgsi.urls")), path("accounts/", include("allauth.urls")), diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 107b945..71cd35f 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -5,6 +5,9 @@ from . import views app_name = "dgsi" urlpatterns = [ + # Misc views + path("", views.IndexView.as_view(), name="dgn-index"), + # Account views path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), path( "accounts/create-kanidm/", @@ -16,6 +19,7 @@ urlpatterns = [ views.TemplateView.as_view(template_name="accounts/forbidden_category.html"), name="dgn-forbidden_account", ), + # Services views path("services/", views.ServiceListView.as_view(), name="dgn-services"), path( "services/redirect/", diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 5b7d3c1..47ea5dc 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -16,6 +16,10 @@ from dgsi.models import Service, User from shared.kanidm import client +class IndexView(TemplateView): + template_name = "dgsi/index.html" + + class ProfileView(LoginRequiredMixin, TemplateView): template_name = "dgsi/profile.html" diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index 4fd16c8..9267783 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -6,7 +6,7 @@
From 2490d83459ecd289e6dc613b099c9d80caedbd94 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 11:31:54 +0200 Subject: [PATCH 049/172] feat(index): Add parametrization to links --- src/dgsi/templates/_index_link.html | 4 +++ src/dgsi/templates/dgsi/index.html | 20 +++++++++++++++ src/dgsi/views.py | 39 ++++++++++++++++++++++++++++- src/shared/templates/home.html | 16 ------------ 4 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 src/dgsi/templates/_index_link.html create mode 100644 src/dgsi/templates/dgsi/index.html delete mode 100644 src/shared/templates/home.html diff --git a/src/dgsi/templates/_index_link.html b/src/dgsi/templates/_index_link.html new file mode 100644 index 0000000..ae98b12 --- /dev/null +++ b/src/dgsi/templates/_index_link.html @@ -0,0 +1,4 @@ + + {% if link.icon %}{% endif %} + {{ link.text }} + diff --git a/src/dgsi/templates/dgsi/index.html b/src/dgsi/templates/dgsi/index.html new file mode 100644 index 0000000..840804a --- /dev/null +++ b/src/dgsi/templates/dgsi/index.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +
+ {% if user.is_authenticated %} + {% for link in links.authenticated %} + {% include "_index_link.html" %} + {% endfor %} + {% endif %} + + {% if user.is_admin %} +
+ {% for link in links.admin %} + {% include "_index_link.html" %} + {% endfor %} + {% endif %} +
+{% endblock content %} diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 47ea5dc..239f30d 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, NamedTuple from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin @@ -6,6 +6,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.mail import EmailMessage from django.template.loader import render_to_string from django.urls import reverse_lazy +from django.utils.functional import Promise from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, ListView, RedirectView, TemplateView from django.views.generic.detail import SingleObjectMixin @@ -16,9 +17,45 @@ from dgsi.models import Service, User from shared.kanidm import client +class Link(NamedTuple): + color: str + reverse: str + text: str | Promise + icon: str | None = None + + class IndexView(TemplateView): template_name = "dgsi/index.html" + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + return super().get_context_data( + links={ + "authenticated": [ + Link( + "is-primary", + "dgsi:dgn-profile", + _("Mon profil"), + "user-filled", + ), + ], + "admin": [ + Link( + "is-danger", + "dgsi:dgn-create_user", + _("Créer un nouveau compte Kanidm"), + "user-plus", + ), + Link( + "is-warning", + "admin:index", + _("Interface d'administration"), + "settings-filled", + ), + ], + }, + **kwargs, + ) + class ProfileView(LoginRequiredMixin, TemplateView): template_name = "dgsi/profile.html" diff --git a/src/shared/templates/home.html b/src/shared/templates/home.html deleted file mode 100644 index ffe3eb9..0000000 --- a/src/shared/templates/home.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} - -{% block content %} -
- {% if user.is_authenticated %} - {% trans "Mon profil" %} - {% endif %} - {% if user.is_admin %} - {% trans "Créer un nouveau compte Kanidm" %} - {% trans "Interface admin" %} - {% endif %} -
-{% endblock content %} From 1dc83050277a8d6fb6565bf1d899f09b3714ca85 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 14:27:44 +0200 Subject: [PATCH 050/172] chore(index): Move the links outside of the class --- src/dgsi/templates/_index_link.html | 2 +- src/dgsi/views.py | 42 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/dgsi/templates/_index_link.html b/src/dgsi/templates/_index_link.html index ae98b12..c89b0cf 100644 --- a/src/dgsi/templates/_index_link.html +++ b/src/dgsi/templates/_index_link.html @@ -1,4 +1,4 @@ - + {% if link.icon %}{% endif %} {{ link.text }} diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 239f30d..0d142fb 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -24,34 +24,32 @@ class Link(NamedTuple): icon: str | None = None +AUTHENTICATED_LINKS: list[Link] = [ + Link("is-primary", "dgsi:dgn-profile", _("Mon profil"), "user-filled"), + Link("is-primary", "dgsi:dgn-legal_documents", _("Documents Légaux"), "script"), +] + +ADMIN_LINKS: list[Link] = [ + Link( + "is-danger", + "dgsi:dgn-create_user", + _("Créer un nouveau compte Kanidm"), + "user-plus", + ), + Link( + "is-warning", "admin:index", _("Interface d'administration"), "settings-filled" + ), +] + + class IndexView(TemplateView): template_name = "dgsi/index.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: return super().get_context_data( links={ - "authenticated": [ - Link( - "is-primary", - "dgsi:dgn-profile", - _("Mon profil"), - "user-filled", - ), - ], - "admin": [ - Link( - "is-danger", - "dgsi:dgn-create_user", - _("Créer un nouveau compte Kanidm"), - "user-plus", - ), - Link( - "is-warning", - "admin:index", - _("Interface d'administration"), - "settings-filled", - ), - ], + "authenticated": AUTHENTICATED_LINKS, + "admin": ADMIN_LINKS, }, **kwargs, ) From abdcb2c8ad038d4187887e12c804dfbfda40da3e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 14:28:24 +0200 Subject: [PATCH 051/172] feat: Setup media --- src/app/settings.py | 1 + src/app/urls.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index 41fb61e..1a22f82 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -165,6 +165,7 @@ USE_TZ = True # -> https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = "static/" +MEDIA_URL = "media/" STATICFILES_DIRS = [BASE_DIR / "shared" / "static"] STATICFILES_FINDERS = [ diff --git a/src/app/urls.py b/src/app/urls.py index 5d0ec79..96be59a 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -28,7 +28,11 @@ urlpatterns = [ ] if settings.DEBUG: - urlpatterns += [ - path("__reload__/", include("django_browser_reload.urls")), - path("__debug__/", include("debug_toolbar.urls")), - ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += ( + [ + path("__reload__/", include("django_browser_reload.urls")), + path("__debug__/", include("debug_toolbar.urls")), + ] + + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + ) From b5cedebda1824bfa76e9a40916648ec187604a25 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 14:29:51 +0200 Subject: [PATCH 052/172] feat(legal-documents): Init list view and acceptance flow --- src/dgsi/admin.py | 4 +- ..._statutes_user_accepted_bylaws_and_more.py | 81 +++++++++++++++++++ src/dgsi/models.py | 52 +++++++++++- src/dgsi/templates/_legal_document.html | 23 ++++++ src/dgsi/templates/dgsi/legal_documents.html | 17 ++++ src/dgsi/urls.py | 11 +++ src/dgsi/views.py | 42 +++++++++- 7 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 src/dgsi/migrations/0004_bylaws_statutes_user_accepted_bylaws_and_more.py create mode 100644 src/dgsi/templates/_legal_document.html create mode 100644 src/dgsi/templates/dgsi/legal_documents.html diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index cbccfc7..8c1fb0d 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from unfold.admin import ModelAdmin -from dgsi.models import Service, User +from dgsi.models import Bylaws, Service, Statutes, User @admin.register(User) @@ -10,6 +10,6 @@ class UserAdmin(BaseUserAdmin, ModelAdmin): pass -@admin.register(Service) +@admin.register(Bylaws, Service, Statutes) class AdminClass(ModelAdmin): compressed_fields = True diff --git a/src/dgsi/migrations/0004_bylaws_statutes_user_accepted_bylaws_and_more.py b/src/dgsi/migrations/0004_bylaws_statutes_user_accepted_bylaws_and_more.py new file mode 100644 index 0000000..e7bd985 --- /dev/null +++ b/src/dgsi/migrations/0004_bylaws_statutes_user_accepted_bylaws_and_more.py @@ -0,0 +1,81 @@ +# Generated by Django 4.2.12 on 2024-09-24 08:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("dgsi", "0003_service_icon"), + ] + + operations = [ + migrations.CreateModel( + name="Bylaws", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Date du document")), + ( + "name", + models.CharField(max_length=255, verbose_name="Nom du document"), + ), + ("file", models.FileField(upload_to="", verbose_name="Fichier PDF")), + ], + options={ + "get_latest_by": "date", + "abstract": False, + }, + ), + migrations.CreateModel( + name="Statutes", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Date du document")), + ( + "name", + models.CharField(max_length=255, verbose_name="Nom du document"), + ), + ("file", models.FileField(upload_to="", verbose_name="Fichier PDF")), + ], + options={ + "get_latest_by": "date", + "abstract": False, + }, + ), + migrations.AddField( + model_name="user", + name="accepted_bylaws", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="dgsi.bylaws", + ), + ), + migrations.AddField( + model_name="user", + name="accepted_statutes", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="dgsi.statutes", + ), + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 9b15ff6..44cc14f 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from functools import cached_property -from typing import Optional +from typing import Optional, Self from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser @@ -26,6 +26,48 @@ class Service(models.Model): return f"{self.name} [{self.url}]" +class LegalDocument(models.Model): + date = models.DateField(_("Date du document")) + name = models.CharField(_("Nom du document"), max_length=255) + file = models.FileField(_("Fichier PDF")) + + @classmethod + def latest(cls, **kwargs) -> Self | None: + return cls.objects.filter(**kwargs).latest() + + def __str__(self) -> str: + return self.name + + class Meta: # pyright: ignore + abstract = True + + +class Statutes(LegalDocument): + """ + Statutes of the association. + """ + + kind = "statutes" + + class Meta: # pyright: ignore + get_latest_by = "date" + verbose_name = _("Statuts") + verbose_name_plural = _("Statuts") + + +class Bylaws(LegalDocument): + """ + Bylaws of the association. + """ + + kind = "bylaws" + + class Meta: # pyright: ignore + get_latest_by = "date" + verbose_name = _("Règlement Intérieur") + verbose_name_plural = _("Règlements Intérieurs") + + @dataclass class KanidmProfile: person: Person @@ -37,6 +79,14 @@ class User(AbstractUser): Custom User class, to have a direct link to the Kanidm data. """ + accepted_statutes = models.ForeignKey( + Statutes, on_delete=models.SET_NULL, null=True, default=None + ) + accepted_bylaws = models.ForeignKey( + Bylaws, on_delete=models.SET_NULL, null=True, default=None + ) + # accepted_terms = models.ManyToManyField(TermsAndConditions) + @cached_property def kanidm(self) -> Optional[KanidmProfile]: try: diff --git a/src/dgsi/templates/_legal_document.html b/src/dgsi/templates/_legal_document.html new file mode 100644 index 0000000..8bbc557 --- /dev/null +++ b/src/dgsi/templates/_legal_document.html @@ -0,0 +1,23 @@ +{% load i18n %} + +

+ {{ title }} + + {% if user_document != document %} + + {{ accept_question }} + + + {% else %} + + {% trans "Accepté" %} + + + {% endif %} + {{ document.date }} + +

+ +{{ document }} diff --git a/src/dgsi/templates/dgsi/legal_documents.html b/src/dgsi/templates/dgsi/legal_documents.html new file mode 100644 index 0000000..1c65b99 --- /dev/null +++ b/src/dgsi/templates/dgsi/legal_documents.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

Documents Légaux

+
+ +
+ + {% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %} + +
+ + {% include "_legal_document.html" with document=bylaws user_document=user.accepted_bylaws title=_("Règlement Intérieur") accept_question=_("Accepter le règlement intérieur") %} + +{% endblock content %} diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 71cd35f..ddc1e32 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -7,6 +7,17 @@ app_name = "dgsi" urlpatterns = [ # Misc views path("", views.IndexView.as_view(), name="dgn-index"), + # Legal documents + path( + "legal-documents/", + views.LegalDocumentsView.as_view(), + name="dgn-legal_documents", + ), + path( + "legal-documents/accept//", + views.AcceptLegalDocumentView.as_view(), + name="dgn-accept_legal_document", + ), # Account views path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), path( diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 0d142fb..5b1310f 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,9 +1,11 @@ from typing import Any, NamedTuple from asgiref.sync import async_to_sync +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.mail import EmailMessage +from django.http import HttpRequest, HttpResponseBase from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.functional import Promise @@ -13,7 +15,7 @@ from django.views.generic.detail import SingleObjectMixin from dgsi.forms import CreateKanidmAccountForm from dgsi.mixins import StaffRequiredMixin -from dgsi.models import Service, User +from dgsi.models import Bylaws, Service, Statutes, User from shared.kanidm import client @@ -68,6 +70,44 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) +class LegalDocumentsView(LoginRequiredMixin, TemplateView): + template_name = "dgsi/legal_documents.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + return super().get_context_data( + statutes=Statutes.latest(), + bylaws=Bylaws.latest(), + **kwargs, + ) + + +class AcceptLegalDocumentView(LoginRequiredMixin, RedirectView): + url = reverse_lazy("dgsi:dgn-legal_documents") + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: + u = User.from_request(self.request) + + match kwargs.get("kind"): + case "statutes": + u.accepted_statutes = Statutes.latest() + u.save() + case "bylaws": + u.accepted_bylaws = Bylaws.latest() + u.save() + case k: + messages.add_message( + request, + messages.WARNING, + _("Type de document invalide : %(kind)s") % {"kind": k}, + ) + + return super().get(request, *args, **kwargs) + + +## +# INFO: Below are classes related to services offered by the DGNum + + class ServiceListView(LoginRequiredMixin, ListView): model = Service From a9d369d55da34501979640edf6ee05a4ab1b7517 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 14:30:33 +0200 Subject: [PATCH 053/172] feat(kanidm): Use an async function to get the data --- src/dgsi/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 44cc14f..9ee00b3 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -88,12 +88,13 @@ class User(AbstractUser): # accepted_terms = models.ManyToManyField(TermsAndConditions) @cached_property - def kanidm(self) -> Optional[KanidmProfile]: + @async_to_sync + async def kanidm(self) -> Optional[KanidmProfile]: try: - radius_data = async_to_sync(client.get_radius_token)(self.username).data + radius_data = (await client.get_radius_token(self.username)).data return KanidmProfile( - person=async_to_sync(client.person_account_get)(self.username), + person=(await client.person_account_get(self.username)), radius_secret=radius_data and radius_data.get("secret"), ) except NoMatchingEntries: From e370977aacbdf00fcd60307f7176ae28566c74e7 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 14:30:58 +0200 Subject: [PATCH 054/172] feat(user): Add a type guard to extract the user from a request --- src/dgsi/models.py | 9 +++++++++ src/dgsi/views.py | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 9ee00b3..59c5f4d 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -5,6 +5,7 @@ from typing import Optional, Self from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser from django.db import models +from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person @@ -87,6 +88,14 @@ class User(AbstractUser): ) # accepted_terms = models.ManyToManyField(TermsAndConditions) + @classmethod + def from_request(cls, request: HttpRequest) -> Self: + u = request.user + + assert isinstance(u, cls) + + return u + @cached_property @async_to_sync async def kanidm(self) -> Optional[KanidmProfile]: diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 5b1310f..ba78f82 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -61,8 +61,7 @@ class ProfileView(LoginRequiredMixin, TemplateView): template_name = "dgsi/profile.html" def get_context_data(self, **kwargs): - u = self.request.user - assert isinstance(u, User) + u = User.from_request(self.request) return super().get_context_data( displayname=f"{u.first_name} {u.last_name}", From 6c18ec3855445d835261e6ef72851550d22f7634 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 15:55:29 +0200 Subject: [PATCH 055/172] chore(legal-documents): Add an icon --- src/dgsi/templates/_legal_document.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dgsi/templates/_legal_document.html b/src/dgsi/templates/_legal_document.html index 8bbc557..8316bd5 100644 --- a/src/dgsi/templates/_legal_document.html +++ b/src/dgsi/templates/_legal_document.html @@ -20,4 +20,7 @@ -{{ document }} + + {{ document }} + + From 5f0dfff4aedfeec2c722a34d970de3efb2d36e5c Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 15:57:28 +0200 Subject: [PATCH 056/172] feat(accout): Add a view to self-create an account Various checks are in place to make sure that the user does not already have an account, and has accepted the rules of the association --- src/dgsi/forms.py | 8 ++ .../templates/dgsi/create_self_account.html | 15 +++ src/dgsi/templates/dgsi/legal_documents.html | 15 +++ src/dgsi/templates/dgsi/profile.html | 7 +- src/dgsi/urls.py | 9 +- src/dgsi/views.py | 98 +++++++++++++++++-- 6 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 src/dgsi/templates/dgsi/create_self_account.html diff --git a/src/dgsi/forms.py b/src/dgsi/forms.py index 2bc31a7..404328c 100644 --- a/src/dgsi/forms.py +++ b/src/dgsi/forms.py @@ -33,3 +33,11 @@ class CreateKanidmAccountForm(forms.Form): help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members"), required=False, ) + + +class CreateSelfAccountForm(forms.Form): + displayname = CharField(label=_("Nom d'usage")) + mail = EmailField( + label=_("Adresse e-mail"), + help_text=_("De préférence l'adresse '@ens.psl.eu'"), + ) diff --git a/src/dgsi/templates/dgsi/create_self_account.html b/src/dgsi/templates/dgsi/create_self_account.html new file mode 100644 index 0000000..9e06865 --- /dev/null +++ b/src/dgsi/templates/dgsi/create_self_account.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Création d'un compte DGNum" %}

+
+ + + {% csrf_token %} + {% include "bulma/form.html" with form=form %} + + + +{% endblock content %} diff --git a/src/dgsi/templates/dgsi/legal_documents.html b/src/dgsi/templates/dgsi/legal_documents.html index 1c65b99..36ec809 100644 --- a/src/dgsi/templates/dgsi/legal_documents.html +++ b/src/dgsi/templates/dgsi/legal_documents.html @@ -6,6 +6,21 @@

Documents Légaux


+ {% if user.kanidm is None %} + {% if show_message %} +
+ {% trans "Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant de pouvoir créer un compte." %} +
+ {% else %} +
+ {% trans "Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer un." %} +
+ {% trans "Poursuivre la création d'un compte DGNum" %} +
+ {% endif %} + {% endif %} +
{% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %} diff --git a/src/dgsi/templates/dgsi/profile.html b/src/dgsi/templates/dgsi/profile.html index eed8365..0790195 100644 --- a/src/dgsi/templates/dgsi/profile.html +++ b/src/dgsi/templates/dgsi/profile.html @@ -47,8 +47,11 @@ {% endfor %}
{% else %} -
- {% trans "Pas de compte DGNum répertorié." %} +
+ {% trans "Pas de compte DGNum répertorié." %} +
+ {% trans "Créer un compte DGNum" %}
{% endif %} {% endblock content %} diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index ddc1e32..cf7945c 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -20,10 +20,15 @@ urlpatterns = [ ), # Account views path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), + path( + "accounts/create/", + views.CreateSelfAccountView.as_view(), + name="dgn-create_self_account", + ), path( "accounts/create-kanidm/", views.CreateKanidmAccountView.as_view(), - name="dgn-create_user", + name="dgn-create_kanidm_user", ), path( "accounts/forbidden/", @@ -33,7 +38,7 @@ urlpatterns = [ # Services views path("services/", views.ServiceListView.as_view(), name="dgn-services"), path( - "services/redirect/", + "services/redirect//", views.ServiceRedirectView.as_view(), name="dgn-services_redirect", ), diff --git a/src/dgsi/views.py b/src/dgsi/views.py index ba78f82..3d3df6c 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -2,10 +2,10 @@ from typing import Any, NamedTuple from asgiref.sync import async_to_sync from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.mail import EmailMessage -from django.http import HttpRequest, HttpResponseBase +from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.functional import Promise @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, ListView, RedirectView, TemplateView from django.views.generic.detail import SingleObjectMixin -from dgsi.forms import CreateKanidmAccountForm +from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm from dgsi.mixins import StaffRequiredMixin from dgsi.models import Bylaws, Service, Statutes, User from shared.kanidm import client @@ -34,7 +34,7 @@ AUTHENTICATED_LINKS: list[Link] = [ ADMIN_LINKS: list[Link] = [ Link( "is-danger", - "dgsi:dgn-create_user", + "dgsi:dgn-create_kanidm_user", _("Créer un nouveau compte Kanidm"), "user-plus", ), @@ -69,13 +69,97 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) +# INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to +# use dispatch means that we need to execute the login check anyways. +class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView): + template_name = "dgsi/create_self_account.html" + form_class = CreateSelfAccountForm + success_message = _("Compte DGNum créé avec succès") + success_url = reverse_lazy("dgsi:dgn-profile") + + def dispatch( + self, request: HttpRequest, *args: Any, **kwargs: Any + ) -> HttpResponseBase: + if not request.user.is_authenticated: + return self.handle_no_permission() + + u = User.from_request(request) + + # Check that the user does not already exist + if u.kanidm is not None: + messages.add_message( + request, + messages.WARNING, + _("Vous possédez déjà un compte DGNum !"), + ) + return HttpResponseRedirect(reverse_lazy("dgsi:dgn-profile")) + + # Check that the Statutes and Bylaws have been accepted + if ( + u.accepted_statutes != Statutes.latest() + or u.accepted_bylaws != Bylaws.latest() + ): + messages.add_message( + request, + messages.WARNING, + _("Vous devez accepter les Statuts et le Règlement Intérieur."), + ) + return HttpResponseRedirect(reverse_lazy("dgsi:dgn-legal_documents")) + + return super().dispatch(request, *args, **kwargs) + + @async_to_sync + async def form_valid(self, form): + ttl = 86400 # 24h + + d = form.cleaned_data + u = User.from_request(self.request) + + # Create the base account + await client.person_account_create(u.username, d["displayname"]) + + # Update the information + await client.person_account_update(u.username, mail=[d["mail"]]) + + # FIXME: Will maybe change when kanidm gets its shit together and switches to POST + r = await client.call_get( + f"/v1/person/{u.username}/_credential/_update_intent/{ttl}" + ) + + assert r.data is not None + + token: str = r.data["token"] + link = f"https://sso.dgnum.eu/ui/reset?token={token}" + + # Send an email to the new user with the given email address + EmailMessage( + subject="Réinitialisation de mot de passe DGNum -- DGNum password reset", + body=render_to_string( + "mail/credentials_reset.txt", + context={"link": link}, + ), + from_email="To Be Determined ", + to=[d["mail"]], + headers={"Reply-To": "contact@dgnum.eu"}, + ).send() + + return super().form_valid(form) + + class LegalDocumentsView(LoginRequiredMixin, TemplateView): template_name = "dgsi/legal_documents.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + u = User.from_request(self.request) + statutes = Statutes.latest() + bylaws = Bylaws.latest() + return super().get_context_data( - statutes=Statutes.latest(), - bylaws=Bylaws.latest(), + statutes=statutes, + bylaws=bylaws, + show_message=( + (u.accepted_bylaws != bylaws) or (u.accepted_statutes != statutes) + ), **kwargs, ) @@ -129,7 +213,7 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView) template_name = "dgsi/create_kanidm_account.html" success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.") - success_url = reverse_lazy("dgsi:dgn-create_user") + success_url = reverse_lazy("dgsi:dgn-create_kanidm_user") @async_to_sync async def form_valid(self, form): From 8a46e4ddb522a145046d9a5bfc729a8e46d99f44 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 16:45:06 +0200 Subject: [PATCH 057/172] feat(pkgs/pykanidm): Update to current version --- pkgs/pykanidm/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/pykanidm/default.nix b/pkgs/pykanidm/default.nix index 8f03681..ce8e09f 100644 --- a/pkgs/pykanidm/default.nix +++ b/pkgs/pykanidm/default.nix @@ -11,14 +11,14 @@ buildPythonPackage rec { pname = "kanidm"; - version = "1.2.3"; + version = "1.3.3"; pyproject = true; src = fetchFromGitHub { owner = "kanidm"; repo = "kanidm"; rev = "v${version}"; - hash = "sha256-J02IbAY5lyoMaq6wJiHizqeFBd5hB6id2YMPxlPsASM="; + hash = "sha256-W5G7osV4du6w/BfyY9YrDzorcLNizRsoz70RMfO2AbY="; }; sourceRoot = "source/pykanidm"; From 7537b26fbe97258f04941417ec47719e31cd3c88 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 22:06:50 +0200 Subject: [PATCH 058/172] feat(settings): Make databases configurable --- src/app/settings.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index 1a22f82..ccccb63 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -98,12 +98,15 @@ ASGI_APPLICATION = "app.asgi.application" # Database configuration # -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} +DATABASES = credentials.get_json( + "DATABASES", + { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + }, +) ### From 76c07e308629ae8717a400e744ef0735e4cd7114 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 22:08:09 +0200 Subject: [PATCH 059/172] feat(settings): Make MEDIA_ROOT configurable --- src/app/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/settings.py b/src/app/settings.py index ccccb63..92db313 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -177,7 +177,8 @@ STATICFILES_FINDERS = [ "sass_processor.finders.CssFinder", ] -STATIC_ROOT = credentials["STATIC_ROOT"] +STATIC_ROOT = credentials.get("STATIC_ROOT") +MEDIA_ROOT = credentials.get("MEDIA_ROOT") ### # Storages configuration From fee28c598c3c02d7980074cca0f9deacfbe6a444 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 22:35:50 +0200 Subject: [PATCH 060/172] chore(wsgi): Don't set an invalid settings module --- src/app/wsgi.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/wsgi.py b/src/app/wsgi.py index bfbc45a..63c3d04 100644 --- a/src/app/wsgi.py +++ b/src/app/wsgi.py @@ -7,10 +7,6 @@ For more information on this file, see https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ """ -import os - from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") - application = get_wsgi_application() From 49f1133fedb15791a2aa6ae0527ce8966f17f1c0 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 22:40:53 +0200 Subject: [PATCH 061/172] fix(settings): We use wsgi --- src/app/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index 92db313..7ef813f 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -90,9 +90,9 @@ TEMPLATES = [ ] ### -# ASGI application configuration +# WSGI application configuration -ASGI_APPLICATION = "app.asgi.application" +WSGI_APPLICATION = "app.wsgi.application" ### # Database configuration From 7064a3aa4b8367c53a0688574cf08b9ac72da08e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 22:45:42 +0200 Subject: [PATCH 062/172] feat(settings): Add e-mail configuration --- .credentials/EMAIL_HOST | 1 + .credentials/FROM_EMAIL | 1 + .credentials/SERVER_EMAIL | 1 + src/app/settings.py | 10 ++++++++++ 4 files changed, 13 insertions(+) create mode 100644 .credentials/EMAIL_HOST create mode 100644 .credentials/FROM_EMAIL create mode 100644 .credentials/SERVER_EMAIL diff --git a/.credentials/EMAIL_HOST b/.credentials/EMAIL_HOST new file mode 100644 index 0000000..2fbb50c --- /dev/null +++ b/.credentials/EMAIL_HOST @@ -0,0 +1 @@ +localhost diff --git a/.credentials/FROM_EMAIL b/.credentials/FROM_EMAIL new file mode 100644 index 0000000..083bff6 --- /dev/null +++ b/.credentials/FROM_EMAIL @@ -0,0 +1 @@ +Délégation Générale Numérique diff --git a/.credentials/SERVER_EMAIL b/.credentials/SERVER_EMAIL new file mode 100644 index 0000000..bad804c --- /dev/null +++ b/.credentials/SERVER_EMAIL @@ -0,0 +1 @@ +dgsi@localhost diff --git a/src/app/settings.py b/src/app/settings.py index 7ef813f..dbece05 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -94,6 +94,16 @@ TEMPLATES = [ WSGI_APPLICATION = "app.wsgi.application" +### +# E-Mail configuration + +DEFAULT_FROM_EMAIL = credentials["FROM_EMAIL"] +EMAIL_HOST = credentials.get("EMAIL_HOST", "localhost") +EMAIL_HOST_PASSWORD = credentials.get("EMAIL_HOST_PASSWORD", "") +EMAIL_HOST_USER = credentials.get("EMAIL_HOST_USER", "") +EMAIL_USE_SSL = credentials.get("EMAIL_USE_SSL", False) +SERVER_EMAIL = credentials["SERVER_EMAIL"] + ### # Database configuration # -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases From 1080ec0c440208ab680b16634087c7bd674e283b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 22:45:56 +0200 Subject: [PATCH 063/172] feat(pkgs): Update loadcredential --- pkgs/loadcredential/default.nix | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pkgs/loadcredential/default.nix diff --git a/pkgs/loadcredential/default.nix b/pkgs/loadcredential/default.nix new file mode 100644 index 0000000..2d8ced5 --- /dev/null +++ b/pkgs/loadcredential/default.nix @@ -0,0 +1,34 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + wheel, +}: + +buildPythonPackage rec { + pname = "loadcredential"; + version = "1.2"; + pyproject = true; + + src = fetchFromGitHub { + owner = "Tom-Hubrecht"; + repo = "loadcredential"; + rev = "v${version}"; + hash = "sha256-rNWFD89h1p1jYWLcfzsa/w8nK3bR4aVJsUPx0UtZnIw="; + }; + + build-system = [ + setuptools + wheel + ]; + + pythonImportsCheck = [ "loadcredential" ]; + + meta = { + description = "A simple python package to read credentials passed through systemd's LoadCredential, with a fallback on env variables "; + homepage = "https://github.com/Tom-Hubrecht/loadcredential"; + license = lib.licenses.mit; + maintainers = [ ]; # with lib.maintainers; [ thubrecht ]; + }; +} From 088d6d613ac43e5638c8a3db2d4fe4677d73bb27 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 23:06:09 +0200 Subject: [PATCH 064/172] fix: Always include django-browser-reload --- src/app/settings.py | 4 ++-- src/app/urls.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index dbece05..f5af54d 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -33,6 +33,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "shared.staticfiles.StaticFilesApp", # Overrides the default staticfiles app to filter out the sccs sources + "django_browser_reload", "sass_processor", "bulma", # Authentication @@ -57,6 +58,7 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_browser_reload.middleware.BrowserReloadMiddleware", "allauth.account.middleware.AccountMiddleware", ] @@ -244,12 +246,10 @@ UNFOLD = { if DEBUG: INSTALLED_APPS += [ "debug_toolbar", - "django_browser_reload", ] MIDDLEWARE += [ "debug_toolbar.middleware.DebugToolbarMiddleware", - "django_browser_reload.middleware.BrowserReloadMiddleware", ] EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/src/app/urls.py b/src/app/urls.py index 96be59a..7869da3 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -25,12 +25,12 @@ urlpatterns = [ path("", include("dgsi.urls")), path("accounts/", include("allauth.urls")), path("admin/", admin.site.urls), + path("__reload__/", include("django_browser_reload.urls")), ] if settings.DEBUG: urlpatterns += ( [ - path("__reload__/", include("django_browser_reload.urls")), path("__debug__/", include("debug_toolbar.urls")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) From 59fa7409501d3b9f98a6935e815e9f535111e401 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 23:29:01 +0200 Subject: [PATCH 065/172] fix(css): Don't overflow on group names --- src/shared/static/bulma/bulma.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index fd9e8a7..65676da 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -42,7 +42,7 @@ body { } .grid.groups { - --bulma-grid-column-min: 24rem; + --bulma-grid-column-min: min(24rem, 100%); } .groups > .button > span { From 91d5d68da3a89d2ce8c7df1c094afb7bdff29a70 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 23:33:24 +0200 Subject: [PATCH 066/172] feat(settings): Add console logging --- src/app/settings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/settings.py b/src/app/settings.py index f5af54d..7f65e4d 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -62,6 +62,23 @@ MIDDLEWARE = [ "allauth.account.middleware.AccountMiddleware", ] +### +# Logging configuration + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": credentials.get("LOG_LEVEL", "WARNING"), + }, +} + ### # The main url configuration From 48005ae251846df525d6f811ff562b933f288da4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 25 Sep 2024 13:56:34 +0200 Subject: [PATCH 067/172] chore(migrations): Reflect the change in models.Meta --- ...r_bylaws_options_alter_statutes_options.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/dgsi/migrations/0005_alter_bylaws_options_alter_statutes_options.py diff --git a/src/dgsi/migrations/0005_alter_bylaws_options_alter_statutes_options.py b/src/dgsi/migrations/0005_alter_bylaws_options_alter_statutes_options.py new file mode 100644 index 0000000..1bafc09 --- /dev/null +++ b/src/dgsi/migrations/0005_alter_bylaws_options_alter_statutes_options.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.12 on 2024-09-24 20:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("dgsi", "0004_bylaws_statutes_user_accepted_bylaws_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="bylaws", + options={ + "get_latest_by": "date", + "verbose_name": "Règlement Intérieur", + "verbose_name_plural": "Règlements Intérieurs", + }, + ), + migrations.AlterModelOptions( + name="statutes", + options={ + "get_latest_by": "date", + "verbose_name": "Statuts", + "verbose_name_plural": "Statuts", + }, + ), + ] From 88e2c25ce483e86f5d7478d9f28f16dfa3371319 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 25 Sep 2024 13:57:01 +0200 Subject: [PATCH 068/172] feat(shell): Add media root --- default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/default.nix b/default.nix index 3facc5c..6598c74 100644 --- a/default.nix +++ b/default.nix @@ -75,6 +75,7 @@ in CREDENTIALS_DIRECTORY = builtins.toString ./.credentials; DGSI_DEBUG = "true"; DGSI_STATIC_ROOT = builtins.toString ./.static; + DGSI_MEDIA_ROOT = builtins.toString ./.media; DGSI_KANIDM_CLIENT = "dgsi_test"; }; From 5381b0379b112778cee05f7fa5dc989da96a77ba Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 25 Sep 2024 13:57:16 +0200 Subject: [PATCH 069/172] feat(index): Add a link to the services --- src/dgsi/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 3d3df6c..c842073 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -29,6 +29,12 @@ class Link(NamedTuple): AUTHENTICATED_LINKS: list[Link] = [ Link("is-primary", "dgsi:dgn-profile", _("Mon profil"), "user-filled"), Link("is-primary", "dgsi:dgn-legal_documents", _("Documents Légaux"), "script"), + Link( + "is-info", + "dgsi:dgn-services", + _("Services proposés par la DGNum"), + "apps-filled", + ), ] ADMIN_LINKS: list[Link] = [ From 9119ad38b026a644edbccd494578552555e1cd13 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 12:04:38 +0200 Subject: [PATCH 070/172] feat(accounts): Allow divergence between cas_login and username - Adds a Translation table between cas_login and the effective username - Show more descriptive errors when the connection cannot happen TODO: The translation mechanism is currently fragile, we need to update usernames when a translation is created/deleted and also disable updates to a translation --- src/dgsi/admin.py | 4 +- src/dgsi/migrations/0006_translation.py | 32 +++++ src/dgsi/models.py | 12 ++ src/shared/account.py | 110 ++++++++++++++---- .../accounts/forbidden_category.html | 2 +- 5 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 src/dgsi/migrations/0006_translation.py diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 8c1fb0d..a12277d 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from unfold.admin import ModelAdmin -from dgsi.models import Bylaws, Service, Statutes, User +from dgsi.models import Bylaws, Service, Statutes, Translation, User @admin.register(User) @@ -10,6 +10,6 @@ class UserAdmin(BaseUserAdmin, ModelAdmin): pass -@admin.register(Bylaws, Service, Statutes) +@admin.register(Bylaws, Service, Statutes, Translation) class AdminClass(ModelAdmin): compressed_fields = True diff --git a/src/dgsi/migrations/0006_translation.py b/src/dgsi/migrations/0006_translation.py new file mode 100644 index 0000000..df54ca2 --- /dev/null +++ b/src/dgsi/migrations/0006_translation.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.12 on 2024-09-26 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("dgsi", "0005_alter_bylaws_options_alter_statutes_options"), + ] + + operations = [ + migrations.CreateModel( + name="Translation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("cas_login", models.CharField(max_length=255, unique=True)), + ("username", models.CharField(max_length=255, unique=True)), + ], + options={ + "verbose_name": "Correspondance de login", + "verbose_name_plural": "Correspondances de login", + }, + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 59c5f4d..da77aad 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -69,6 +69,18 @@ class Bylaws(LegalDocument): verbose_name_plural = _("Règlements Intérieurs") +class Translation(models.Model): + cas_login = models.CharField(max_length=255, unique=True) + username = models.CharField(max_length=255, unique=True) + + def __str__(self) -> str: + return f"{self.cas_login} → {self.username}" + + class Meta: # pyright: ignore + verbose_name = _("Correspondance de login") + verbose_name_plural = _("Correspondances de login") + + @dataclass class KanidmProfile: person: Person diff --git a/src/shared/account.py b/src/shared/account.py index 863d1c7..f3f91bb 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -1,10 +1,15 @@ +from functools import lru_cache +from typing import Optional + from allauth.core.exceptions import ImmediateHttpResponse from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.models import SocialLogin -from django.http import HttpResponseRedirect +from django.contrib import messages +from django.http import HttpRequest, HttpResponseRedirect from django.urls import reverse +from django.utils.translation import gettext_lazy as _ -from dgsi.models import User +from dgsi.models import Translation, User class SharedAccountAdapter(DefaultSocialAccountAdapter): @@ -12,42 +17,101 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): Overrides the Account Adapter, to allow a simpler connection via CAS. """ - def pre_social_login(self, request, sociallogin): - match sociallogin.account.provider: - # TODO: Add a correspondance table between ENS logins and ours - case "ens_cas": - # In this case, the username is located in extra_data["uid"] - username = sociallogin.account.extra_data["uid"] + @lru_cache + def _get_username(self, request: HttpRequest, sociallogin: SocialLogin) -> str: + """ + Returns the required username + """ - # Validate that the user is a regular one + match sociallogin.account.provider: + case "ens_cas": + cas_login = sociallogin.account.extra_data["uid"] + + # Verify that this user can indeed connect to the website home = sociallogin.account.extra_data["homeDirectory"].split("/") if (home[1] != "users") or ( home[2] in ["absint", "algo", "grecc", "guests", "spi", "spi1", "staffs"] ): + messages.error(request, _("Catégorie de compte ENS interdite.")) raise ImmediateHttpResponse( HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account")) ) + # Continue with the login flow + try: + return Translation.objects.get(cas_login=cas_login).username + except Translation.DoesNotExist: + return cas_login + case "kanidm": - username = sociallogin.account.extra_data["preferred_username"] - case _p: - raise KeyError(f"No sociallogin '{_p}' is supposed to exist.") + return sociallogin.account.extra_data["preferred_username"] - try: - # Connect an existing user if the login already exists, even if it - # with another social method - user = User.objects.get(username=username) - sociallogin.connect(request, user) - except User.DoesNotExist: - return + case _: + # INFO: This should never happen + messages.error(request, _("Méthode de connexion invalide.")) + raise ImmediateHttpResponse( + HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account")) + ) - # We now know that a user exists, copy the distant attributes - user.is_staff = user.is_admin - user.is_superuser = user.is_admin + def _get_user( + self, request: HttpRequest, sociallogin: SocialLogin + ) -> Optional[User]: + """ + Returns the required user for completing the login + """ - user.save() + # The user is already linked to the social login, no reason to change it + if sociallogin.is_existing: + return sociallogin.user + + # No user is currently linked to this social login, either the user has already + # logged in with another method, or it truly does not exist + return User.objects.filter( + username=self._get_username(request, sociallogin) + ).first() + + def _update_user(self, request: HttpRequest, sociallogin: SocialLogin): + """ + Updates the required attributes of the user: + - username + - permissions + """ + + u = sociallogin.user + + assert isinstance(u, User) + + # Update the username first, so that calls to kanidm return the correct information + u.username = self._get_username(request, sociallogin) + + # Update the global permissions + u.is_staff = u.is_admin + u.is_superuser = u.is_admin + + # Save the updated user if needed + if sociallogin.is_existing: + u.save() + + def pre_social_login(self, request, sociallogin: SocialLogin): + ### + # The flow is the following: + # - Get the correct user + # - Do the connection if possible + # - Update the required attributes + + user = self._get_user(request, sociallogin) + + if user is not None: + sociallogin.user = user + + # If the user exists, connect to it + # FIXME: May not be necessary + # if sociallogin.is_existing: + # sociallogin.connect(request, sociallogin.user) + + self._update_user(request, sociallogin) def populate_user(self, request, sociallogin, data): return super().populate_user(request, sociallogin, data) diff --git a/src/shared/templates/accounts/forbidden_category.html b/src/shared/templates/accounts/forbidden_category.html index 887aa24..e400a6e 100644 --- a/src/shared/templates/accounts/forbidden_category.html +++ b/src/shared/templates/accounts/forbidden_category.html @@ -7,6 +7,6 @@
- {% blocktrans %}Votre catégorie de compte ENS ne permet pas de vous identifier auprès de la DGNum.
Si vous pensez qu'il s'agit une erreur, merci de contacter la DGNum à l'adresse : contact@dgnum.eu{% endblocktrans %} + {% blocktrans %}Vos informations ne permettent pas de vous identifier auprès de la DGNum.
Si vous pensez qu'il s'agit une erreur, merci de nous contacter à l'adresse : contact@dgnum.eu{% endblocktrans %}
{% endblock content %} From 3b3f2dd34d0161eac52a6fae1a6ccb7787accae6 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 12:12:26 +0200 Subject: [PATCH 071/172] fix(accounts): This was, in fact, necessary --- src/shared/account.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/shared/account.py b/src/shared/account.py index f3f91bb..a4682ea 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -107,9 +107,8 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): sociallogin.user = user # If the user exists, connect to it - # FIXME: May not be necessary - # if sociallogin.is_existing: - # sociallogin.connect(request, sociallogin.user) + if sociallogin.is_existing: + sociallogin.connect(request, sociallogin.user) self._update_user(request, sociallogin) From 941f2b031e85ba58bdce1251457026ed94a8539f Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 12:12:58 +0200 Subject: [PATCH 072/172] fix(translations): Disable updates to an existing translation --- src/dgsi/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index da77aad..32e706d 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -69,6 +69,14 @@ class Bylaws(LegalDocument): verbose_name_plural = _("Règlements Intérieurs") +# class TermsAndConditions(LegalDocument): +# """ +# Terms and Conditions of use regarding a service offered by the association. +# """ +# +# service = models.ForeignKey(Service, on_delete=models.CASCADE) + + class Translation(models.Model): cas_login = models.CharField(max_length=255, unique=True) username = models.CharField(max_length=255, unique=True) @@ -76,6 +84,12 @@ class Translation(models.Model): def __str__(self) -> str: return f"{self.cas_login} → {self.username}" + def save(self, **kwargs) -> None: # pyright: ignore + # INFO: Only update the model if it does not already exist + # This will prevent a lot of pain + if self.pk is None: + return super().save(**kwargs) + class Meta: # pyright: ignore verbose_name = _("Correspondance de login") verbose_name_plural = _("Correspondances de login") From b33f13db301da068635b48b5e02597f7a5b75e87 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 13:29:36 +0200 Subject: [PATCH 073/172] feat(accounts): Add a mechanism to automatically change the local username when creating or deleting translations --- src/dgsi/models.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 32e706d..a851d3e 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -2,9 +2,12 @@ from dataclasses import dataclass from functools import cached_property from typing import Optional, Self +from allauth.socialaccount.models import SocialAccount from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser from django.db import models +from django.db.models.signals import pre_delete +from django.dispatch import receiver from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries @@ -84,17 +87,47 @@ class Translation(models.Model): def __str__(self) -> str: return f"{self.cas_login} → {self.username}" - def save(self, **kwargs) -> None: # pyright: ignore + def save(self, *args, **kwargs) -> None: # INFO: Only update the model if it does not already exist # This will prevent a lot of pain if self.pk is None: - return super().save(**kwargs) + try: + # Find out if a user exists with the cas_login to update + account = SocialAccount.objects.get( + provider="ens_cas", uid=self.cas_login + ) + account.user.username = self.username + account.user.save() + + # TODO: Update the distant kanidm data + except SocialAccount.DoesNotExist: + # No user has registered with this cas_login yet + pass + return super().save(*args, **kwargs) class Meta: # pyright: ignore verbose_name = _("Correspondance de login") verbose_name_plural = _("Correspondances de login") +# INFO: We need to use a signal receiver here, as the delete method is not called +# when deleteing objects from the admin interface +@receiver(pre_delete, sender=Translation) +def restore_username(**kwargs): + """ + Restore the username to the cas_login + """ + + cas_login = kwargs["instance"].cas_login + try: + account = SocialAccount.objects.get(provider="ens_cas", uid=cas_login) + account.user.username = cas_login + account.user.save() + except SocialAccount.DoesNotExist: + # No user has registered with this cas_login yet + pass + + @dataclass class KanidmProfile: person: Person From be0cf4c0f5ee5f806f3b94ab0c0ef66ffbef5f0f Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 13:31:40 +0200 Subject: [PATCH 074/172] chore(kanidm): Rename client to klient --- src/dgsi/forms.py | 4 ++-- src/dgsi/models.py | 6 +++--- src/dgsi/views.py | 16 ++++++++-------- src/shared/kanidm.py | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/dgsi/forms.py b/src/dgsi/forms.py index 404328c..1f5e474 100644 --- a/src/dgsi/forms.py +++ b/src/dgsi/forms.py @@ -3,13 +3,13 @@ from django.core.exceptions import ValidationError from django.forms import BooleanField, CharField, EmailField, forms from django.utils.translation import gettext_lazy as _ -from shared.kanidm import client +from shared.kanidm import klient @async_to_sync async def name_validator(value: str) -> None: try: - await client.person_account_get(value) + await klient.person_account_get(value) except ValueError: return diff --git a/src/dgsi/models.py b/src/dgsi/models.py index a851d3e..54a8e7c 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person -from shared.kanidm import client +from shared.kanidm import klient ADMIN_GROUP = "dgnum_admins@sso.dgnum.eu" @@ -159,10 +159,10 @@ class User(AbstractUser): @async_to_sync async def kanidm(self) -> Optional[KanidmProfile]: try: - radius_data = (await client.get_radius_token(self.username)).data + radius_data = (await klient.get_radius_token(self.username)).data return KanidmProfile( - person=(await client.person_account_get(self.username)), + person=(await klient.person_account_get(self.username)), radius_secret=radius_data and radius_data.get("secret"), ) except NoMatchingEntries: diff --git a/src/dgsi/views.py b/src/dgsi/views.py index c842073..623b104 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -16,7 +16,7 @@ from django.views.generic.detail import SingleObjectMixin from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm from dgsi.mixins import StaffRequiredMixin from dgsi.models import Bylaws, Service, Statutes, User -from shared.kanidm import client +from shared.kanidm import klient class Link(NamedTuple): @@ -122,13 +122,13 @@ class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView): u = User.from_request(self.request) # Create the base account - await client.person_account_create(u.username, d["displayname"]) + await klient.person_account_create(u.username, d["displayname"]) # Update the information - await client.person_account_update(u.username, mail=[d["mail"]]) + await klient.person_account_update(u.username, mail=[d["mail"]]) # FIXME: Will maybe change when kanidm gets its shit together and switches to POST - r = await client.call_get( + r = await klient.call_get( f"/v1/person/{u.username}/_credential/_update_intent/{ttl}" ) @@ -228,17 +228,17 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView) d = form.cleaned_data # Create the base account - await client.person_account_create(d["name"], d["displayname"]) + await klient.person_account_create(d["name"], d["displayname"]) # Update the information - await client.person_account_update(d["name"], mail=[d["mail"]]) + await klient.person_account_update(d["name"], mail=[d["mail"]]) # If necessary, add the user to the active members group if d["active"]: - await client.group_add_members("dgnum_members", [d["name"]]) + await klient.group_add_members("dgnum_members", [d["name"]]) # FIXME: Will maybe change when kanidm gets its shit together and switches to POST - r = await client.call_get( + r = await klient.call_get( f"/v1/person/{d['name']}/_credential/_update_intent/{ttl}" ) diff --git a/src/shared/kanidm.py b/src/shared/kanidm.py index 32a77e1..3102da4 100644 --- a/src/shared/kanidm.py +++ b/src/shared/kanidm.py @@ -3,6 +3,6 @@ from loadcredential import Credentials credentials = Credentials(env_prefix="DGSI_") -client = KanidmClient( +klient = KanidmClient( uri=credentials["KANIDM_URI"], token=credentials["KANIDM_AUTH_TOKEN"] ) From 5019b89ef2dfc19f027f5f24d2d840254db75654 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 14:23:35 +0200 Subject: [PATCH 075/172] feat(translations): Factorize the user renaming scheme, and update the remote data --- src/dgsi/models.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 54a8e7c..8666680 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -87,22 +87,27 @@ class Translation(models.Model): def __str__(self) -> str: return f"{self.cas_login} → {self.username}" + def update_user(self, username: str): + # Update the username of the person with the required cas_login + try: + # Find out if a user exists with the cas_login to update + account = SocialAccount.objects.get(provider="ens_cas", uid=self.cas_login) + + # WARNING: This updates the remote data, we need to be careful with what we do + async_to_sync(klient.person_account_update)(account.user.username, username) + + account.user.username = username + account.user.save() + except SocialAccount.DoesNotExist: + # No user has registered with this cas_login yet + pass + def save(self, *args, **kwargs) -> None: # INFO: Only update the model if it does not already exist # This will prevent a lot of pain if self.pk is None: - try: - # Find out if a user exists with the cas_login to update - account = SocialAccount.objects.get( - provider="ens_cas", uid=self.cas_login - ) - account.user.username = self.username - account.user.save() + self.update_user(self.username) - # TODO: Update the distant kanidm data - except SocialAccount.DoesNotExist: - # No user has registered with this cas_login yet - pass return super().save(*args, **kwargs) class Meta: # pyright: ignore @@ -118,14 +123,8 @@ def restore_username(**kwargs): Restore the username to the cas_login """ - cas_login = kwargs["instance"].cas_login - try: - account = SocialAccount.objects.get(provider="ens_cas", uid=cas_login) - account.user.username = cas_login - account.user.save() - except SocialAccount.DoesNotExist: - # No user has registered with this cas_login yet - pass + self = kwargs["instance"] + self.update_user(self.cas_login) @dataclass From 97e3ae9702b3594ced2abe8934b343399c92d398 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 14:40:58 +0200 Subject: [PATCH 076/172] fix(settings): Allow specifying the EMAIL_PORT --- src/app/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/settings.py b/src/app/settings.py index 7f65e4d..665a524 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -120,6 +120,7 @@ DEFAULT_FROM_EMAIL = credentials["FROM_EMAIL"] EMAIL_HOST = credentials.get("EMAIL_HOST", "localhost") EMAIL_HOST_PASSWORD = credentials.get("EMAIL_HOST_PASSWORD", "") EMAIL_HOST_USER = credentials.get("EMAIL_HOST_USER", "") +EMAIL_PORT = credentials.get_json("EMAIL_PORT", 465) EMAIL_USE_SSL = credentials.get("EMAIL_USE_SSL", False) SERVER_EMAIL = credentials["SERVER_EMAIL"] From 7885252b4a745426ff9cc062ca00d18b98f0723a Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 14:50:45 +0200 Subject: [PATCH 077/172] fix(views): Use default from email --- src/dgsi/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 623b104..bb705ac 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -254,7 +254,6 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView) "mail/credentials_reset.txt", context={"link": link}, ), - from_email="To Be Determined ", to=[d["mail"]], headers={"Reply-To": "contact@dgnum.eu"}, ).send() From 2978c1facba9fb18e56ac4521b1178f360c76029 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 15:25:26 +0200 Subject: [PATCH 078/172] feat(settings): Allow giving the admin list --- src/app/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/settings.py b/src/app/settings.py index 665a524..1192bda 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -22,6 +22,8 @@ DEBUG = credentials.get_json("DEBUG", False) ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) +ADMINS = credentials.get_json("ADMINS", []) + ### # List the installed applications From bf5bce5fc548115c26e1beec8be145bcb9d16688 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 16:00:35 +0200 Subject: [PATCH 079/172] chore(templates): Add checkmarcks on buttons --- src/dgsi/templates/dgsi/create_kanidm_account.html | 5 ++++- src/dgsi/templates/dgsi/create_self_account.html | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/dgsi/templates/dgsi/create_kanidm_account.html b/src/dgsi/templates/dgsi/create_kanidm_account.html index c17aa91..62ef674 100644 --- a/src/dgsi/templates/dgsi/create_kanidm_account.html +++ b/src/dgsi/templates/dgsi/create_kanidm_account.html @@ -10,6 +10,9 @@ {% csrf_token %} {% include "bulma/form.html" with form=form %} - + {% endblock content %} diff --git a/src/dgsi/templates/dgsi/create_self_account.html b/src/dgsi/templates/dgsi/create_self_account.html index 9e06865..5b2fc62 100644 --- a/src/dgsi/templates/dgsi/create_self_account.html +++ b/src/dgsi/templates/dgsi/create_self_account.html @@ -10,6 +10,9 @@ {% csrf_token %} {% include "bulma/form.html" with form=form %} - + {% endblock content %} From 997fd254ca9b69996de74c835f4fea87ec6a0d76 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 16:11:06 +0200 Subject: [PATCH 080/172] chore(accounts): Remove unused login view --- src/app/urls.py | 1 - src/shared/templates/login.html | 29 ----------------------------- 2 files changed, 30 deletions(-) delete mode 100644 src/shared/templates/login.html diff --git a/src/app/urls.py b/src/app/urls.py index 7869da3..0e5a90a 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -21,7 +21,6 @@ from django.urls import include, path from django.views.generic import TemplateView urlpatterns = [ - path("login", TemplateView.as_view(template_name="login.html"), name="login"), path("", include("dgsi.urls")), path("accounts/", include("allauth.urls")), path("admin/", admin.site.urls), diff --git a/src/shared/templates/login.html b/src/shared/templates/login.html deleted file mode 100644 index d6a5160..0000000 --- a/src/shared/templates/login.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "base.html" %} - -{% load i18n socialaccount %} - -{% block content %} - -{% endblock content %} From 45bd436ea786d8a44dcd589f705721bb8c6066ff Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 26 Sep 2024 23:01:09 +0200 Subject: [PATCH 081/172] feat(i18n): Add an english translation --- default.nix | 3 +- src/app/settings.py | 14 +- src/app/urls.py | 1 + src/shared/locale/en/LC_MESSAGES/django.mo | Bin 0 -> 5262 bytes src/shared/locale/en/LC_MESSAGES/django.po | 269 +++++++++++++++++++++ src/shared/static/bulma/bulma.scss | 6 + src/shared/templates/_footer.html | 4 +- src/shared/templates/_hero.html | 80 ++++-- 8 files changed, 348 insertions(+), 29 deletions(-) create mode 100644 src/shared/locale/en/LC_MESSAGES/django.mo create mode 100644 src/shared/locale/en/LC_MESSAGES/django.po diff --git a/default.nix b/default.nix index 6598c74..0210594 100644 --- a/default.nix +++ b/default.nix @@ -48,8 +48,9 @@ in name = "dgsi.dev"; packages = [ - pkgs.jq pkgs.dart-sass + pkgs.gettext + pkgs.jq # Python dependencies (python.withPackages (ps: [ diff --git a/src/app/settings.py b/src/app/settings.py index 1192bda..98e7808 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -187,9 +188,18 @@ AUTH_USER_MODEL = "dgsi.User" # Internationalization configuration # -> https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = "fr-fr" +LANGUAGE_CODE = "fr" -TIME_ZONE = "UTC" +LANGUAGES = [ + ("en", "English"), + ("fr", "Français"), +] + +LOCALE_PATHS = [ + (BASE_DIR / "shared" / "locale"), +] + +TIME_ZONE = "Europe/Paris" USE_I18N = True USE_TZ = True diff --git a/src/app/urls.py b/src/app/urls.py index 0e5a90a..c33a36f 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path("", include("dgsi.urls")), path("accounts/", include("allauth.urls")), path("admin/", admin.site.urls), + path("i18n/", include("django.conf.urls.i18n")), path("__reload__/", include("django_browser_reload.urls")), ] diff --git a/src/shared/locale/en/LC_MESSAGES/django.mo b/src/shared/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6b1f9befecf2385570989679def79d9c2949933e GIT binary patch literal 5262 zcmbuCTWlOx8OKi{P%tf&1SsWFPE&$&@va?{hGgqlNqh+=abjX80R@DPch2rkGCQ-K zGh=6~O0AIKi5FD$CRMb3K`|gC1iV!UX{nHa2P7mU#7z_-R0*{&AQcq>zwgZK#U{MK zXpjFhXXc#m_W!=~`FFc_J)yX2j1MvXa+^{Yz!%@lAFkJTDRmC~J6Hu5-=frgU=F?q zdF8e(Io(3<0sH~m_KMFn%{sMdnyaqml&>3(S zN`4wV1zx>PsmH-9Jim6QQv1O(1WEkU0Y&dGf|#P7ET5kP#n0ab`Kh1qN9_0&DDiv+ zlz89CIo2Ps%Jsb`vp+!_(u8s15o_;V^H*b1r)je0EJh3Q0^=^0gAjEp!oUA zAg)wj2SwlSfD->7f)dYPgYx~KLCKrHfnvvQgovI8LHT|a6g}e-b5Qho1QgzU4Lk+D z2#Wsy1V#Ss7;zIE2QPp>2PZ%cDNlmSAmQ}B2E1(1M`BQA$hUHdujCRNL=UlDczuZR zF^1S8{E_PzLv-$6xJZ41v9FxC7ZjfzW{B=`9c2g)L}$4^&X9PBEpkmW-sJC;s(|7f z%@FSPuW@d0pTER6Q{}z*b&`SUo)a>CgmHi&<>??pa^*Oq#voPvg$oCl-|}Nto&=*LACdq@K5JoN3FCC7n*1p*s2U1B=5&Y^OyolQ5wx*bf} zO523djz?Wna|V&k1fjA^sp`V{RgEq-4Z_S;)m$qHZ^-M&@Qa(dRdb^@)zjWqZwsf{ zx^{WJ-n;3xf6z(tRPWm#gsu~rZfx52fYVp^?Jw>=uZs;3l*F;UfkneR9k#`%;d&%O zQ|#*`abRKz3<@R9P(B`=cOx%Qtn|?MjzFln;k$v|XH6UiZU2hc+-gNJ9SZ>r>?piY zE}J(*8Q1p-qvoydAf-V}ttcD@&?tN;x0XW5_nSd;XJMoN5Ll<1q;UsydANbSj$~LSm&oo0iL#tvCGn#oSbZg2 z4CQriD{CcUu8&gTT@q!(M1;1g&L?dQ$(?E1;(mk+{@oxgMGCYglUI!A>$Vz!mtjLj zQuem0YK7zx9kBtMB~|_#rB}S_qTP;MQx}Id2A}t9%y8D};j~k$MeU#*Ql^UBb9y6` z||Y#P`NIgWD@7?8r-QY z1(nNC0S?t5hYPEHkBwclog~dF%dQy)mD73CRo0T49$UFkxo9^+Sy7Q{T+`E2(?=>( z$1BrE_4IV@$nir{(^FGp=ZwoLYbix6GNf5euaUW*%qbJ~R;FjNq+M;5PlG7UvWlaR z+^(!y)2``7DeA4kY)v=PvE`-Zg`v)et5aihzC%D7G_FzmZe)|3fnofhuD49;YS<}bX!)$CN%2N|n7IB!X>A2$`-HGW3b#Z%Qf2?Qq;R9oz zshqJf8RqReLx)frsgE439v)Nrbho4vdr#V*Q*w)GcEdqYWVCI^ExMR85JiO-L!^PhF(a#G!OM?5J<_(k{Ma*tSe>&?_8qh_Vl~HuT7`&; zPDOJfTiO+l=N&P>#9<;`r0U-c7#PN0Nvtr^rBv0Wq8l03|C(C!t%#U3dU(LE(N?7B zgr!SSt4SlsL7>ujU(KYcC4NR?9Afg41{7px9-;U=KPB zid)K=mT`njMf8QkNR+(X!e0n7IlAK}DKJt?eLsPkQfDcq@pbu2uR?F7+qC57x}Bhc zTNvtVDGdbS(kA<#+Xkt^?c8Nj4EW!6M7^SMpyL(377@Tn5ZG%, 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: dgsi.dgnum.eu\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-09-26 22:48+0200\n" +"PO-Revision-Date: 2024-09-26 22:49+0200\n" +"Last-Translator: Tom Hubrecht \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 46.1\n" + +#: app/settings.py:276 +msgid "Administration de DGSI" +msgstr "DGSI Administration" + +#: dgsi/forms.py:16 +msgid "Identifiant déjà présent dans la base de données." +msgstr "Username already in the database." + +#: dgsi/forms.py:22 +msgid "Identifiant" +msgstr "Username" + +#: dgsi/forms.py:23 +msgid "De préférence identique au login ENS de la personne concernée" +msgstr "Preferably identical to the ENS login of the person concerned" + +#: dgsi/forms.py:26 dgsi/forms.py:39 +msgid "Nom d'usage" +msgstr "Name in use" + +#: dgsi/forms.py:28 dgsi/forms.py:41 +msgid "Adresse e-mail" +msgstr "E-mail address" + +#: dgsi/forms.py:29 dgsi/forms.py:42 +msgid "De préférence l'adresse '@ens.psl.eu'" +msgstr "Preferably the ‘@ens.psl.eu’ address" + +#: dgsi/forms.py:32 +msgid "Membre actif" +msgstr "Active member" + +#: dgsi/forms.py:33 +msgid "Si selectionné, la personne sera ajoutée au groupe dgnum_members" +msgstr "If selected, the person will be added to the dgnum_members group." + +#: dgsi/models.py:22 +msgid "Nom du service proposé" +msgstr "Name of the proposed service" + +#: dgsi/models.py:23 +msgid "Adresse du service" +msgstr "Address of the service" + +#: dgsi/models.py:24 +msgid "Icône du service" +msgstr "Icon of the service" + +#: dgsi/models.py:34 +msgid "Date du document" +msgstr "Document date" + +#: dgsi/models.py:35 +msgid "Nom du document" +msgstr "Document name" + +#: dgsi/models.py:36 +msgid "Fichier PDF" +msgstr "PDF file" + +#: dgsi/models.py:58 dgsi/models.py:59 +#: dgsi/templates/dgsi/legal_documents.html:26 +msgid "Statuts" +msgstr "Statutes" + +#: dgsi/models.py:71 dgsi/templates/dgsi/legal_documents.html:30 +msgid "Règlement Intérieur" +msgstr "Bylaws" + +#: dgsi/models.py:72 +msgid "Règlements Intérieurs" +msgstr "Bylaws" + +#: dgsi/models.py:114 +msgid "Correspondance de login" +msgstr "Login mapping" + +#: dgsi/models.py:115 +msgid "Correspondances de login" +msgstr "Login mappings" + +#: dgsi/templates/_legal_document.html:9 +msgid "" +" En acceptant, vous assurez avoir lu ce document et en approuver le contenu." +msgstr "" +" By accepting, you confirm that you have read this document and agree with " +"its content." + +#: dgsi/templates/_legal_document.html:15 +msgid "Accepté" +msgstr "Accepted" + +#: dgsi/templates/dgsi/create_kanidm_account.html:6 +msgid "Création de compte Kanidm" +msgstr "Kanidm account creation" + +#: dgsi/templates/dgsi/create_kanidm_account.html:14 +#: dgsi/templates/dgsi/create_self_account.html:14 +msgid "Enregistrer" +msgstr "Save" + +#: dgsi/templates/dgsi/create_self_account.html:6 +msgid "Création d'un compte DGNum" +msgstr "DGNum account creation" + +#: dgsi/templates/dgsi/legal_documents.html:12 +msgid "" +"Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant " +"de pouvoir créer un compte." +msgstr "" +"You must accept the DGNum Statutes and Bylaws before you can create an " +"account." + +#: dgsi/templates/dgsi/legal_documents.html:16 +msgid "" +"Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer " +"un." +msgstr "You do not yet have a DGNum account, but you can now create one." + +#: dgsi/templates/dgsi/legal_documents.html:19 +msgid "Poursuivre la création d'un compte DGNum" +msgstr "Continue the creation of a DGNum account" + +#: dgsi/templates/dgsi/legal_documents.html:26 +msgid "Accepter les statuts" +msgstr "Accept the statutes" + +#: dgsi/templates/dgsi/legal_documents.html:30 +msgid "Accepter le règlement intérieur" +msgstr "Accept the bylaws" + +#: dgsi/templates/dgsi/profile.html:7 +#, python-format +msgid "Profil de %(displayname)s" +msgstr "Profile of %(displayname)s" + +#: dgsi/templates/dgsi/profile.html:13 +msgid "Mot de passe WiFi :" +msgstr "WiFi password:" + +#: dgsi/templates/dgsi/profile.html:23 +msgid "Adresse e-mail :" +msgstr "E-mail address:" + +#: dgsi/templates/dgsi/profile.html:28 +msgid "Informations techniques" +msgstr "Technical informations" + +#: dgsi/templates/dgsi/profile.html:31 +msgid "Identifiant unique :" +msgstr "Unique identifier:" + +#: dgsi/templates/dgsi/profile.html:40 +msgid "Membre des groupes suivants :" +msgstr "Member of the following groups:" + +#: dgsi/templates/dgsi/profile.html:51 +msgid "Pas de compte DGNum répertorié." +msgstr "No DGNum account found." + +#: dgsi/templates/dgsi/profile.html:54 +msgid "Créer un compte DGNum" +msgstr "Create a DGNum account" + +#: dgsi/templates/dgsi/service_list.html:6 +msgid "Services accessibles via la DGNum" +msgstr "Services accessible via the DGNum" + +#: dgsi/views.py:30 +msgid "Mon profil" +msgstr "My profile" + +#: dgsi/views.py:31 +msgid "Documents Légaux" +msgstr "Legal Documents" + +#: dgsi/views.py:35 +msgid "Services proposés par la DGNum" +msgstr "Services offered by the DGNum" + +#: dgsi/views.py:44 +msgid "Créer un nouveau compte Kanidm" +msgstr "Create a new Kanidm account" + +#: dgsi/views.py:48 +msgid "Interface d'administration" +msgstr "Administration interface" + +#: dgsi/views.py:83 +msgid "Compte DGNum créé avec succès" +msgstr "DGNum account successfully created" + +#: dgsi/views.py:99 +msgid "Vous possédez déjà un compte DGNum !" +msgstr "You already have a DGNum account!" + +#: dgsi/views.py:111 +msgid "Vous devez accepter les Statuts et le Règlement Intérieur." +msgstr "You must accept the Statutes and the Bylaws." + +#: dgsi/views.py:190 +#, python-format +msgid "Type de document invalide : %(kind)s" +msgstr "Invalid document type: %(kind)s" + +#: dgsi/views.py:220 +#, python-format +msgid "Compte DGNum pour %(displayname)s [%(name)s] créé." +msgstr "DGNum account for %(displayname)s [%(name)s] created." + +#: shared/account.py:37 +msgid "Catégorie de compte ENS interdite." +msgstr "ENS account category not permitted." + +#: shared/account.py:53 +msgid "Méthode de connexion invalide." +msgstr "Invalid connection method." + +#: shared/templates/_footer.html:4 +msgid "" +"Logiciel développé pour et par la DGNum." +msgstr "" +"Software developed for and by the DGNum." + +#: shared/templates/_hero.html:18 +msgid "Déconnexion" +msgstr "Logout" + +#: shared/templates/_hero.html:27 +msgid "Connexion" +msgstr "Login" + +#: shared/templates/_hero.html:41 +msgid "Choix de la langue" +msgstr "Language selection" + +#: shared/templates/accounts/forbidden_category.html:6 +msgid "Connexion impossible" +msgstr "Unable to connect" + +#: shared/templates/accounts/forbidden_category.html:10 +msgid "" +"Vos informations ne permettent pas de vous identifier auprès de la DGNum." +"
Si vous pensez qu'il s'agit une erreur, merci de nous contacter à " +"l'adresse : contact@dgnum.eu" +msgstr "" +"Your details do not allow the DGNum to authenticate you.
If you think " +"this is a mistake, please contact us at: contact@dgnum.eu" diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 65676da..1cf47fc 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -62,3 +62,9 @@ body { .notification { margin-bottom: var(--bulma-block-spacing); } + +.dropdown.is-fullwidth, +.dropdown.is-fullwidth > .dropdown-trigger, +.dropdown.is-fullwidth > .dropdown-menu { + width: 100%; +} diff --git a/src/shared/templates/_footer.html b/src/shared/templates/_footer.html index f58de5e..af91d07 100644 --- a/src/shared/templates/_footer.html +++ b/src/shared/templates/_footer.html @@ -1,6 +1,6 @@ -{% load django_browser_reload %} +{% load i18n django_browser_reload %}
- Logiciel développé pour et par la DGNum. + {%blocktrans %}Logiciel développé pour et par la DGNum.{% endblocktrans %} {% django_browser_reload_script %}
diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index 9267783..78ba4b7 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -1,35 +1,67 @@ {% load i18n %}
-
-
-
-
-

- Dossier Général des Services Informagiques -

-

Système d'information de la DGNum

-
-
+
+
+
+

+ Dossier Général des Services Informagiques +

+

Système d'information de la DGNum

+
+
+
{% if user.is_authenticated %} - - - {% trans "Déconnexion" %} - - + + + {% trans "Déconnexion" %} + + + - - + {% else %} - - - {% trans "Connexion" %} - - + + + {% trans "Connexion" %} + + + - - + {% endif %} +
From 224f9df858e205157dcc9aee37b2927ffda27ee6 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 10:38:32 +0200 Subject: [PATCH 082/172] chore(npins): Update --- npins/sources.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/npins/sources.json b/npins/sources.json index 7ffea61..9061aeb 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -8,15 +8,15 @@ "repo": "git-hooks.nix" }, "branch": "master", - "revision": "e35aed5fda3cc79f88ed7f1795021e559582093a", - "url": "https://github.com/cachix/pre-commit-hooks.nix/archive/e35aed5fda3cc79f88ed7f1795021e559582093a.tar.gz", - "hash": "1bq0yrjmkddj964s2q6393nwp4mqrlmc2i5wsy992r034awyywp1" + "revision": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74", + "url": "https://github.com/cachix/git-hooks.nix/archive/4e743a6920eab45e8ba0fbe49dc459f1423a4b74.tar.gz", + "hash": "0fc69dsn5rhv2zb16c2bfgx84ja8cmn7d7j2mrw3n4m8y611x40g" }, "nixpkgs": { "type": "Channel", "name": "nixpkgs-unstable", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre630522.3305b2b25e4a/nixexprs.tar.xz", - "hash": "1bg240s2jbyvdixpy14rc4fcn9zrjf36mcd2xv59rcxx508gwhi2" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre685691.28b5b8af91ff/nixexprs.tar.xz", + "hash": "14ldh9js6l9nqch7j8z6nhyplxc5d9jw375pg8h4s24m7x37xnvy" } }, "version": 3 From 97dc77fd5d40fd8a54e6d0bf64998e092916f7c4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 10:55:05 +0200 Subject: [PATCH 083/172] feat(apps): Add django-import-export --- default.nix | 1 + src/app/settings.py | 5 +++++ src/dgsi/admin.py | 17 ++++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/default.nix b/default.nix index 0210594..1008f2a 100644 --- a/default.nix +++ b/default.nix @@ -61,6 +61,7 @@ in ps.django-bulma-forms ps.django-compressor ps.django-debug-toolbar + ps.django-import-export ps.django-sass-processor ps.django-sass-processor-dart-sass ps.django-stubs diff --git a/src/app/settings.py b/src/app/settings.py index 98e7808..50967a5 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -28,16 +28,21 @@ ADMINS = credentials.get_json("ADMINS", []) # List the installed applications INSTALLED_APPS = [ + # Unfold apps "unfold", + "unfold.contrib.import_export", + # Django standard apps "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", + # Custom apps "shared.staticfiles.StaticFilesApp", # Overrides the default staticfiles app to filter out the sccs sources "django_browser_reload", "sass_processor", "bulma", + "import_export", # Authentication "allauth", "allauth.account", diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index a12277d..7ec4ca8 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -1,15 +1,26 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from import_export.admin import ImportExportMixin from unfold.admin import ModelAdmin +from unfold.contrib.import_export.forms import ( + ExportForm, + ImportForm, + SelectableFieldsExportForm, +) from dgsi.models import Bylaws, Service, Statutes, Translation, User @admin.register(User) -class UserAdmin(BaseUserAdmin, ModelAdmin): - pass +class UserAdmin(BaseUserAdmin, ImportExportMixin, ModelAdmin): + import_form_class = ImportForm + export_form_class = ExportForm + export_form_class = SelectableFieldsExportForm @admin.register(Bylaws, Service, Statutes, Translation) -class AdminClass(ModelAdmin): +class AdminClass(ImportExportMixin, ModelAdmin): compressed_fields = True + import_form_class = ImportForm + export_form_class = ExportForm + export_form_class = SelectableFieldsExportForm From 2f5482a091c6ba539d3519197c3899594a9f03b3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 10:56:22 +0200 Subject: [PATCH 084/172] fix(isort): Make it compatible with black --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 60895e3..940e775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,3 +28,6 @@ profile = "django" [tool.djlint.js] indent_size = 4 + +[tool.isort] +profile = "black" From dc8f89be8630fd215b2924b5fbe716f658c4fc6e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 11:08:35 +0200 Subject: [PATCH 085/172] feat(admin): Unregister most socialaccount models, and re-register SocialAccount This allows to use the django-unfold ModelAdmin class --- src/dgsi/admin.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 7ec4ca8..f4b8357 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -1,3 +1,4 @@ +from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from import_export.admin import ImportExportMixin @@ -11,6 +12,29 @@ from unfold.contrib.import_export.forms import ( from dgsi.models import Bylaws, Service, Statutes, Translation, User +# Unregister allauth models +def unregister(*models, site=None): + """ + Unregister the given model(s) classes. + + unregister(Author) + + The `site` kwarg is an admin site to use instead of the default admin site. + """ + + from django.contrib.admin.sites import site as default_site + + admin_site = site or default_site + + if not isinstance(admin_site, admin.AdminSite): + raise ValueError("site must subclass AdminSite") + + admin_site.unregister(models) + + +unregister(SocialAccount, SocialApp, SocialToken) + + @admin.register(User) class UserAdmin(BaseUserAdmin, ImportExportMixin, ModelAdmin): import_form_class = ImportForm @@ -18,7 +42,7 @@ class UserAdmin(BaseUserAdmin, ImportExportMixin, ModelAdmin): export_form_class = SelectableFieldsExportForm -@admin.register(Bylaws, Service, Statutes, Translation) +@admin.register(Bylaws, Service, SocialAccount, Statutes, Translation) class AdminClass(ImportExportMixin, ModelAdmin): compressed_fields = True import_form_class = ImportForm From c7e13b76f26014b3c11e2afe83e1e642fa892bb9 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 12:41:02 +0200 Subject: [PATCH 086/172] feat(admin): Add fields to the user admin panel --- src/dgsi/admin.py | 16 +++++++-- ...007_alter_user_accepted_bylaws_and_more.py | 36 +++++++++++++++++++ src/dgsi/models.py | 12 +++++-- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/dgsi/migrations/0007_alter_user_accepted_bylaws_and_more.py diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index f4b8357..29a6911 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -1,6 +1,7 @@ from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from django.utils.translation import gettext_lazy as _ from import_export.admin import ImportExportMixin from unfold.admin import ModelAdmin from unfold.contrib.import_export.forms import ( @@ -11,6 +12,8 @@ from unfold.contrib.import_export.forms import ( from dgsi.models import Bylaws, Service, Statutes, Translation, User +assert DjangoUserAdmin.fieldsets is not None + # Unregister allauth models def unregister(*models, site=None): @@ -36,11 +39,20 @@ unregister(SocialAccount, SocialApp, SocialToken) @admin.register(User) -class UserAdmin(BaseUserAdmin, ImportExportMixin, ModelAdmin): +class UserAdmin(DjangoUserAdmin, ImportExportMixin, ModelAdmin): import_form_class = ImportForm export_form_class = ExportForm export_form_class = SelectableFieldsExportForm + # Add the local fields + fieldsets = ( + *DjangoUserAdmin.fieldsets, + ( + _("Documents DGNum"), + {"fields": ("accepted_statutes", "accepted_bylaws")}, + ), + ) + @admin.register(Bylaws, Service, SocialAccount, Statutes, Translation) class AdminClass(ImportExportMixin, ModelAdmin): diff --git a/src/dgsi/migrations/0007_alter_user_accepted_bylaws_and_more.py b/src/dgsi/migrations/0007_alter_user_accepted_bylaws_and_more.py new file mode 100644 index 0000000..cd203b0 --- /dev/null +++ b/src/dgsi/migrations/0007_alter_user_accepted_bylaws_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.16 on 2024-09-27 10:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dgsi", "0006_translation"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="accepted_bylaws", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="dgsi.bylaws", + verbose_name="Dernier Règlement Intérieur accepté", + ), + ), + migrations.AlterField( + model_name="user", + name="accepted_statutes", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="dgsi.statutes", + verbose_name="Derniers statuts acceptés", + ), + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 8666680..d2837cb 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -139,10 +139,18 @@ class User(AbstractUser): """ accepted_statutes = models.ForeignKey( - Statutes, on_delete=models.SET_NULL, null=True, default=None + Statutes, + on_delete=models.SET_NULL, + null=True, + default=None, + verbose_name=_("Derniers statuts acceptés"), ) accepted_bylaws = models.ForeignKey( - Bylaws, on_delete=models.SET_NULL, null=True, default=None + Bylaws, + on_delete=models.SET_NULL, + null=True, + default=None, + verbose_name=_("Dernier Règlement Intérieur accepté"), ) # accepted_terms = models.ManyToManyField(TermsAndConditions) From fa8f8a214fce9b8eaea1aa7da530237d1e650ff2 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 12:42:47 +0200 Subject: [PATCH 087/172] feat(templates): Add analytics --- src/shared/templates/_links.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/templates/_links.html b/src/shared/templates/_links.html index 84065b7..c602519 100644 --- a/src/shared/templates/_links.html +++ b/src/shared/templates/_links.html @@ -13,3 +13,4 @@ + From 6a581fcec418197343e08c5cd42ee694ad9f3738 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 27 Sep 2024 14:41:41 +0200 Subject: [PATCH 088/172] feat(scss): More idiomatic writing --- src/shared/static/bulma/bulma.scss | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 1cf47fc..6005991 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -35,19 +35,19 @@ body { .bt-links { justify-content: space-evenly; -} -.bt-links > .button { - width: 47.5%; + .button { + width: 47.5%; + } } .grid.groups { --bulma-grid-column-min: min(24rem, 100%); -} -.groups > .button > span { - overflow-x: hidden; - text-overflow: ellipsis; + .button > span { + overflow-x: hidden; + text-overflow: ellipsis; + } } #notifications { @@ -63,8 +63,11 @@ body { margin-bottom: var(--bulma-block-spacing); } -.dropdown.is-fullwidth, -.dropdown.is-fullwidth > .dropdown-trigger, -.dropdown.is-fullwidth > .dropdown-menu { +.dropdown.is-fullwidth { width: 100%; + + .dropdown-trigger, + .dropdown-menu { + width: 100%; + } } From fb70bf13f8f405eafc345387b30876b74254e89a Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 29 Sep 2024 19:50:35 +0200 Subject: [PATCH 089/172] feat(profile): Add a way to reset/generate a wifi password TODO: Maybe switch to a post request, as it modifies the internal state --- src/dgsi/templates/dgsi/profile.html | 29 ++++++++++++++++++++-------- src/dgsi/urls.py | 5 +++++ src/dgsi/views.py | 16 +++++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/dgsi/templates/dgsi/profile.html b/src/dgsi/templates/dgsi/profile.html index 0790195..7053901 100644 --- a/src/dgsi/templates/dgsi/profile.html +++ b/src/dgsi/templates/dgsi/profile.html @@ -9,15 +9,28 @@
- {% if user.kanidm.radius_secret %} -

{% trans "Mot de passe WiFi :" %}

+ {% if user.kanidm %} +

+ {% trans "Mot de passe WiFi :" %} + {% if user.kanidm.radius_secret %} + {% trans "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?" as confirm_wifi_reset %} + {% trans "Réinitialiser le mot de passe WiFi" %} + {% endif %} +

- -
+ {% if user.kanidm.radius_secret %} + +
+ {% else %} + {% trans "Générer un mot de passe WiFi" %} + {% endif %} {% endif %}

{% trans "Adresse e-mail :" %}

diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index cf7945c..4236850 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -20,6 +20,11 @@ urlpatterns = [ ), # Account views path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), + path( + "accounts/generate-wifi-password/", + views.GenerateWiFiPasswordView.as_view(), + name="dgn-generate_wifi_password", + ), path( "accounts/create/", views.CreateSelfAccountView.as_view(), diff --git a/src/dgsi/views.py b/src/dgsi/views.py index bb705ac..d446b3e 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -75,6 +75,22 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) +class GenerateWiFiPasswordView(LoginRequiredMixin, RedirectView): + url = reverse_lazy("dgsi:dgn-profile") + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: + user = User.from_request(self.request) + + if user.kanidm is None: + messages.error(self.request, _("Compte DGNum inexistant.")) + elif not user.kanidm.radius_secret: + messages.error(self.request, _("Mot de passe WiFi déjà existant.")) + else: + async_to_sync(klient.call_post)(f"/v1/person/{user.username}/_radius") + + return super().get(request, *args, **kwargs) + + # INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to # use dispatch means that we need to execute the login check anyways. class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView): From f4428ace59892179457b5607079cbecd7418091d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 29 Sep 2024 19:58:29 +0200 Subject: [PATCH 090/172] feat(kanidm): Log some errors --- src/dgsi/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index d2837cb..aad5918 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -1,3 +1,4 @@ +import logging from dataclasses import dataclass from functools import cached_property from typing import Optional, Self @@ -12,6 +13,7 @@ from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person +from kanidm.radius import ClientConnectorError from shared.kanidm import klient @@ -174,6 +176,9 @@ class User(AbstractUser): ) except NoMatchingEntries: return None + except (TimeoutError, ClientConnectorError) as e: + logging.error(f"Erreur lors de la requête à Kanidm: {e}") + return None @property def is_admin(self) -> bool: From a88d31541cfd836ba2bd4bb3c8ec8142e4cd8aa2 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 29 Sep 2024 20:05:05 +0200 Subject: [PATCH 091/172] feat(radius): Add the user to the correct group When generating the first WiFi password, the user is added to the `radius_access` group, which will allow them to connect. This also fix a previous incorrect logic where users without wifi password could not generate one. --- src/dgsi/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index d446b3e..8c3b9f8 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -83,9 +83,12 @@ class GenerateWiFiPasswordView(LoginRequiredMixin, RedirectView): if user.kanidm is None: messages.error(self.request, _("Compte DGNum inexistant.")) - elif not user.kanidm.radius_secret: - messages.error(self.request, _("Mot de passe WiFi déjà existant.")) else: + # Give access to the wifi network when the user creates its first password + if not user.kanidm.radius_secret: + async_to_sync(klient.group_add_members)( + "radius_access", [user.username] + ) async_to_sync(klient.call_post)(f"/v1/person/{user.username}/_radius") return super().get(request, *args, **kwargs) From 61e3d4ed1951123aadf431af2a8ffe42ad0231ca Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 30 Sep 2024 10:14:45 +0200 Subject: [PATCH 092/172] feat(bulma): Tweak bt-link width --- src/shared/static/bulma/bulma.scss | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 6005991..8e656ef 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -10,6 +10,9 @@ $dark: rgb(46, 46, 46); $dark: $dark ); +@use "./sass/utilities/mixins" as mx; +@use "./sass/utilities/initial-variables.scss" as iv; + body { min-height: 100vh; display: flex; @@ -37,7 +40,13 @@ body { justify-content: space-evenly; .button { - width: 47.5%; + @include mx.from(iv.$desktop) { + width: 47.5%; + } + + @include mx.until(iv.$desktop) { + width: 100%; + } } } From 119867a91ed07cfd01536eb0000aeaa3da7a05c4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 4 Oct 2024 14:43:42 +0200 Subject: [PATCH 093/172] fix(index): Shorten names to fit on small screens --- src/dgsi/views.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 8c3b9f8..aa8e646 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -29,19 +29,14 @@ class Link(NamedTuple): AUTHENTICATED_LINKS: list[Link] = [ Link("is-primary", "dgsi:dgn-profile", _("Mon profil"), "user-filled"), Link("is-primary", "dgsi:dgn-legal_documents", _("Documents Légaux"), "script"), - Link( - "is-info", - "dgsi:dgn-services", - _("Services proposés par la DGNum"), - "apps-filled", - ), + Link("is-info", "dgsi:dgn-services", _("Services proposés"), "apps-filled"), ] ADMIN_LINKS: list[Link] = [ Link( "is-danger", "dgsi:dgn-create_kanidm_user", - _("Créer un nouveau compte Kanidm"), + _("Créer un compte Kanidm"), "user-plus", ), Link( From 3fa4591665f82fdf6af688b42e0bc64cb787289a Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 4 Oct 2024 14:48:00 +0200 Subject: [PATCH 094/172] feat(shell): Add SAML dependencies --- default.nix | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/default.nix b/default.nix index 1008f2a..28d9316 100644 --- a/default.nix +++ b/default.nix @@ -53,24 +53,28 @@ in pkgs.jq # Python dependencies - (python.withPackages (ps: [ - ps.django - ps.django-allauth - ps.django-allauth-cas - ps.django-browser-reload - ps.django-bulma-forms - ps.django-compressor - ps.django-debug-toolbar - ps.django-import-export - ps.django-sass-processor - ps.django-sass-processor-dart-sass - ps.django-stubs - ps.django-unfold - ps.ipython - ps.loadcredential - ps.pykanidm - ps.python-cas - ])) + (python.withPackages ( + ps: + [ + ps.django + ps.django-allauth + ps.django-allauth-cas + ps.django-browser-reload + ps.django-bulma-forms + ps.django-compressor + ps.django-debug-toolbar + ps.django-import-export + ps.django-sass-processor + ps.django-sass-processor-dart-sass + ps.django-stubs + ps.django-unfold + ps.ipython + ps.loadcredential + ps.pykanidm + ps.python-cas + ] + ++ ps.django-allauth.optional-dependencies.saml + )) ] ++ check.enabledPackages; env = { From 9c4413faa1610167d65b5c6110cdbc714eb14887 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 6 Oct 2024 14:43:41 +0200 Subject: [PATCH 095/172] feat(settings): Add SAML auth --- src/app/settings.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/app/settings.py b/src/app/settings.py index 50967a5..38d8a7a 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.openid_connect", + "allauth.socialaccount.providers.saml", "allauth_cas", "shared.cas", # Main app @@ -178,6 +179,50 @@ SOCIALACCOUNT_PROVIDERS = { "settings": {"color": "danger"}, }, }, + "saml": { + "APPS": [ + { + "provider_id": "ens_saml", + "name": "SSO ENS", + "client_id": "ens", + "settings": { + "color": "info", + "idp": { + "entity_id": "https://federation-test.ens.psl.eu/idp/shibboleth", + "metadata_url": "https://federation-test.ens.psl.eu/idp/shibboleth", + }, + # Our configuration + "sp": { + "entity_id": "https://profil.dgnum.eu/accounts/saml/ens/metadata", + }, + "advanced": { + "authn_request_signed": True, + "metadata_signed": True, + "private_key": credentials["X509_KEY"], + "x509cert": credentials["X509_CERT"], + "want_assertion_encrypted": True, + }, + "organization": { + "en": { + "name": "Délégation Générale Numérique", + "displayname": "Délégation Générale Numérique", + "url": "https://dgnum.eu", + }, + }, + "contact_person": { + "technical": { + "givenName": "Tom Hubrecht", + "emailAddress": "admins@dgnum.eu", + }, + "administrative": { + "givenName": "Jean-Marc Gailis", + "emailAddress": "bureau@dgnum.eu", + }, + }, + }, + } + ], + }, } SOCIALACCOUNT_ONLY = True From 61bb59d2448dc33deedf2831bddcf230c2c3f903 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 6 Oct 2024 16:52:15 +0200 Subject: [PATCH 096/172] chore(app/urls): Cleaner file --- src/app/urls.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app/urls.py b/src/app/urls.py index c33a36f..8cc4af3 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -14,11 +14,11 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -from django.views.generic import TemplateView urlpatterns = [ path("", include("dgsi.urls")), @@ -29,10 +29,8 @@ urlpatterns = [ ] if settings.DEBUG: - urlpatterns += ( - [ - path("__debug__/", include("debug_toolbar.urls")), - ] - + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - ) + urlpatterns += [ + path("__debug__/", include("debug_toolbar.urls")), + *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT), + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), + ] From c2911123509bdaa4a6fa83cfbab2e4a7c3d2dbc3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 6 Oct 2024 16:52:49 +0200 Subject: [PATCH 097/172] feat(commit-hooks): Add ruff --- default.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/default.nix b/default.nix index 28d9316..5c9096a 100644 --- a/default.nix +++ b/default.nix @@ -19,6 +19,11 @@ let stages = [ "pre-push" ]; }; + ruff = { + enable = true; + stages = [ "pre-push" ]; + }; + # Nix Hooks statix = { enable = true; From 833c855b5c2c9b2c7c14837e1fa7fa6555d169a1 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 7 Oct 2024 23:33:22 +0200 Subject: [PATCH 098/172] feat(templates): Update --- src/shared/templates/_footer.html | 2 +- src/shared/templates/account/login.html | 36 +++++++++++++++++++ src/shared/templates/account/logout.html | 18 ++++++++++ .../templates/allauth/elements/button.html | 1 + .../templates/allauth/elements/provider.html | 1 - .../allauth/elements/provider_list.html | 6 ---- .../templates/allauth/layouts/base.html | 15 +++++--- .../socialaccount/authentication_error.html | 13 +++++++ src/shared/templates/socialaccount/login.html | 23 ++++++++++++ .../socialaccount/snippets/provider_list.html | 19 ---------- 10 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 src/shared/templates/account/login.html create mode 100644 src/shared/templates/account/logout.html delete mode 100644 src/shared/templates/allauth/elements/provider.html delete mode 100644 src/shared/templates/allauth/elements/provider_list.html create mode 100644 src/shared/templates/socialaccount/authentication_error.html create mode 100644 src/shared/templates/socialaccount/login.html delete mode 100644 src/shared/templates/socialaccount/snippets/provider_list.html diff --git a/src/shared/templates/_footer.html b/src/shared/templates/_footer.html index af91d07..63052d4 100644 --- a/src/shared/templates/_footer.html +++ b/src/shared/templates/_footer.html @@ -1,6 +1,6 @@ {% load i18n django_browser_reload %}
- {%blocktrans %}Logiciel développé pour et par la DGNum.{% endblocktrans %} + {% blocktrans %}Logiciel développé pour et par la DGNum.{% endblocktrans %} {% django_browser_reload_script %}
diff --git a/src/shared/templates/account/login.html b/src/shared/templates/account/login.html new file mode 100644 index 0000000..87eb4cb --- /dev/null +++ b/src/shared/templates/account/login.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% load i18n socialaccount %} +{% load allauth account %} + +{% block content %} +

{% trans "Connexion via un compte tiers" %}

+
+ + {% get_providers as providers %} + +
+ {% for provider in providers %} + {% if provider.id == "openid" %} + {% for brand in provider.get_brands %} + {{ brand.name }} + {% endfor %} + {% endif %} + + {{ provider.name }} + {% endfor %} +
+ + +{% endblock content %} + +{% block extra_body %} + {{ block.super }} + {% if PASSKEY_LOGIN_ENABLED %} + {% include "mfa/webauthn/snippets/login_script.html" with button_id="passkey_login" %} + {% endif %} +{% endblock extra_body %} diff --git a/src/shared/templates/account/logout.html b/src/shared/templates/account/logout.html new file mode 100644 index 0000000..8ff4077 --- /dev/null +++ b/src/shared/templates/account/logout.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load allauth i18n %} + +{% block content %} +

{% trans "Déconnexion" %}

+
+ +

+ {% trans "Êtes vous certain·e de vouloir vous déconnecter ?" %} +

+ +
+ {% csrf_token %} + {{ redirect_field }} + +
+{% endblock content %} diff --git a/src/shared/templates/allauth/elements/button.html b/src/shared/templates/allauth/elements/button.html index e168e8e..7fe19ef 100644 --- a/src/shared/templates/allauth/elements/button.html +++ b/src/shared/templates/allauth/elements/button.html @@ -1,4 +1,5 @@ {% load allauth %} + {% comment %} djlint:off {% endcomment %} <{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %} {% if attrs.form %}form="{{ attrs.form }}"{% endif %} diff --git a/src/shared/templates/allauth/elements/provider.html b/src/shared/templates/allauth/elements/provider.html deleted file mode 100644 index 89b8094..0000000 --- a/src/shared/templates/allauth/elements/provider.html +++ /dev/null @@ -1 +0,0 @@ -{{ attrs.name }} diff --git a/src/shared/templates/allauth/elements/provider_list.html b/src/shared/templates/allauth/elements/provider_list.html deleted file mode 100644 index 8430750..0000000 --- a/src/shared/templates/allauth/elements/provider_list.html +++ /dev/null @@ -1,6 +0,0 @@ -{% load allauth %} - -
- {% slot default %} - {% endslot %} -
diff --git a/src/shared/templates/allauth/layouts/base.html b/src/shared/templates/allauth/layouts/base.html index eb6082e..08c8108 100644 --- a/src/shared/templates/allauth/layouts/base.html +++ b/src/shared/templates/allauth/layouts/base.html @@ -1,5 +1,3 @@ -{% load django_browser_reload i18n sass_tags static %} - @@ -15,8 +13,17 @@ {% include "_hero.html" %} -
-
+
+
+ {% for message in messages %} +
+ + {{ message|safe }} +
+ {% endfor %} +
+ +
{% block content %} {% endblock content %}
diff --git a/src/shared/templates/socialaccount/authentication_error.html b/src/shared/templates/socialaccount/authentication_error.html new file mode 100644 index 0000000..679c2ea --- /dev/null +++ b/src/shared/templates/socialaccount/authentication_error.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% load i18n %} +{% load allauth %} + +{% block content %} +

{% trans "Erreur lors de la connexion" %}

+
+ +

+ {% trans "Une erreur est survenue lors de votre tentative de connexion avec un compte tiers." %} +

+{% endblock content %} diff --git a/src/shared/templates/socialaccount/login.html b/src/shared/templates/socialaccount/login.html new file mode 100644 index 0000000..86b9081 --- /dev/null +++ b/src/shared/templates/socialaccount/login.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% load allauth i18n %} + +{% block title %} + {% trans "Connexion" %} +{% endblock title %} + +{% block content %} +

+ {% blocktrans with provider.name as provider %}Se connecter via un compte {{ provider }}{% endblocktrans %} +

+
+ +

+ {% blocktrans with provider.name as provider %}Vous vous apprêtez à vous connecter à l'aide d'un compte tiers provenant de {{ provider }}.{% endblocktrans %} +

+ +
+ {% csrf_token %} + +
+{% endblock content %} diff --git a/src/shared/templates/socialaccount/snippets/provider_list.html b/src/shared/templates/socialaccount/snippets/provider_list.html deleted file mode 100644 index 3eb191e..0000000 --- a/src/shared/templates/socialaccount/snippets/provider_list.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load allauth socialaccount %} - -{% get_providers as socialaccount_providers %} -{% if socialaccount_providers %} - {% element provider_list %} - {% for provider in socialaccount_providers %} - {% if provider.id == "openid" %} - {% for brand in provider.get_brands %} - {% provider_login_url provider openid=brand.openid_url process=process as href %} - {% element provider name=brand.name provider_id=provider.id href=href color=provider.app.settings.color %} - {% endelement %} - {% endfor %} - {% endif %} - {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} - {% element provider name=provider.name provider_id=provider.id href=href color=provider.app.settings.color %} -{% endelement %} -{% endfor %} -{% endelement %} -{% endif %} From 6caf3dbf610ed17f07de18a6ead62a782e42b942 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 7 Oct 2024 23:34:00 +0200 Subject: [PATCH 099/172] feat(project/djlint): Declare custom blocks used by allauth --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 940e775..6dbc2e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ Repository = "https://git.dgnum.eu/DGNum/dgsi" [tool.djlint] blank_line_after_tag = "load,extends" +custom_blocks = "slot,element" format_js = true indent = 2 max_blank_lines = 1 From f6fcd90622151e116adedb41f53da0445f1ee387 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 9 Oct 2024 10:41:15 +0200 Subject: [PATCH 100/172] feat(account): WIP --- src/shared/account.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/account.py b/src/shared/account.py index a4682ea..6d4eef6 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -1,3 +1,4 @@ +import logging from functools import lru_cache from typing import Optional @@ -11,6 +12,8 @@ from django.utils.translation import gettext_lazy as _ from dgsi.models import Translation, User +logger = logging.getLogger(__name__) + class SharedAccountAdapter(DefaultSocialAccountAdapter): """ @@ -49,6 +52,7 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): return sociallogin.account.extra_data["preferred_username"] case _: + logger.warning(sociallogin.user) # INFO: This should never happen messages.error(request, _("Méthode de connexion invalide.")) raise ImmediateHttpResponse( From 6921373d6c73245d5812b77844460218a2568022 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 07:58:19 +0200 Subject: [PATCH 101/172] feat(templates): Add a visual indication that some pages give more power --- src/dgsi/mixins.py | 4 ++++ src/shared/templates/_hero.html | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/dgsi/mixins.py b/src/dgsi/mixins.py index 90deae0..77da158 100644 --- a/src/dgsi/mixins.py +++ b/src/dgsi/mixins.py @@ -14,3 +14,7 @@ class StaffRequiredMixin(UserPassesTestMixin): assert isinstance(self.request.user, User) return self.request.user.is_admin + + def get_context_data(self, **kwargs): + # NOTE: We are only allowed to do this if a class is supplied to the right when constructing the view + return super().get_context_data(admin_view=True, **kwargs) # pyright: ignore diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index 78ba4b7..858b1d5 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -1,6 +1,6 @@ {% load i18n %} -
+
@@ -66,4 +66,15 @@
+ + {% if admin_view %} +
+
+ + + + {% trans "Interface d'administration" %} +
+
+ {% endif %}
From faa8ea051a7d6312089426a13e6572e4f3baaca0 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 08:03:25 +0200 Subject: [PATCH 102/172] fix(bulma): Only use dark text when the button is light This makes the documents readable when using dark mode --- src/shared/static/bulma/bulma.scss | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index 8e656ef..e358754 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -26,14 +26,16 @@ body { margin-bottom: calc(0.5 * var(--bulma-block-spacing)); font-size: 1.25rem; - // Dark color for text - --bulma-color-l: var(--bulma-dark-l); - --bulma-color-l-delta: 0%; - color: hsl( - var(--bulma-dark-h), - var(--bulma-dark-s), - calc(var(--bulma-color-l) + var(--bulma-color-l-delta)) - ); + &.is-light { + // Dark color for text + --bulma-color-l: var(--bulma-dark-l); + --bulma-color-l-delta: 0%; + color: hsl( + var(--bulma-dark-h), + var(--bulma-dark-s), + calc(var(--bulma-color-l) + var(--bulma-color-l-delta)) + ); + } } .bt-links { From 5a84ea6e0f58bde65be9a2780f99019cd8b919c4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 08:05:01 +0200 Subject: [PATCH 103/172] chore(app): Disable SAML auth for now This does not work, and quite a bit of time will be necessary to fix it --- src/app/settings.py | 92 +++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index 38d8a7a..a201185 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -48,7 +48,7 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.openid_connect", - "allauth.socialaccount.providers.saml", + # "allauth.socialaccount.providers.saml", "allauth_cas", "shared.cas", # Main app @@ -179,50 +179,52 @@ SOCIALACCOUNT_PROVIDERS = { "settings": {"color": "danger"}, }, }, - "saml": { - "APPS": [ - { - "provider_id": "ens_saml", - "name": "SSO ENS", - "client_id": "ens", - "settings": { - "color": "info", - "idp": { - "entity_id": "https://federation-test.ens.psl.eu/idp/shibboleth", - "metadata_url": "https://federation-test.ens.psl.eu/idp/shibboleth", - }, - # Our configuration - "sp": { - "entity_id": "https://profil.dgnum.eu/accounts/saml/ens/metadata", - }, - "advanced": { - "authn_request_signed": True, - "metadata_signed": True, - "private_key": credentials["X509_KEY"], - "x509cert": credentials["X509_CERT"], - "want_assertion_encrypted": True, - }, - "organization": { - "en": { - "name": "Délégation Générale Numérique", - "displayname": "Délégation Générale Numérique", - "url": "https://dgnum.eu", - }, - }, - "contact_person": { - "technical": { - "givenName": "Tom Hubrecht", - "emailAddress": "admins@dgnum.eu", - }, - "administrative": { - "givenName": "Jean-Marc Gailis", - "emailAddress": "bureau@dgnum.eu", - }, - }, - }, - } - ], - }, + # "saml": { + # "APPS": [ + # { + # "provider_id": "ens_saml", + # "name": "SSO ENS", + # "client_id": "ens", + # "settings": { + # "color": "info", + # "idp": { + # "entity_id": "https://federation-test.ens.psl.eu/idp/shibboleth", + # "metadata_url": "https://federation-test.ens.psl.eu/idp/shibboleth", + # }, + # # Our configuration + # "sp": { + # "entity_id": "https://profil.dgnum.eu/accounts/saml/ens/metadata", + # }, + # "advanced": { + # "authn_request_signed": True, + # "metadata_signed": True, + # "private_key": credentials["X509_KEY"], + # "x509cert": credentials["X509_CERT"], + # "want_assertion_encrypted": False, + # "want_attribute_statement": True, + # "want_name_id": True, + # }, + # "organization": { + # "en": { + # "name": "Délégation Générale Numérique", + # "displayname": "Délégation Générale Numérique", + # "url": "https://dgnum.eu", + # }, + # }, + # "contact_person": { + # "technical": { + # "givenName": "Tom Hubrecht", + # "emailAddress": "admins@dgnum.eu", + # }, + # "administrative": { + # "givenName": "Jean-Marc Gailis", + # "emailAddress": "bureau@dgnum.eu", + # }, + # }, + # }, + # } + # ], + # }, } SOCIALACCOUNT_ONLY = True From 615eb6041f483ac6e45983da40bf296b02ecf7b6 Mon Sep 17 00:00:00 2001 From: jemagius Date: Sat, 12 Oct 2024 10:52:15 +0200 Subject: [PATCH 104/172] feat: precising the conditions of some operations when creating account as admin Signed-off-by: jemagius --- src/dgsi/forms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dgsi/forms.py b/src/dgsi/forms.py index 1f5e474..154bc5c 100644 --- a/src/dgsi/forms.py +++ b/src/dgsi/forms.py @@ -20,17 +20,17 @@ class CreateKanidmAccountForm(forms.Form): # TODO: Add a field for the clipper login information for the local mapping name = CharField( label=_("Identifiant"), - help_text=_("De préférence identique au login ENS de la personne concernée"), + help_text=_("De préférence identique au login ENS de la personne concernée."), validators=[name_validator], ) displayname = CharField(label=_("Nom d'usage")) mail = EmailField( label=_("Adresse e-mail"), - help_text=_("De préférence l'adresse '@ens.psl.eu'"), + help_text=_("De préférence l'adresse '@ens.psl.eu' (si en scola) ou '@normalesup.org' (si archikhûbe), sauf si exté (accord explicite du bureau nécessaire)."), ) active = BooleanField( label=_("Membre actif"), - help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members"), + help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members. L'accord du bureau est nécessaire pour cette opération donnant des privilèges."), required=False, ) From 868c406af930343e24dcb3f5f05eb4ab54912a67 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 21:47:22 +0200 Subject: [PATCH 105/172] feat(translations): Update en --- .../0008_alter_user_accepted_statutes.py | 25 +++ src/dgsi/models.py | 2 +- src/shared/locale/en/LC_MESSAGES/django.mo | Bin 5262 -> 6630 bytes src/shared/locale/en/LC_MESSAGES/django.po | 143 +++++++++++++----- 4 files changed, 131 insertions(+), 39 deletions(-) create mode 100644 src/dgsi/migrations/0008_alter_user_accepted_statutes.py diff --git a/src/dgsi/migrations/0008_alter_user_accepted_statutes.py b/src/dgsi/migrations/0008_alter_user_accepted_statutes.py new file mode 100644 index 0000000..7b261df --- /dev/null +++ b/src/dgsi/migrations/0008_alter_user_accepted_statutes.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-10-12 20:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dgsi", "0007_alter_user_accepted_bylaws_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="accepted_statutes", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="dgsi.statutes", + verbose_name="Derniers Statuts acceptés", + ), + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index aad5918..a02ea21 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -145,7 +145,7 @@ class User(AbstractUser): on_delete=models.SET_NULL, null=True, default=None, - verbose_name=_("Derniers statuts acceptés"), + verbose_name=_("Derniers Statuts acceptés"), ) accepted_bylaws = models.ForeignKey( Bylaws, diff --git a/src/shared/locale/en/LC_MESSAGES/django.mo b/src/shared/locale/en/LC_MESSAGES/django.mo index 6b1f9befecf2385570989679def79d9c2949933e..4b433d5f1fc34af80ff68f98b3ae6bc7ce951e44 100644 GIT binary patch delta 2833 zcmZvcTWl0n7{^a*rP_i(xfhWGaur%AVDv?d2~t!Z?1LJj53JFc5P2X*V-z1S>H`{LG!f&2zyDquD4gs!zjMx<^WA5U zFaD)5bET%@gkf|b_an)2V;uN?1rNsZF~)Sl23QYYgmd5ra5B6E?}0zVsqlAL11lc{#V?u#o9F1^PujnhcjUp ztc3@l961Jc?j0xx&cluHB3uZ^VPpaJ%_=6kxDSqpgK#1|0>{EPU^P4qW$44=`fK+hk?-ze@^SD};jG^hZcfO4c8%FZF!0Z+iO*f&2hQH0kZKU0OZNpK3B4HrW>(7{6v zc0nn0p-S-rTnyiWGvHTn3cLz+?gmuqClZZHxC%;75@w_zW}=#$gw1dWl5KMh&Vzr! z9dO1tV=luKd=_q`!0X|Aa69}B?t|;lu2Q}Z+u%jGw~U~u!*13A=~@K8oJjtay6Zd? zaXDU49y1kcJsYZ)jS$^t3zX-3pd#`hKl3t=x$qQJigQpUxB~BnlTju+wNL>qg|f4K z68R?>^E4YW&C`S(!c@*+tm^Yz5 zthXV)nbR32^5he!+FgL?G(QyguS1pS2GoU_6jp}np$xUc$Kf+jj-7?-nUA4*={u;v zu0mzzPe?nMaVXLG%nT;_?iWHu+yWKRKBx&vpgy-+ z4sC)PSiHSioP(|cqiQjRnr z`hr>!jSU6lKR-4wF0rje^&%$ z)t>J@=S8vO$MxA=V|LH?wGMnc;5jz!+eFaUAGRs6jyam0Rau$c zT=~)%H}rX%{j7RgqmxLu{c&!{v=lZYyEAs;bR5~zo5-{TiFBXqv)9tTHQkrZRW5C7 z4MR5_+EfrmHtE`wgWaMdrp-(AqTfE*(q`IoLw;^3WQA*eL9E05P87Lzx7X$k|64hu zJhLM=QY%|SCGb-9!`IMm^&N0hivXtVQ0ie?W7hu#9)&PN0rek zI@hP-vUzi`^(J>TRw&Y*aKqT~{0krDU1KX1cqD6&-rbxH+f=w!m%kO|&WEF}gtr!? z5ch4xvrXC8#%`}}SYFq#!memow{rFHA7i`9YqthA-=Ln~Q)n2qQjk`=Hjm#{M@#x{ zA})2TW8+>gOxE=~VSKQ3a-?swua57_mQ6UEafmY3Yr54<+UA2PXD~9IqqYmZ>(a7n zR6EkRrqk_lQa7)Io**5Yom9=x%0Xz8=}-=*f*#K|bgtd+rCihQ`fiAkiPp(CY`%8~ zgD|;ncxb}qvW^{YhF-i`r!qWddo7D-a7QG(~XJZJ^7W6r00&Q xxBJ7OZ&Z(MLd8K;@kJCSgKEWs8*;wM+;SrBb>jT_xutVqK24|yZ_Q$p`5%1tJ@5bk delta 1439 zcmX}sTS!zv9LMol)6{fxb1k!r9kVo7Gg~jE6j+I2mKqU4_F}b_v27-9B?t>a7b=1{ zXa$n_5>gVx;+sTJ5>!MW1W_3g6++ZYfnikN-{I8Y|9sBPng9G}&VDMtTojwkOzbnX z8e$3Y#bZn}j?Lmgn@%)lJO0E{tWPo~52KicS1|=gZ~=~DCO*Tt_zr#e74z|r`#gWP zF_tkgQ^iRI4_Yx5FQGc_$85ZV3-KW?#y7YKr%?m?lZ{!10n|X-uol~}5C?D`KEiZ- ziD~%3F=os+_k|hvftTr2@CD29*{H;dQ3Ez& z2DYO5JA`?RZ(^Jj;SlEFQ=E?MF3x&af( zawl%WHkQ}MR<6H!8G98cTUjWTs1ud>X=E~{-@P70l{$|6nI{}HlUJxUokFd-mz&Gc zLRD}JvfQQz)qe~b(_BIIGnm2ptK+-e&`ifs*Kbj4{T=lWJ{EQ(=Ashbi##%CkZoZ4 zko{=}P>BzrCUV=ozK^QlV^pG3sBwPzsJ}LspY$5B1U29(RH@G*)ik$I8Q(+gfd{BH zeT{nmGwSR4ftpArqbSh;>iu1)L?frP+_7Q~1%zg!O{BJ-P}ct%)iUddmF|sP zRMplJN?xss&|Xl|YUPBMM3Yeq5(&R-Q+dkVbCq@t!Q`EN zq1zQi5ux9y0HH6Yny4Z8V4Ow~|2KVEZ2bRC%bH~3rL!X*do;PhE=W0-961&}5o(Xx zBPkc`^QnQf`i}jFJ3?JnOI>|@IyEQ3?oO}pTAl4(R=C}&+u9U8Y7eI$kB7a_J+|N1 zl2%r|I#^`|gEbY^@rZACf<2kpWbg5ZXGKCM?Q#EE`(oDO6gsmGc7#Lm*I5%DdwN08 VYwhc?x(|ok9s}9o_+<7W&tK37m^}ah diff --git a/src/shared/locale/en/LC_MESSAGES/django.po b/src/shared/locale/en/LC_MESSAGES/django.po index d3aeb5d..591645a 100644 --- a/src/shared/locale/en/LC_MESSAGES/django.po +++ b/src/shared/locale/en/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: dgsi.dgnum.eu\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-26 22:48+0200\n" -"PO-Revision-Date: 2024-09-26 22:49+0200\n" +"POT-Creation-Date: 2024-10-12 21:42+0200\n" +"PO-Revision-Date: 2024-10-12 21:46+0200\n" "Last-Translator: Tom Hubrecht \n" "Language-Team: French\n" "Language: fr\n" @@ -18,10 +18,14 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1)\n" "X-Generator: Gtranslator 46.1\n" -#: app/settings.py:276 +#: app/settings.py:321 msgid "Administration de DGSI" msgstr "DGSI Administration" +#: dgsi/admin.py:51 +msgid "Documents DGNum" +msgstr "DGNum Documents" + #: dgsi/forms.py:16 msgid "Identifiant déjà présent dans la base de données." msgstr "Username already in the database." @@ -54,51 +58,59 @@ msgstr "Active member" msgid "Si selectionné, la personne sera ajoutée au groupe dgnum_members" msgstr "If selected, the person will be added to the dgnum_members group." -#: dgsi/models.py:22 +#: dgsi/models.py:24 msgid "Nom du service proposé" msgstr "Name of the proposed service" -#: dgsi/models.py:23 +#: dgsi/models.py:25 msgid "Adresse du service" msgstr "Address of the service" -#: dgsi/models.py:24 +#: dgsi/models.py:26 msgid "Icône du service" msgstr "Icon of the service" -#: dgsi/models.py:34 +#: dgsi/models.py:36 msgid "Date du document" msgstr "Document date" -#: dgsi/models.py:35 +#: dgsi/models.py:37 msgid "Nom du document" msgstr "Document name" -#: dgsi/models.py:36 +#: dgsi/models.py:38 msgid "Fichier PDF" msgstr "PDF file" -#: dgsi/models.py:58 dgsi/models.py:59 +#: dgsi/models.py:60 dgsi/models.py:61 #: dgsi/templates/dgsi/legal_documents.html:26 msgid "Statuts" msgstr "Statutes" -#: dgsi/models.py:71 dgsi/templates/dgsi/legal_documents.html:30 +#: dgsi/models.py:73 dgsi/templates/dgsi/legal_documents.html:30 msgid "Règlement Intérieur" msgstr "Bylaws" -#: dgsi/models.py:72 +#: dgsi/models.py:74 msgid "Règlements Intérieurs" msgstr "Bylaws" -#: dgsi/models.py:114 +#: dgsi/models.py:116 msgid "Correspondance de login" msgstr "Login mapping" -#: dgsi/models.py:115 +#: dgsi/models.py:117 msgid "Correspondances de login" msgstr "Login mappings" +#: dgsi/models.py:148 +msgid "Derniers Statuts acceptés" +msgstr "Latest accepted Statutes" + +#: dgsi/models.py:155 +msgid "Dernier Règlement Intérieur accepté" +msgstr "Latest accepted Bylaws" + #: dgsi/templates/_legal_document.html:9 msgid "" " En acceptant, vous assurez avoir lu ce document et en approuver le contenu." @@ -154,31 +166,43 @@ msgstr "Accept the bylaws" msgid "Profil de %(displayname)s" msgstr "Profile of %(displayname)s" -#: dgsi/templates/dgsi/profile.html:13 +#: dgsi/templates/dgsi/profile.html:14 msgid "Mot de passe WiFi :" msgstr "WiFi password:" -#: dgsi/templates/dgsi/profile.html:23 +#: dgsi/templates/dgsi/profile.html:16 +msgid "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?" +msgstr "Are you sure that you want to reset your WiFi password?" + +#: dgsi/templates/dgsi/profile.html:19 +msgid "Réinitialiser le mot de passe WiFi" +msgstr "Reset the WiFi password" + +#: dgsi/templates/dgsi/profile.html:32 +msgid "Générer un mot de passe WiFi" +msgstr "Generate a WiFi password:" + +#: dgsi/templates/dgsi/profile.html:36 msgid "Adresse e-mail :" msgstr "E-mail address:" -#: dgsi/templates/dgsi/profile.html:28 +#: dgsi/templates/dgsi/profile.html:41 msgid "Informations techniques" msgstr "Technical informations" -#: dgsi/templates/dgsi/profile.html:31 +#: dgsi/templates/dgsi/profile.html:44 msgid "Identifiant unique :" msgstr "Unique identifier:" -#: dgsi/templates/dgsi/profile.html:40 +#: dgsi/templates/dgsi/profile.html:53 msgid "Membre des groupes suivants :" msgstr "Member of the following groups:" -#: dgsi/templates/dgsi/profile.html:51 +#: dgsi/templates/dgsi/profile.html:64 msgid "Pas de compte DGNum répertorié." msgstr "No DGNum account found." -#: dgsi/templates/dgsi/profile.html:54 +#: dgsi/templates/dgsi/profile.html:67 msgid "Créer un compte DGNum" msgstr "Create a DGNum account" @@ -194,45 +218,49 @@ msgstr "My profile" msgid "Documents Légaux" msgstr "Legal Documents" -#: dgsi/views.py:35 -msgid "Services proposés par la DGNum" -msgstr "Services offered by the DGNum" +#: dgsi/views.py:32 +msgid "Services proposés" +msgstr "Services offered" -#: dgsi/views.py:44 -msgid "Créer un nouveau compte Kanidm" -msgstr "Create a new Kanidm account" +#: dgsi/views.py:39 +msgid "Créer un compte Kanidm" +msgstr "Create a Kanidm account" -#: dgsi/views.py:48 +#: dgsi/views.py:43 shared/templates/_hero.html:76 msgid "Interface d'administration" msgstr "Administration interface" -#: dgsi/views.py:83 +#: dgsi/views.py:80 +msgid "Compte DGNum inexistant." +msgstr "No existing DGNum account." + +#: dgsi/views.py:97 msgid "Compte DGNum créé avec succès" msgstr "DGNum account successfully created" -#: dgsi/views.py:99 +#: dgsi/views.py:113 msgid "Vous possédez déjà un compte DGNum !" msgstr "You already have a DGNum account!" -#: dgsi/views.py:111 +#: dgsi/views.py:125 msgid "Vous devez accepter les Statuts et le Règlement Intérieur." msgstr "You must accept the Statutes and the Bylaws." -#: dgsi/views.py:190 +#: dgsi/views.py:204 #, python-format msgid "Type de document invalide : %(kind)s" msgstr "Invalid document type: %(kind)s" -#: dgsi/views.py:220 +#: dgsi/views.py:234 #, python-format msgid "Compte DGNum pour %(displayname)s [%(name)s] créé." msgstr "DGNum account for %(displayname)s [%(name)s] created." -#: shared/account.py:37 +#: shared/account.py:40 msgid "Catégorie de compte ENS interdite." msgstr "ENS account category not permitted." -#: shared/account.py:53 +#: shared/account.py:57 msgid "Méthode de connexion invalide." msgstr "Invalid connection method." @@ -242,18 +270,30 @@ msgid "" msgstr "" "Software developed for and by the DGNum." -#: shared/templates/_hero.html:18 +#: shared/templates/_hero.html:18 shared/templates/account/logout.html:6 msgid "Déconnexion" msgstr "Logout" -#: shared/templates/_hero.html:27 +#: shared/templates/_hero.html:27 shared/templates/socialaccount/login.html:6 msgid "Connexion" msgstr "Login" -#: shared/templates/_hero.html:41 +#: shared/templates/_hero.html:40 msgid "Choix de la langue" msgstr "Language selection" +#: shared/templates/account/login.html:7 +msgid "Connexion via un compte tiers" +msgstr "Connection via a third-party account" + +#: shared/templates/account/logout.html:10 +msgid "Êtes vous certain·e de vouloir vous déconnecter ?" +msgstr "Are you sure you want to log out?" + +#: shared/templates/account/logout.html:16 +msgid "Se déconnecter" +msgstr "Log out" + #: shared/templates/accounts/forbidden_category.html:6 msgid "Connexion impossible" msgstr "Unable to connect" @@ -267,3 +307,30 @@ msgstr "" "Your details do not allow the DGNum to authenticate you.
If you think " "this is a mistake, please contact us at: contact@dgnum.eu" + +#: shared/templates/socialaccount/authentication_error.html:7 +msgid "Erreur lors de la connexion" +msgstr "Error during login" + +#: shared/templates/socialaccount/authentication_error.html:11 +msgid "" +"Une erreur est survenue lors de votre tentative de connexion avec un compte " +"tiers." +msgstr "" +"An error has occurred while trying to login with a third-party account." + +#: shared/templates/socialaccount/login.html:11 +#, python-format +msgid "Se connecter via un compte %(provider)s" +msgstr "Log in with a %(provider)s account" + +#: shared/templates/socialaccount/login.html:16 +#, python-format +msgid "" +"Vous vous apprêtez à vous connecter à l'aide d'un compte tiers provenant de " +"%(provider)s." +msgstr "You are about to log in using a third-party account from %(provider)s." + +#: shared/templates/socialaccount/login.html:21 +msgid "Continuer" +msgstr "Continue" From 37ddb5f075ffe1706e826d07bc6a7ee41d05e4ce Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 21:49:02 +0200 Subject: [PATCH 106/172] chore: Don't commit the auth token --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 48ab904..89db8df 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__/ !.static/.gitkeep src/shared/static/bulma/bulma.css src/shared/static/bulma/bulma.css.map +.credentials/KANIDM_AUTH_TOKEN From 41396ccae23fbf9ad682675f80f201fc664eada4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 21:51:10 +0200 Subject: [PATCH 107/172] fix(dgsi/models): Import from the original source --- src/dgsi/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dgsi/models.py b/src/dgsi/models.py index a02ea21..0be7927 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from functools import cached_property from typing import Optional, Self +from aiohttp.client_exceptions import ClientConnectorError from allauth.socialaccount.models import SocialAccount from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser @@ -13,7 +14,6 @@ from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person -from kanidm.radius import ClientConnectorError from shared.kanidm import klient From 304fe1f24968263872a8d73dc5fc7c4d7e4a7db1 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 22:05:33 +0200 Subject: [PATCH 108/172] fix(forms): Reword what JM wrote --- src/dgsi/forms.py | 12 +++++- src/shared/locale/en/LC_MESSAGES/django.mo | Bin 6630 -> 7279 bytes src/shared/locale/en/LC_MESSAGES/django.po | 41 ++++++++++++++------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/dgsi/forms.py b/src/dgsi/forms.py index 154bc5c..b12ac9f 100644 --- a/src/dgsi/forms.py +++ b/src/dgsi/forms.py @@ -26,11 +26,19 @@ class CreateKanidmAccountForm(forms.Form): displayname = CharField(label=_("Nom d'usage")) mail = EmailField( label=_("Adresse e-mail"), - help_text=_("De préférence l'adresse '@ens.psl.eu' (si en scola) ou '@normalesup.org' (si archikhûbe), sauf si exté (accord explicite du bureau nécessaire)."), + help_text=_( + "De préférence :
" + "- l'adresse @ens.psl.eu pour les personnes en scolarité ;
" + "- l'adresse @normalesup.org pour les personnes ayant fini leur scolarité ;
" + "Pour les personnes extérieures, le bureau doit donner son accord." + ), ) active = BooleanField( label=_("Membre actif"), - help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members. L'accord du bureau est nécessaire pour cette opération donnant des privilèges."), + help_text=_( + "Si selectionné, la personne sera ajoutée au groupe dgnum_members.
" + "L'accord préalable du bureau est nécessaire !" + ), required=False, ) diff --git a/src/shared/locale/en/LC_MESSAGES/django.mo b/src/shared/locale/en/LC_MESSAGES/django.mo index 4b433d5f1fc34af80ff68f98b3ae6bc7ce951e44..40c380e107b51d4f66fe6317b56524f61a6c57b2 100644 GIT binary patch delta 2298 zcmaLXZEO@p9LMqL1BC*W+7?knnF3P4azH7xwC7Q@R#AwBsC_{b*z0ZEHP_u^_m23K zTw-G)f+VEz6$yz?2~CWB(N`vzV44_A#E2RnLPDZ1(8LGS1Yb$;``bH6G2kS#pV_^g z`Oo}k_AYI{+*!QXQ2nN%JV9MX%~l%Y;^}HGl>QoHcHtgO;7M%8bJ&EJaUNFBHRdj? z!vf%5M!p*5yxj?uWAAEvM!&tffJMFp74==b3=)blP}g1xAIgLp3% za48-~P2_#ldo!pBT);UM#{7yqxc`g6T8MAj=Nq${2ZN{q#&Hpz#5?f=T!?3I0iHtz z{JwmD1@Gs+f*1CTzqcF2{c4 zRg*(a@MYA1M^RhyF0RE-@E-gF7vuc8(t8%Q1)Zp^9K@mqIzmGOoIvf}NBBB^f$W0W z$40KgA>50v;a7M8pTj8zN#auSyaxwx5GPSvT0>g;up9TW1+U-$_ZJ(;{~8)wStifS ze$QoKke)(K_~WwY zkq^iGP^6(R?Pt^qenU-2QM8A($fC?j)bkG1R`sCXPa|DS0Tt*dK8f$4Cio}n>|8^g zspY(+1+GJ7sJM-W4%O4B4hK;G13uEn96_z<9aM+UP&5Cj?C+@euAmNI9kbS%=|D|% zJL>aua2!L_LjFc3Ry5aWJjR3NjD8A7@F}e48*9Y_*n@AN4$T)c7;S1rz7z zG!`|jEmV~rs`B(WmBHiOKwOj`mD&D3Ob@3Ad2k5I?cG zpb*-u8`+#2MuG3sa(x>O1z9Kb@-vgR`!+-QK{)2nv5-py;mFOy6>l2PdBO4XcG&Yh z0uk-MgD2Ce{#(rAd7_6N9o(pujy6rpDcDTl<+(6ZMhblE3=IWgCXw8bPMKcU=E9lD z;fb&3byg&%7uJ4R^-O8ynGwG*wtvhWOS8=KWD{B$!FRQk#)$hmStm`fOyPPwH_BUo zW^#x$I$r47W<{F*Wnr{OJE=+R4OwVTS`Io-brvKOdEoJFDNV z(>l9v&JA+RaMU^E`6F>!qurx!#xjUk8rgG81GBb1f(3e<_8&yIuvdtc6Bf9MhE`Y?7l48~>!^CBC% Q2MeSxQ%YZU$%~c$0+!Wt82|tP delta 1676 zcmZA1OGs2v9LMqF%w#n>)%a}2N-fh&o3!zrm6}#s5(W~f6pCU+YFQ=-HLDQ9Y9Si6 z$q39|2F<33HU&i>1w|B*wlJ!Nkwig4i@ra-v#5jr`J8jT_dNdR-21)Yvp+Bq{93 z4P6Y9%J?RiiXLph#n_E*?8QjDgb_H58t9(Yet|1!zen9aV_lDM(kJbB)B>te6KO(? z(}R^bfRT)EK2p&Nr;tD6WVC3E#Y9X;O`wt=O|TZ#VGC+2PGLG;K@UF1Se!)NH-k#O zo7HFwb5Z>?V?Z68prSn)#4@~rWZO((3jV}B=y4hI8js*%EMbF-@fOzLS8Tu{de>H7 z#BDf+^8@DP%Aow{F!t7 zti&Nyil3miU;^i144r8lFKR)VsBvnd$v=xRbzIN@2T=odqf&R?Y7blON2vSX;3k|z ztt6R^)^k;;L)nO$XpiMt1t?pgq72U!_J502%|RWa9aft|Zwd(( zG&?aeH~Zm8MP*C}N~u-0wAZT%rF1LdC(4NeLRr!RLPgopcSCPZDdU^xhknzIHlv zWPJ9MT!#8`Z6K0@)!}z-{WfP-(C>U@cQv=RceNd8YiVn0={z2+TNJQ6vWs%^g5&Oh dz26__54Oe?*kk^=E2*`!qwA\n" "Language-Team: French\n" "Language: fr\n" @@ -35,28 +35,43 @@ msgid "Identifiant" msgstr "Username" #: dgsi/forms.py:23 -msgid "De préférence identique au login ENS de la personne concernée" -msgstr "Preferably identical to the ENS login of the person concerned" +msgid "De préférence identique au login ENS de la personne concernée." +msgstr "Preferably identical to the ENS login of the person concerned." -#: dgsi/forms.py:26 dgsi/forms.py:39 +#: dgsi/forms.py:26 dgsi/forms.py:47 msgid "Nom d'usage" msgstr "Name in use" -#: dgsi/forms.py:28 dgsi/forms.py:41 +#: dgsi/forms.py:28 dgsi/forms.py:49 msgid "Adresse e-mail" msgstr "E-mail address" -#: dgsi/forms.py:29 dgsi/forms.py:42 -msgid "De préférence l'adresse '@ens.psl.eu'" -msgstr "Preferably the ‘@ens.psl.eu’ address" +#: dgsi/forms.py:30 +msgid "" +"De préférence :
- l'adresse @ens.psl.eu pour les personnes " +"en scolarité ;
- l'adresse @normalesup.org pour les " +"personnes ayant fini leur scolarité ;
Pour les personnes extérieures, " +"le bureau doit donner son accord." +msgstr "" +"Preferably:
- the @ens.psl.eu address for students;
- the " +"@normalesup.org address for people having finished their " +"studies;
For outsiders, the board must give its approval." -#: dgsi/forms.py:32 +#: dgsi/forms.py:37 msgid "Membre actif" msgstr "Active member" -#: dgsi/forms.py:33 -msgid "Si selectionné, la personne sera ajoutée au groupe dgnum_members" -msgstr "If selected, the person will be added to the dgnum_members group." +#: dgsi/forms.py:39 +msgid "" +"Si selectionné, la personne sera ajoutée au groupe dgnum_members.
L'accord préalable du bureau est nécessaire !" +msgstr "" +"If selected, the person will be added to the dgnum_members " +"group.
Prior approval from the board is required!" + +#: dgsi/forms.py:50 +msgid "De préférence l'adresse '@ens.psl.eu'" +msgstr "Preferably the ‘@ens.psl.eu’ address" #: dgsi/models.py:24 msgid "Nom du service proposé" From 24f825c1dd34a3948cce7d39c19623ecf948ffa7 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 12 Oct 2024 22:24:16 +0200 Subject: [PATCH 109/172] feat(templates): Tweak base template This allows content to stay at a reasonable size --- src/shared/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/templates/base.html b/src/shared/templates/base.html index e535d9a..8aed9cb 100644 --- a/src/shared/templates/base.html +++ b/src/shared/templates/base.html @@ -16,7 +16,7 @@ {% include "_hero.html" %} -
+
{% for message in messages %}
From b859a72747648b7e2ce6384de02354bf3e6837cd Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 25 Jan 2025 19:37:50 +0100 Subject: [PATCH 110/172] feat(dgsi): Add archives --- default.nix | 1 + src/app/settings.py | 8 +++ src/app/urls.py | 4 ++ src/dgsi/admin.py | 4 +- src/dgsi/migrations/0009_archive.py | 48 +++++++++++++ src/dgsi/models.py | 24 ++++++- src/dgsi/templates/dgsi/archive_list.html | 51 ++++++++++++++ src/dgsi/urls.py | 7 ++ src/dgsi/views.py | 43 +++++++++++- src/shared/locale/en/LC_MESSAGES/django.mo | Bin 7279 -> 7504 bytes src/shared/locale/en/LC_MESSAGES/django.po | 74 +++++++++++++-------- 11 files changed, 229 insertions(+), 35 deletions(-) create mode 100644 src/dgsi/migrations/0009_archive.py create mode 100644 src/dgsi/templates/dgsi/archive_list.html diff --git a/default.nix b/default.nix index 5c9096a..5bb3ece 100644 --- a/default.nix +++ b/default.nix @@ -88,6 +88,7 @@ in DGSI_STATIC_ROOT = builtins.toString ./.static; DGSI_MEDIA_ROOT = builtins.toString ./.media; DGSI_KANIDM_CLIENT = "dgsi_test"; + DGSI_ARCHIVES_ROOT = builtins.toString ./.archives; }; shellHook = '' diff --git a/src/app/settings.py b/src/app/settings.py index a201185..6a6c6a8 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -277,6 +277,7 @@ MEDIA_ROOT = credentials.get("MEDIA_ROOT") ### # Storages configuration +ARCHIVES_INTERNAL = credentials.get("ARCHIVES_INTERNAL", "_archives") STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", @@ -284,6 +285,13 @@ STORAGES = { "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage", }, + "archives": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + "OPTIONS": { + "location": credentials["ARCHIVES_ROOT"], + "base_url": f"/{ARCHIVES_INTERNAL}/", + }, + }, } ### diff --git a/src/app/urls.py b/src/app/urls.py index 8cc4af3..4907912 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -33,4 +33,8 @@ if settings.DEBUG: path("__debug__/", include("debug_toolbar.urls")), *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT), *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), + *static( + settings.STORAGES["archives"]["OPTIONS"]["base_url"], + document_root=settings.STORAGES["archives"]["OPTIONS"]["location"], + ), ] diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 29a6911..8e374a7 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -10,7 +10,7 @@ from unfold.contrib.import_export.forms import ( SelectableFieldsExportForm, ) -from dgsi.models import Bylaws, Service, Statutes, Translation, User +from dgsi.models import Archive, Bylaws, Service, Statutes, Translation, User assert DjangoUserAdmin.fieldsets is not None @@ -54,7 +54,7 @@ class UserAdmin(DjangoUserAdmin, ImportExportMixin, ModelAdmin): ) -@admin.register(Bylaws, Service, SocialAccount, Statutes, Translation) +@admin.register(Archive, Bylaws, Service, SocialAccount, Statutes, Translation) class AdminClass(ImportExportMixin, ModelAdmin): compressed_fields = True import_form_class = ImportForm diff --git a/src/dgsi/migrations/0009_archive.py b/src/dgsi/migrations/0009_archive.py new file mode 100644 index 0000000..efb7aab --- /dev/null +++ b/src/dgsi/migrations/0009_archive.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.16 on 2025-01-25 15:27 + +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dgsi", "0008_alter_user_accepted_statutes"), + ] + + operations = [ + migrations.CreateModel( + name="Archive", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Date du document")), + ( + "name", + models.CharField(max_length=255, verbose_name="Nom du document"), + ), + ( + "file", + models.FileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/archives/", + location="/home/thubrecht/Software/git.dgnum.eu/DGNum/dgsi/.archives", + ), + upload_to="", + verbose_name="Fichier PDF", + ), + ), + ], + options={ + "verbose_name": "Document d'archives", + "verbose_name_plural": "Documents d'archives", + }, + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 0be7927..2314b3a 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -7,6 +7,7 @@ from aiohttp.client_exceptions import ClientConnectorError from allauth.socialaccount.models import SocialAccount from asgiref.sync import async_to_sync from django.contrib.auth.models import AbstractUser +from django.core.files import storage from django.db import models from django.db.models.signals import pre_delete from django.dispatch import receiver @@ -74,6 +75,23 @@ class Bylaws(LegalDocument): verbose_name_plural = _("Règlements Intérieurs") +class Archive(models.Model): + """ + Archived documents for the association. + """ + + date = models.DateField(_("Date du document")) + name = models.CharField(_("Nom du document"), max_length=255) + file = models.FileField(_("Fichier PDF"), storage=storage.storages["archives"]) + + def __str__(self) -> str: + return self.name + + class Meta: # pyright: ignore + verbose_name = _("Document d'archives") + verbose_name_plural = _("Documents d'archives") + + # class TermsAndConditions(LegalDocument): # """ # Terms and Conditions of use regarding a service offered by the association. @@ -118,7 +136,7 @@ class Translation(models.Model): # INFO: We need to use a signal receiver here, as the delete method is not called -# when deleteing objects from the admin interface +# when deleting objects from the admin interface @receiver(pre_delete, sender=Translation) def restore_username(**kwargs): """ @@ -185,3 +203,7 @@ class User(AbstractUser): return (self.kanidm is not None) and ( ADMIN_GROUP in self.kanidm.person.memberof ) + + def can_access_archive(self, archive: Archive) -> bool: + # Prepare a more complex workflow + return True diff --git a/src/dgsi/templates/dgsi/archive_list.html b/src/dgsi/templates/dgsi/archive_list.html new file mode 100644 index 0000000..2badaf4 --- /dev/null +++ b/src/dgsi/templates/dgsi/archive_list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Archives de la DGNum" %}

+
+ + {% for file in archive_list %} + + + + + + {{ file }} + {{ file.date }} + + {% endfor %} + +
+

{% trans "Statuts" %}

+ + {% for document in statutes_list %} + + + + + + {{ document }} + {{ document.date }} + + {% endfor %} + +
+

{% trans "Règlements Intérieurs" %}

+ + {% for document in bylaws_list %} + + + + + + {{ document }} + {{ document.date }} + + {% endfor %} + +{% endblock content %} diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index 4236850..d3d546e 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -7,6 +7,13 @@ app_name = "dgsi" urlpatterns = [ # Misc views path("", views.IndexView.as_view(), name="dgn-index"), + # Archives + path("archives/", views.ArchiveListView.as_view(), name="dgn-archives"), + path( + "archives//", + views.ProtectedArchiveView.as_view(), + name="dgn-protected-archive", + ), # Legal documents path( "legal-documents/", diff --git a/src/dgsi/views.py b/src/dgsi/views.py index aa8e646..d3d1a0a 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,21 +1,23 @@ from typing import Any, NamedTuple from asgiref.sync import async_to_sync +from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.mail import EmailMessage -from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect +from django.http import Http404, HttpRequest, HttpResponseBase, HttpResponseRedirect +from django.http.response import HttpResponse from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.functional import Promise from django.utils.translation import gettext_lazy as _ -from django.views.generic import FormView, ListView, RedirectView, TemplateView +from django.views.generic import FormView, ListView, RedirectView, TemplateView, View from django.views.generic.detail import SingleObjectMixin from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm from dgsi.mixins import StaffRequiredMixin -from dgsi.models import Bylaws, Service, Statutes, User +from dgsi.models import Archive, Bylaws, Service, Statutes, User from shared.kanidm import klient @@ -30,6 +32,7 @@ AUTHENTICATED_LINKS: list[Link] = [ Link("is-primary", "dgsi:dgn-profile", _("Mon profil"), "user-filled"), Link("is-primary", "dgsi:dgn-legal_documents", _("Documents Légaux"), "script"), Link("is-info", "dgsi:dgn-services", _("Services proposés"), "apps-filled"), + Link("is-success", "dgsi:dgn-archives", _("Archives"), "archive"), ] ADMIN_LINKS: list[Link] = [ @@ -166,6 +169,40 @@ class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView): return super().form_valid(form) +class ArchiveListView(LoginRequiredMixin, ListView): + model = Archive + ordering = ["-date"] + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + return super().get_context_data( + bylaws_list=Bylaws.objects.all(), + statutes_list=Statutes.objects.all(), + **kwargs, + ) + + +class ProtectedArchiveView(LoginRequiredMixin, View): + http_method_names = ["get"] + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: + u = User.from_request(request) + archive = Archive.objects.get(pk=self.kwargs["pk"]) + + if u.can_access_archive(archive): + # INFO: When in DEBUG mode, redirect to the "real" file + if settings.DEBUG: + return HttpResponseRedirect(redirect_to=archive.file.url) + + return HttpResponse( + headers={ + "Content-Disposition": f"attachment; filename={archive.file.name}", + "X-Accel-Redirect": f"/{settings.ARCHIVES_INTERNAL}/{archive.file.name}", + } + ) + else: + raise Http404 + + class LegalDocumentsView(LoginRequiredMixin, TemplateView): template_name = "dgsi/legal_documents.html" diff --git a/src/shared/locale/en/LC_MESSAGES/django.mo b/src/shared/locale/en/LC_MESSAGES/django.mo index 40c380e107b51d4f66fe6317b56524f61a6c57b2..d3248fc23eddf58adbf821b0f40e12c4eae5fd7c 100644 GIT binary patch delta 1898 zcmZ|POH5Q(9LMoLBZv%Ihldow$3^8)5DUo2Lq$fk;0vlIR3k~FFp4dYMDUTO_BLr_ zlg5Nb#BLhnLK8J5_9ecK=R&zS(`+kV!WB4)IhdPhmX7r}3tKP)qnL_c;|JJ> zAv}v}cLnv{HC$@u+FdGo;RQMvPpeGK#d55{Bb+6S#;2Ht ziM*_dEkU)bLQQBpCd9dB`=~^C&>l3)$IF<9qo@vEVj89=o6W@nOuhL3s;(Y8wcOI2XRMc<;ITIU0o%Ji!iVHbNbx@5usuuha zyOC3~|8N-wXuJdS@d18?UtlBqy#}x2cAUf(Y)Ik!b;cLimrw99?ulbCM){2U<1DWL z|3Gcs6!NnW+r{zPGSq!B>Wm}ETx=I=;s;Rej-nR&Eoy;hPzxDOXaAQ_xyu7>`M(&( zWLhU+1T}CyYK6N|9e#ssj{SfdZ~!%d^QfJ?;om>-?!@hp zX5?p`T(;l|)Qm?_$u*8jI`yg*C!=;O3zc+XRQm|(f20W++d5DS{2tZ*SJcFBPwU$6 zRMhYxDm$N{k}8wdnyG`@iS_s$?nJF*484hAJ@@+NEACCVg7^2PN6PvYBhSU-yoPI;U#^ zB0T-P%u1-NAXX7G2z_QN3FSbnl=_t_IkatNFm7SwP;2|aBhem%lny delta 1676 zcmX}sSx8h-9LMqFHl}8pqgiS>F1cIdG&*j%lv%dmLg0f!RzwjI75NYa7m^^;BpK>O z5Ud1)$cK3f46GmwGK`=FLFlCgDl0GuGW-6ng9rZibIzT6&-%YJQ#oI5%_W52G?cwW z8gVMrm}58*&V|zAFlIlt;uajn44lS9oX1rd9%;-vjKu`Z#x>}}cx=QhY!Ci^5naYu zW`N3iei%nDPGJ--q6Ubh_l=l_x?hRO*noPk6H~DpQ*aP9k^89Uo}wl&ixDBle8qa& zzvwKN@r^gym>h0&qB`ioI2^{c_yAYqB*x%0YQXow_5yCA9m0=#-i3P3!=)5!PzyPa znouuloDpndd^15s130*7rB0+zQ-V&c#&m2!9yMoB6TF1#upf0KcQGHI;RgJHNf;fw z{G1DQ1m&os>_kf)^-)m=L#VTRgx7EeIR(?fL1yDoY{RSg8fWn&j?+msrm)Yg*p8ie z4|SvtwxtoPu!AGGjP11N64?JdDrGE_d*%>o#b>#2Oy)|kJ%BpX+sK&aA!_1Ns1?0K zP5durVjTOeE%u;}-~dKo4{DrV)PjZ**?$c*$qg23o}&hMiyH7FYKs?x?IiY7?IP6k z4fqDzum$UU{TSR_8GN;Z>R}r6rEudvM7^s-U8zI*59q3-v#68hOX`p;mMU z^}-9(%s&PFj(TnZm3*4umOk_$Iki%@Q>!GJf_EBF3))GP5gJ^j+P>BHCDs_bRX%9i?_TGS9v_C3Q%dA|rd=)pbgV?GD zMJcNER`C*SvAy?dlW=wdkeHSUqoKp&gEv)#it=4WX{iig+wA=U`_En$5Svw@=#a|o z+VX{Cu`@FdiwR|q&ZC5=Cqju#B14;$L&Xyqbwq~*rX!of{O*|4Kvzs$WW2Yy#9dtC lad|wIZf~L6, 2024. +# Tom Hubrecht , 2024-2025. # msgid "" msgstr "" "Project-Id-Version: dgsi.dgnum.eu\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-12 21:59+0200\n" -"PO-Revision-Date: 2024-10-12 22:04+0200\n" +"POT-Creation-Date: 2025-01-25 19:38+0100\n" +"PO-Revision-Date: 2025-01-25 19:40+0100\n" "Last-Translator: Tom Hubrecht \n" "Language-Team: French\n" "Language: fr\n" @@ -16,9 +16,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" -"X-Generator: Gtranslator 46.1\n" +"X-Generator: Gtranslator 47.1\n" -#: app/settings.py:321 +#: app/settings.py:329 msgid "Administration de DGSI" msgstr "DGSI Administration" @@ -73,56 +73,64 @@ msgstr "" msgid "De préférence l'adresse '@ens.psl.eu'" msgstr "Preferably the ‘@ens.psl.eu’ address" -#: dgsi/models.py:24 +#: dgsi/models.py:27 msgid "Nom du service proposé" msgstr "Name of the proposed service" -#: dgsi/models.py:25 +#: dgsi/models.py:28 msgid "Adresse du service" msgstr "Address of the service" -#: dgsi/models.py:26 +#: dgsi/models.py:29 msgid "Icône du service" msgstr "Icon of the service" -#: dgsi/models.py:36 +#: dgsi/models.py:39 dgsi/models.py:85 msgid "Date du document" msgstr "Document date" -#: dgsi/models.py:37 +#: dgsi/models.py:40 dgsi/models.py:86 msgid "Nom du document" msgstr "Document name" -#: dgsi/models.py:38 +#: dgsi/models.py:41 dgsi/models.py:87 msgid "Fichier PDF" msgstr "PDF file" -#: dgsi/models.py:60 dgsi/models.py:61 +#: dgsi/models.py:63 dgsi/models.py:64 dgsi/templates/dgsi/archive_list.html:22 #: dgsi/templates/dgsi/legal_documents.html:26 msgid "Statuts" msgstr "Statutes" -#: dgsi/models.py:73 dgsi/templates/dgsi/legal_documents.html:30 +#: dgsi/models.py:76 dgsi/templates/dgsi/legal_documents.html:30 msgid "Règlement Intérieur" msgstr "Bylaws" -#: dgsi/models.py:74 +#: dgsi/models.py:77 dgsi/templates/dgsi/archive_list.html:37 msgid "Règlements Intérieurs" msgstr "Bylaws" -#: dgsi/models.py:116 +#: dgsi/models.py:93 +msgid "Document d'archives" +msgstr "Archive document" + +#: dgsi/models.py:94 +msgid "Documents d'archives" +msgstr "Archive documents" + +#: dgsi/models.py:136 msgid "Correspondance de login" msgstr "Login mapping" -#: dgsi/models.py:117 +#: dgsi/models.py:137 msgid "Correspondances de login" msgstr "Login mappings" -#: dgsi/models.py:148 +#: dgsi/models.py:168 msgid "Derniers Statuts acceptés" msgstr "Latest accepted Statutes" -#: dgsi/models.py:155 +#: dgsi/models.py:175 msgid "Dernier Règlement Intérieur accepté" msgstr "Latest accepted Bylaws" @@ -137,6 +145,10 @@ msgstr "" msgid "Accepté" msgstr "Accepted" +#: dgsi/templates/dgsi/archive_list.html:6 +msgid "Archives de la DGNum" +msgstr "Archives of the DGNum" + #: dgsi/templates/dgsi/create_kanidm_account.html:6 msgid "Création de compte Kanidm" msgstr "Kanidm account creation" @@ -225,48 +237,52 @@ msgstr "Create a DGNum account" msgid "Services accessibles via la DGNum" msgstr "Services accessible via the DGNum" -#: dgsi/views.py:30 +#: dgsi/views.py:32 msgid "Mon profil" msgstr "My profile" -#: dgsi/views.py:31 +#: dgsi/views.py:33 msgid "Documents Légaux" msgstr "Legal Documents" -#: dgsi/views.py:32 +#: dgsi/views.py:34 msgid "Services proposés" msgstr "Services offered" -#: dgsi/views.py:39 +#: dgsi/views.py:35 +msgid "Archives" +msgstr "Archives" + +#: dgsi/views.py:42 msgid "Créer un compte Kanidm" msgstr "Create a Kanidm account" -#: dgsi/views.py:43 shared/templates/_hero.html:76 +#: dgsi/views.py:46 shared/templates/_hero.html:76 msgid "Interface d'administration" msgstr "Administration interface" -#: dgsi/views.py:80 +#: dgsi/views.py:83 msgid "Compte DGNum inexistant." msgstr "No existing DGNum account." -#: dgsi/views.py:97 +#: dgsi/views.py:100 msgid "Compte DGNum créé avec succès" msgstr "DGNum account successfully created" -#: dgsi/views.py:113 +#: dgsi/views.py:116 msgid "Vous possédez déjà un compte DGNum !" msgstr "You already have a DGNum account!" -#: dgsi/views.py:125 +#: dgsi/views.py:128 msgid "Vous devez accepter les Statuts et le Règlement Intérieur." msgstr "You must accept the Statutes and the Bylaws." -#: dgsi/views.py:204 +#: dgsi/views.py:241 #, python-format msgid "Type de document invalide : %(kind)s" msgstr "Invalid document type: %(kind)s" -#: dgsi/views.py:234 +#: dgsi/views.py:271 #, python-format msgid "Compte DGNum pour %(displayname)s [%(name)s] créé." msgstr "DGNum account for %(displayname)s [%(name)s] created." From 73acb2c2a48a3968bce639d706d97bc8906a46a0 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 25 Jan 2025 20:25:38 +0100 Subject: [PATCH 111/172] fix(dgsi/archives): Set the Content-Type header so that django's default does not apply --- src/dgsi/views.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/dgsi/views.py b/src/dgsi/views.py index d3d1a0a..859bd1b 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -1,3 +1,4 @@ +from mimetypes import guess_type from typing import Any, NamedTuple from asgiref.sync import async_to_sync @@ -193,9 +194,21 @@ class ProtectedArchiveView(LoginRequiredMixin, View): if settings.DEBUG: return HttpResponseRedirect(redirect_to=archive.file.url) + content_type, encoding = guess_type(archive.file.name) + + if encoding is not None: + content_type = { + "br": "application/x-brotli", + "bzip2": "application/x-bzip", + "compress": "application/x-compress", + "gzip": "application/gzip", + "xz": "application/x-xz", + }.get(encoding, content_type) + return HttpResponse( headers={ - "Content-Disposition": f"attachment; filename={archive.file.name}", + "Content-Type": content_type, + "Content-Disposition": f"inline; filename={archive.file.name}", "X-Accel-Redirect": f"/{settings.ARCHIVES_INTERNAL}/{archive.file.name}", } ) From 5937f2a0d078b2823475866d9e1939f239a05d30 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 26 Jan 2025 00:49:10 +0100 Subject: [PATCH 112/172] fix(mobile): Make it readable --- src/dgsi/templates/_legal_document.html | 2 +- src/dgsi/templates/dgsi/archive_list.html | 17 ++++++----------- src/shared/static/bulma/bulma.scss | 13 +++++++++++++ src/shared/templates/base.html | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/dgsi/templates/_legal_document.html b/src/dgsi/templates/_legal_document.html index 8316bd5..a69159e 100644 --- a/src/dgsi/templates/_legal_document.html +++ b/src/dgsi/templates/_legal_document.html @@ -21,6 +21,6 @@ - {{ document }} + {{ document }} diff --git a/src/dgsi/templates/dgsi/archive_list.html b/src/dgsi/templates/dgsi/archive_list.html index 2badaf4..2b6bcb2 100644 --- a/src/dgsi/templates/dgsi/archive_list.html +++ b/src/dgsi/templates/dgsi/archive_list.html @@ -7,13 +7,12 @@
{% for file in archive_list %} - - - {{ file }} + {{ file }} {{ file.date }} {% endfor %} @@ -22,13 +21,11 @@

{% trans "Statuts" %}

{% for document in statutes_list %} - + - - {{ document }} + {{ document }} {{ document.date }} {% endfor %} @@ -37,13 +34,11 @@

{% trans "Règlements Intérieurs" %}

{% for document in bylaws_list %} - + - - {{ document }} + {{ document }} {{ document.date }} {% endfor %} diff --git a/src/shared/static/bulma/bulma.scss b/src/shared/static/bulma/bulma.scss index e358754..8f95fe5 100644 --- a/src/shared/static/bulma/bulma.scss +++ b/src/shared/static/bulma/bulma.scss @@ -20,6 +20,19 @@ body { justify-content: space-between; } +.ellipsis { + overflow-x: hidden; + text-overflow: ellipsis; + display: block; +} + +.bt-archive { + display: flex; + width: 100%; + margin-bottom: calc(0.5 * var(--bulma-block-spacing)); + justify-content: space-between; +} + .bt-link { display: flex; width: 100%; diff --git a/src/shared/templates/base.html b/src/shared/templates/base.html index 8aed9cb..005a7be 100644 --- a/src/shared/templates/base.html +++ b/src/shared/templates/base.html @@ -16,7 +16,7 @@ {% include "_hero.html" %} -
+
{% for message in messages %}
From 57fb348bdb4f3320bcd524920e004cce4074060b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 26 Jan 2025 00:49:32 +0100 Subject: [PATCH 113/172] chore(icons): Put all the icons before the text --- src/dgsi/templates/dgsi/create_kanidm_account.html | 2 +- src/dgsi/templates/dgsi/create_self_account.html | 2 +- src/shared/templates/_hero.html | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dgsi/templates/dgsi/create_kanidm_account.html b/src/dgsi/templates/dgsi/create_kanidm_account.html index 62ef674..178e2cf 100644 --- a/src/dgsi/templates/dgsi/create_kanidm_account.html +++ b/src/dgsi/templates/dgsi/create_kanidm_account.html @@ -11,8 +11,8 @@ {% include "bulma/form.html" with form=form %} {% endblock content %} diff --git a/src/dgsi/templates/dgsi/create_self_account.html b/src/dgsi/templates/dgsi/create_self_account.html index 5b2fc62..7ebe3bd 100644 --- a/src/dgsi/templates/dgsi/create_self_account.html +++ b/src/dgsi/templates/dgsi/create_self_account.html @@ -11,8 +11,8 @@ {% include "bulma/form.html" with form=form %} {% endblock content %} diff --git a/src/shared/templates/_hero.html b/src/shared/templates/_hero.html index 858b1d5..9efcf35 100644 --- a/src/shared/templates/_hero.html +++ b/src/shared/templates/_hero.html @@ -15,19 +15,19 @@ - {% trans "Déconnexion" %} + {% trans "Déconnexion" %} {% else %} - {% trans "Connexion" %} + {% trans "Connexion" %} {% endif %} From 2b08dfa40ec26a3f6f358526c4b93065159b6ddf Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 27 Jan 2025 21:30:13 +0100 Subject: [PATCH 114/172] feat(index): Add outline link --- src/dgsi/templates/_index_link.html | 3 +- src/dgsi/views.py | 8 +++ src/shared/locale/en/LC_MESSAGES/django.mo | Bin 7504 -> 7563 bytes src/shared/locale/en/LC_MESSAGES/django.po | 70 +++++++++++---------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/dgsi/templates/_index_link.html b/src/dgsi/templates/_index_link.html index c89b0cf..85142be 100644 --- a/src/dgsi/templates/_index_link.html +++ b/src/dgsi/templates/_index_link.html @@ -1,4 +1,5 @@ - + {% if link.icon %}{% endif %} {{ link.text }} diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 859bd1b..73cc253 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -27,10 +27,18 @@ class Link(NamedTuple): reverse: str text: str | Promise icon: str | None = None + absolute: bool = False AUTHENTICATED_LINKS: list[Link] = [ Link("is-primary", "dgsi:dgn-profile", _("Mon profil"), "user-filled"), + Link( + "is-success", + "https://docs.dgnum.eu/s/doc-publique", + _("Aide et Documention"), + "help", + True, + ), Link("is-primary", "dgsi:dgn-legal_documents", _("Documents Légaux"), "script"), Link("is-info", "dgsi:dgn-services", _("Services proposés"), "apps-filled"), Link("is-success", "dgsi:dgn-archives", _("Archives"), "archive"), diff --git a/src/shared/locale/en/LC_MESSAGES/django.mo b/src/shared/locale/en/LC_MESSAGES/django.mo index d3248fc23eddf58adbf821b0f40e12c4eae5fd7c..96170c23b9231ff7cf267996919b30609b149470 100644 GIT binary patch delta 1782 zcmY+^OGs2v9LMo9Hlt?wnl#HYC(B;iLvykmO)H%=r=%oWWPvq9R5nIvtEn3aK^9SH zv?z*MRU3&0LTe#K5E4aExahfP6SWFTi0JYCUGFS9%zr-T+&=rC{D*d|6(!D z$ebL&YScjMQTJ;@rS1SO#j|M5rjnqNkIzsYKcQ0c3zhR?5~&+)L)~C6DpjYj9|w@^ znR#?th>LIsw&E+ij)(9V8=)58<8~~}W&Jy-bg>@F@fZg25AG%+km8AUn9umNvlQ1Aa2 zD!Oq6?^HFmBYV#-jLrTAC^cGU6)nH^OtREcX(F_5*gp0?(ftBMh}cM|Y$Y^79%oN* z17@z}Z#l7=NEROz&76VTGvl?kzkEStAK7KuK-^O>aL41el!R4;l2=dY%b@wIG!pu9 zY#|y5twSB5q9?B-v?f}&WLZo_3Dg>|AhcV^{eMLX-$by}4w*74`mX7HP-eAzRJ3dS z#5SUtP$5?)g;=8orGi*VXx*m}r9>5>geOa-Q>jVBQ?p&(gW-rT(&KCF-rsv9674zI r9Zd|SJ;_L`_ElEYRIW?JyuVyCnma!wruF8jm*41r_<)y`JUHv&Yts}=leX*IhRXcmN*|1d>uxq zB^DA_!p#ojA0H=5_jI!w?7@5-$5c#@H1lH>Mqwk)#U@O^(>NR3Fb;2`+V!HIyN657 z9D7DZ4~$_7`e>De=~#&CaRweoJ=lti@S1nO59e_`hEL`>!y-`|Sf2b(Bai8etad#pSpK8<4}gI4Sc1WK8xFHL&-%1b-l1S?sLIfi6Jx zmy24mO3cP4oR4kj%%RdlMGYS!Yhoj)wVps_oWnw@gA&wIHR2^ahOCpavN2a&PZ9@M}OqS~E6C3+T>;0;tFgMQ{ejmk4_Xv%+L0Apw!fkD)Zt56y4MRj-v znH;-_dSM4@0Jl&x`M|p#_O3@!?ZavG2IFu&es!oQ!wjCHYIUexd;qme9aJV)Jv&hY z>O;OndyM*0pQ18)jvC-+)EbYXX2?gUx}Stv%2ZT)r;>_ZybC$(2&Zj$0X5\n" "Language-Team: French\n" "Language: fr\n" @@ -73,64 +73,64 @@ msgstr "" msgid "De préférence l'adresse '@ens.psl.eu'" msgstr "Preferably the ‘@ens.psl.eu’ address" -#: dgsi/models.py:27 +#: dgsi/models.py:25 msgid "Nom du service proposé" msgstr "Name of the proposed service" -#: dgsi/models.py:28 +#: dgsi/models.py:26 msgid "Adresse du service" msgstr "Address of the service" -#: dgsi/models.py:29 +#: dgsi/models.py:27 msgid "Icône du service" msgstr "Icon of the service" -#: dgsi/models.py:39 dgsi/models.py:85 +#: dgsi/models.py:37 dgsi/models.py:83 msgid "Date du document" msgstr "Document date" -#: dgsi/models.py:40 dgsi/models.py:86 +#: dgsi/models.py:38 dgsi/models.py:84 msgid "Nom du document" msgstr "Document name" -#: dgsi/models.py:41 dgsi/models.py:87 +#: dgsi/models.py:39 dgsi/models.py:85 msgid "Fichier PDF" msgstr "PDF file" -#: dgsi/models.py:63 dgsi/models.py:64 dgsi/templates/dgsi/archive_list.html:22 +#: dgsi/models.py:61 dgsi/models.py:62 dgsi/templates/dgsi/archive_list.html:21 #: dgsi/templates/dgsi/legal_documents.html:26 msgid "Statuts" msgstr "Statutes" -#: dgsi/models.py:76 dgsi/templates/dgsi/legal_documents.html:30 +#: dgsi/models.py:74 dgsi/templates/dgsi/legal_documents.html:30 msgid "Règlement Intérieur" msgstr "Bylaws" -#: dgsi/models.py:77 dgsi/templates/dgsi/archive_list.html:37 +#: dgsi/models.py:75 dgsi/templates/dgsi/archive_list.html:34 msgid "Règlements Intérieurs" msgstr "Bylaws" -#: dgsi/models.py:93 +#: dgsi/models.py:91 msgid "Document d'archives" msgstr "Archive document" -#: dgsi/models.py:94 +#: dgsi/models.py:92 msgid "Documents d'archives" msgstr "Archive documents" -#: dgsi/models.py:136 +#: dgsi/models.py:134 msgid "Correspondance de login" msgstr "Login mapping" -#: dgsi/models.py:137 +#: dgsi/models.py:135 msgid "Correspondances de login" msgstr "Login mappings" -#: dgsi/models.py:168 +#: dgsi/models.py:166 msgid "Derniers Statuts acceptés" msgstr "Latest accepted Statutes" -#: dgsi/models.py:175 +#: dgsi/models.py:173 msgid "Dernier Règlement Intérieur accepté" msgstr "Latest accepted Bylaws" @@ -153,8 +153,8 @@ msgstr "Archives of the DGNum" msgid "Création de compte Kanidm" msgstr "Kanidm account creation" -#: dgsi/templates/dgsi/create_kanidm_account.html:14 -#: dgsi/templates/dgsi/create_self_account.html:14 +#: dgsi/templates/dgsi/create_kanidm_account.html:15 +#: dgsi/templates/dgsi/create_self_account.html:15 msgid "Enregistrer" msgstr "Save" @@ -237,52 +237,56 @@ msgstr "Create a DGNum account" msgid "Services accessibles via la DGNum" msgstr "Services accessible via the DGNum" -#: dgsi/views.py:32 +#: dgsi/views.py:34 msgid "Mon profil" msgstr "My profile" -#: dgsi/views.py:33 +#: dgsi/views.py:38 +msgid "Aide et Documention" +msgstr "Help and documentation" + +#: dgsi/views.py:42 msgid "Documents Légaux" msgstr "Legal Documents" -#: dgsi/views.py:34 +#: dgsi/views.py:43 msgid "Services proposés" msgstr "Services offered" -#: dgsi/views.py:35 +#: dgsi/views.py:44 msgid "Archives" msgstr "Archives" -#: dgsi/views.py:42 +#: dgsi/views.py:51 msgid "Créer un compte Kanidm" msgstr "Create a Kanidm account" -#: dgsi/views.py:46 shared/templates/_hero.html:76 +#: dgsi/views.py:55 shared/templates/_hero.html:76 msgid "Interface d'administration" msgstr "Administration interface" -#: dgsi/views.py:83 +#: dgsi/views.py:92 msgid "Compte DGNum inexistant." msgstr "No existing DGNum account." -#: dgsi/views.py:100 +#: dgsi/views.py:109 msgid "Compte DGNum créé avec succès" msgstr "DGNum account successfully created" -#: dgsi/views.py:116 +#: dgsi/views.py:125 msgid "Vous possédez déjà un compte DGNum !" msgstr "You already have a DGNum account!" -#: dgsi/views.py:128 +#: dgsi/views.py:137 msgid "Vous devez accepter les Statuts et le Règlement Intérieur." msgstr "You must accept the Statutes and the Bylaws." -#: dgsi/views.py:241 +#: dgsi/views.py:262 #, python-format msgid "Type de document invalide : %(kind)s" msgstr "Invalid document type: %(kind)s" -#: dgsi/views.py:271 +#: dgsi/views.py:292 #, python-format msgid "Compte DGNum pour %(displayname)s [%(name)s] créé." msgstr "DGNum account for %(displayname)s [%(name)s] created." @@ -301,11 +305,11 @@ msgid "" msgstr "" "Software developed for and by the DGNum." -#: shared/templates/_hero.html:18 shared/templates/account/logout.html:6 +#: shared/templates/_hero.html:21 shared/templates/account/logout.html:6 msgid "Déconnexion" msgstr "Logout" -#: shared/templates/_hero.html:27 shared/templates/socialaccount/login.html:6 +#: shared/templates/_hero.html:30 shared/templates/socialaccount/login.html:6 msgid "Connexion" msgstr "Login" From 0e22a71df6e47ecf781ac7ef8c419215151ab703 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 28 Jan 2025 08:54:48 +0100 Subject: [PATCH 115/172] feat(account): Distinguish between staff and superuser --- src/app/settings.py | 3 +++ src/dgsi/models.py | 10 +++++----- src/shared/account.py | 5 +++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index 6a6c6a8..25a7a5e 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -235,6 +235,9 @@ ACCOUNT_AUTHENTICATION_METHOD = "username" AUTH_PASSWORD_VALIDATORS = [] AUTH_USER_MODEL = "dgsi.User" +DGSI_STAFF_GROUP = credentials.get("STAFF_GROUP", "dgnum_admins@sso.dgnum.eu") +DGSI_SUPERUSER_GROUP = credentials.get("SUPERUSER_GROUP", "dgnum_admins@sso.dgnum.eu") + ### # Internationalization configuration diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 2314b3a..94a5173 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -6,6 +6,7 @@ from typing import Optional, Self from aiohttp.client_exceptions import ClientConnectorError from allauth.socialaccount.models import SocialAccount from asgiref.sync import async_to_sync +from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.files import storage from django.db import models @@ -18,8 +19,6 @@ from kanidm.models.person import Person from shared.kanidm import klient -ADMIN_GROUP = "dgnum_admins@sso.dgnum.eu" - class Service(models.Model): name = models.CharField(_("Nom du service proposé"), max_length=255) @@ -200,9 +199,10 @@ class User(AbstractUser): @property def is_admin(self) -> bool: - return (self.kanidm is not None) and ( - ADMIN_GROUP in self.kanidm.person.memberof - ) + return self.part_of(settings.DGSI_STAFF_GROUP) + + def part_of(self, group: str) -> bool: + return (self.kanidm is not None) and group in self.kanidm.person.memberof def can_access_archive(self, archive: Archive) -> bool: # Prepare a more complex workflow diff --git a/src/shared/account.py b/src/shared/account.py index 6d4eef6..956fe5b 100644 --- a/src/shared/account.py +++ b/src/shared/account.py @@ -5,6 +5,7 @@ from typing import Optional from allauth.core.exceptions import ImmediateHttpResponse from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.models import SocialLogin +from django.conf import settings from django.contrib import messages from django.http import HttpRequest, HttpResponseRedirect from django.urls import reverse @@ -91,8 +92,8 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter): u.username = self._get_username(request, sociallogin) # Update the global permissions - u.is_staff = u.is_admin - u.is_superuser = u.is_admin + u.is_superuser = u.part_of(settings.DGSI_SUPERUSER_GROUP) + u.is_staff = u.is_superuser or u.part_of(settings.DGSI_STAFF_GROUP) # Save the updated user if needed if sociallogin.is_existing: From 21fe6c01a8eec76d220d12802ee699de03e4efaa Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 28 Jan 2025 18:10:42 +0100 Subject: [PATCH 116/172] chore(dgsi): Add a legal notice --- src/dgsi/templates/dgsi/mentions-legales.html | 43 +++++++++++++++++++ src/dgsi/urls.py | 6 +++ src/shared/templates/_footer.html | 2 + 3 files changed, 51 insertions(+) create mode 100644 src/dgsi/templates/dgsi/mentions-legales.html diff --git a/src/dgsi/templates/dgsi/mentions-legales.html b/src/dgsi/templates/dgsi/mentions-legales.html new file mode 100644 index 0000000..8bc7695 --- /dev/null +++ b/src/dgsi/templates/dgsi/mentions-legales.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

Mentions Légales

+
+ +
+

Éditeur

+

Ce site web est édité par la Délégation Générale Numérique.

+

+ Délégation Générale Numérique (DGNum) +
+ Association de loi 1901 +

+

+ Siège social : +
+ 45 rue d'Ulm, 75005 Paris - France +

+

Directeur de publication : Jean-Marc Gailis

+

Contact : contact[at]dgnum.eu

+ +
+

Hébergeur

+

Ce site web est hébergé par la Délégation Générale Numérique.

+

+ Délégation Générale Numérique (DGNum) +
+ Association de loi 1901 +

+

+ Siège social : +
+ 45 rue d'Ulm, 75005 Paris - France +

+

Directeur de publication : Jean-Marc Gailis

+

Contact : contact[at]dgnum.eu

+ +
+ +{% endblock content %} diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index d3d546e..d2392f4 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -1,4 +1,5 @@ from django.urls import path +from django.views.generic import TemplateView from . import views @@ -7,6 +8,11 @@ app_name = "dgsi" urlpatterns = [ # Misc views path("", views.IndexView.as_view(), name="dgn-index"), + path( + "mentions-legales", + TemplateView.as_view(template_name="dgsi/mentions-legales.html"), + name="dgn-mentions-legales", + ), # Archives path("archives/", views.ArchiveListView.as_view(), name="dgn-archives"), path( diff --git a/src/shared/templates/_footer.html b/src/shared/templates/_footer.html index 63052d4..58e27ea 100644 --- a/src/shared/templates/_footer.html +++ b/src/shared/templates/_footer.html @@ -2,5 +2,7 @@
{% blocktrans %}Logiciel développé pour et par la DGNum.{% endblocktrans %} +
+ Mentions Légales {% django_browser_reload_script %}
From 5940aaa2846b07a1293e972267b056255f96ee4e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 30 Jan 2025 09:19:46 +0100 Subject: [PATCH 117/172] fix(models/archive): Don't serialize local dev info in the migration --- src/dgsi/migrations/0009_archive.py | 8 +++----- src/dgsi/models.py | 6 +++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/dgsi/migrations/0009_archive.py b/src/dgsi/migrations/0009_archive.py index efb7aab..8127218 100644 --- a/src/dgsi/migrations/0009_archive.py +++ b/src/dgsi/migrations/0009_archive.py @@ -1,8 +1,9 @@ # Generated by Django 4.2.16 on 2025-01-25 15:27 -import django.core.files.storage from django.db import migrations, models +import dgsi.models + class Migration(migrations.Migration): @@ -31,10 +32,7 @@ class Migration(migrations.Migration): ( "file", models.FileField( - storage=django.core.files.storage.FileSystemStorage( - base_url="/archives/", - location="/home/thubrecht/Software/git.dgnum.eu/DGNum/dgsi/.archives", - ), + storage=dgsi.models.get_storage, upload_to="", verbose_name="Fichier PDF", ), diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 94a5173..54c2b49 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -74,6 +74,10 @@ class Bylaws(LegalDocument): verbose_name_plural = _("Règlements Intérieurs") +def get_storage(*args, **kwargs): + return storage.storages["archives"] + + class Archive(models.Model): """ Archived documents for the association. @@ -81,7 +85,7 @@ class Archive(models.Model): date = models.DateField(_("Date du document")) name = models.CharField(_("Nom du document"), max_length=255) - file = models.FileField(_("Fichier PDF"), storage=storage.storages["archives"]) + file = models.FileField(_("Fichier PDF"), storage=get_storage) def __str__(self) -> str: return self.name From 6860d6cb3ba36812f5a6c5119f98780943a095d1 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 30 Jan 2025 09:46:58 +0100 Subject: [PATCH 118/172] feat(shared/kanidm): Add helper function sync_call This avoids wrapping the methods each time we want to call the API in sync mode --- src/shared/kanidm.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/kanidm.py b/src/shared/kanidm.py index 3102da4..3a09e27 100644 --- a/src/shared/kanidm.py +++ b/src/shared/kanidm.py @@ -1,3 +1,4 @@ +from asgiref.sync import async_to_sync from kanidm import KanidmClient from loadcredential import Credentials @@ -6,3 +7,10 @@ credentials = Credentials(env_prefix="DGSI_") klient = KanidmClient( uri=credentials["KANIDM_URI"], token=credentials["KANIDM_AUTH_TOKEN"] ) + + +def sync_call(name, *args, **kwargs): + """ + Wraps the required action for use in sync contexts + """ + return async_to_sync(getattr(klient, name))(*args, **kwargs) From d3d342879c1e1bd3ad6bc29aad00d6a8477a13e6 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 30 Jan 2025 10:03:48 +0100 Subject: [PATCH 119/172] feat(dgsi/user): Add VLAN information and machinery Only via the shell for now, we can attribute a VLAN to a user and reclaim it if needed --- src/app/settings.py | 3 + src/dgsi/admin.py | 6 ++ ...lter_user_options_user_vlan_id_and_more.py | 30 ++++++ src/dgsi/models.py | 98 ++++++++++++++++++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/dgsi/migrations/0010_alter_user_options_user_vlan_id_and_more.py diff --git a/src/app/settings.py b/src/app/settings.py index 25a7a5e..31c6a11 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -238,6 +238,9 @@ AUTH_USER_MODEL = "dgsi.User" DGSI_STAFF_GROUP = credentials.get("STAFF_GROUP", "dgnum_admins@sso.dgnum.eu") DGSI_SUPERUSER_GROUP = credentials.get("SUPERUSER_GROUP", "dgnum_admins@sso.dgnum.eu") +VLAN_ID_MAX = 4094 +VLAN_ID_MIN = (VLAN_ID_MAX - 850) + 1 + ### # Internationalization configuration diff --git a/src/dgsi/admin.py b/src/dgsi/admin.py index 8e374a7..6347f0c 100644 --- a/src/dgsi/admin.py +++ b/src/dgsi/admin.py @@ -44,9 +44,15 @@ class UserAdmin(DjangoUserAdmin, ImportExportMixin, ModelAdmin): export_form_class = ExportForm export_form_class = SelectableFieldsExportForm + readonly_fields = ("vlan_id",) + # Add the local fields fieldsets = ( *DjangoUserAdmin.fieldsets, + ( + _("Informations réseau"), + {"fields": ("vlan_id",)}, + ), ( _("Documents DGNum"), {"fields": ("accepted_statutes", "accepted_bylaws")}, diff --git a/src/dgsi/migrations/0010_alter_user_options_user_vlan_id_and_more.py b/src/dgsi/migrations/0010_alter_user_options_user_vlan_id_and_more.py new file mode 100644 index 0000000..4f55801 --- /dev/null +++ b/src/dgsi/migrations/0010_alter_user_options_user_vlan_id_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.16 on 2025-01-30 08:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dgsi", "0009_archive"), + ] + + operations = [ + migrations.AlterModelOptions( + name="user", + options={}, + ), + migrations.AddField( + model_name="user", + name="vlan_id", + field=models.PositiveSmallIntegerField( + null=True, verbose_name="VLAN associé au compte" + ), + ), + migrations.AddConstraint( + model_name="user", + constraint=models.UniqueConstraint( + fields=("vlan_id",), name="unique_vlan_attribution" + ), + ), + ] diff --git a/src/dgsi/models.py b/src/dgsi/models.py index 54c2b49..69372c9 100644 --- a/src/dgsi/models.py +++ b/src/dgsi/models.py @@ -9,7 +9,7 @@ from asgiref.sync import async_to_sync from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.files import storage -from django.db import models +from django.db import models, transaction from django.db.models.signals import pre_delete from django.dispatch import receiver from django.http import HttpRequest @@ -17,7 +17,9 @@ from django.utils.translation import gettext_lazy as _ from kanidm.exceptions import NoMatchingEntries from kanidm.models.person import Person -from shared.kanidm import klient +from shared.kanidm import klient, sync_call + +logger = logging.getLogger(__name__) class Service(models.Model): @@ -177,6 +179,8 @@ class User(AbstractUser): ) # accepted_terms = models.ManyToManyField(TermsAndConditions) + vlan_id = models.PositiveSmallIntegerField(_("VLAN associé au compte"), null=True) + @classmethod def from_request(cls, request: HttpRequest) -> Self: u = request.user @@ -211,3 +215,93 @@ class User(AbstractUser): def can_access_archive(self, archive: Archive) -> bool: # Prepare a more complex workflow return True + + ### + # VLAN attribution machinery + # + # NOTE: It is a bit cumbersome because we need to store the vlan_id + # information both in DG·SI and in Kanidm + # Now the question will be « Which is the source of truth ? » + # For now, I believe it has to be DG·SI, so a sync script will + # have to be run regularly. + + @transaction.atomic + def set_unique_vlan_id(self): + if self.vlan_id is not None: + raise ValueError(_("Ce compte a déjà un VLAN associé")) + + self.vlan_id = min( + set(range(settings.VLAN_ID_MIN, settings.VLAN_ID_MAX)) + - set(User.objects.exclude(vlan_id__isnull=True).values_list("vlan_id")) + ) + + # Preempt the vlan attribution + self.save(update_fields=["vlan_id"]) + + @transaction.atomic + def register_unique_vlan(self) -> None: + self.set_unique_vlan_id() + + group_name = f"vlan_{self.vlan_id}" + + res = sync_call("group_create", group_name) + + if res.data is not None: + if ( + res.data.get("plugin", {}).get("attrunique") + == "duplicate value detected" + ): + logger.info(f"The {group_name} group already exists") + group = sync_call("group_get", group_name) + + if group.member != []: + raise ValueError( + _("Le VLAN {} est déjà attribué.").format(self.vlan_id) + ) + + # The group is created and should be empty, so we can add the user to it + sync_call("group_add_members", group_name, [self.username]) + + # Check that we succeeded in setting a VLAN that is unique to the current user + group = sync_call("group_get", group_name) + + if group.member != [f"{self.username}@sso.dgnum.eu"]: + # Remove the user from the group + sync_call("group_delete_members", self.username) + self.vlan_id = None + self.save(update_fields=["vlan_id"]) + raise RuntimeError("Duplicate VLAN attribution detected") + + def reclaim_vlan(self): + if self.vlan_id is None: + # Nothing to do, just return + logger.warning( + f"Reclaiming VLAN for {self.username} who does not have one." + ) + return + + group_name = f"vlan_{self.vlan_id}" + + sync_call("group_delete_members", group_name, [self.username]) + + # Check that the call succeeded + try: + group = sync_call("group_get", group_name) + + if "{self.username}@sso.dgnum.eu" in group.member: + raise RuntimeError( + f"Something went wrong in trying to reclaim vlan {self.vlan_id}" + ) + except ValueError: + # The group does not exist apparently, keep going + logger.warning( + f"Reclaiming VLAN {self.vlan_id}, but the associated group does not exist." + ) + finally: + self.vlan_id = None + self.save(update_fields=["vlan_id"]) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["vlan_id"], name="unique_vlan_attribution") + ] From ad244a26f5d0e35b58bbc0747950c5570164b8de Mon Sep 17 00:00:00 2001 From: sinavir Date: Tue, 28 Jan 2025 21:57:07 +0100 Subject: [PATCH 120/172] fix(profile): Emphasize username --- src/dgsi/templates/dgsi/profile.html | 6 +- src/shared/locale/en/LC_MESSAGES/django.mo | Bin 7563 -> 7610 bytes src/shared/locale/en/LC_MESSAGES/django.po | 62 +++++++++++---------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/dgsi/templates/dgsi/profile.html b/src/dgsi/templates/dgsi/profile.html index 7053901..8de06c7 100644 --- a/src/dgsi/templates/dgsi/profile.html +++ b/src/dgsi/templates/dgsi/profile.html @@ -5,10 +5,14 @@ {% block content %}

{% blocktrans %}Profil de {{ displayname }}{% endblocktrans %} - {{ user.username }}


+

{% trans "Nom d'utilisateur :" %}

+ {{ user.username }} +
+ + {% if user.kanidm %}

{% trans "Mot de passe WiFi :" %} diff --git a/src/shared/locale/en/LC_MESSAGES/django.mo b/src/shared/locale/en/LC_MESSAGES/django.mo index 96170c23b9231ff7cf267996919b30609b149470..ab77488a0fc85dcd87736015bfae60fa1a298de7 100644 GIT binary patch delta 1541 zcmXZcOGs2v9LMo9H8YkuzA|kr8y}ShnrIEFQ{&j=GfN99L!m?@G))?nh@uI@MY<@M za^=DzD##L(=m8Z3hC$Gxuv%n`%2gRrt?T<6?`6*YoOA9u=l;**&am%^FFcoQziPM| zNoz@?ON=?Lyn`FpjMJEUe1)4aHO81Mti%*NhRg6Qrs5zb<0!`C1SaA$T#0W`?Y|)X z!{$4g94c(os^bRC#1dSMhjA;0a5>&Wy?7Ec@TFD%71Jm$p!zv!yaqj(jy_a?2T|?1 zF_HJ1K{5`TF(X*PgV9)Ha&ZnfqI1RK00pQS)L;U(U=ntr0y>9r7(sq!jGF=&x5_iP zj`BNHzu&6o{l-BrFD9V|szycFf{MHkYw;5DGatEW=JQC5#_n1SECD@~bC9u3(5i1h z1>A-jw->dA!x-K~<}MjGzQ!8#sgyk=-;qX|x{e za6k6oTYQ0?IKqM0j(PRjQG5LZt1z3x)QCW(;|^nXu}gquS4=u>P9yXDT$)zo;3xsm#WF)D|?L7dufO#uzGq zo2Z#SLJjx{Sv)h3dhs7rAWOMvMblB`Vyj#mCZi6mIEy{#$0}NDrh`b8xraK{)2PpB z4mG2nmj6%zCD4d3)nuU^^H38iKm`~;ZFMbb<-%>&gHxzIJ&)>e9rfY|$j`jywi~~p z0xn_{9kz1RAv}VbaVKiUPNNRt73=vJs^1;d|IA|~=CGM1qnR$CI;L^c42msFk%lIK zI;>5oLpFd4cnGy3<9Gp|qGnpbx2FK>uo^q@Hr~euY$l3)vyF(z1gQ9cI&^NnEe(7W zd29xdGZ?*yG5ViS9?5WcywM{{4TmOrD%7i#w1*TV>Do)ubTns8V`t=~qtI3zx#rkX z6rijRxrU^xlC*=Qb*>;al5`k!m5|CJ4rigOE?Qn(?Ol-yr^i)KjhCcp)keCVL7Q)C p(iw5a=l1t?banK$_O-RzV68J-)x|T;|-*Ip>~p?*BaQOov87_F{(jw&Cg| z<&(zN7;_GjecZSvEn^}$g}cyNXUrB1Vixvb0`}tu9KZ}5#T0yiY4`*)aR$|X9_eqJ zPh_@I@dMQ{o2Q$x6gOcTR^tgw#5<@Lk7EHokJo>|jg%Kr{Z=p!Q|Xk8J5T|JQSFXm z8t*p)WPBcDu3-}oMw5)$j&HF9S5N~KtY4i$Ev8bAU^@1p0y>Y$IE4JnO>PR{Uc5Ys zg_K{R`YoxR_nT!hLG;jD1J$4+jG$h865G&5erArFX1;*LW`3fU_8$f?)4v)(6)MmM z)Oek!tviincpdE=GBGmQ_y*N*3AH7^QF|U>BQ;P5YJg*?t-6fE=pegi3TdHSvv`q%uopjL7v`q3{(WTnSr6^;9ER~P_Oc}{yrqZoIMXY|U&td<#4>5d)yOuR z)_A!Kwa2HBm`oITw;74o-$k{5hMMq;EY@E$eNTmEw2VcVz*G0oV32Yc^+7~Y0bE7R zbPP4%6tY<6CF;fRP=S0wt>{X;oI<6_#i;gmHkk!7?O2aRJk?B(BUNS?b(+UfpV2gG zMz7<3L}wPHt*^K34l+DB3UE5k@^<{@f=Ge~>ed?lmE*HV&6sD|mNQ(A^P zT-~TZdr>Pfh(mY-HNyGcAomGDjTTNhpX^( z?I-EOK1gaLDa<;Ou3FM=C)=vfBD$AWx!LKm0)FjBkkmnHbq1|w&z{(<\n" "Language-Team: French\n" @@ -18,7 +18,7 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1)\n" "X-Generator: Gtranslator 47.1\n" -#: app/settings.py:329 +#: app/settings.py:332 msgid "Administration de DGSI" msgstr "DGSI Administration" @@ -73,64 +73,64 @@ msgstr "" msgid "De préférence l'adresse '@ens.psl.eu'" msgstr "Preferably the ‘@ens.psl.eu’ address" -#: dgsi/models.py:25 +#: dgsi/models.py:24 msgid "Nom du service proposé" msgstr "Name of the proposed service" -#: dgsi/models.py:26 +#: dgsi/models.py:25 msgid "Adresse du service" msgstr "Address of the service" -#: dgsi/models.py:27 +#: dgsi/models.py:26 msgid "Icône du service" msgstr "Icon of the service" -#: dgsi/models.py:37 dgsi/models.py:83 +#: dgsi/models.py:36 dgsi/models.py:82 msgid "Date du document" msgstr "Document date" -#: dgsi/models.py:38 dgsi/models.py:84 +#: dgsi/models.py:37 dgsi/models.py:83 msgid "Nom du document" msgstr "Document name" -#: dgsi/models.py:39 dgsi/models.py:85 +#: dgsi/models.py:38 dgsi/models.py:84 msgid "Fichier PDF" msgstr "PDF file" -#: dgsi/models.py:61 dgsi/models.py:62 dgsi/templates/dgsi/archive_list.html:21 +#: dgsi/models.py:60 dgsi/models.py:61 dgsi/templates/dgsi/archive_list.html:21 #: dgsi/templates/dgsi/legal_documents.html:26 msgid "Statuts" msgstr "Statutes" -#: dgsi/models.py:74 dgsi/templates/dgsi/legal_documents.html:30 +#: dgsi/models.py:73 dgsi/templates/dgsi/legal_documents.html:30 msgid "Règlement Intérieur" msgstr "Bylaws" -#: dgsi/models.py:75 dgsi/templates/dgsi/archive_list.html:34 +#: dgsi/models.py:74 dgsi/templates/dgsi/archive_list.html:34 msgid "Règlements Intérieurs" msgstr "Bylaws" -#: dgsi/models.py:91 +#: dgsi/models.py:90 msgid "Document d'archives" msgstr "Archive document" -#: dgsi/models.py:92 +#: dgsi/models.py:91 msgid "Documents d'archives" msgstr "Archive documents" -#: dgsi/models.py:134 +#: dgsi/models.py:133 msgid "Correspondance de login" msgstr "Login mapping" -#: dgsi/models.py:135 +#: dgsi/models.py:134 msgid "Correspondances de login" msgstr "Login mappings" -#: dgsi/models.py:166 +#: dgsi/models.py:165 msgid "Derniers Statuts acceptés" msgstr "Latest accepted Statutes" -#: dgsi/models.py:173 +#: dgsi/models.py:172 msgid "Dernier Règlement Intérieur accepté" msgstr "Latest accepted Bylaws" @@ -193,43 +193,47 @@ msgstr "Accept the bylaws" msgid "Profil de %(displayname)s" msgstr "Profile of %(displayname)s" -#: dgsi/templates/dgsi/profile.html:14 +#: dgsi/templates/dgsi/profile.html:11 +msgid "Nom d'utilisateur :" +msgstr "Username :" + +#: dgsi/templates/dgsi/profile.html:18 msgid "Mot de passe WiFi :" msgstr "WiFi password:" -#: dgsi/templates/dgsi/profile.html:16 +#: dgsi/templates/dgsi/profile.html:20 msgid "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?" msgstr "Are you sure that you want to reset your WiFi password?" -#: dgsi/templates/dgsi/profile.html:19 +#: dgsi/templates/dgsi/profile.html:23 msgid "Réinitialiser le mot de passe WiFi" msgstr "Reset the WiFi password" -#: dgsi/templates/dgsi/profile.html:32 +#: dgsi/templates/dgsi/profile.html:36 msgid "Générer un mot de passe WiFi" msgstr "Generate a WiFi password:" -#: dgsi/templates/dgsi/profile.html:36 +#: dgsi/templates/dgsi/profile.html:40 msgid "Adresse e-mail :" msgstr "E-mail address:" -#: dgsi/templates/dgsi/profile.html:41 +#: dgsi/templates/dgsi/profile.html:45 msgid "Informations techniques" msgstr "Technical informations" -#: dgsi/templates/dgsi/profile.html:44 +#: dgsi/templates/dgsi/profile.html:48 msgid "Identifiant unique :" msgstr "Unique identifier:" -#: dgsi/templates/dgsi/profile.html:53 +#: dgsi/templates/dgsi/profile.html:57 msgid "Membre des groupes suivants :" msgstr "Member of the following groups:" -#: dgsi/templates/dgsi/profile.html:64 +#: dgsi/templates/dgsi/profile.html:68 msgid "Pas de compte DGNum répertorié." msgstr "No DGNum account found." -#: dgsi/templates/dgsi/profile.html:67 +#: dgsi/templates/dgsi/profile.html:71 msgid "Créer un compte DGNum" msgstr "Create a DGNum account" @@ -291,11 +295,11 @@ msgstr "Invalid document type: %(kind)s" msgid "Compte DGNum pour %(displayname)s [%(name)s] créé." msgstr "DGNum account for %(displayname)s [%(name)s] created." -#: shared/account.py:40 +#: shared/account.py:41 msgid "Catégorie de compte ENS interdite." msgstr "ENS account category not permitted." -#: shared/account.py:57 +#: shared/account.py:58 msgid "Méthode de connexion invalide." msgstr "Invalid connection method." From 1a842639f595a0569f3cfcdd803cf675988f1ff7 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 30 Jan 2025 10:15:54 +0100 Subject: [PATCH 121/172] feat(dgsi/profile): Hide the technical info by default --- src/dgsi/templates/dgsi/profile.html | 37 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/dgsi/templates/dgsi/profile.html b/src/dgsi/templates/dgsi/profile.html index 8de06c7..5284cce 100644 --- a/src/dgsi/templates/dgsi/profile.html +++ b/src/dgsi/templates/dgsi/profile.html @@ -12,7 +12,6 @@ {{ user.username }}
- {% if user.kanidm %}

{% trans "Mot de passe WiFi :" %} @@ -42,26 +41,32 @@
{% if user.kanidm %} -

{% trans "Informations techniques" %}

+

+ {% trans "Informations techniques" %} + {% trans "Afficher" %} +


-

{% trans "Identifiant unique :" %}

+

{% if user.kanidm.radius_secret %} - @@ -43,16 +42,19 @@ {% if user.kanidm %}

{% trans "Informations techniques" %} - {% trans "Afficher" %} + " + data-off-html="{% trans "Cacher" %}">{% trans "Afficher" %}


{% else %} - {% trans "Générer un mot de passe WiFi" %} {% endif %} {% endif %} diff --git a/src/dgsi/views.py b/src/dgsi/views.py index 205216b..b1b0063 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -23,7 +23,7 @@ from dgsi.mixins import ( StaffRequiredMixin, ) from dgsi.models import Archive, Bylaws, Service, Statutes, User -from shared.kanidm import klient +from shared.kanidm import klient, sync_call class Link(NamedTuple): @@ -90,27 +90,23 @@ class AppleProfileView(KanidmAccountRequiredMixin, TemplateView): return super().render_to_response(context, headers=headers, **response_kwargs) -class GenerateWiFiPasswordView(LoginRequiredMixin, RedirectView): +class GenerateWiFiPasswordView(KanidmAccountRequiredMixin, View): url = reverse_lazy("dgsi:dgn-profile") + http_method_names = ["post"] - def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: - user = User.from_request(self.request) + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: + assert self._user.kanidm is not None - if user.kanidm is None: - messages.error(self.request, _("Compte DGNum inexistant.")) + # Give access to the wifi network when the user creates its first password + if not self._user.kanidm.radius_secret: + message = _("Mot de passe Wi-Fi généré avec succès.") + sync_call("group_add_members", "radius_access", [self._user.username]) else: - # Give access to the wifi network when the user creates its first password - if not user.kanidm.radius_secret: - message = _("Mot de passe Wi-Fi généré avec succès.") - async_to_sync(klient.group_add_members)( - "radius_access", [user.username] - ) - else: - message = _("Mot de passe Wi-Fi reinitialisé avec succès.") - async_to_sync(klient.call_post)(f"/v1/person/{user.username}/_radius") - messages.add_message(request, messages.SUCCESS, message) + message = _("Mot de passe Wi-Fi reinitialisé avec succès.") + sync_call("call_post", f"/v1/person/{self._user.username}/_radius") + messages.add_message(request, messages.SUCCESS, message) - return super().get(request, *args, **kwargs) + return HttpResponse(*args, headers={"HX-Redirect": self.url}, **kwargs) # INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to From d34e500ea6696f9be27343ab9737399f9c7a21e4 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 2 Feb 2025 21:20:30 +0100 Subject: [PATCH 150/172] chore(dgsi/urls): Drop the dgn- prefix --- src/dgsi/mixins.py | 4 +- src/dgsi/templates/_legal_document.html | 2 +- src/dgsi/templates/_subtitle.html | 2 +- src/dgsi/templates/dgsi/archive_list.html | 2 +- src/dgsi/templates/dgsi/legal_documents.html | 2 +- ...ons-legales.html => mentions_legales.html} | 0 .../dgsi/partials/profile-radius_secret.html | 4 +- .../dgsi/partials/user_list-user.html | 4 +- src/dgsi/templates/dgsi/profile.html | 4 +- src/dgsi/templates/dgsi/service_list.html | 2 +- src/dgsi/urls.py | 149 +++++++++--------- src/dgsi/views.py | 30 ++-- src/shared/account.py | 4 +- src/shared/templates/_footer.html | 2 +- src/shared/templates/_hero.html | 2 +- 15 files changed, 110 insertions(+), 103 deletions(-) rename src/dgsi/templates/dgsi/{mentions-legales.html => mentions_legales.html} (100%) diff --git a/src/dgsi/mixins.py b/src/dgsi/mixins.py index 0a74c14..82c1e21 100644 --- a/src/dgsi/mixins.py +++ b/src/dgsi/mixins.py @@ -32,7 +32,7 @@ class KanidmAccountRequiredMixin(AccessMixin): messages.WARNING, _("Veuillez créer un compte DGNum."), ) - return HttpResponseRedirect(reverse_lazy("dgsi:dgn-create_self_account")) + return HttpResponseRedirect(reverse_lazy("dgsi:create_self_account")) if self.require_radius_secret and self._user.kanidm.radius_secret is None: messages.add_message( @@ -40,7 +40,7 @@ class KanidmAccountRequiredMixin(AccessMixin): messages.WARNING, _("Veuillez générer un mot de passe Wi-Fi."), ) - return HttpResponseRedirect(reverse_lazy("dgsi:dgn-profile")) + return HttpResponseRedirect(reverse_lazy("dgsi:profile")) return super().dispatch(request, *args, **kwargs) # type: ignore diff --git a/src/dgsi/templates/_legal_document.html b/src/dgsi/templates/_legal_document.html index a69159e..bcde23a 100644 --- a/src/dgsi/templates/_legal_document.html +++ b/src/dgsi/templates/_legal_document.html @@ -5,7 +5,7 @@ {% if user_document != document %} {{ accept_question }} diff --git a/src/dgsi/templates/_subtitle.html b/src/dgsi/templates/_subtitle.html index e69f188..0f1cdf1 100644 --- a/src/dgsi/templates/_subtitle.html +++ b/src/dgsi/templates/_subtitle.html @@ -2,7 +2,7 @@

{% trans subtitle %} - + diff --git a/src/dgsi/templates/dgsi/archive_list.html b/src/dgsi/templates/dgsi/archive_list.html index 02cf566..cd2afb4 100644 --- a/src/dgsi/templates/dgsi/archive_list.html +++ b/src/dgsi/templates/dgsi/archive_list.html @@ -8,7 +8,7 @@ {% for file in document_list %} + {% if file.kind == "statutes" or file.kind == "bylaws" %} href="{{ file.file.url }}" {% else %} href="{% url "dgsi:protected-archive" file.pk %}" {% endif %}> diff --git a/src/dgsi/templates/dgsi/legal_documents.html b/src/dgsi/templates/dgsi/legal_documents.html index b9abc97..f3ca729 100644 --- a/src/dgsi/templates/dgsi/legal_documents.html +++ b/src/dgsi/templates/dgsi/legal_documents.html @@ -16,7 +16,7 @@ {% trans "Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer un." %}
{% trans "Poursuivre la création d'un compte DGNum" %} + href="{% url "dgsi:create_self_account" %}">{% trans "Poursuivre la création d'un compte DGNum" %}

{% endif %} {% endif %} diff --git a/src/dgsi/templates/dgsi/mentions-legales.html b/src/dgsi/templates/dgsi/mentions_legales.html similarity index 100% rename from src/dgsi/templates/dgsi/mentions-legales.html rename to src/dgsi/templates/dgsi/mentions_legales.html diff --git a/src/dgsi/templates/dgsi/partials/profile-radius_secret.html b/src/dgsi/templates/dgsi/partials/profile-radius_secret.html index 12dbc62..5769cb3 100644 --- a/src/dgsi/templates/dgsi/partials/profile-radius_secret.html +++ b/src/dgsi/templates/dgsi/partials/profile-radius_secret.html @@ -5,7 +5,7 @@ {% trans "Mot de passe WiFi :" %} {% if user.kanidm.radius_secret %} {% trans "Réinitialiser le mot de passe WiFi" %} @@ -24,7 +24,7 @@
{% else %} - {% trans "Générer un mot de passe WiFi" %} {% endif %} {% endif %} diff --git a/src/dgsi/templates/dgsi/partials/user_list-user.html b/src/dgsi/templates/dgsi/partials/user_list-user.html index 600d78e..dbc9b5b 100644 --- a/src/dgsi/templates/dgsi/partials/user_list-user.html +++ b/src/dgsi/templates/dgsi/partials/user_list-user.html @@ -7,11 +7,11 @@ {{ person.vlan_id|default:"" }} {% if person.vlan_id %} - {% trans "Désallouer" %} {% else %} - {% trans "Allouer" %} {% endif %} diff --git a/src/dgsi/templates/dgsi/profile.html b/src/dgsi/templates/dgsi/profile.html index a48ae1b..02e8784 100644 --- a/src/dgsi/templates/dgsi/profile.html +++ b/src/dgsi/templates/dgsi/profile.html @@ -50,7 +50,7 @@ {% if user.kanidm and user.kanidm.radius_secret %}
- + {% trans "Télécharger le profil Wi-Fi DGNum pour iOS, iPadOS et macOS" %} @@ -106,7 +106,7 @@ {% trans "Pas de compte DGNum répertorié." %}
{% trans "Créer un compte DGNum" %} + href="{% url "dgsi:create_self_account" %}">{% trans "Créer un compte DGNum" %}
{% endif %} {% endblock content %} diff --git a/src/dgsi/templates/dgsi/service_list.html b/src/dgsi/templates/dgsi/service_list.html index 5dfe92b..ba06bb3 100644 --- a/src/dgsi/templates/dgsi/service_list.html +++ b/src/dgsi/templates/dgsi/service_list.html @@ -9,7 +9,7 @@