feat(legal-documents): Init list view and acceptance flow
This commit is contained in:
parent
abdcb2c8ad
commit
b5cedebda1
7 changed files with 226 additions and 4 deletions
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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:
|
||||||
|
|
23
src/dgsi/templates/_legal_document.html
Normal file
23
src/dgsi/templates/_legal_document.html
Normal 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>
|
17
src/dgsi/templates/dgsi/legal_documents.html
Normal file
17
src/dgsi/templates/dgsi/legal_documents.html
Normal 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 %}
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue