Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
f97b39b87f |
38 changed files with 271 additions and 1011 deletions
|
@ -68,7 +68,6 @@ in
|
|||
ps.django-bulma-forms
|
||||
ps.django-compressor
|
||||
ps.django-debug-toolbar
|
||||
ps.django-htmx
|
||||
ps.django-import-export
|
||||
ps.django-sass-processor
|
||||
ps.django-sass-processor-dart-sass
|
||||
|
@ -78,9 +77,6 @@ in
|
|||
ps.loadcredential
|
||||
ps.pykanidm
|
||||
ps.python-cas
|
||||
ps.django-extensions
|
||||
ps.werkzeug
|
||||
ps.pyopenssl
|
||||
]
|
||||
++ ps.django-allauth.optional-dependencies.saml
|
||||
))
|
||||
|
@ -92,7 +88,6 @@ in
|
|||
DGSI_STATIC_ROOT = builtins.toString ./.static;
|
||||
DGSI_MEDIA_ROOT = builtins.toString ./.media;
|
||||
DGSI_KANIDM_CLIENT = "dgsi_test";
|
||||
DGSI_ARCHIVES_ROOT = builtins.toString ./.archives;
|
||||
};
|
||||
|
||||
shellHook = ''
|
||||
|
|
|
@ -8,15 +8,15 @@
|
|||
"repo": "git-hooks.nix"
|
||||
},
|
||||
"branch": "master",
|
||||
"revision": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17",
|
||||
"url": "https://github.com/cachix/git-hooks.nix/archive/9364dc02281ce2d37a1f55b6e51f7c0f65a75f17.tar.gz",
|
||||
"hash": "1n2qlj5l8c4g7gm5v6rvc4hff3ka8ljv7y62inybli093bd2ypa7"
|
||||
"revision": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74",
|
||||
"url": "https://github.com/cachix/git-hooks.nix/archive/4e743a6920eab45e8ba0fbe49dc459f1423a4b74.tar.gz",
|
||||
"hash": "0fc69dsn5rhv2zb16c2bfgx84ja8cmn7d7j2mrw3n4m8y611x40g"
|
||||
},
|
||||
"nixpkgs": {
|
||||
"type": "Channel",
|
||||
"name": "nixpkgs-unstable",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.05pre745387.762a39889257/nixexprs.tar.xz",
|
||||
"hash": "1pyr54allmvlxd5q4nhflmbdmma41nv2cib3i1qbrhaqm65vf97x"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre685691.28b5b8af91ff/nixexprs.tar.xz",
|
||||
"hash": "14ldh9js6l9nqch7j8z6nhyplxc5d9jw375pg8h4s24m7x37xnvy"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
|
|
|
@ -43,7 +43,6 @@ INSTALLED_APPS = [
|
|||
"sass_processor",
|
||||
"bulma",
|
||||
"import_export",
|
||||
"django_htmx",
|
||||
# Authentication
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
|
@ -68,7 +67,6 @@ MIDDLEWARE = [
|
|||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware",
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
@ -237,12 +235,6 @@ ACCOUNT_AUTHENTICATION_METHOD = "username"
|
|||
AUTH_PASSWORD_VALIDATORS = []
|
||||
AUTH_USER_MODEL = "dgsi.User"
|
||||
|
||||
DGSI_STAFF_GROUP = credentials.get("STAFF_GROUP", "dgnum_bureau@sso.dgnum.eu")
|
||||
DGSI_SUPERUSER_GROUP = credentials.get("SUPERUSER_GROUP", "dgnum_admins@sso.dgnum.eu")
|
||||
|
||||
VLAN_ID_MAX = 4094
|
||||
VLAN_ID_MIN = (VLAN_ID_MAX - 850) + 1
|
||||
|
||||
|
||||
###
|
||||
# Internationalization configuration
|
||||
|
@ -285,7 +277,6 @@ MEDIA_ROOT = credentials.get("MEDIA_ROOT")
|
|||
###
|
||||
# Storages configuration
|
||||
|
||||
ARCHIVES_INTERNAL = credentials.get("ARCHIVES_INTERNAL", "_archives")
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
|
@ -293,13 +284,6 @@ STORAGES = {
|
|||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||
},
|
||||
"archives": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
"OPTIONS": {
|
||||
"location": credentials["ARCHIVES_ROOT"],
|
||||
"base_url": f"/{ARCHIVES_INTERNAL}/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
###
|
||||
|
@ -344,7 +328,6 @@ UNFOLD = {
|
|||
if DEBUG:
|
||||
INSTALLED_APPS += [
|
||||
"debug_toolbar",
|
||||
"django_extensions",
|
||||
]
|
||||
|
||||
MIDDLEWARE += [
|
||||
|
|
|
@ -33,8 +33,4 @@ if settings.DEBUG:
|
|||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
|
||||
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
||||
*static(
|
||||
settings.STORAGES["archives"]["OPTIONS"]["base_url"],
|
||||
document_root=settings.STORAGES["archives"]["OPTIONS"]["location"],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ from unfold.contrib.import_export.forms import (
|
|||
SelectableFieldsExportForm,
|
||||
)
|
||||
|
||||
from dgsi.models import Archive, Bylaws, Service, Statutes, Translation, User
|
||||
from dgsi.models import Bylaws, Service, Statutes, Translation, User
|
||||
|
||||
assert DjangoUserAdmin.fieldsets is not None
|
||||
|
||||
|
@ -44,15 +44,9 @@ class UserAdmin(DjangoUserAdmin, ImportExportMixin, ModelAdmin):
|
|||
export_form_class = ExportForm
|
||||
export_form_class = SelectableFieldsExportForm
|
||||
|
||||
readonly_fields = ("vlan_id",)
|
||||
|
||||
# Add the local fields
|
||||
fieldsets = (
|
||||
*DjangoUserAdmin.fieldsets,
|
||||
(
|
||||
_("Informations réseau"),
|
||||
{"fields": ("vlan_id",)},
|
||||
),
|
||||
(
|
||||
_("Documents DGNum"),
|
||||
{"fields": ("accepted_statutes", "accepted_bylaws")},
|
||||
|
@ -60,7 +54,7 @@ class UserAdmin(DjangoUserAdmin, ImportExportMixin, ModelAdmin):
|
|||
)
|
||||
|
||||
|
||||
@admin.register(Archive, Bylaws, Service, SocialAccount, Statutes, Translation)
|
||||
@admin.register(Bylaws, Service, SocialAccount, Statutes, Translation)
|
||||
class AdminClass(ImportExportMixin, ModelAdmin):
|
||||
compressed_fields = True
|
||||
import_form_class = ImportForm
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
# Generated by Django 4.2.16 on 2025-01-25 15:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import dgsi.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dgsi", "0008_alter_user_accepted_statutes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Archive",
|
||||
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(
|
||||
storage=dgsi.models.get_storage,
|
||||
upload_to="",
|
||||
verbose_name="Fichier PDF",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Document d'archives",
|
||||
"verbose_name_plural": "Documents d'archives",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,30 +0,0 @@
|
|||
# Generated by Django 4.2.16 on 2025-01-30 08:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dgsi", "0009_archive"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="vlan_id",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
null=True, verbose_name="VLAN associé au compte"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="user",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("vlan_id",), name="unique_vlan_attribution"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,50 +1,9 @@
|
|||
from typing import Any
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin, UserPassesTestMixin
|
||||
from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.http import HttpRequest
|
||||
|
||||
from dgsi.models import User
|
||||
|
||||
|
||||
class KanidmAccountRequiredMixin(AccessMixin):
|
||||
"""
|
||||
Mixin used to require the existence of a kanidm account.
|
||||
"""
|
||||
|
||||
require_radius_secret: bool = False
|
||||
|
||||
def dispatch(
|
||||
self, request: HttpRequest, *args: Any, **kwargs: Any
|
||||
) -> HttpResponseBase:
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
|
||||
self._user = User.from_request(request)
|
||||
|
||||
if self._user.kanidm is None:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_("<b>Veuillez créer un compte DGNum.</b>"),
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:create_self_account"))
|
||||
|
||||
if self.require_radius_secret and self._user.kanidm.radius_secret is None:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_("<b>Veuillez générer un mot de passe Wi-Fi.</b>"),
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:profile"))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs) # type: ignore
|
||||
|
||||
|
||||
class StaffRequiredMixin(UserPassesTestMixin):
|
||||
request: HttpRequest
|
||||
|
||||
|
@ -54,29 +13,8 @@ class StaffRequiredMixin(UserPassesTestMixin):
|
|||
|
||||
assert isinstance(self.request.user, User)
|
||||
|
||||
return self.request.user.is_staff
|
||||
return self.request.user.is_admin
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# NOTE: We are only allowed to do this if a class is supplied to the right when constructing the view
|
||||
return super().get_context_data(admin_view=True, **kwargs) # pyright: ignore
|
||||
|
||||
|
||||
class HtmxPostMixin(TemplateResponseMixin, ContextMixin):
|
||||
http_method_names = ["post"]
|
||||
|
||||
def execute_action(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Execute action
|
||||
self.execute_action()
|
||||
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class HtmxPostObjectMixin(SingleObjectMixin, HtmxPostMixin):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
|
|
@ -6,10 +6,8 @@ from typing import Optional, Self
|
|||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from allauth.socialaccount.models import SocialAccount
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.files import storage
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
|
@ -17,9 +15,9 @@ from django.utils.translation import gettext_lazy as _
|
|||
from kanidm.exceptions import NoMatchingEntries
|
||||
from kanidm.models.person import Person
|
||||
|
||||
from shared.kanidm import klient, sync_call
|
||||
from shared.kanidm import klient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ADMIN_GROUP = "dgnum_admins@sso.dgnum.eu"
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
|
@ -39,8 +37,6 @@ class LegalDocument(models.Model):
|
|||
name = models.CharField(_("Nom du document"), max_length=255)
|
||||
file = models.FileField(_("Fichier PDF"))
|
||||
|
||||
icon = "script"
|
||||
|
||||
@classmethod
|
||||
def latest(cls, **kwargs) -> Self | None:
|
||||
return cls.objects.filter(**kwargs).latest()
|
||||
|
@ -58,7 +54,6 @@ class Statutes(LegalDocument):
|
|||
"""
|
||||
|
||||
kind = "statutes"
|
||||
color = "primary"
|
||||
|
||||
class Meta: # pyright: ignore
|
||||
get_latest_by = "date"
|
||||
|
@ -72,7 +67,6 @@ class Bylaws(LegalDocument):
|
|||
"""
|
||||
|
||||
kind = "bylaws"
|
||||
color = "success"
|
||||
|
||||
class Meta: # pyright: ignore
|
||||
get_latest_by = "date"
|
||||
|
@ -80,30 +74,6 @@ class Bylaws(LegalDocument):
|
|||
verbose_name_plural = _("Règlements Intérieurs")
|
||||
|
||||
|
||||
def get_storage(*args, **kwargs):
|
||||
return storage.storages["archives"]
|
||||
|
||||
|
||||
class Archive(models.Model):
|
||||
"""
|
||||
Archived documents for the association.
|
||||
"""
|
||||
|
||||
date = models.DateField(_("Date du document"))
|
||||
name = models.CharField(_("Nom du document"), max_length=255)
|
||||
file = models.FileField(_("Fichier PDF"), storage=get_storage)
|
||||
|
||||
icon = "archive"
|
||||
color = "warning"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
class Meta: # pyright: ignore
|
||||
verbose_name = _("Document d'archives")
|
||||
verbose_name_plural = _("Documents d'archives")
|
||||
|
||||
|
||||
# class TermsAndConditions(LegalDocument):
|
||||
# """
|
||||
# Terms and Conditions of use regarding a service offered by the association.
|
||||
|
@ -148,7 +118,7 @@ class Translation(models.Model):
|
|||
|
||||
|
||||
# INFO: We need to use a signal receiver here, as the delete method is not called
|
||||
# when deleting objects from the admin interface
|
||||
# when deleteing objects from the admin interface
|
||||
@receiver(pre_delete, sender=Translation)
|
||||
def restore_username(**kwargs):
|
||||
"""
|
||||
|
@ -186,8 +156,6 @@ class User(AbstractUser):
|
|||
)
|
||||
# accepted_terms = models.ManyToManyField(TermsAndConditions)
|
||||
|
||||
vlan_id = models.PositiveSmallIntegerField(_("VLAN associé au compte"), null=True)
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, request: HttpRequest) -> Self:
|
||||
u = request.user
|
||||
|
@ -212,94 +180,8 @@ class User(AbstractUser):
|
|||
logging.error(f"Erreur lors de la requête à Kanidm: {e}")
|
||||
return None
|
||||
|
||||
def part_of(self, group: str) -> bool:
|
||||
return (self.kanidm is not None) and (group in self.kanidm.person.memberof)
|
||||
|
||||
def can_access_archive(self, archive: Archive) -> bool:
|
||||
# Prepare a more complex workflow
|
||||
return True
|
||||
|
||||
###
|
||||
# VLAN attribution machinery
|
||||
#
|
||||
# NOTE: It is a bit cumbersome because we need to store the vlan_id
|
||||
# information both in DG·SI and in Kanidm
|
||||
# Now the question will be « Which is the source of truth ? »
|
||||
# For now, I believe it has to be DG·SI, so a sync script will
|
||||
# have to be run regularly.
|
||||
|
||||
@transaction.atomic
|
||||
def set_unique_vlan_id(self):
|
||||
if self.vlan_id is not None:
|
||||
raise ValueError(_("Ce compte a déjà un VLAN associé"))
|
||||
|
||||
self.vlan_id = min(
|
||||
set(range(settings.VLAN_ID_MIN, settings.VLAN_ID_MAX))
|
||||
- set(
|
||||
User.objects.exclude(vlan_id__isnull=True).values_list(
|
||||
"vlan_id", flat=True
|
||||
)
|
||||
)
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
return (self.kanidm is not None) and (
|
||||
ADMIN_GROUP in self.kanidm.person.memberof
|
||||
)
|
||||
|
||||
# Preempt the vlan attribution
|
||||
self.save(update_fields=["vlan_id"])
|
||||
|
||||
@transaction.atomic
|
||||
def register_unique_vlan(self) -> None:
|
||||
self.set_unique_vlan_id()
|
||||
|
||||
group_name = f"vlan_{self.vlan_id}"
|
||||
|
||||
# Add the user to the group requested group
|
||||
sync_call("group_add_members", group_name, [self.username])
|
||||
|
||||
# Check that we succeeded in setting a VLAN that is unique to the current user
|
||||
group = sync_call("group_get", group_name)
|
||||
|
||||
if group.member == []:
|
||||
# Something went wrong
|
||||
self.vlan_id = None
|
||||
self.save(update_fields=["vlan_id"])
|
||||
raise RuntimeError("VLAN attribution failed")
|
||||
|
||||
if group.member != [f"{self.username}@sso.dgnum.eu"]:
|
||||
# Remove the user from the group
|
||||
sync_call("group_delete_members", group_name, [self.username])
|
||||
self.vlan_id = None
|
||||
self.save(update_fields=["vlan_id"])
|
||||
raise RuntimeError("Duplicate VLAN attribution detected")
|
||||
|
||||
def reclaim_vlan(self):
|
||||
if self.vlan_id is None:
|
||||
# Nothing to do, just return
|
||||
logger.warning(
|
||||
f"Reclaiming VLAN for {self.username} who does not have one."
|
||||
)
|
||||
return
|
||||
|
||||
group_name = f"vlan_{self.vlan_id}"
|
||||
|
||||
sync_call("group_delete_members", group_name, [self.username])
|
||||
|
||||
# Check that the call succeeded
|
||||
try:
|
||||
group = sync_call("group_get", group_name)
|
||||
|
||||
if "{self.username}@sso.dgnum.eu" in group.member:
|
||||
raise RuntimeError(
|
||||
f"Something went wrong in trying to reclaim vlan {self.vlan_id}"
|
||||
)
|
||||
except ValueError:
|
||||
# The group does not exist apparently, keep going
|
||||
logger.warning(
|
||||
f"Reclaiming VLAN {self.vlan_id}, but the associated group does not exist."
|
||||
)
|
||||
finally:
|
||||
self.vlan_id = None
|
||||
self.save(update_fields=["vlan_id"])
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["vlan_id"], name="unique_vlan_attribution")
|
||||
]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<a class="button bt-link is-light {{ link.color }}"
|
||||
href="{% if link.absolute %}{{ link.reverse }}{% else %}{% url link.reverse %}{% endif %}">
|
||||
<a class="button bt-link is-light {{ link.color }}" href="{% url link.reverse %}">
|
||||
{% if link.icon %}<span class="icon"><i class="ti ti-{{ link.icon }}"></i></span>{% endif %}
|
||||
<span>{{ link.text }}</span>
|
||||
</a>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<span class="tags is-pulled-right">
|
||||
{% if user_document != document %}
|
||||
<a class="tag is-warning"
|
||||
href="{% url "dgsi:accept_legal_document" document.kind %}"
|
||||
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>
|
||||
|
@ -21,6 +21,6 @@
|
|||
</h2>
|
||||
|
||||
<a class="button bt-link" href="{{ document.file.url }}">
|
||||
<span class="ellipsis">{{ document }}</span>
|
||||
<span>{{ document }}</span>
|
||||
<span class="icon"><i class="ti ti-file-download"></i></span>
|
||||
</a>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<h2 class="subtitle">
|
||||
{% trans subtitle %}
|
||||
<a class="button is-small is-pulled-right is-primary" href="{% url backlink|default:'dgsi:index' %}">
|
||||
<span class="icon">
|
||||
<i class="ti ti-arrow-big-left-filled"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</h2>
|
||||
<hr>
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Archives de la DGNum" as subtitle %}
|
||||
{% include "_subtitle.html" %}
|
||||
|
||||
{% for file in document_list %}
|
||||
<a class="button bt-archive"
|
||||
{% if file.kind == "statutes" or file.kind == "bylaws" %} href="{{ file.file.url }}" {% else %} href="{% url "dgsi:protected_archive" file.pk %}" {% endif %}>
|
||||
<span class="tag is-{{ file.color }} is-pulled-left">
|
||||
<span class="icon"><i class="ti ti-{{ file.icon }}"></i></span>
|
||||
</span>
|
||||
<span class="ellipsis mx-2">{{ file }}</span>
|
||||
<span class="tag is-pulled-right">{{ file.date }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock content %}
|
|
@ -3,16 +3,16 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Création de compte Kanidm" as subtitle %}
|
||||
{% include "_subtitle.html" %}
|
||||
<h2 class="subtitle">{% trans "Création de compte Kanidm" %}</h2>
|
||||
<hr>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "bulma/form.html" with form=form %}
|
||||
|
||||
<button class="button is-fullwidth mt-6">
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Création d'un compte DGNum" as subtitle %}
|
||||
{% include "_subtitle.html" %}
|
||||
<h2 class="subtitle">{% trans "Création d'un compte DGNum" %}</h2>
|
||||
<hr>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "bulma/form.html" with form=form %}
|
||||
|
||||
<button class="button is-fullwidth mt-6">
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_staff %}
|
||||
{% if user.is_admin %}
|
||||
<hr>
|
||||
{% for link in links.admin %}
|
||||
{% include "_index_link.html" %}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Documents légaux" as subtitle %}
|
||||
{% include "_subtitle.html" %}
|
||||
<h2 class="subtitle">Documents Légaux</h2>
|
||||
<hr>
|
||||
|
||||
{% if user.kanidm is None %}
|
||||
{% if show_message %}
|
||||
|
@ -16,7 +16,7 @@
|
|||
<b>{% trans "Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer un." %}</b>
|
||||
<br>
|
||||
<a class="button mt-5 is-light"
|
||||
href="{% url "dgsi:create_self_account" %}">{% trans "Poursuivre la création d'un compte DGNum" %}</a>
|
||||
href="{% url "dgsi:dgn-create_self_account" %}">{% trans "Poursuivre la création d'un compte DGNum" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_subtitle.html" with subtitle="Mentions Légales" %}
|
||||
|
||||
<section class="section content">
|
||||
<p class="is-size-4">Éditeur</p>
|
||||
<p>Ce site web est édité par la Délégation Générale Numérique.</p>
|
||||
<p>
|
||||
<b>Délégation Générale Numérique (DGNum)</b>
|
||||
<br>
|
||||
Association de loi 1901
|
||||
</p>
|
||||
<p>
|
||||
Siège social :
|
||||
<br>
|
||||
<i>45 rue d'Ulm, 75005 Paris - France</i>
|
||||
</p>
|
||||
<p>Directeur de publication : Jean-Marc Gailis</p>
|
||||
<p>Contact : contact[at]dgnum.eu</p>
|
||||
|
||||
<hr>
|
||||
<p class="is-size-4">Hébergeur</p>
|
||||
<p>Ce site web est hébergé par la Délégation Générale Numérique.</p>
|
||||
<p>
|
||||
<b>Délégation Générale Numérique (DGNum)</b>
|
||||
<br>
|
||||
Association de loi 1901
|
||||
</p>
|
||||
<p>
|
||||
Siège social :
|
||||
<br>
|
||||
<i>45 rue d'Ulm, 75005 Paris - France</i>
|
||||
</p>
|
||||
<p>Directeur de publication : Jean-Marc Gailis</p>
|
||||
<p>Contact : contact[at]dgnum.eu</p>
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,30 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% if user.kanidm %}
|
||||
<h3 class="has-text-weight-bold mb-3">
|
||||
<span>{% trans "Mot de passe WiFi :" %}</span>
|
||||
{% if user.kanidm.radius_secret %}
|
||||
<a class="button is-small is-danger is-pulled-right"
|
||||
hx-post="{% url "dgsi:generate_wifi_password" %}"
|
||||
hx-confirm="{% trans "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?" %}">
|
||||
<span class="icon"><i class="ti ti-refresh"></i></span>
|
||||
<span class="has-text-weight-normal">{% trans "Réinitialiser le mot de passe WiFi" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
{% if user.kanidm.radius_secret %}
|
||||
<div class="buttons has-addons is-flex">
|
||||
<input id="radius-secret"
|
||||
data-select
|
||||
class="button is-primary is-size-4 is-flex-grow-2"
|
||||
value="{{ user.kanidm.radius_secret }}"
|
||||
type="password"
|
||||
readonly />
|
||||
<a id="secret-toggle" class="button is-size-4 is-warning is-light"><span class="icon"><i class="ti ti-eye"></i></span></a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a hx-post="{% url "dgsi:generate_wifi_password" %}"
|
||||
class="button is-fullwidth is-primary is-light is-size-4 block">{% trans "Générer un mot de passe WiFi" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
|
@ -1,21 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<tr id="user-{{ person.pk }}">
|
||||
<th>{{ person.username }}</th>
|
||||
<td>{{ person.first_name }} {{ person.last_name }}</td>
|
||||
<td>{{ person.email }}</td>
|
||||
<td>{{ person.vlan_id|default:"" }}</td>
|
||||
<td>
|
||||
{% if person.vlan_id %}
|
||||
<a hx-post="{% url "dgsi:user_deassign_vlan" person.pk %}"
|
||||
hx-target="#user-{{ person.pk }}"
|
||||
class="button is-fullwidth is-light is-warning">{% trans "Désallouer" %}</a>
|
||||
{% elif person.kanidm %}
|
||||
<a hx-post="{% url "dgsi:user_assign_vlan" person.pk %}"
|
||||
hx-target="#user-{{ person.pk }}"
|
||||
class="button is-fullwidth is-light is-primary">{% trans "Allouer" %}</a>
|
||||
{% else %}
|
||||
<button class="button is-fullwidth is-static">{% trans "Pas de compte" %}</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
|
@ -2,111 +2,69 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const secret = document.getElementById("radius-secret");
|
||||
const toggle = document.getElementById("secret-toggle");
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
if (secret.type === "password") {
|
||||
secret.type = "text";
|
||||
toggle.innerHTML = `<span class="icon"><i class="ti ti-eye-off"></i></span>`;
|
||||
} else {
|
||||
secret.type = "password";
|
||||
toggle.innerHTML = `<span class="icon"><i class="ti ti-eye"></i></span>`;
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Profil personnel" as subtitle %}
|
||||
{% include "_subtitle.html" %}
|
||||
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Nom d'utilisateur :" %}</h3>
|
||||
<input data-select
|
||||
class="button is-fullwidth"
|
||||
value="{{ user.username }}"
|
||||
readonly />
|
||||
<br>
|
||||
|
||||
{% include "dgsi/partials/profile-radius_secret.html" %}
|
||||
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Nom d'usage :" %}</h3>
|
||||
<input data-select
|
||||
class="button is-fullwidth"
|
||||
value="{{ user.first_name }} {{ user.last_name|upper }}"
|
||||
readonly />
|
||||
<br>
|
||||
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Adresse e-mail :" %}</h3>
|
||||
<input data-select
|
||||
class="button is-fullwidth"
|
||||
value="{{ user.email }}"
|
||||
readonly />
|
||||
<br>
|
||||
|
||||
{% if user.kanidm and user.kanidm.radius_secret %}
|
||||
<div class="buttons">
|
||||
<a href="{% url "dgsi:apple_profile" %}" class="button is-light">
|
||||
<span class="icon"><i class="ti ti-brand-apple-filled"></i></span>
|
||||
<span>{% trans "Télécharger le profil Wi-Fi DGNum pour iOS, iPadOS et macOS" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2 class="subtitle">
|
||||
<span>{% blocktrans %}Profil de {{ displayname }}{% endblocktrans %}</span>
|
||||
<span class="tag is-primary is-medium is-pulled-right">{{ user.username }}</span>
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
{% if user.kanidm %}
|
||||
<h2 class="subtitle mt-4">
|
||||
{% trans "Informations techniques" %}
|
||||
<a class="button is-small is-primary is-light is-pulled-right"
|
||||
data-toggle="on"
|
||||
data-target="#technical-info"
|
||||
data-class="is-hidden"
|
||||
data-on-html="<span>{% trans "Afficher" %}</span>"
|
||||
data-off-html="<span>{% trans "Cacher" %}</span>">{% trans "Afficher" %}</a>
|
||||
</h2>
|
||||
<hr>
|
||||
<h3 class="has-text-weight-bold mb-3">
|
||||
<span>{% trans "Mot de passe WiFi :" %}</span>
|
||||
{% if user.kanidm.radius_secret %}
|
||||
{% trans "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?" as confirm_wifi_reset %}
|
||||
<a href="{% url "dgsi:dgn-generate_wifi_password" %}"
|
||||
class="tag is-warning is-light is-medium is-pulled-right"
|
||||
onclick="return confirm('{{ confirm_wifi_reset }}')">{% trans "Réinitialiser le mot de passe WiFi" %}</a>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<div id="technical-info" class="is-hidden">
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Identifiant interne :" %}</h3>
|
||||
|
||||
<input data-select
|
||||
class="button is-fullwidth is-light"
|
||||
value="{{ user.kanidm.person.uuid }}"
|
||||
{% if user.kanidm.radius_secret %}
|
||||
<input id="radius-secret"
|
||||
onclick="document.querySelector('#radius-secret').select()"
|
||||
class="button is-fullwidth is-primary is-size-4"
|
||||
value="{{ user.kanidm.radius_secret }}"
|
||||
readonly />
|
||||
<br>
|
||||
{% else %}
|
||||
<a href="{% url "dgsi:dgn-generate_wifi_password" %}"
|
||||
class="button is-fullwidth is-primary is-light is-size-4 block">{% trans "Générer un mot de passe WiFi" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if user.vlan_id %}
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "VLAN attribué :" %}</h3>
|
||||
<input data-select
|
||||
class="button is-fullwidth"
|
||||
value="{{ user.vlan_id }}"
|
||||
readonly />
|
||||
<br>
|
||||
{% else %}
|
||||
<div class="notification is-warning is-light has-text-centered">
|
||||
<b>{% trans "Pas de VLAN attribué." %}</b>
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Adresse e-mail :" %}</h3>
|
||||
<span class="button is-fullwidth">{{ user.email }}</span>
|
||||
<br>
|
||||
|
||||
{% if user.kanidm %}
|
||||
<h2 class="subtitle mt-4">{% trans "Informations techniques" %}</h2>
|
||||
<hr>
|
||||
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Identifiant unique :" %}</h3>
|
||||
|
||||
<input id="uuid"
|
||||
onclick="document.querySelector('#uuid').select()"
|
||||
class="button is-fullwidth"
|
||||
value="{{ user.kanidm.person.uuid }}"
|
||||
readonly />
|
||||
<br>
|
||||
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Membre des groupes suivants :" %}</h3>
|
||||
|
||||
<div class="grid groups">
|
||||
{% for group in user.kanidm.person.memberof %}
|
||||
<div class="cell button is-static">
|
||||
<span>{{ group }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="has-text-weight-bold mb-3">{% trans "Membre des groupes suivants :" %}</h3>
|
||||
|
||||
<div class="grid groups">
|
||||
{% for group in user.kanidm.person.memberof %}
|
||||
<div class="cell button is-static">
|
||||
<span>{{ group }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="notification is-primary is-light has-text-centered mt-6">
|
||||
<b>{% trans "Pas de compte DGNum répertorié." %}</b>
|
||||
<br>
|
||||
<a class="button mt-5 is-light"
|
||||
href="{% url "dgsi:create_self_account" %}">{% trans "Créer un compte DGNum" %}</a>
|
||||
href="{% url "dgsi:dgn-create_self_account" %}">{% trans "Créer un compte DGNum" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
|
|
@ -5,12 +5,8 @@
|
|||
<key>ConsentText</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<string>Souhaitez-vous configurer votre appareil pour utiliser le Wi-Fi DGNum ?</string>
|
||||
<key>en</key>
|
||||
<string>Dou you want to configure your device to use the DGNum Wi-Fi ?</string>
|
||||
<string>Consent Message</string>
|
||||
</dict>
|
||||
<key>PayloadUUID</key>
|
||||
<string>304283f2-b4df-4f54-9fd9-9c8e1fdc778f</string>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
@ -32,17 +28,15 @@
|
|||
<string>1.2</string>
|
||||
<key>TLSTrustedServerNames</key>
|
||||
<array>
|
||||
<string>radius.dgnum.eu</string>
|
||||
</array>
|
||||
<string>radius.dgnum.eu</string>
|
||||
</array>
|
||||
<key>UserName</key>
|
||||
<string>{{ user.username }}</string>
|
||||
<string>{{ user.username }}</string>
|
||||
<key>UserPassword</key>
|
||||
<string>{{ user.kanidm.radius_secret }}</string>
|
||||
<string>{{ user.kanidm.radius_secret }}</string>
|
||||
<key>TTLSInnerAuthentication</key>
|
||||
<string>MSCHAPv2</string>
|
||||
</dict>
|
||||
<key>PayloadUUID</key>
|
||||
<string>a9a6e20c-1d9e-497a-b10c-f93a62e3e7df</string>
|
||||
<key>EncryptionType</key>
|
||||
<string>WPA2</string>
|
||||
<key>HIDDEN_NETWORK</key>
|
||||
|
@ -50,11 +44,11 @@
|
|||
<key>IsHotspot</key>
|
||||
<false/>
|
||||
<key>PayloadDescription</key>
|
||||
<string>DGNum Wi-Fi configuration</string>
|
||||
<string>DGNum Wi-Fi setup</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Wi-Fi</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.apple.wifi.managed.a9a6e20c-1d9e-497a-b10c-f93a62e3e7df</string>
|
||||
<string>com.apple.wifi.managed</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.wifi.managed</string>
|
||||
<key>PayloadVersion</key>
|
||||
|
@ -66,17 +60,19 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>PayloadDescription</key>
|
||||
<string>DGNum Wi-Fi configuration</string>
|
||||
<string>Wi-Fi</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Wi-Fi DGNum</string>
|
||||
<string>wifi-dgnum</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>dgnum-radius.304283f2-b4df-4f54-9fd9-9c8e1fdc778f</string>
|
||||
<string>kanidm</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>Délégation Générale Numérique</string>
|
||||
<string>dgnum.eu</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
<false/>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>E3A2F3F5-88E9-40B9-985C-1B35BD5314B3</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
|
@ -3,13 +3,13 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Services accessibles via la DGNum" as subtitle %}
|
||||
{% include "_subtitle.html" %}
|
||||
<h2 class="subtitle">{% trans "Services accessibles via la DGNum" %}</h2>
|
||||
<hr>
|
||||
|
||||
<div class="buttons bt-links">
|
||||
{% for service in service_list %}
|
||||
<a class="button is-medium"
|
||||
href="{% url "dgsi:service_redirect" service.pk %}">
|
||||
href="{% url "dgsi:dgn-services_redirect" service.pk %}">
|
||||
<span class="icon"><i class="ti ti-{{ service.icon }}"></i></span>
|
||||
<span>{{ service.name }}</span>
|
||||
</a>
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_subtitle.html" with subtitle="Comptes DG·SI" %}
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-striped is-narrow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Nom d'utilisateur" %}</th>
|
||||
<th>{% trans "Nom d'usage" %}</th>
|
||||
<th>{% trans "Adresse e-mail" %}</th>
|
||||
<th>{% trans "VLAN attribué" %}</th>
|
||||
<th>{% trans "Gestion du VLAN" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="is-centered">
|
||||
{% for person in user_list %}
|
||||
{% include "dgsi/partials/user_list-user.html" %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,106 +1,50 @@
|
|||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "dgsi"
|
||||
|
||||
urlpatterns = [
|
||||
###
|
||||
# Miscelleanous views
|
||||
path(
|
||||
"",
|
||||
views.IndexView.as_view(),
|
||||
name="index",
|
||||
),
|
||||
path(
|
||||
"mentions-legales/",
|
||||
TemplateView.as_view(template_name="dgsi/mentions_legales.html"),
|
||||
name="mentions_legales",
|
||||
),
|
||||
path(
|
||||
"accounts/forbidden/",
|
||||
views.TemplateView.as_view(template_name="accounts/forbidden_category.html"),
|
||||
name="forbidden_account",
|
||||
),
|
||||
###
|
||||
# Archives views
|
||||
path(
|
||||
"archives/",
|
||||
views.ArchiveListView.as_view(),
|
||||
name="archive_list",
|
||||
),
|
||||
path(
|
||||
"archives/<int:pk>/",
|
||||
views.ProtectedArchiveView.as_view(),
|
||||
name="protected_archive",
|
||||
),
|
||||
###
|
||||
# Misc views
|
||||
path("", views.IndexView.as_view(), name="dgn-index"),
|
||||
# Legal documents
|
||||
path(
|
||||
"legal-documents/",
|
||||
views.LegalDocumentsView.as_view(),
|
||||
name="legal_documents",
|
||||
name="dgn-legal_documents",
|
||||
),
|
||||
path(
|
||||
"legal-documents/accept/<slug:kind>/",
|
||||
views.AcceptLegalDocumentView.as_view(),
|
||||
name="accept_legal_document",
|
||||
),
|
||||
###
|
||||
# Services views
|
||||
path(
|
||||
"services/",
|
||||
views.ServiceListView.as_view(),
|
||||
name="service_list",
|
||||
),
|
||||
path(
|
||||
"services/redirect/<int:pk>/",
|
||||
views.ServiceRedirectView.as_view(),
|
||||
name="service_redirect",
|
||||
),
|
||||
###
|
||||
# Profile views
|
||||
path(
|
||||
"accounts/profile/",
|
||||
views.ProfileView.as_view(),
|
||||
name="profile",
|
||||
),
|
||||
path(
|
||||
"accounts/profile/apple/",
|
||||
views.AppleProfileView.as_view(),
|
||||
name="apple_profile",
|
||||
name="dgn-accept_legal_document",
|
||||
),
|
||||
# Account views
|
||||
path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"),
|
||||
path(
|
||||
"accounts/generate-wifi-password/",
|
||||
views.GenerateWiFiPasswordView.as_view(),
|
||||
name="generate_wifi_password",
|
||||
name="dgn-generate_wifi_password",
|
||||
),
|
||||
path(
|
||||
"accounts/create/",
|
||||
views.CreateSelfAccountView.as_view(),
|
||||
name="create_self_account",
|
||||
name="dgn-create_self_account",
|
||||
),
|
||||
###
|
||||
# Accounts admin views
|
||||
path(
|
||||
"accounts/create-kanidm/",
|
||||
views.CreateKanidmAccountView.as_view(),
|
||||
name="create_kanidm_account",
|
||||
name="dgn-create_kanidm_user",
|
||||
),
|
||||
path(
|
||||
"accounts/list/",
|
||||
views.UserListView.as_view(),
|
||||
name="user_list",
|
||||
"accounts/forbidden/",
|
||||
views.TemplateView.as_view(template_name="accounts/forbidden_category.html"),
|
||||
name="dgn-forbidden_account",
|
||||
),
|
||||
# Services views
|
||||
path("services/", views.ServiceListView.as_view(), name="dgn-services"),
|
||||
path(
|
||||
"accounts/assign-vlan/<int:pk>",
|
||||
views.UserAssignVlanView.as_view(),
|
||||
name="user_assign_vlan",
|
||||
),
|
||||
path(
|
||||
"accounts/deassign-vlan/<int:pk>",
|
||||
views.UserDeassignVlanView.as_view(),
|
||||
name="user_deassign_vlan",
|
||||
"services/redirect/<int:pk>/",
|
||||
views.ServiceRedirectView.as_view(),
|
||||
name="dgn-services_redirect",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,29 +1,22 @@
|
|||
from mimetypes import guess_type
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.mail import EmailMessage
|
||||
from django.http import Http404, HttpRequest, HttpResponseBase, HttpResponseRedirect
|
||||
from django.http.response import HttpResponse
|
||||
from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView, ListView, RedirectView, TemplateView, View
|
||||
from django.views.generic import FormView, ListView, RedirectView, TemplateView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm
|
||||
from dgsi.mixins import (
|
||||
HtmxPostObjectMixin,
|
||||
KanidmAccountRequiredMixin,
|
||||
StaffRequiredMixin,
|
||||
)
|
||||
from dgsi.models import Archive, Bylaws, Service, Statutes, User
|
||||
from shared.kanidm import klient, sync_call
|
||||
from dgsi.mixins import StaffRequiredMixin
|
||||
from dgsi.models import Bylaws, Service, Statutes, User
|
||||
from shared.kanidm import klient
|
||||
|
||||
|
||||
class Link(NamedTuple):
|
||||
|
@ -31,31 +24,21 @@ class Link(NamedTuple):
|
|||
reverse: str
|
||||
text: str | Promise
|
||||
icon: str | None = None
|
||||
absolute: bool = False
|
||||
|
||||
|
||||
AUTHENTICATED_LINKS = [
|
||||
Link("is-primary", "dgsi:profile", _("Mon profil"), "user-filled"),
|
||||
Link(
|
||||
"is-success",
|
||||
"https://docs.dgnum.eu/s/doc-publique",
|
||||
_("Aide et Documention"),
|
||||
"help",
|
||||
True,
|
||||
),
|
||||
Link("is-primary", "dgsi:legal_documents", _("Documents Légaux"), "script"),
|
||||
Link("is-info", "dgsi:service_list", _("Services proposés"), "apps-filled"),
|
||||
Link("is-success", "dgsi:archive_list", _("Archives"), "archive"),
|
||||
AUTHENTICATED_LINKS: list[Link] = [
|
||||
Link("is-primary", "dgsi:dgn-profile", _("Mon profil"), "user-filled"),
|
||||
Link("is-primary", "dgsi:dgn-legal_documents", _("Documents Légaux"), "script"),
|
||||
Link("is-info", "dgsi:dgn-services", _("Services proposés"), "apps-filled"),
|
||||
]
|
||||
|
||||
ADMIN_LINKS = [
|
||||
ADMIN_LINKS: list[Link] = [
|
||||
Link(
|
||||
"is-danger",
|
||||
"dgsi:create_kanidm_account",
|
||||
"dgsi:dgn-create_kanidm_user",
|
||||
_("Créer un compte Kanidm"),
|
||||
"user-plus",
|
||||
),
|
||||
Link("is-primary", "dgsi:user_list", _("Liste des comptes"), "users"),
|
||||
Link(
|
||||
"is-warning", "admin:index", _("Interface d'administration"), "settings-filled"
|
||||
),
|
||||
|
@ -64,49 +47,46 @@ ADMIN_LINKS = [
|
|||
|
||||
class IndexView(TemplateView):
|
||||
template_name = "dgsi/index.html"
|
||||
extra_context = {
|
||||
"links": {
|
||||
"authenticated": AUTHENTICATED_LINKS,
|
||||
"admin": ADMIN_LINKS,
|
||||
}
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
return super().get_context_data(
|
||||
links={
|
||||
"authenticated": AUTHENTICATED_LINKS,
|
||||
"admin": ADMIN_LINKS,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ProfileView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "dgsi/profile.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
u = User.from_request(self.request)
|
||||
|
||||
class AppleProfileView(KanidmAccountRequiredMixin, TemplateView):
|
||||
content_type = "application/x-apple-aspen-config"
|
||||
template_name = "dgnum_profile.mobileconfig"
|
||||
extra_context = {"dgnum_ssid": "DGNum"}
|
||||
require_radius_secret = True
|
||||
|
||||
def render_to_response(
|
||||
self, context: dict[str, Any], **response_kwargs: Any
|
||||
) -> HttpResponse:
|
||||
headers = response_kwargs.pop("headers", {})
|
||||
headers["Content-Disposition"] = "attachment; filename=wifi_dgnum.mobileconfig"
|
||||
return super().render_to_response(context, headers=headers, **response_kwargs)
|
||||
return super().get_context_data(
|
||||
displayname=f"{u.first_name} {u.last_name}",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class GenerateWiFiPasswordView(KanidmAccountRequiredMixin, View):
|
||||
url = reverse_lazy("dgsi:profile")
|
||||
http_method_names = ["post"]
|
||||
class GenerateWiFiPasswordView(LoginRequiredMixin, RedirectView):
|
||||
url = reverse_lazy("dgsi:dgn-profile")
|
||||
|
||||
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
||||
assert self._user.kanidm is not None
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
||||
user = User.from_request(self.request)
|
||||
|
||||
# Give access to the wifi network when the user creates its first password
|
||||
if not self._user.kanidm.radius_secret:
|
||||
message = _("Mot de passe Wi-Fi généré avec succès.")
|
||||
sync_call("group_add_members", "radius_access", [self._user.username])
|
||||
if user.kanidm is None:
|
||||
messages.error(self.request, _("Compte DGNum inexistant."))
|
||||
else:
|
||||
message = _("Mot de passe Wi-Fi reinitialisé avec succès.")
|
||||
sync_call("call_post", f"/v1/person/{self._user.username}/_radius")
|
||||
messages.add_message(request, messages.SUCCESS, message)
|
||||
# Give access to the wifi network when the user creates its first password
|
||||
if not user.kanidm.radius_secret:
|
||||
async_to_sync(klient.group_add_members)(
|
||||
"radius_access", [user.username]
|
||||
)
|
||||
async_to_sync(klient.call_post)(f"/v1/person/{user.username}/_radius")
|
||||
|
||||
return HttpResponse(*args, headers={"HX-Redirect": self.url}, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
# INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to
|
||||
|
@ -115,8 +95,7 @@ class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView):
|
|||
template_name = "dgsi/create_self_account.html"
|
||||
form_class = CreateSelfAccountForm
|
||||
success_message = _("Compte DGNum créé avec succès")
|
||||
success_url = reverse_lazy("dgsi:profile")
|
||||
extra_context = {"backlink": "dgsi:profile"}
|
||||
success_url = reverse_lazy("dgsi:dgn-profile")
|
||||
|
||||
def dispatch(
|
||||
self, request: HttpRequest, *args: Any, **kwargs: Any
|
||||
|
@ -133,7 +112,7 @@ class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView):
|
|||
messages.WARNING,
|
||||
_("<b>Vous possédez déjà un compte DGNum !</b>"),
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:profile"))
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:dgn-profile"))
|
||||
|
||||
# Check that the Statutes and Bylaws have been accepted
|
||||
if (
|
||||
|
@ -145,7 +124,7 @@ class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView):
|
|||
messages.WARNING,
|
||||
_("Vous devez accepter les Statuts et le Règlement Intérieur."),
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:legal_documents"))
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:dgn-legal_documents"))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
@ -187,58 +166,6 @@ class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ArchiveListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "dgsi/archive_list.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
return super().get_context_data(
|
||||
document_list=sorted(
|
||||
[
|
||||
*Archive.objects.all(),
|
||||
*Bylaws.objects.all(),
|
||||
*Statutes.objects.all(),
|
||||
],
|
||||
key=lambda obj: obj.date,
|
||||
reverse=True,
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ProtectedArchiveView(LoginRequiredMixin, View):
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
||||
u = User.from_request(request)
|
||||
archive = Archive.objects.get(pk=self.kwargs["pk"])
|
||||
|
||||
if u.can_access_archive(archive):
|
||||
# INFO: When in DEBUG mode, redirect to the "real" file
|
||||
if settings.DEBUG:
|
||||
return HttpResponseRedirect(redirect_to=archive.file.url)
|
||||
|
||||
content_type, encoding = guess_type(archive.file.name)
|
||||
|
||||
if encoding is not None:
|
||||
content_type = {
|
||||
"br": "application/x-brotli",
|
||||
"bzip2": "application/x-bzip",
|
||||
"compress": "application/x-compress",
|
||||
"gzip": "application/gzip",
|
||||
"xz": "application/x-xz",
|
||||
}.get(encoding, content_type)
|
||||
|
||||
return HttpResponse(
|
||||
headers={
|
||||
"Content-Type": content_type,
|
||||
"Content-Disposition": f"inline; filename={archive.file.name}",
|
||||
"X-Accel-Redirect": f"/{settings.ARCHIVES_INTERNAL}/{archive.file.name}",
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
|
||||
class LegalDocumentsView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "dgsi/legal_documents.html"
|
||||
|
||||
|
@ -258,7 +185,7 @@ class LegalDocumentsView(LoginRequiredMixin, TemplateView):
|
|||
|
||||
|
||||
class AcceptLegalDocumentView(LoginRequiredMixin, RedirectView):
|
||||
url = reverse_lazy("dgsi:legal_documents")
|
||||
url = reverse_lazy("dgsi:dgn-legal_documents")
|
||||
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
||||
u = User.from_request(self.request)
|
||||
|
@ -306,7 +233,7 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView)
|
|||
template_name = "dgsi/create_kanidm_account.html"
|
||||
success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.")
|
||||
|
||||
success_url = reverse_lazy("dgsi:create_kanidm_user")
|
||||
success_url = reverse_lazy("dgsi:dgn-create_kanidm_user")
|
||||
|
||||
@async_to_sync
|
||||
async def form_valid(self, form):
|
||||
|
@ -346,26 +273,3 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView)
|
|||
).send()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UserListView(StaffRequiredMixin, ListView):
|
||||
model = User
|
||||
ordering = ["-date_joined"]
|
||||
|
||||
|
||||
class UserAssignVlanView(StaffRequiredMixin, HtmxPostObjectMixin, View):
|
||||
model = User
|
||||
template_name = "dgsi/partials/user_list-user.html"
|
||||
context_object_name = "person"
|
||||
|
||||
def execute_action(self, *args, **kwargs) -> None:
|
||||
self.object.register_unique_vlan()
|
||||
|
||||
|
||||
class UserDeassignVlanView(StaffRequiredMixin, HtmxPostObjectMixin, View):
|
||||
model = User
|
||||
template_name = "dgsi/partials/user_list-user.html"
|
||||
context_object_name = "person"
|
||||
|
||||
def execute_action(self, *args, **kwargs) -> None:
|
||||
self.object.reclaim_vlan()
|
||||
|
|
|
@ -5,14 +5,12 @@ from typing import Optional
|
|||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth.socialaccount.models import SocialLogin
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dgsi.models import Translation, User
|
||||
from shared.kanidm import sync_call
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,7 +39,7 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
|||
):
|
||||
messages.error(request, _("Catégorie de compte ENS interdite."))
|
||||
raise ImmediateHttpResponse(
|
||||
HttpResponseRedirect(reverse("dgsi:forbidden_account"))
|
||||
HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account"))
|
||||
)
|
||||
|
||||
# Continue with the login flow
|
||||
|
@ -58,7 +56,7 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
|||
# INFO: This should never happen
|
||||
messages.error(request, _("Méthode de connexion invalide."))
|
||||
raise ImmediateHttpResponse(
|
||||
HttpResponseRedirect(reverse("dgsi:forbidden_account"))
|
||||
HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account"))
|
||||
)
|
||||
|
||||
def _get_user(
|
||||
|
@ -93,14 +91,8 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
|||
u.username = self._get_username(request, sociallogin)
|
||||
|
||||
# Update the global permissions
|
||||
u.is_superuser = u.part_of(settings.DGSI_SUPERUSER_GROUP)
|
||||
u.is_staff = u.is_superuser or u.part_of(settings.DGSI_STAFF_GROUP)
|
||||
|
||||
# Update the e-mail address if possible
|
||||
if u.kanidm is not None:
|
||||
emails = sync_call("call_get", f"/v1/person/{u.username}/_attr/mail").data
|
||||
if emails != []:
|
||||
u.email = emails[0]
|
||||
u.is_staff = u.is_admin
|
||||
u.is_superuser = u.is_admin
|
||||
|
||||
# Save the updated user if needed
|
||||
if sociallogin.is_existing:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from asgiref.sync import async_to_sync
|
||||
from kanidm import KanidmClient
|
||||
from loadcredential import Credentials
|
||||
|
||||
|
@ -7,10 +6,3 @@ credentials = Credentials(env_prefix="DGSI_")
|
|||
klient = KanidmClient(
|
||||
uri=credentials["KANIDM_URI"], token=credentials["KANIDM_AUTH_TOKEN"]
|
||||
)
|
||||
|
||||
|
||||
def sync_call(name, *args, **kwargs):
|
||||
"""
|
||||
Wraps the required action for use in sync contexts
|
||||
"""
|
||||
return async_to_sync(getattr(klient, name))(*args, **kwargs)
|
||||
|
|
Binary file not shown.
|
@ -1,14 +1,14 @@
|
|||
# DG·SI english translation
|
||||
# Copyright (C) 2024 DGNum
|
||||
# This file is distributed under the same license as the dgsi package.
|
||||
# Tom Hubrecht <tom.hubrecht@dgnum.eu>, 2024-2025.
|
||||
# Tom Hubrecht <tom.hubrecht@dgnum.eu>, 2024.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dgsi.dgnum.eu\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-01 21:40+0100\n"
|
||||
"PO-Revision-Date: 2025-02-19 12:00+0100\n"
|
||||
"POT-Creation-Date: 2024-10-12 21:59+0200\n"
|
||||
"PO-Revision-Date: 2024-10-12 22:04+0200\n"
|
||||
"Last-Translator: Tom Hubrecht <tom.hubrecht@dgnum.eu>\n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr\n"
|
||||
|
@ -16,32 +16,37 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
||||
"X-Generator: Gtranslator 47.1\n"
|
||||
"X-Generator: Gtranslator 46.1\n"
|
||||
|
||||
#: app/settings.py:321
|
||||
msgid "Administration de DGSI"
|
||||
msgstr "DGSI Administration"
|
||||
|
||||
msgid "Informations réseau"
|
||||
msgstr "Networking informations"
|
||||
|
||||
#: dgsi/admin.py:51
|
||||
msgid "Documents DGNum"
|
||||
msgstr "DGNum Documents"
|
||||
|
||||
#: dgsi/forms.py:16
|
||||
msgid "Identifiant déjà présent dans la base de données."
|
||||
msgstr "Username already in the database."
|
||||
|
||||
#: dgsi/forms.py:22
|
||||
msgid "Identifiant"
|
||||
msgstr "Username"
|
||||
|
||||
#: dgsi/forms.py:23
|
||||
msgid "De préférence identique au login ENS de la personne concernée."
|
||||
msgstr "Preferably identical to the ENS login of the person concerned."
|
||||
|
||||
#: dgsi/forms.py:26 dgsi/forms.py:47
|
||||
msgid "Nom d'usage"
|
||||
msgstr "Name in use"
|
||||
|
||||
#: dgsi/forms.py:28 dgsi/forms.py:49
|
||||
msgid "Adresse e-mail"
|
||||
msgstr "E-mail address"
|
||||
|
||||
#: dgsi/forms.py:30
|
||||
msgid ""
|
||||
"De préférence :<br>- l'adresse <code>@ens.psl.eu</code> pour les personnes "
|
||||
"en scolarité ;<br>- l'adresse <code>@normalesup.org</code> pour les "
|
||||
|
@ -52,9 +57,11 @@ msgstr ""
|
|||
"<code>@normalesup.org</code> address for people having finished their "
|
||||
"studies;<br><b>For outsiders, the board must give its approval.</b>"
|
||||
|
||||
#: dgsi/forms.py:37
|
||||
msgid "Membre actif"
|
||||
msgstr "Active member"
|
||||
|
||||
#: dgsi/forms.py:39
|
||||
msgid ""
|
||||
"Si selectionné, la personne sera ajoutée au groupe <code>dgnum_members</"
|
||||
"code>.<br><b>L'accord préalable du bureau est nécessaire !</b>"
|
||||
|
@ -62,90 +69,88 @@ msgstr ""
|
|||
"If selected, the person will be added to the <code>dgnum_members</code> "
|
||||
"group.<br><b>Prior approval from the board is required!</b>"
|
||||
|
||||
#: dgsi/forms.py:50
|
||||
msgid "De préférence l'adresse '@ens.psl.eu'"
|
||||
msgstr "Preferably the ‘@ens.psl.eu’ address"
|
||||
|
||||
#: dgsi/models.py:24
|
||||
msgid "Nom du service proposé"
|
||||
msgstr "Name of the proposed service"
|
||||
|
||||
#: dgsi/models.py:25
|
||||
msgid "Adresse du service"
|
||||
msgstr "Address of the service"
|
||||
|
||||
#: dgsi/models.py:26
|
||||
msgid "Icône du service"
|
||||
msgstr "Icon of the service"
|
||||
|
||||
#: dgsi/models.py:36
|
||||
msgid "Date du document"
|
||||
msgstr "Document date"
|
||||
|
||||
#: dgsi/models.py:37
|
||||
msgid "Nom du document"
|
||||
msgstr "Document name"
|
||||
|
||||
#: dgsi/models.py:38
|
||||
msgid "Fichier PDF"
|
||||
msgstr "PDF file"
|
||||
|
||||
#: dgsi/models.py:60 dgsi/models.py:61
|
||||
#: dgsi/templates/dgsi/legal_documents.html:26
|
||||
msgid "Statuts"
|
||||
msgstr "Statutes"
|
||||
|
||||
#: dgsi/models.py:73 dgsi/templates/dgsi/legal_documents.html:30
|
||||
msgid "Règlement Intérieur"
|
||||
msgstr "Bylaws"
|
||||
|
||||
#: dgsi/models.py:74
|
||||
msgid "Règlements Intérieurs"
|
||||
msgstr "Bylaws"
|
||||
|
||||
msgid "Document d'archives"
|
||||
msgstr "Archive document"
|
||||
|
||||
msgid "Documents d'archives"
|
||||
msgstr "Archive documents"
|
||||
|
||||
#: dgsi/models.py:116
|
||||
msgid "Correspondance de login"
|
||||
msgstr "Login mapping"
|
||||
|
||||
#: dgsi/models.py:117
|
||||
msgid "Correspondances de login"
|
||||
msgstr "Login mappings"
|
||||
|
||||
#: dgsi/models.py:148
|
||||
msgid "Derniers Statuts acceptés"
|
||||
msgstr "Latest accepted Statutes"
|
||||
|
||||
#: dgsi/models.py:155
|
||||
msgid "Dernier Règlement Intérieur accepté"
|
||||
msgstr "Latest accepted Bylaws"
|
||||
|
||||
msgid "VLAN associé au compte"
|
||||
msgstr "VLAN assigned to the account"
|
||||
|
||||
msgid "Ce compte a déjà un VLAN associé"
|
||||
msgstr "This account already has an assigned VLAN"
|
||||
|
||||
msgid "Le VLAN {} est déjà attribué."
|
||||
msgstr "The VLAN {} is already assigned."
|
||||
|
||||
#: dgsi/templates/_legal_document.html:9
|
||||
msgid ""
|
||||
" En acceptant, vous assurez avoir lu ce document et en approuver le contenu."
|
||||
msgstr ""
|
||||
" By accepting, you confirm that you have read this document and agree with "
|
||||
"its content."
|
||||
|
||||
#: dgsi/templates/_legal_document.html:15
|
||||
msgid "Accepté"
|
||||
msgstr "Accepted"
|
||||
|
||||
msgid "Retour"
|
||||
msgstr "Go back"
|
||||
|
||||
msgid "Archives de la DGNum"
|
||||
msgstr "Archives of the DGNum"
|
||||
|
||||
#: dgsi/templates/dgsi/create_kanidm_account.html:6
|
||||
msgid "Création de compte Kanidm"
|
||||
msgstr "Kanidm account creation"
|
||||
|
||||
#: dgsi/templates/dgsi/create_kanidm_account.html:14
|
||||
#: dgsi/templates/dgsi/create_self_account.html:14
|
||||
msgid "Enregistrer"
|
||||
msgstr "Save"
|
||||
|
||||
#: dgsi/templates/dgsi/create_self_account.html:6
|
||||
msgid "Création d'un compte DGNum"
|
||||
msgstr "DGNum account creation"
|
||||
|
||||
msgid "Documents légaux"
|
||||
msgstr "Legal documents"
|
||||
|
||||
#: dgsi/templates/dgsi/legal_documents.html:12
|
||||
msgid ""
|
||||
"Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant "
|
||||
"de pouvoir créer un compte."
|
||||
|
@ -153,180 +158,162 @@ msgstr ""
|
|||
"You must accept the DGNum Statutes and Bylaws before you can create an "
|
||||
"account."
|
||||
|
||||
#: dgsi/templates/dgsi/legal_documents.html:16
|
||||
msgid ""
|
||||
"Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer "
|
||||
"un."
|
||||
msgstr "You do not yet have a DGNum account, but you can now create one."
|
||||
|
||||
#: dgsi/templates/dgsi/legal_documents.html:19
|
||||
msgid "Poursuivre la création d'un compte DGNum"
|
||||
msgstr "Continue the creation of a DGNum account"
|
||||
|
||||
#: dgsi/templates/dgsi/legal_documents.html:26
|
||||
msgid "Accepter les statuts"
|
||||
msgstr "Accept the statutes"
|
||||
|
||||
#: dgsi/templates/dgsi/legal_documents.html:30
|
||||
msgid "Accepter le règlement intérieur"
|
||||
msgstr "Accept the bylaws"
|
||||
|
||||
msgid "Profil personnel"
|
||||
msgstr "Personal profile"
|
||||
|
||||
msgid "Nom d'utilisateur :"
|
||||
msgstr "Username :"
|
||||
#: dgsi/templates/dgsi/profile.html:7
|
||||
#, python-format
|
||||
msgid "Profil de %(displayname)s"
|
||||
msgstr "Profile of %(displayname)s"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:14
|
||||
msgid "Mot de passe WiFi :"
|
||||
msgstr "WiFi password:"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:16
|
||||
msgid "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?"
|
||||
msgstr "Are you sure that you want to reset your WiFi password?"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:19
|
||||
msgid "Réinitialiser le mot de passe WiFi"
|
||||
msgstr "Reset the WiFi password"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:32
|
||||
msgid "Générer un mot de passe WiFi"
|
||||
msgstr "Generate a WiFi password:"
|
||||
|
||||
msgid "Nom d'usage :"
|
||||
msgstr "Name in use:"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:36
|
||||
msgid "Adresse e-mail :"
|
||||
msgstr "E-mail address:"
|
||||
|
||||
msgid "Télécharger le profil Wi-Fi DGNum pour iOS, iPadOS et macOS"
|
||||
msgstr "Download the DGNum Wi-Fi profile for iOS, iPadOS or macOS"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:41
|
||||
msgid "Informations techniques"
|
||||
msgstr "Technical informations"
|
||||
|
||||
msgid "Afficher"
|
||||
msgstr "Show"
|
||||
|
||||
msgid "Cacher"
|
||||
msgstr "Hide"
|
||||
|
||||
msgid "Identifiant interne :"
|
||||
msgstr "Internal identifier:"
|
||||
|
||||
msgid "VLAN attribué :"
|
||||
msgstr "Assigned VLAN:"
|
||||
|
||||
msgid "Pas de VLAN attribué."
|
||||
msgstr "No assigned VLAN."
|
||||
#: dgsi/templates/dgsi/profile.html:44
|
||||
msgid "Identifiant unique :"
|
||||
msgstr "Unique identifier:"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:53
|
||||
msgid "Membre des groupes suivants :"
|
||||
msgstr "Member of the following groups:"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:64
|
||||
msgid "Pas de compte DGNum répertorié."
|
||||
msgstr "No DGNum account found."
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:67
|
||||
msgid "Créer un compte DGNum"
|
||||
msgstr "Create a DGNum account"
|
||||
|
||||
#: dgsi/templates/dgsi/service_list.html:6
|
||||
msgid "Services accessibles via la DGNum"
|
||||
msgstr "Services accessible via the DGNum"
|
||||
|
||||
msgid "Nom d'utilisateur"
|
||||
msgstr "Username"
|
||||
|
||||
msgid "VLAN attribué"
|
||||
msgstr "Assigned VLAN"
|
||||
|
||||
msgid "Gestion du VLAN"
|
||||
msgstr "VLAN management"
|
||||
|
||||
msgid "Désallouer"
|
||||
msgstr "Deassign"
|
||||
|
||||
msgid "Allouer"
|
||||
msgstr "Assign"
|
||||
|
||||
#: dgsi/views.py:30
|
||||
msgid "Mon profil"
|
||||
msgstr "My profile"
|
||||
|
||||
msgid "Aide et Documention"
|
||||
msgstr "Help and documentation"
|
||||
|
||||
#: dgsi/views.py:31
|
||||
msgid "Documents Légaux"
|
||||
msgstr "Legal Documents"
|
||||
|
||||
#: dgsi/views.py:32
|
||||
msgid "Services proposés"
|
||||
msgstr "Services offered"
|
||||
|
||||
msgid "Archives"
|
||||
msgstr "Archives"
|
||||
|
||||
#: dgsi/views.py:39
|
||||
msgid "Créer un compte Kanidm"
|
||||
msgstr "Create a Kanidm account"
|
||||
|
||||
msgid "Liste des comptes"
|
||||
msgstr "List of accounts"
|
||||
|
||||
#: dgsi/views.py:43 shared/templates/_hero.html:76
|
||||
msgid "Interface d'administration"
|
||||
msgstr "Administration interface"
|
||||
|
||||
msgid "<b>Veuillez créer un compte DGNum.</b>"
|
||||
msgstr "<b>Please create a DGNum account.</b>"
|
||||
|
||||
msgid "<b>Veuillez générer un mot de passe Wi-Fi.</b>"
|
||||
msgstr "<b>Please generate a WiFi password.</b>"
|
||||
|
||||
#: dgsi/views.py:80
|
||||
msgid "Compte DGNum inexistant."
|
||||
msgstr "No existing DGNum account."
|
||||
|
||||
msgid "Mot de passe Wi-Fi généré avec succès."
|
||||
msgstr "Wi-Fi password generated successfully."
|
||||
|
||||
msgid "Mot de passe Wi-Fi reinitialisé avec succès."
|
||||
msgstr "Wi-Fi password reset successfully."
|
||||
|
||||
#: dgsi/views.py:97
|
||||
msgid "Compte DGNum créé avec succès"
|
||||
msgstr "DGNum account successfully created"
|
||||
|
||||
#: dgsi/views.py:113
|
||||
msgid "<b>Vous possédez déjà un compte DGNum !</b>"
|
||||
msgstr "<b>You already have a DGNum account!</b>"
|
||||
|
||||
#: dgsi/views.py:125
|
||||
msgid "Vous devez accepter les Statuts et le Règlement Intérieur."
|
||||
msgstr "You must accept the Statutes and the Bylaws."
|
||||
|
||||
#: dgsi/views.py:204
|
||||
#, python-format
|
||||
msgid "Type de document invalide : %(kind)s"
|
||||
msgstr "Invalid document type: %(kind)s"
|
||||
|
||||
#: dgsi/views.py:234
|
||||
#, python-format
|
||||
msgid "Compte DGNum pour %(displayname)s [%(name)s] créé."
|
||||
msgstr "DGNum account for %(displayname)s [%(name)s] created."
|
||||
|
||||
#: shared/account.py:40
|
||||
msgid "Catégorie de compte ENS interdite."
|
||||
msgstr "ENS account category not permitted."
|
||||
|
||||
#: shared/account.py:57
|
||||
msgid "Méthode de connexion invalide."
|
||||
msgstr "Invalid connection method."
|
||||
|
||||
#: shared/templates/_footer.html:4
|
||||
msgid ""
|
||||
"Logiciel développé pour et par la <a href=\"https://dgnum.eu\">DGNum</a>."
|
||||
msgstr ""
|
||||
"Software developed for and by the <a href=\"https://dgnum.eu\">DGNum</a>."
|
||||
"Software developed for and by the <a href=‘https://dgnum.eu’>DGNum</a>."
|
||||
|
||||
#: shared/templates/_hero.html:18 shared/templates/account/logout.html:6
|
||||
msgid "Déconnexion"
|
||||
msgstr "Logout"
|
||||
|
||||
#: shared/templates/_hero.html:27 shared/templates/socialaccount/login.html:6
|
||||
msgid "Connexion"
|
||||
msgstr "Login"
|
||||
|
||||
#: shared/templates/_hero.html:40
|
||||
msgid "Choix de la langue"
|
||||
msgstr "Language selection"
|
||||
|
||||
#: shared/templates/account/login.html:7
|
||||
msgid "Connexion via un compte tiers"
|
||||
msgstr "Connection via a third-party account"
|
||||
|
||||
#: shared/templates/account/logout.html:10
|
||||
msgid "Êtes vous certain·e de vouloir vous déconnecter ?"
|
||||
msgstr "Are you sure you want to log out?"
|
||||
|
||||
#: shared/templates/account/logout.html:16
|
||||
msgid "Se déconnecter"
|
||||
msgstr "Log out"
|
||||
|
||||
#: shared/templates/accounts/forbidden_category.html:6
|
||||
msgid "Connexion impossible"
|
||||
msgstr "Unable to connect"
|
||||
|
||||
#: shared/templates/accounts/forbidden_category.html:10
|
||||
msgid ""
|
||||
"Vos informations ne permettent pas de vous identifier auprès de la DGNum."
|
||||
"<br>Si vous pensez qu'il s'agit une erreur, merci de nous contacter à "
|
||||
|
@ -336,28 +323,29 @@ msgstr ""
|
|||
"this is a mistake, please contact us at: <a href=\"mailto:contact@dgnum."
|
||||
"eu\">contact@dgnum.eu</a>"
|
||||
|
||||
#: shared/templates/socialaccount/authentication_error.html:7
|
||||
msgid "Erreur lors de la connexion"
|
||||
msgstr "Error during login"
|
||||
|
||||
#: shared/templates/socialaccount/authentication_error.html:11
|
||||
msgid ""
|
||||
"Une erreur est survenue lors de votre tentative de connexion avec un compte "
|
||||
"tiers."
|
||||
msgstr ""
|
||||
"An error has occurred while trying to login with a third-party account."
|
||||
|
||||
#: shared/templates/socialaccount/login.html:11
|
||||
#, python-format
|
||||
msgid "Se connecter via un compte <b>%(provider)s</b>"
|
||||
msgstr "Log in with a <b>%(provider)s</b> account"
|
||||
|
||||
#: shared/templates/socialaccount/login.html:16
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Vous vous apprêtez à vous connecter à l'aide d'un compte tiers provenant de "
|
||||
"%(provider)s."
|
||||
msgstr "You are about to log in using a third-party account from %(provider)s."
|
||||
|
||||
#: shared/templates/socialaccount/login.html:21
|
||||
msgid "Continuer"
|
||||
msgstr "Continue"
|
||||
|
||||
#, python-format
|
||||
#~ msgid "Profil de %(displayname)s"
|
||||
#~ msgstr "Profile of %(displayname)s"
|
||||
|
|
22
src/shared/static/bulma/bulma.scss
vendored
22
src/shared/static/bulma/bulma.scss
vendored
|
@ -13,15 +13,6 @@ $dark: rgb(46, 46, 46);
|
|||
@use "./sass/utilities/mixins" as mx;
|
||||
@use "./sass/utilities/initial-variables.scss" as iv;
|
||||
|
||||
tbody {
|
||||
&.is-centered {
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
|
@ -29,19 +20,6 @@ body {
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-archive {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: calc(0.5 * var(--bulma-block-spacing));
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bt-link {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
|
|
@ -1,39 +1,11 @@
|
|||
const init = ($node) => {
|
||||
const q = (query, f) => ($node.querySelectorAll(query) || []).forEach(f);
|
||||
|
||||
q(".notification .delete", ($delete) => {
|
||||
const $notification = $delete.parentNode;
|
||||
const dismiss = () => $notification.remove();
|
||||
|
||||
$delete.addEventListener("click", dismiss);
|
||||
setTimeout(dismiss, 15000);
|
||||
});
|
||||
|
||||
q("[data-toggle]", ($toggle) => {
|
||||
const target = $node.querySelector($toggle.dataset.target);
|
||||
|
||||
$toggle.addEventListener("click", () => {
|
||||
if ($toggle.dataset.toggle === "on") {
|
||||
$toggle.dataset.toggle = "off";
|
||||
$toggle.innerHTML = $toggle.dataset.offHtml;
|
||||
target.classList.remove($toggle.dataset.class);
|
||||
} else {
|
||||
$toggle.dataset.toggle = "on";
|
||||
$toggle.innerHTML = $toggle.dataset.onHtml;
|
||||
target.classList.add($toggle.dataset.class);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
q("[data-select]", ($input) => {
|
||||
$input.addEventListener("focus", () => {
|
||||
$input.select();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.body.addEventListener("htmx:load", (e) => {
|
||||
init(e.detail.elt);
|
||||
});
|
||||
(document.querySelectorAll(".notification .delete") || []).forEach(
|
||||
($delete) => {
|
||||
const $notification = $delete.parentNode;
|
||||
const dismiss = () => $notification.parentNode.removeChild($notification);
|
||||
|
||||
$delete.addEventListener("click", dismiss);
|
||||
setTimeout(dismiss, 15000);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
1
src/shared/static/js/htmx.min.js
vendored
1
src/shared/static/js/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -2,7 +2,5 @@
|
|||
|
||||
<footer class="footer has-text-centered">
|
||||
<b>{% blocktrans %}Logiciel développé pour et par la <a href="https://dgnum.eu">DGNum</a>.{% endblocktrans %}</b>
|
||||
<hr class="my-2">
|
||||
<a class="tag is-medium" href="{% url "dgsi:mentions_legales" %}">Mentions Légales</a>
|
||||
{% django_browser_reload_script %}
|
||||
</footer>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="columns mx-6">
|
||||
<div class="column is-three-quarters">
|
||||
<h1 class="title">
|
||||
<a href="{% url 'dgsi:index' %}" class="has-text-dark">Dossier Général des Services Informagiques</a>
|
||||
<a href="{% url 'dgsi:dgn-index' %}" class="has-text-dark">Dossier Général des Services Informagiques</a>
|
||||
</h1>
|
||||
<h2 class="subtitle mt-2">Système d'information de la DGNum</h2>
|
||||
</div>
|
||||
|
@ -15,19 +15,19 @@
|
|||
<a href="{% url 'account_logout' %}"
|
||||
class="button is-light is-fullwidth">
|
||||
<span>
|
||||
<span>{% trans "Déconnexion" %}</span>
|
||||
<span class="icon">
|
||||
<i class="ti ti-door-exit"></i>
|
||||
</span>
|
||||
<span>{% trans "Déconnexion" %}</span>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'account_login' %}" class="button is-fullwidth is-light">
|
||||
<span>
|
||||
<span>{% trans "Connexion" %}</span>
|
||||
<span class="icon">
|
||||
<i class="ti ti-door-enter"></i>
|
||||
</span>
|
||||
<span>{% trans "Connexion" %}</span>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,33 +1,16 @@
|
|||
{% load django_htmx sass_tags static %}
|
||||
{% load sass_tags static %}
|
||||
|
||||
<!-- Icons -->
|
||||
<link href="{% static 'favicon.ico' %}" rel="icon" />
|
||||
<link href="{% static 'apple-touch-icon.png' %}" rel="apple-touch-icon" />
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
href="{% static 'favicon-16x16.png' %}"
|
||||
sizes="16x16" />
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
href="{% static 'favicon-32x32.png' %}"
|
||||
sizes="32x32" />
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
href="{% static 'android-chrome-192x192.png' %}"
|
||||
sizes="192x192" />
|
||||
<link rel="icon" type="image/png" href="{% static 'favicon-16x16.png' %}" sizes="16x16" />
|
||||
<link rel="icon" type="image/png" href="{% static 'favicon-32x32.png' %}" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="{% static 'android-chrome-192x192.png' %}" sizes="192x192" />
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="{% sass_src 'bulma/bulma.scss' %}"
|
||||
rel="stylesheet"
|
||||
type="text/css" />
|
||||
<link href="{% static 'tabler-icons/tabler-icons.min.css' %}"
|
||||
rel="stylesheet"
|
||||
type="text/css" />
|
||||
<link href="{% sass_src 'bulma/bulma.scss' %}" rel="stylesheet" type="text/css" />
|
||||
<link href="{% static 'tabler-icons/tabler-icons.min.css' %}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<!-- JS -->
|
||||
<script src="{% static 'js/dgsi.js' %}"></script>
|
||||
<script defer src="{% static 'js/htmx.min.js' %}"></script>
|
||||
<script defer
|
||||
data-domain="profil.dgnum.eu"
|
||||
src="https://analytics.dgnum.eu/js/script.js"></script>
|
||||
{% django_htmx_script %}
|
||||
<script defer data-domain="profil.dgnum.eu" src="https://analytics.dgnum.eu/js/script.js"></script>
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="keywords" content="dgnum,dgsi,ens" />
|
||||
<meta name="description" content="Système d'information de la DGNum" />
|
||||
<meta name="htmx-config"
|
||||
content='{"defaultSwapStyle":"outerHTML","requestClass":"is-loading"}' />
|
||||
<title>DGNum</title>
|
||||
|
||||
{% block extra_head %}
|
||||
|
@ -15,13 +13,16 @@
|
|||
{% include "_links.html" %}
|
||||
</head>
|
||||
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<body>
|
||||
{% include "_hero.html" %}
|
||||
|
||||
<section class="container is-max-widescreen py-6 px-4">
|
||||
<section class="container is-max-widescreen py-6">
|
||||
<div id="notifications">
|
||||
{% for message in messages %}
|
||||
{% include "partials/notification.html" %}
|
||||
<article class="notification is-light has-text-centered {{ message.tags }}">
|
||||
<button class="delete"></button>
|
||||
{{ message|safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<article class="notification is-light has-text-centered {{ message.tags }}">
|
||||
<button class="delete"></button>
|
||||
{{ message|safe }}
|
||||
</article>
|
Loading…
Add table
Reference in a new issue