feat(legal-documents): Init list view and acceptance flow

This commit is contained in:
Tom Hubrecht 2024-09-24 14:29:51 +02:00
parent abdcb2c8ad
commit b5cedebda1
Signed by: thubrecht
SSH key fingerprint: SHA256:r+nK/SIcWlJ0zFZJGHtlAoRwq1Rm+WcKAm5ADYMoQPc
7 changed files with 226 additions and 4 deletions

View file

@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from unfold.admin import ModelAdmin from unfold.admin import ModelAdmin
from dgsi.models import Service, User from dgsi.models import Bylaws, Service, Statutes, User
@admin.register(User) @admin.register(User)
@ -10,6 +10,6 @@ class UserAdmin(BaseUserAdmin, ModelAdmin):
pass pass
@admin.register(Service) @admin.register(Bylaws, Service, Statutes)
class AdminClass(ModelAdmin): class AdminClass(ModelAdmin):
compressed_fields = True compressed_fields = True

View file

@ -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",
),
),
]

View file

@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from typing import Optional from typing import Optional, Self
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -26,6 +26,48 @@ class Service(models.Model):
return f"{self.name} [{self.url}]" 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 @dataclass
class KanidmProfile: class KanidmProfile:
person: Person person: Person
@ -37,6 +79,14 @@ class User(AbstractUser):
Custom User class, to have a direct link to the Kanidm data. 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 @cached_property
def kanidm(self) -> Optional[KanidmProfile]: def kanidm(self) -> Optional[KanidmProfile]:
try: try:

View file

@ -0,0 +1,23 @@
{% load i18n %}
<h2 class="subtitle">
{{ title }}
<span class="tags is-pulled-right">
{% if user_document != document %}
<a class="tag is-warning"
href="{% url "dgsi:dgn-accept_legal_document" document.kind %}"
onclick="return confirm(('{% trans " En acceptant, vous assurez avoir lu ce document et en approuver le contenu." %}'))">
<span>{{ accept_question }}</span>
<span class="icon is-size-6"><i class="ti ti-alert-circle"></i></span>
</a>
{% else %}
<span class="tag is-success">
<span>{% trans "Accepté" %}</span>
<span class="icon is-size-6"><i class="ti ti-checkbox"></i></span>
</span>
{% endif %}
<span class="tag is-dark">{{ document.date }}</span>
</span>
</h2>
<a class="button bt-link" href="{{ document.file.url }}">{{ document }}</a>

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h2 class="subtitle">Documents Légaux</h2>
<hr>
<br class="my-5">
{% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %}
<br class="my-4">
{% 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 %}

View file

@ -7,6 +7,17 @@ app_name = "dgsi"
urlpatterns = [ urlpatterns = [
# Misc views # Misc views
path("", views.IndexView.as_view(), name="dgn-index"), 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/<slug:kind>/",
views.AcceptLegalDocumentView.as_view(),
name="dgn-accept_legal_document",
),
# Account views # Account views
path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"),
path( path(

View file

@ -1,9 +1,11 @@
from typing import Any, NamedTuple from typing import Any, NamedTuple
from asgiref.sync import async_to_sync 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 LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.http import HttpRequest, HttpResponseBase
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.functional import Promise from django.utils.functional import Promise
@ -13,7 +15,7 @@ from django.views.generic.detail import SingleObjectMixin
from dgsi.forms import CreateKanidmAccountForm from dgsi.forms import CreateKanidmAccountForm
from dgsi.mixins import StaffRequiredMixin from dgsi.mixins import StaffRequiredMixin
from dgsi.models import Service, User from dgsi.models import Bylaws, Service, Statutes, User
from shared.kanidm import client 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): class ServiceListView(LoginRequiredMixin, ListView):
model = Service model = Service