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 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
|
||||
|
|
|
@ -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 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:
|
||||
|
|
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 = [
|
||||
# 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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue