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 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

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 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:

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 = [
# 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/<slug:kind>/",
views.AcceptLegalDocumentView.as_view(),
name="dgn-accept_legal_document",
),
# Account views
path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"),
path(

View file

@ -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