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