Compare commits
108 commits
profile-ge
...
main
Author | SHA1 | Date | |
---|---|---|---|
24f825c1dd | |||
304fe1f249 | |||
41396ccae2 | |||
37ddb5f075 | |||
868c406af9 | |||
615eb6041f | |||
5a84ea6e0f | |||
faa8ea051a | |||
6921373d6c | |||
f6fcd90622 | |||
6caf3dbf61 | |||
833c855b5c | |||
c291112350 | |||
61bb59d244 | |||
9c4413faa1 | |||
3fa4591665 | |||
119867a91e | |||
61e3d4ed19 | |||
a88d31541c | |||
f4428ace59 | |||
fb70bf13f8 | |||
6a581fcec4 | |||
fa8f8a214f | |||
c7e13b76f2 | |||
dc8f89be86 | |||
2f5482a091 | |||
97dc77fd5d | |||
224f9df858 | |||
45bd436ea7 | |||
997fd254ca | |||
bf5bce5fc5 | |||
2978c1facb | |||
7885252b4a | |||
97e3ae9702 | |||
5019b89ef2 | |||
be0cf4c0f5 | |||
b33f13db30 | |||
941f2b031e | |||
3b3f2dd34d | |||
9119ad38b0 | |||
5381b0379b | |||
88e2c25ce4 | |||
48005ae251 | |||
91d5d68da3 | |||
59fa740950 | |||
088d6d613a | |||
1080ec0c44 | |||
7064a3aa4b | |||
49f1133fed | |||
fee28c598c | |||
76c07e3086 | |||
7537b26fbe | |||
8a46e4ddb5 | |||
5f0dfff4ae | |||
6c18ec3855 | |||
e370977aac | |||
a9d369d55d | |||
b5cedebda1 | |||
abdcb2c8ad | |||
1dc8305027 | |||
2490d83459 | |||
b9f165c1e6 | |||
e10d33176b | |||
e8ce6f343b | |||
87e13b357e | |||
cab8369558 | |||
8c3cba0af8 | |||
9b7f4f17e8 | |||
cfa14a6aae | |||
39623a8802 | |||
0c54fd29ab | |||
26fdcbe8ef | |||
ef9877ea60 | |||
ef80f9389b | |||
01597ffb7b | |||
5ec6a482cc | |||
e31731f8e6 | |||
7c11b0de4b | |||
283815d555 | |||
ea3c5cf6fd | |||
36ccc6be24 | |||
cd8859f610 | |||
5af8e2fd24 | |||
c3f0e70384 | |||
35d6a7fa8c | |||
97535a6bc0 | |||
1d2f4a5866 | |||
1732249a2d | |||
763c2dfcbb | |||
590ac25b8c | |||
0272edc266 | |||
fd5e036f49 | |||
82123717cc | |||
99764928c6 | |||
8baad602c6 | |||
abc71eb52d | |||
2817054e7e | |||
2642a64116 | |||
06351ca22e | |||
3b2937b6d1 | |||
f56961c7bb | |||
c02b29a6f5 | |||
855664e410 | |||
aed32e0725 | |||
7491fdb376 | |||
7581bf59df | |||
8599992dd7 | |||
772ca45292 |
67 changed files with 2577 additions and 176 deletions
1
.credentials/EMAIL_HOST
Normal file
1
.credentials/EMAIL_HOST
Normal file
|
@ -0,0 +1 @@
|
|||
localhost
|
1
.credentials/FROM_EMAIL
Normal file
1
.credentials/FROM_EMAIL
Normal file
|
@ -0,0 +1 @@
|
|||
Délégation Générale Numérique <dgsi@localhost>
|
1
.credentials/KANIDM_URI
Normal file
1
.credentials/KANIDM_URI
Normal file
|
@ -0,0 +1 @@
|
|||
https://sso.dgnum.eu
|
1
.credentials/SERVER_EMAIL
Normal file
1
.credentials/SERVER_EMAIL
Normal file
|
@ -0,0 +1 @@
|
|||
dgsi@localhost
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,9 +2,11 @@
|
|||
.pre-commit-config.yaml
|
||||
|
||||
db.sqlite3
|
||||
db.sqlite3.*
|
||||
__pycache__/
|
||||
|
||||
.static/*
|
||||
!.static/.gitkeep
|
||||
src/shared/static/bulma/bulma.css
|
||||
src/shared/static/bulma/bulma.css.map
|
||||
.credentials/KANIDM_AUTH_TOKEN
|
||||
|
|
65
default.nix
65
default.nix
|
@ -8,22 +8,44 @@ let
|
|||
src = ./.;
|
||||
|
||||
hooks = {
|
||||
# JS hooks
|
||||
eslint.enable = true;
|
||||
|
||||
# Python hooks
|
||||
ruff.enable = true;
|
||||
black.enable = true;
|
||||
isort.enable = true;
|
||||
black = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
isort = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
ruff = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
# Nix Hooks
|
||||
statix.enable = true;
|
||||
deadnix.enable = true;
|
||||
statix = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
deadnix = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
# Misc Hooks
|
||||
commitizen.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
python = pkgs.python312.override {
|
||||
packageOverrides =
|
||||
self: _:
|
||||
pkgs.lib.genAttrs (builtins.attrNames (builtins.readDir ./pkgs)) (
|
||||
p: self.callPackage ./pkgs/${p} { }
|
||||
);
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
|
@ -31,28 +53,32 @@ in
|
|||
name = "dgsi.dev";
|
||||
|
||||
packages = [
|
||||
pkgs.jq
|
||||
pkgs.dart-sass
|
||||
pkgs.gettext
|
||||
pkgs.jq
|
||||
|
||||
# Python dependencies
|
||||
(pkgs.python3.withPackages (
|
||||
(python.withPackages (
|
||||
ps:
|
||||
[
|
||||
ps.daphne
|
||||
ps.django
|
||||
ps.django-allauth
|
||||
ps.django-allauth-cas
|
||||
ps.django-browser-reload
|
||||
ps.django-bulma-forms
|
||||
ps.django-compressor
|
||||
ps.django-debug-toolbar
|
||||
ps.django-types
|
||||
ps.django-import-export
|
||||
ps.django-sass-processor
|
||||
ps.django-sass-processor-dart-sass
|
||||
ps.django-stubs
|
||||
ps.django-unfold
|
||||
ps.ipython
|
||||
ps.loadcredential
|
||||
ps.pykanidm
|
||||
ps.python-cas
|
||||
]
|
||||
++ (builtins.map (p: ps.callPackage ./pkgs/${p} { }) [
|
||||
"django-browser-reload"
|
||||
"django-bulma-forms"
|
||||
"django-sass-processor"
|
||||
"django-sass-processor-dart-sass"
|
||||
"pykanidm"
|
||||
])
|
||||
++ ps.django-allauth.optional-dependencies.saml
|
||||
))
|
||||
] ++ check.enabledPackages;
|
||||
|
||||
|
@ -60,6 +86,7 @@ in
|
|||
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
|
||||
DGSI_DEBUG = "true";
|
||||
DGSI_STATIC_ROOT = builtins.toString ./.static;
|
||||
DGSI_MEDIA_ROOT = builtins.toString ./.media;
|
||||
DGSI_KANIDM_CLIENT = "dgsi_test";
|
||||
};
|
||||
|
||||
|
|
|
@ -8,15 +8,15 @@
|
|||
"repo": "git-hooks.nix"
|
||||
},
|
||||
"branch": "master",
|
||||
"revision": "e35aed5fda3cc79f88ed7f1795021e559582093a",
|
||||
"url": "https://github.com/cachix/pre-commit-hooks.nix/archive/e35aed5fda3cc79f88ed7f1795021e559582093a.tar.gz",
|
||||
"hash": "1bq0yrjmkddj964s2q6393nwp4mqrlmc2i5wsy992r034awyywp1"
|
||||
"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-24.11pre630522.3305b2b25e4a/nixexprs.tar.xz",
|
||||
"hash": "1bg240s2jbyvdixpy14rc4fcn9zrjf36mcd2xv59rcxx508gwhi2"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre685691.28b5b8af91ff/nixexprs.tar.xz",
|
||||
"hash": "14ldh9js6l9nqch7j8z6nhyplxc5d9jw375pg8h4s24m7x37xnvy"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
|
|
12
pkgs/django-allauth-cas/01-setup.patch
Normal file
12
pkgs/django-allauth-cas/01-setup.patch
Normal file
|
@ -0,0 +1,12 @@
|
|||
diff --git a/setup.py b/setup.py
|
||||
index fb06ec0..506677f 100644
|
||||
--- a/setup.py
|
||||
+++ b/setup.py
|
||||
@@ -52,7 +52,6 @@ setup(
|
||||
install_requires=[
|
||||
"django-allauth",
|
||||
"python-cas",
|
||||
- "six",
|
||||
],
|
||||
extras_require={
|
||||
"docs": ["sphinx"],
|
39
pkgs/django-allauth-cas/02-registry.patch
Normal file
39
pkgs/django-allauth-cas/02-registry.patch
Normal file
|
@ -0,0 +1,39 @@
|
|||
diff --git a/allauth_cas/signals.py b/allauth_cas/signals.py
|
||||
index 36c9b24..530c26e 100644
|
||||
--- a/allauth_cas/signals.py
|
||||
+++ b/allauth_cas/signals.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-from allauth.account.adapter import get_adapter
|
||||
+from allauth.socialaccount.adapter import get_adapter
|
||||
from allauth.account.utils import get_next_redirect_url
|
||||
from allauth.socialaccount import providers
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
@@ -14,7 +14,7 @@ def cas_account_logout(sender, request, **kwargs):
|
||||
if not provider_id:
|
||||
return
|
||||
|
||||
- provider = providers.registry.by_id(provider_id, request)
|
||||
+ provider = get_adapter(request).get_provider(request, provider_id)
|
||||
|
||||
if not provider.message_suggest_caslogout_on_logout(request):
|
||||
return
|
||||
diff --git a/allauth_cas/views.py b/allauth_cas/views.py
|
||||
index d08e354..9e81e53 100644
|
||||
--- a/allauth_cas/views.py
|
||||
+++ b/allauth_cas/views.py
|
||||
@@ -1,5 +1,5 @@
|
||||
import cas
|
||||
-from allauth.account.adapter import get_adapter
|
||||
+from allauth.socialaccount.adapter import get_adapter
|
||||
from allauth.account.utils import get_next_redirect_url
|
||||
from allauth.socialaccount import providers
|
||||
from allauth.socialaccount.helpers import (
|
||||
@@ -56,7 +56,7 @@ class CASAdapter:
|
||||
"""
|
||||
Returns a provider instance for the current request.
|
||||
"""
|
||||
- return providers.registry.by_id(self.provider_id, self.request)
|
||||
+ return get_adapter(self.request).get_provider(self.request, self.provider_id)
|
||||
|
||||
def complete_login(self, request, response):
|
||||
"""
|
49
pkgs/django-allauth-cas/default.nix
Normal file
49
pkgs/django-allauth-cas/default.nix
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
django-allauth,
|
||||
python-cas,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-allauth-cas";
|
||||
version = "unstable-2024-01-25";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "jlucasp25";
|
||||
repo = "django-allauth-cas";
|
||||
rev = "77e02f3796cd564a9a0c48b5b568b14d4d4c5687";
|
||||
hash = "sha256-y/IquXl/4+9MJmsgbWtPun3tBbRJ4kJFzWo5c+5WeHk=";
|
||||
};
|
||||
|
||||
patches = [
|
||||
./01-setup.patch
|
||||
./02-registry.patch
|
||||
];
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django-allauth
|
||||
python-cas
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
"allauth_cas"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "CAS support for django-allauth";
|
||||
homepage = "https://github.com/jlucasp25/django-allauth-cas";
|
||||
changelog = "https://github.com/jlucasp25/django-allauth-cas/blob/${src.rev}/CHANGELOG.rst";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
96
pkgs/django-allauth/default.nix
Normal file
96
pkgs/django-allauth/default.nix
Normal file
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
pythonOlder,
|
||||
# build-system
|
||||
setuptools,
|
||||
# dependencies
|
||||
django,
|
||||
python3-openid,
|
||||
requests,
|
||||
requests-oauthlib,
|
||||
pyjwt,
|
||||
# optional-dependencies
|
||||
python3-saml,
|
||||
qrcode,
|
||||
fido2,
|
||||
# tests
|
||||
pillow,
|
||||
pytestCheckHook,
|
||||
pytest-django,
|
||||
# passthru tests
|
||||
dj-rest-auth,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-allauth";
|
||||
version = "64.2.1";
|
||||
pyproject = true;
|
||||
|
||||
disabled = pythonOlder "3.7";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "pennersr";
|
||||
repo = "django-allauth";
|
||||
rev = "refs/tags/${version}";
|
||||
hash = "sha256-JKjM+zqrXidxpbi+fo6wbvdXlw2oDYH51EsvQ5yp3R8=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
setuptools
|
||||
];
|
||||
|
||||
propagatedBuildInputs = [
|
||||
django
|
||||
];
|
||||
|
||||
passthru.optional-dependencies = {
|
||||
mfa = [
|
||||
qrcode
|
||||
fido2
|
||||
];
|
||||
openid = [
|
||||
python3-openid
|
||||
];
|
||||
saml = [
|
||||
python3-saml
|
||||
];
|
||||
socialaccount = [
|
||||
pyjwt
|
||||
requests
|
||||
requests-oauthlib
|
||||
] ++ pyjwt.optional-dependencies.crypto;
|
||||
steam = [
|
||||
python3-openid
|
||||
];
|
||||
};
|
||||
|
||||
pythonImportsCheck = [
|
||||
"allauth"
|
||||
];
|
||||
|
||||
nativeCheckInputs = [
|
||||
pillow
|
||||
pytestCheckHook
|
||||
pytest-django
|
||||
] ++ lib.flatten (builtins.attrValues passthru.optional-dependencies);
|
||||
|
||||
disabledTests = [
|
||||
# Tests require network access
|
||||
"test_login"
|
||||
];
|
||||
|
||||
passthru.tests = {
|
||||
inherit dj-rest-auth;
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
changelog = "https://github.com/pennersr/django-allauth/blob/${version}/ChangeLog.rst";
|
||||
description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication";
|
||||
downloadPage = "https://github.com/pennersr/django-allauth";
|
||||
homepage = "https://www.intenct.nl/projects/django-allauth";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ derdennisop ];
|
||||
};
|
||||
}
|
40
pkgs/django-unfold/default.nix
Normal file
40
pkgs/django-unfold/default.nix
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
poetry-core,
|
||||
django,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-unfold";
|
||||
version = "0.39.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "unfoldadmin";
|
||||
repo = "django-unfold";
|
||||
rev = version;
|
||||
hash = "sha256-CmmlTx2eLcANc6ANy25ii1KVebkmUEJmDCe+/RwakAg=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
poetry-core
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
"unfold"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Modern Django admin theme for seamless interface development";
|
||||
homepage = "https://github.com/unfoldadmin/django-unfold";
|
||||
changelog = "https://github.com/unfoldadmin/django-unfold/blob/${src.rev}/CHANGELOG.md";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
34
pkgs/loadcredential/default.nix
Normal file
34
pkgs/loadcredential/default.nix
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "loadcredential";
|
||||
version = "1.2";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "Tom-Hubrecht";
|
||||
repo = "loadcredential";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-rNWFD89h1p1jYWLcfzsa/w8nK3bR4aVJsUPx0UtZnIw=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
pythonImportsCheck = [ "loadcredential" ];
|
||||
|
||||
meta = {
|
||||
description = "A simple python package to read credentials passed through systemd's LoadCredential, with a fallback on env variables ";
|
||||
homepage = "https://github.com/Tom-Hubrecht/loadcredential";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = [ ]; # with lib.maintainers; [ thubrecht ];
|
||||
};
|
||||
}
|
|
@ -11,14 +11,14 @@
|
|||
|
||||
buildPythonPackage rec {
|
||||
pname = "kanidm";
|
||||
version = "1.1.0-rc.16";
|
||||
version = "1.3.3";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "kanidm";
|
||||
repo = "kanidm";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-NH9V5KKI9LAtJ2/WuWtUJUzkjVMfO7Q5NQkK7Ys2olU=";
|
||||
hash = "sha256-W5G7osV4du6w/BfyY9YrDzorcLNizRsoz70RMfO2AbY=";
|
||||
};
|
||||
|
||||
sourceRoot = "source/pykanidm";
|
||||
|
|
43
pkgs/python-cas/default.nix
Normal file
43
pkgs/python-cas/default.nix
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
lxml,
|
||||
requests,
|
||||
six,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "python-cas";
|
||||
version = "1.6.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "python-cas";
|
||||
repo = "python-cas";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-0lpjG/Sma0tJGtahiFE1CjvTyswrBUp+F6f1S65b+lk=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
propagatedBuildInputs = [
|
||||
lxml
|
||||
requests
|
||||
six
|
||||
];
|
||||
|
||||
pythonImportsCheck = [ "cas" ];
|
||||
|
||||
meta = with lib; {
|
||||
description = "Python CAS (Central Authentication Service) client library support CAS 1.0/2.0/3.0";
|
||||
homepage = "https://github.com/python-cas/python-cas";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -21,6 +21,7 @@ Repository = "https://git.dgnum.eu/DGNum/dgsi"
|
|||
|
||||
[tool.djlint]
|
||||
blank_line_after_tag = "load,extends"
|
||||
custom_blocks = "slot,element"
|
||||
format_js = true
|
||||
indent = 2
|
||||
max_blank_lines = 1
|
||||
|
@ -28,3 +29,6 @@ profile = "django"
|
|||
|
||||
[tool.djlint.js]
|
||||
indent_size = 4
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
|
|
@ -4,6 +4,8 @@ Django settings for the DGSI project.
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from loadcredential import Credentials
|
||||
|
||||
credentials = Credentials(env_prefix="DGSI_")
|
||||
|
@ -20,24 +22,35 @@ DEBUG = credentials.get_json("DEBUG", False)
|
|||
|
||||
ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", [])
|
||||
|
||||
ADMINS = credentials.get_json("ADMINS", [])
|
||||
|
||||
###
|
||||
# List the installed applications
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"daphne",
|
||||
# Unfold apps
|
||||
"unfold",
|
||||
"unfold.contrib.import_export",
|
||||
# Django standard apps
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
# Custom apps
|
||||
"shared.staticfiles.StaticFilesApp", # Overrides the default staticfiles app to filter out the sccs sources
|
||||
"django_browser_reload",
|
||||
"sass_processor",
|
||||
"bulma",
|
||||
"import_export",
|
||||
# Authentication
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"allauth.socialaccount.providers.openid_connect",
|
||||
# "allauth.socialaccount.providers.saml",
|
||||
"allauth_cas",
|
||||
"shared.cas",
|
||||
# Main app
|
||||
"dgsi",
|
||||
]
|
||||
|
@ -48,14 +61,33 @@ INSTALLED_APPS = [
|
|||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
###
|
||||
# Logging configuration
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": credentials.get("LOG_LEVEL", "WARNING"),
|
||||
},
|
||||
}
|
||||
|
||||
###
|
||||
# The main url configuration
|
||||
|
||||
|
@ -86,20 +118,34 @@ TEMPLATES = [
|
|||
]
|
||||
|
||||
###
|
||||
# ASGI application configuration
|
||||
# WSGI application configuration
|
||||
|
||||
ASGI_APPLICATION = "app.asgi.application"
|
||||
WSGI_APPLICATION = "app.wsgi.application"
|
||||
|
||||
###
|
||||
# E-Mail configuration
|
||||
|
||||
DEFAULT_FROM_EMAIL = credentials["FROM_EMAIL"]
|
||||
EMAIL_HOST = credentials.get("EMAIL_HOST", "localhost")
|
||||
EMAIL_HOST_PASSWORD = credentials.get("EMAIL_HOST_PASSWORD", "")
|
||||
EMAIL_HOST_USER = credentials.get("EMAIL_HOST_USER", "")
|
||||
EMAIL_PORT = credentials.get_json("EMAIL_PORT", 465)
|
||||
EMAIL_USE_SSL = credentials.get("EMAIL_USE_SSL", False)
|
||||
SERVER_EMAIL = credentials["SERVER_EMAIL"]
|
||||
|
||||
###
|
||||
# Database configuration
|
||||
# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
DATABASES = credentials.get_json(
|
||||
"DATABASES",
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
###
|
||||
|
@ -110,7 +156,6 @@ AUTHENTICATION_BACKENDS = [
|
|||
"allauth.account.auth_backends.AuthenticationBackend",
|
||||
]
|
||||
|
||||
SOCIALACCOUNT_ONLY = True
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
"openid_connect": {
|
||||
"OAUTH_PKCE_ENABLED": True,
|
||||
|
@ -122,22 +167,91 @@ SOCIALACCOUNT_PROVIDERS = {
|
|||
"secret": credentials["KANIDM_SECRET"],
|
||||
"settings": {
|
||||
"server_url": f"https://sso.dgnum.eu/oauth2/openid/{credentials['KANIDM_CLIENT']}",
|
||||
"color": "primary",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
"cas": {
|
||||
"APP": {
|
||||
"provider_id": "ens_cas",
|
||||
"name": "CAS ENS",
|
||||
"settings": {"color": "danger"},
|
||||
},
|
||||
},
|
||||
# "saml": {
|
||||
# "APPS": [
|
||||
# {
|
||||
# "provider_id": "ens_saml",
|
||||
# "name": "SSO ENS",
|
||||
# "client_id": "ens",
|
||||
# "settings": {
|
||||
# "color": "info",
|
||||
# "idp": {
|
||||
# "entity_id": "https://federation-test.ens.psl.eu/idp/shibboleth",
|
||||
# "metadata_url": "https://federation-test.ens.psl.eu/idp/shibboleth",
|
||||
# },
|
||||
# # Our configuration
|
||||
# "sp": {
|
||||
# "entity_id": "https://profil.dgnum.eu/accounts/saml/ens/metadata",
|
||||
# },
|
||||
# "advanced": {
|
||||
# "authn_request_signed": True,
|
||||
# "metadata_signed": True,
|
||||
# "private_key": credentials["X509_KEY"],
|
||||
# "x509cert": credentials["X509_CERT"],
|
||||
# "want_assertion_encrypted": False,
|
||||
# "want_attribute_statement": True,
|
||||
# "want_name_id": True,
|
||||
# },
|
||||
# "organization": {
|
||||
# "en": {
|
||||
# "name": "Délégation Générale Numérique",
|
||||
# "displayname": "Délégation Générale Numérique",
|
||||
# "url": "https://dgnum.eu",
|
||||
# },
|
||||
# },
|
||||
# "contact_person": {
|
||||
# "technical": {
|
||||
# "givenName": "Tom Hubrecht",
|
||||
# "emailAddress": "admins@dgnum.eu",
|
||||
# },
|
||||
# "administrative": {
|
||||
# "givenName": "Jean-Marc Gailis",
|
||||
# "emailAddress": "bureau@dgnum.eu",
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# }
|
||||
# ],
|
||||
# },
|
||||
}
|
||||
|
||||
SOCIALACCOUNT_ONLY = True
|
||||
SOCIALACCOUNT_ADAPTER = "shared.account.SharedAccountAdapter"
|
||||
ACCOUNT_EMAIL_VERIFICATION = "none"
|
||||
ACCOUNT_AUTHENTICATION_METHOD = "username"
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = []
|
||||
AUTH_USER_MODEL = "dgsi.User"
|
||||
|
||||
|
||||
###
|
||||
# Internationalization configuration
|
||||
# -> https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "fr-fr"
|
||||
LANGUAGE_CODE = "fr"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
LANGUAGES = [
|
||||
("en", "English"),
|
||||
("fr", "Français"),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
(BASE_DIR / "shared" / "locale"),
|
||||
]
|
||||
|
||||
TIME_ZONE = "Europe/Paris"
|
||||
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
@ -148,6 +262,7 @@ USE_TZ = True
|
|||
# -> https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
MEDIA_URL = "media/"
|
||||
|
||||
STATICFILES_DIRS = [BASE_DIR / "shared" / "static"]
|
||||
STATICFILES_FINDERS = [
|
||||
|
@ -156,7 +271,8 @@ STATICFILES_FINDERS = [
|
|||
"sass_processor.finders.CssFinder",
|
||||
]
|
||||
|
||||
STATIC_ROOT = credentials["STATIC_ROOT"]
|
||||
STATIC_ROOT = credentials.get("STATIC_ROOT")
|
||||
MEDIA_ROOT = credentials.get("MEDIA_ROOT")
|
||||
|
||||
###
|
||||
# Storages configuration
|
||||
|
@ -186,20 +302,40 @@ SASS_PROCESSOR_ENABLED = True
|
|||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
|
||||
##
|
||||
# Messages configuration
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
messages.DEBUG: "is-dark",
|
||||
messages.INFO: "is-primary",
|
||||
messages.SUCCESS: "is-success",
|
||||
messages.WARNING: "is-warning",
|
||||
messages.ERROR: "is-danger",
|
||||
}
|
||||
|
||||
|
||||
###
|
||||
# Unfold Interface configuration
|
||||
|
||||
UNFOLD = {
|
||||
"SITE_HEADER": _("Administration de DGSI"),
|
||||
}
|
||||
|
||||
|
||||
###
|
||||
# Extend settings when running in dev mode
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += [
|
||||
"debug_toolbar",
|
||||
"django_browser_reload",
|
||||
]
|
||||
|
||||
MIDDLEWARE += [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
]
|
||||
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {"INSERT_BEFORE": "</footer>"}
|
||||
|
|
|
@ -14,22 +14,23 @@ Including another URLconf
|
|||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
urlpatterns = [
|
||||
path("", TemplateView.as_view(template_name="home.html"), name="index"),
|
||||
path("login", TemplateView.as_view(template_name="login.html"), name="login"),
|
||||
path("", include("dgsi.urls")),
|
||||
path("accounts/", include("allauth.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("admin/", admin.site.urls),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
|
||||
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
||||
]
|
||||
|
|
|
@ -7,10 +7,6 @@ For more information on this file, see
|
|||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
|
@ -1 +1,62 @@
|
|||
# Register your models here.
|
||||
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from import_export.admin import ImportExportMixin
|
||||
from unfold.admin import ModelAdmin
|
||||
from unfold.contrib.import_export.forms import (
|
||||
ExportForm,
|
||||
ImportForm,
|
||||
SelectableFieldsExportForm,
|
||||
)
|
||||
|
||||
from dgsi.models import Bylaws, Service, Statutes, Translation, User
|
||||
|
||||
assert DjangoUserAdmin.fieldsets is not None
|
||||
|
||||
|
||||
# Unregister allauth models
|
||||
def unregister(*models, site=None):
|
||||
"""
|
||||
Unregister the given model(s) classes.
|
||||
|
||||
unregister(Author)
|
||||
|
||||
The `site` kwarg is an admin site to use instead of the default admin site.
|
||||
"""
|
||||
|
||||
from django.contrib.admin.sites import site as default_site
|
||||
|
||||
admin_site = site or default_site
|
||||
|
||||
if not isinstance(admin_site, admin.AdminSite):
|
||||
raise ValueError("site must subclass AdminSite")
|
||||
|
||||
admin_site.unregister(models)
|
||||
|
||||
|
||||
unregister(SocialAccount, SocialApp, SocialToken)
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(DjangoUserAdmin, ImportExportMixin, ModelAdmin):
|
||||
import_form_class = ImportForm
|
||||
export_form_class = ExportForm
|
||||
export_form_class = SelectableFieldsExportForm
|
||||
|
||||
# Add the local fields
|
||||
fieldsets = (
|
||||
*DjangoUserAdmin.fieldsets,
|
||||
(
|
||||
_("Documents DGNum"),
|
||||
{"fields": ("accepted_statutes", "accepted_bylaws")},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Bylaws, Service, SocialAccount, Statutes, Translation)
|
||||
class AdminClass(ImportExportMixin, ModelAdmin):
|
||||
compressed_fields = True
|
||||
import_form_class = ImportForm
|
||||
export_form_class = ExportForm
|
||||
export_form_class = SelectableFieldsExportForm
|
||||
|
|
51
src/dgsi/forms.py
Normal file
51
src/dgsi/forms.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from asgiref.sync import async_to_sync
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import BooleanField, CharField, EmailField, forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from shared.kanidm import klient
|
||||
|
||||
|
||||
@async_to_sync
|
||||
async def name_validator(value: str) -> None:
|
||||
try:
|
||||
await klient.person_account_get(value)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
raise ValidationError(_("Identifiant déjà présent dans la base de données."))
|
||||
|
||||
|
||||
class CreateKanidmAccountForm(forms.Form):
|
||||
# TODO: Add a field for the clipper login information for the local mapping
|
||||
name = CharField(
|
||||
label=_("Identifiant"),
|
||||
help_text=_("De préférence identique au login ENS de la personne concernée."),
|
||||
validators=[name_validator],
|
||||
)
|
||||
displayname = CharField(label=_("Nom d'usage"))
|
||||
mail = EmailField(
|
||||
label=_("Adresse e-mail"),
|
||||
help_text=_(
|
||||
"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 personnes ayant fini leur scolarité ;<br>"
|
||||
"<b>Pour les personnes extérieures, le bureau doit donner son accord.</b>"
|
||||
),
|
||||
)
|
||||
active = BooleanField(
|
||||
label=_("Membre actif"),
|
||||
help_text=_(
|
||||
"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>"
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class CreateSelfAccountForm(forms.Form):
|
||||
displayname = CharField(label=_("Nom d'usage"))
|
||||
mail = EmailField(
|
||||
label=_("Adresse e-mail"),
|
||||
help_text=_("De préférence l'adresse '@ens.psl.eu'"),
|
||||
)
|
131
src/dgsi/migrations/0001_initial.py
Normal file
131
src/dgsi/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
# Generated by Django 4.2.12 on 2024-09-14 13:35
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
33
src/dgsi/migrations/0002_service.py
Normal file
33
src/dgsi/migrations/0002_service.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 4.2.12 on 2024-09-23 15:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("dgsi", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Service",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
max_length=255, verbose_name="Nom du service proposé"
|
||||
),
|
||||
),
|
||||
("url", models.URLField(verbose_name="Adresse du service")),
|
||||
],
|
||||
),
|
||||
]
|
19
src/dgsi/migrations/0003_service_icon.py
Normal file
19
src/dgsi/migrations/0003_service_icon.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.2.12 on 2024-09-23 21:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("dgsi", "0002_service"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="service",
|
||||
name="icon",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=255, verbose_name="Icône du service"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.2.12 on 2024-09-24 20:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("dgsi", "0004_bylaws_statutes_user_accepted_bylaws_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="bylaws",
|
||||
options={
|
||||
"get_latest_by": "date",
|
||||
"verbose_name": "Règlement Intérieur",
|
||||
"verbose_name_plural": "Règlements Intérieurs",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="statutes",
|
||||
options={
|
||||
"get_latest_by": "date",
|
||||
"verbose_name": "Statuts",
|
||||
"verbose_name_plural": "Statuts",
|
||||
},
|
||||
),
|
||||
]
|
32
src/dgsi/migrations/0006_translation.py
Normal file
32
src/dgsi/migrations/0006_translation.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.2.12 on 2024-09-26 09:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("dgsi", "0005_alter_bylaws_options_alter_statutes_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Translation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("cas_login", models.CharField(max_length=255, unique=True)),
|
||||
("username", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Correspondance de login",
|
||||
"verbose_name_plural": "Correspondances de login",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2.16 on 2024-09-27 10:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dgsi", "0006_translation"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="accepted_bylaws",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="dgsi.bylaws",
|
||||
verbose_name="Dernier Règlement Intérieur accepté",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="accepted_statutes",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="dgsi.statutes",
|
||||
verbose_name="Derniers statuts acceptés",
|
||||
),
|
||||
),
|
||||
]
|
25
src/dgsi/migrations/0008_alter_user_accepted_statutes.py
Normal file
25
src/dgsi/migrations/0008_alter_user_accepted_statutes.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.2.16 on 2024-10-12 20:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dgsi", "0007_alter_user_accepted_bylaws_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="accepted_statutes",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="dgsi.statutes",
|
||||
verbose_name="Derniers Statuts acceptés",
|
||||
),
|
||||
),
|
||||
]
|
20
src/dgsi/mixins.py
Normal file
20
src/dgsi/mixins.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.http import HttpRequest
|
||||
|
||||
from dgsi.models import User
|
||||
|
||||
|
||||
class StaffRequiredMixin(UserPassesTestMixin):
|
||||
request: HttpRequest
|
||||
|
||||
def test_func(self) -> bool:
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
assert isinstance(self.request.user, User)
|
||||
|
||||
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
|
|
@ -1 +1,187 @@
|
|||
# Create your models here.
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
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.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from kanidm.exceptions import NoMatchingEntries
|
||||
from kanidm.models.person import Person
|
||||
|
||||
from shared.kanidm import klient
|
||||
|
||||
ADMIN_GROUP = "dgnum_admins@sso.dgnum.eu"
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
name = models.CharField(_("Nom du service proposé"), max_length=255)
|
||||
url = models.URLField(_("Adresse du service"))
|
||||
icon = models.CharField(_("Icône du service"), max_length=255, blank=True)
|
||||
|
||||
# TODO: Add a group field, to show only if the user really has access,
|
||||
# when this field is null, then it is open bar
|
||||
|
||||
def __str__(self) -> str:
|
||||
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")
|
||||
|
||||
|
||||
# class TermsAndConditions(LegalDocument):
|
||||
# """
|
||||
# Terms and Conditions of use regarding a service offered by the association.
|
||||
# """
|
||||
#
|
||||
# service = models.ForeignKey(Service, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Translation(models.Model):
|
||||
cas_login = models.CharField(max_length=255, unique=True)
|
||||
username = models.CharField(max_length=255, unique=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.cas_login} → {self.username}"
|
||||
|
||||
def update_user(self, username: str):
|
||||
# Update the username of the person with the required cas_login
|
||||
try:
|
||||
# Find out if a user exists with the cas_login to update
|
||||
account = SocialAccount.objects.get(provider="ens_cas", uid=self.cas_login)
|
||||
|
||||
# WARNING: This updates the remote data, we need to be careful with what we do
|
||||
async_to_sync(klient.person_account_update)(account.user.username, username)
|
||||
|
||||
account.user.username = username
|
||||
account.user.save()
|
||||
except SocialAccount.DoesNotExist:
|
||||
# No user has registered with this cas_login yet
|
||||
pass
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
# INFO: Only update the model if it does not already exist
|
||||
# This will prevent a lot of pain
|
||||
if self.pk is None:
|
||||
self.update_user(self.username)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class Meta: # pyright: ignore
|
||||
verbose_name = _("Correspondance de login")
|
||||
verbose_name_plural = _("Correspondances de login")
|
||||
|
||||
|
||||
# INFO: We need to use a signal receiver here, as the delete method is not called
|
||||
# when deleteing objects from the admin interface
|
||||
@receiver(pre_delete, sender=Translation)
|
||||
def restore_username(**kwargs):
|
||||
"""
|
||||
Restore the username to the cas_login
|
||||
"""
|
||||
|
||||
self = kwargs["instance"]
|
||||
self.update_user(self.cas_login)
|
||||
|
||||
|
||||
@dataclass
|
||||
class KanidmProfile:
|
||||
person: Person
|
||||
radius_secret: Optional[str]
|
||||
|
||||
|
||||
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,
|
||||
verbose_name=_("Derniers Statuts acceptés"),
|
||||
)
|
||||
accepted_bylaws = models.ForeignKey(
|
||||
Bylaws,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("Dernier Règlement Intérieur accepté"),
|
||||
)
|
||||
# accepted_terms = models.ManyToManyField(TermsAndConditions)
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, request: HttpRequest) -> Self:
|
||||
u = request.user
|
||||
|
||||
assert isinstance(u, cls)
|
||||
|
||||
return u
|
||||
|
||||
@cached_property
|
||||
@async_to_sync
|
||||
async def kanidm(self) -> Optional[KanidmProfile]:
|
||||
try:
|
||||
radius_data = (await klient.get_radius_token(self.username)).data
|
||||
|
||||
return KanidmProfile(
|
||||
person=(await klient.person_account_get(self.username)),
|
||||
radius_secret=radius_data and radius_data.get("secret"),
|
||||
)
|
||||
except NoMatchingEntries:
|
||||
return None
|
||||
except (TimeoutError, ClientConnectorError) as e:
|
||||
logging.error(f"Erreur lors de la requête à Kanidm: {e}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
return (self.kanidm is not None) and (
|
||||
ADMIN_GROUP in self.kanidm.person.memberof
|
||||
)
|
||||
|
|
4
src/dgsi/templates/_index_link.html
Normal file
4
src/dgsi/templates/_index_link.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<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>
|
26
src/dgsi/templates/_legal_document.html
Normal file
26
src/dgsi/templates/_legal_document.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% 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 }}">
|
||||
<span>{{ document }}</span>
|
||||
<span class="icon"><i class="ti ti-file-download"></i></span>
|
||||
</a>
|
18
src/dgsi/templates/dgsi/create_kanidm_account.html
Normal file
18
src/dgsi/templates/dgsi/create_kanidm_account.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<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>{% trans "Enregistrer" %}</span>
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endblock content %}
|
18
src/dgsi/templates/dgsi/create_self_account.html
Normal file
18
src/dgsi/templates/dgsi/create_self_account.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<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>{% trans "Enregistrer" %}</span>
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endblock content %}
|
20
src/dgsi/templates/dgsi/index.html
Normal file
20
src/dgsi/templates/dgsi/index.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
{% if user.is_authenticated %}
|
||||
{% for link in links.authenticated %}
|
||||
{% include "_index_link.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_admin %}
|
||||
<hr>
|
||||
{% for link in links.admin %}
|
||||
{% include "_index_link.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock content %}
|
32
src/dgsi/templates/dgsi/legal_documents.html
Normal file
32
src/dgsi/templates/dgsi/legal_documents.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="subtitle">Documents Légaux</h2>
|
||||
<hr>
|
||||
|
||||
{% if user.kanidm is None %}
|
||||
{% if show_message %}
|
||||
<div class="notification is-warning is-light has-text-centered">
|
||||
<b>{% trans "Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant de pouvoir créer un compte." %}</b>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="notification is-primary is-light has-text-centered">
|
||||
<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:dgn-create_self_account" %}">{% trans "Poursuivre la création d'un compte DGNum" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<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 %}
|
70
src/dgsi/templates/dgsi/profile.html
Normal file
70
src/dgsi/templates/dgsi/profile.html
Normal file
|
@ -0,0 +1,70 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<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 %}
|
||||
<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>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
<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>
|
||||
{% 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:dgn-create_self_account" %}">{% trans "Créer un compte DGNum" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
18
src/dgsi/templates/dgsi/service_list.html
Normal file
18
src/dgsi/templates/dgsi/service_list.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<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:dgn-services_redirect" service.pk %}">
|
||||
<span class="icon"><i class="ti ti-{{ service.icon }}"></i></span>
|
||||
<span>{{ service.name }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1 +1,50 @@
|
|||
urlpatterns = []
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
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(
|
||||
"accounts/generate-wifi-password/",
|
||||
views.GenerateWiFiPasswordView.as_view(),
|
||||
name="dgn-generate_wifi_password",
|
||||
),
|
||||
path(
|
||||
"accounts/create/",
|
||||
views.CreateSelfAccountView.as_view(),
|
||||
name="dgn-create_self_account",
|
||||
),
|
||||
path(
|
||||
"accounts/create-kanidm/",
|
||||
views.CreateKanidmAccountView.as_view(),
|
||||
name="dgn-create_kanidm_user",
|
||||
),
|
||||
path(
|
||||
"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(
|
||||
"services/redirect/<int:pk>/",
|
||||
views.ServiceRedirectView.as_view(),
|
||||
name="dgn-services_redirect",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1 +1,275 @@
|
|||
# Create your views here.
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
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 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
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm
|
||||
from dgsi.mixins import StaffRequiredMixin
|
||||
from dgsi.models import Bylaws, Service, Statutes, User
|
||||
from shared.kanidm import klient
|
||||
|
||||
|
||||
class Link(NamedTuple):
|
||||
color: str
|
||||
reverse: str
|
||||
text: str | Promise
|
||||
icon: str | None = None
|
||||
|
||||
|
||||
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: list[Link] = [
|
||||
Link(
|
||||
"is-danger",
|
||||
"dgsi:dgn-create_kanidm_user",
|
||||
_("Créer un compte Kanidm"),
|
||||
"user-plus",
|
||||
),
|
||||
Link(
|
||||
"is-warning", "admin:index", _("Interface d'administration"), "settings-filled"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class IndexView(TemplateView):
|
||||
template_name = "dgsi/index.html"
|
||||
|
||||
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)
|
||||
|
||||
return super().get_context_data(
|
||||
displayname=f"{u.first_name} {u.last_name}",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class GenerateWiFiPasswordView(LoginRequiredMixin, RedirectView):
|
||||
url = reverse_lazy("dgsi:dgn-profile")
|
||||
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
||||
user = User.from_request(self.request)
|
||||
|
||||
if user.kanidm is None:
|
||||
messages.error(self.request, _("Compte DGNum inexistant."))
|
||||
else:
|
||||
# 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 super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
# INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to
|
||||
# use dispatch means that we need to execute the login check anyways.
|
||||
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:dgn-profile")
|
||||
|
||||
def dispatch(
|
||||
self, request: HttpRequest, *args: Any, **kwargs: Any
|
||||
) -> HttpResponseBase:
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
|
||||
u = User.from_request(request)
|
||||
|
||||
# Check that the user does not already exist
|
||||
if u.kanidm is not None:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_("<b>Vous possédez déjà un compte DGNum !</b>"),
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:dgn-profile"))
|
||||
|
||||
# Check that the Statutes and Bylaws have been accepted
|
||||
if (
|
||||
u.accepted_statutes != Statutes.latest()
|
||||
or u.accepted_bylaws != Bylaws.latest()
|
||||
):
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_("Vous devez accepter les Statuts et le Règlement Intérieur."),
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy("dgsi:dgn-legal_documents"))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@async_to_sync
|
||||
async def form_valid(self, form):
|
||||
ttl = 86400 # 24h
|
||||
|
||||
d = form.cleaned_data
|
||||
u = User.from_request(self.request)
|
||||
|
||||
# Create the base account
|
||||
await klient.person_account_create(u.username, d["displayname"])
|
||||
|
||||
# Update the information
|
||||
await klient.person_account_update(u.username, mail=[d["mail"]])
|
||||
|
||||
# FIXME: Will maybe change when kanidm gets its shit together and switches to POST
|
||||
r = await klient.call_get(
|
||||
f"/v1/person/{u.username}/_credential/_update_intent/{ttl}"
|
||||
)
|
||||
|
||||
assert r.data is not None
|
||||
|
||||
token: str = r.data["token"]
|
||||
link = f"https://sso.dgnum.eu/ui/reset?token={token}"
|
||||
|
||||
# Send an email to the new user with the given email address
|
||||
EmailMessage(
|
||||
subject="Réinitialisation de mot de passe DGNum -- DGNum password reset",
|
||||
body=render_to_string(
|
||||
"mail/credentials_reset.txt",
|
||||
context={"link": link},
|
||||
),
|
||||
from_email="To Be Determined <dgsi@infra.dgnum.eu>",
|
||||
to=[d["mail"]],
|
||||
headers={"Reply-To": "contact@dgnum.eu"},
|
||||
).send()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class LegalDocumentsView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "dgsi/legal_documents.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
u = User.from_request(self.request)
|
||||
statutes = Statutes.latest()
|
||||
bylaws = Bylaws.latest()
|
||||
|
||||
return super().get_context_data(
|
||||
statutes=statutes,
|
||||
bylaws=bylaws,
|
||||
show_message=(
|
||||
(u.accepted_bylaws != bylaws) or (u.accepted_statutes != statutes)
|
||||
),
|
||||
**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
|
||||
|
||||
# TODO: Only show available websites
|
||||
|
||||
|
||||
class ServiceRedirectView(LoginRequiredMixin, SingleObjectMixin, RedirectView):
|
||||
model = Service
|
||||
|
||||
def get_redirect_url(self, *args: Any, **kwargs: Any) -> str:
|
||||
return self.get_object().url
|
||||
|
||||
|
||||
##
|
||||
# INFO: Below are views related to the administration of DGSI
|
||||
|
||||
|
||||
class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView):
|
||||
form_class = CreateKanidmAccountForm
|
||||
template_name = "dgsi/create_kanidm_account.html"
|
||||
success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.")
|
||||
|
||||
success_url = reverse_lazy("dgsi:dgn-create_kanidm_user")
|
||||
|
||||
@async_to_sync
|
||||
async def form_valid(self, form):
|
||||
ttl = 86400 # 24h
|
||||
|
||||
d = form.cleaned_data
|
||||
|
||||
# Create the base account
|
||||
await klient.person_account_create(d["name"], d["displayname"])
|
||||
|
||||
# Update the information
|
||||
await klient.person_account_update(d["name"], mail=[d["mail"]])
|
||||
|
||||
# If necessary, add the user to the active members group
|
||||
if d["active"]:
|
||||
await klient.group_add_members("dgnum_members", [d["name"]])
|
||||
|
||||
# FIXME: Will maybe change when kanidm gets its shit together and switches to POST
|
||||
r = await klient.call_get(
|
||||
f"/v1/person/{d['name']}/_credential/_update_intent/{ttl}"
|
||||
)
|
||||
|
||||
assert r.data is not None
|
||||
|
||||
token: str = r.data["token"]
|
||||
link = f"https://sso.dgnum.eu/ui/reset?token={token}"
|
||||
|
||||
# Send an email to the new user with the given email address
|
||||
EmailMessage(
|
||||
subject="Réinitialisation de mot de passe DGNum -- DGNum password reset",
|
||||
body=render_to_string(
|
||||
"mail/credentials_reset.txt",
|
||||
context={"link": link},
|
||||
),
|
||||
to=[d["mail"]],
|
||||
headers={"Reply-To": "contact@dgnum.eu"},
|
||||
).send()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
|
123
src/shared/account.py
Normal file
123
src/shared/account.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import logging
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth.socialaccount.models import SocialLogin
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
||||
"""
|
||||
Overrides the Account Adapter, to allow a simpler connection via CAS.
|
||||
"""
|
||||
|
||||
@lru_cache
|
||||
def _get_username(self, request: HttpRequest, sociallogin: SocialLogin) -> str:
|
||||
"""
|
||||
Returns the required username
|
||||
"""
|
||||
|
||||
match sociallogin.account.provider:
|
||||
case "ens_cas":
|
||||
cas_login = sociallogin.account.extra_data["uid"]
|
||||
|
||||
# Verify that this user can indeed connect to the website
|
||||
home = sociallogin.account.extra_data["homeDirectory"].split("/")
|
||||
|
||||
if (home[1] != "users") or (
|
||||
home[2]
|
||||
in ["absint", "algo", "grecc", "guests", "spi", "spi1", "staffs"]
|
||||
):
|
||||
messages.error(request, _("Catégorie de compte ENS interdite."))
|
||||
raise ImmediateHttpResponse(
|
||||
HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account"))
|
||||
)
|
||||
|
||||
# Continue with the login flow
|
||||
try:
|
||||
return Translation.objects.get(cas_login=cas_login).username
|
||||
except Translation.DoesNotExist:
|
||||
return cas_login
|
||||
|
||||
case "kanidm":
|
||||
return sociallogin.account.extra_data["preferred_username"]
|
||||
|
||||
case _:
|
||||
logger.warning(sociallogin.user)
|
||||
# INFO: This should never happen
|
||||
messages.error(request, _("Méthode de connexion invalide."))
|
||||
raise ImmediateHttpResponse(
|
||||
HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account"))
|
||||
)
|
||||
|
||||
def _get_user(
|
||||
self, request: HttpRequest, sociallogin: SocialLogin
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
Returns the required user for completing the login
|
||||
"""
|
||||
|
||||
# The user is already linked to the social login, no reason to change it
|
||||
if sociallogin.is_existing:
|
||||
return sociallogin.user
|
||||
|
||||
# No user is currently linked to this social login, either the user has already
|
||||
# logged in with another method, or it truly does not exist
|
||||
return User.objects.filter(
|
||||
username=self._get_username(request, sociallogin)
|
||||
).first()
|
||||
|
||||
def _update_user(self, request: HttpRequest, sociallogin: SocialLogin):
|
||||
"""
|
||||
Updates the required attributes of the user:
|
||||
- username
|
||||
- permissions
|
||||
"""
|
||||
|
||||
u = sociallogin.user
|
||||
|
||||
assert isinstance(u, User)
|
||||
|
||||
# Update the username first, so that calls to kanidm return the correct information
|
||||
u.username = self._get_username(request, sociallogin)
|
||||
|
||||
# Update the global permissions
|
||||
u.is_staff = u.is_admin
|
||||
u.is_superuser = u.is_admin
|
||||
|
||||
# Save the updated user if needed
|
||||
if sociallogin.is_existing:
|
||||
u.save()
|
||||
|
||||
def pre_social_login(self, request, sociallogin: SocialLogin):
|
||||
###
|
||||
# The flow is the following:
|
||||
# - Get the correct user
|
||||
# - Do the connection if possible
|
||||
# - Update the required attributes
|
||||
|
||||
user = self._get_user(request, sociallogin)
|
||||
|
||||
if user is not None:
|
||||
sociallogin.user = user
|
||||
|
||||
# If the user exists, connect to it
|
||||
if sociallogin.is_existing:
|
||||
sociallogin.connect(request, sociallogin.user)
|
||||
|
||||
self._update_user(request, sociallogin)
|
||||
|
||||
def populate_user(self, request, sociallogin, data):
|
||||
return super().populate_user(request, sociallogin, data)
|
||||
|
||||
def save_user(self, request, sociallogin: SocialLogin, form=None):
|
||||
return super().save_user(request, sociallogin, form)
|
0
src/shared/cas/__init__.py
Normal file
0
src/shared/cas/__init__.py
Normal file
11
src/shared/cas/provider.py
Normal file
11
src/shared/cas/provider.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from allauth.socialaccount.providers.base import ProviderAccount
|
||||
from allauth_cas.providers import CASProvider as Provider
|
||||
|
||||
|
||||
class CASProvider(Provider):
|
||||
id = "cas" # Choose an identifier for your provider
|
||||
name = "CAS ENS" # Verbose name of your provider
|
||||
account_class = ProviderAccount
|
||||
|
||||
|
||||
provider_classes = [CASProvider]
|
5
src/shared/cas/urls.py
Normal file
5
src/shared/cas/urls.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from allauth_cas.urls import default_urlpatterns
|
||||
|
||||
from .provider import CASProvider
|
||||
|
||||
urlpatterns = default_urlpatterns(CASProvider)
|
14
src/shared/cas/views.py
Normal file
14
src/shared/cas/views.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from allauth_cas.views import CASAdapter as Adapter
|
||||
from allauth_cas.views import CASCallbackView, CASLoginView
|
||||
|
||||
from .provider import CASProvider
|
||||
|
||||
|
||||
class CASAdapter(Adapter):
|
||||
provider_id = CASProvider.id
|
||||
url = "https://cas.eleves.ens.fr"
|
||||
version = 3
|
||||
|
||||
|
||||
login = CASLoginView.adapter_view(CASAdapter)
|
||||
callback = CASCallbackView.adapter_view(CASAdapter)
|
8
src/shared/kanidm.py
Normal file
8
src/shared/kanidm.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from kanidm import KanidmClient
|
||||
from loadcredential import Credentials
|
||||
|
||||
credentials = Credentials(env_prefix="DGSI_")
|
||||
|
||||
klient = KanidmClient(
|
||||
uri=credentials["KANIDM_URI"], token=credentials["KANIDM_AUTH_TOKEN"]
|
||||
)
|
BIN
src/shared/locale/en/LC_MESSAGES/django.mo
Normal file
BIN
src/shared/locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
351
src/shared/locale/en/LC_MESSAGES/django.po
Normal file
351
src/shared/locale/en/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,351 @@
|
|||
# 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.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dgsi.dgnum.eu\n"
|
||||
"Report-Msgid-Bugs-To: \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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
||||
"X-Generator: Gtranslator 46.1\n"
|
||||
|
||||
#: app/settings.py:321
|
||||
msgid "Administration de DGSI"
|
||||
msgstr "DGSI Administration"
|
||||
|
||||
#: 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 "
|
||||
"personnes ayant fini leur scolarité ;<br><b>Pour les personnes extérieures, "
|
||||
"le bureau doit donner son accord.</b>"
|
||||
msgstr ""
|
||||
"Preferably:<br>- the <code>@ens.psl.eu</code> address for students;<br>- the "
|
||||
"<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>"
|
||||
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"
|
||||
|
||||
#: 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"
|
||||
|
||||
#: 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"
|
||||
|
||||
#: 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"
|
||||
|
||||
#: 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."
|
||||
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"
|
||||
|
||||
#: 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:"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:36
|
||||
msgid "Adresse e-mail :"
|
||||
msgstr "E-mail address:"
|
||||
|
||||
#: dgsi/templates/dgsi/profile.html:41
|
||||
msgid "Informations techniques"
|
||||
msgstr "Technical informations"
|
||||
|
||||
#: 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"
|
||||
|
||||
#: dgsi/views.py:30
|
||||
msgid "Mon profil"
|
||||
msgstr "My profile"
|
||||
|
||||
#: dgsi/views.py:31
|
||||
msgid "Documents Légaux"
|
||||
msgstr "Legal Documents"
|
||||
|
||||
#: dgsi/views.py:32
|
||||
msgid "Services proposés"
|
||||
msgstr "Services offered"
|
||||
|
||||
#: dgsi/views.py:39
|
||||
msgid "Créer un compte Kanidm"
|
||||
msgstr "Create a Kanidm account"
|
||||
|
||||
#: dgsi/views.py:43 shared/templates/_hero.html:76
|
||||
msgid "Interface d'administration"
|
||||
msgstr "Administration interface"
|
||||
|
||||
#: dgsi/views.py:80
|
||||
msgid "Compte DGNum inexistant."
|
||||
msgstr "No existing DGNum account."
|
||||
|
||||
#: 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>."
|
||||
|
||||
#: 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 à "
|
||||
"l'adresse : <a href=\"mailto:contact@dgnum.eu\">contact@dgnum.eu</a>"
|
||||
msgstr ""
|
||||
"Your details do not allow the DGNum to authenticate you.<br>If you think "
|
||||
"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"
|
68
src/shared/static/bulma/bulma.scss
vendored
68
src/shared/static/bulma/bulma.scss
vendored
|
@ -7,12 +7,78 @@ $dark: rgb(46, 46, 46);
|
|||
@use "sass" with (
|
||||
$primary: $blue,
|
||||
$link: rgb(72, 95, 199),
|
||||
$dark: $dark,
|
||||
$dark: $dark
|
||||
);
|
||||
|
||||
@use "./sass/utilities/mixins" as mx;
|
||||
@use "./sass/utilities/initial-variables.scss" as iv;
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bt-link {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: calc(0.5 * var(--bulma-block-spacing));
|
||||
font-size: 1.25rem;
|
||||
|
||||
&.is-light {
|
||||
// Dark color for text
|
||||
--bulma-color-l: var(--bulma-dark-l);
|
||||
--bulma-color-l-delta: 0%;
|
||||
color: hsl(
|
||||
var(--bulma-dark-h),
|
||||
var(--bulma-dark-s),
|
||||
calc(var(--bulma-color-l) + var(--bulma-color-l-delta))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.bt-links {
|
||||
justify-content: space-evenly;
|
||||
|
||||
.button {
|
||||
@include mx.from(iv.$desktop) {
|
||||
width: 47.5%;
|
||||
}
|
||||
|
||||
@include mx.until(iv.$desktop) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid.groups {
|
||||
--bulma-grid-column-min: min(24rem, 100%);
|
||||
|
||||
.button > span {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
#notifications {
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
position: sticky;
|
||||
display: block;
|
||||
inset: 1.5rem;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.notification {
|
||||
margin-bottom: var(--bulma-block-spacing);
|
||||
}
|
||||
|
||||
.dropdown.is-fullwidth {
|
||||
width: 100%;
|
||||
|
||||
.dropdown-trigger,
|
||||
.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
11
src/shared/static/js/dgsi.js
Normal file
11
src/shared/static/js/dgsi.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
(document.querySelectorAll(".notification .delete") || []).forEach(
|
||||
($delete) => {
|
||||
const $notification = $delete.parentNode;
|
||||
const dismiss = () => $notification.parentNode.removeChild($notification);
|
||||
|
||||
$delete.addEventListener("click", dismiss);
|
||||
setTimeout(dismiss, 15000);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
{% load django_browser_reload %}
|
||||
{% load i18n django_browser_reload %}
|
||||
|
||||
<footer class="footer has-text-centered">
|
||||
<b>Logiciel développé pour et par la <a href="https://dgnum.eu">DGNum</a>.</b>
|
||||
<b>{% blocktrans %}Logiciel développé pour et par la <a href="https://dgnum.eu">DGNum</a>.{% endblocktrans %}</b>
|
||||
{% django_browser_reload_script %}
|
||||
</footer>
|
||||
|
|
|
@ -1,37 +1,80 @@
|
|||
{% load i18n %}
|
||||
|
||||
<section class="hero is-dark is-primary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="grid">
|
||||
<div class="cell">
|
||||
<h1 class="title">
|
||||
<a href="{% url 'index' %}" class="has-text-dark">Dossier Général des Services Informagiques</a>
|
||||
</h1>
|
||||
<h2 class="subtitle">Système d'information de la DGNum</h2>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<section class="hero {% if admin_view %}is-danger{% else %}is-primary{% endif %}">
|
||||
<div class="hero-body px-0">
|
||||
<div class="columns mx-6">
|
||||
<div class="column is-three-quarters">
|
||||
<h1 class="title">
|
||||
<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>
|
||||
<div class="column">
|
||||
<div class="buttons mt-5">
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'account_logout' %}" class="button is-light is-pulled-right">
|
||||
<span>
|
||||
<span>{% trans "Déconnexion" %}</span>
|
||||
<span class="icon">
|
||||
<i class="ti ti-door-exit"></i>
|
||||
<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>
|
||||
</span>
|
||||
</a>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="button is-light is-pulled-right">
|
||||
<span>
|
||||
<span>{% trans "Connexion" %}</span>
|
||||
<span class="icon">
|
||||
<i class="ti ti-door-enter"></i>
|
||||
<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>
|
||||
</span>
|
||||
</a>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="dropdown is-hoverable is-fullwidth">
|
||||
<div class="dropdown-trigger"
|
||||
aria-haspopup="true"
|
||||
aria-controls="lang-menu">
|
||||
<button class="button is-primary is-light is-fullwidth has-text-dark">
|
||||
<span class="icon"><i class="ti ti-language"></i></span>
|
||||
<span>{% trans "Choix de la langue" %}</span>
|
||||
<span class="icon is-pulled-right">
|
||||
<i class="ti ti-chevron-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-menu" id="lang-menu" role="menu">
|
||||
<div class="dropdown-content px-2">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ redirect }}">
|
||||
<input type="hidden" name="language" value="{{ language.code }}">
|
||||
<button class="dropdown-item {% if language.code == LANGUAGE_CODE %}is-active{% endif %}">
|
||||
{{ language.name_local|capfirst }}
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if admin_view %}
|
||||
<div class="hero is-dark">
|
||||
<div class="hero-body py-2 has-text-centered">
|
||||
<span class="icon">
|
||||
<i class="ti ti-lock-open-2"></i>
|
||||
</span>
|
||||
<span>{% trans "Interface d'administration" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
|
|
@ -10,3 +10,7 @@
|
|||
<!-- 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 data-domain="profil.dgnum.eu" src="https://analytics.dgnum.eu/js/script.js"></script>
|
||||
|
|
36
src/shared/templates/account/login.html
Normal file
36
src/shared/templates/account/login.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n socialaccount %}
|
||||
{% load allauth account %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="subtitle">{% trans "Connexion via un compte tiers" %}</h2>
|
||||
<hr>
|
||||
|
||||
{% get_providers as providers %}
|
||||
|
||||
<div class="grid mt-5">
|
||||
{% for provider in providers %}
|
||||
{% if provider.id == "openid" %}
|
||||
{% for brand in provider.get_brands %}
|
||||
<a class="cell button is-{{ provider.app.settings.color }} is-light py-5 is-size-4"
|
||||
title="{{ brand.name }}"
|
||||
href="{% provider_login_url provider openid=brand.openid_url process="login" %}"><b>{{ brand.name }}</b></a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<a class="cell button is-{{ provider.app.settings.color }} is-light py-5 is-size-5"
|
||||
title="{{ provider.id }}"
|
||||
href="{% provider_login_url provider process="login" scope=scope auth_params=auth_params %}">{{ provider.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- TODO: Write a text explaining how the different methods work -->
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_body %}
|
||||
{{ block.super }}
|
||||
{% if PASSKEY_LOGIN_ENABLED %}
|
||||
{% include "mfa/webauthn/snippets/login_script.html" with button_id="passkey_login" %}
|
||||
{% endif %}
|
||||
{% endblock extra_body %}
|
18
src/shared/templates/account/logout.html
Normal file
18
src/shared/templates/account/logout.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load allauth i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="subtitle">{% trans "Déconnexion" %}</h2>
|
||||
<hr>
|
||||
|
||||
<p class="notification is-warning is-light has-text-centered">
|
||||
{% trans "Êtes vous certain·e de vouloir vous déconnecter ?" %}
|
||||
</p>
|
||||
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
{{ redirect_field }}
|
||||
<button class="button is-fullwidth" type="submit">{% trans "Se déconnecter" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
12
src/shared/templates/accounts/forbidden_category.html
Normal file
12
src/shared/templates/accounts/forbidden_category.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="subtitle">{% trans "Connexion impossible" %}</h2>
|
||||
<hr>
|
||||
|
||||
<div class="notification is-warning is-light px-5 py-5 has-text-centered is-size-5">
|
||||
{% blocktrans %}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 à l'adresse : <a href="mailto:contact@dgnum.eu">contact@dgnum.eu</a>{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,4 +1,5 @@
|
|||
{% load allauth %}
|
||||
|
||||
{% comment %} djlint:off {% endcomment %}
|
||||
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
|
||||
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<a class="cell button is-primary is-light" title="{{ attrs.name }}" href="{{ attrs.href }}">{{ attrs.name }}</a>
|
|
@ -1,6 +0,0 @@
|
|||
{% load allauth %}
|
||||
|
||||
<div class="grid mt-5">
|
||||
{% slot default %}
|
||||
{% endslot %}
|
||||
</div>
|
|
@ -1,5 +1,3 @@
|
|||
{% load django_browser_reload i18n sass_tags static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
|
@ -15,58 +13,17 @@
|
|||
<body>
|
||||
{% include "_hero.html" %}
|
||||
|
||||
<section class="section container">
|
||||
<nav class="level">
|
||||
{% if user.is_authenticated %}
|
||||
{% url 'account_email' as email_url %}
|
||||
{% if email_url %}
|
||||
<li class="level-item button is-light">
|
||||
<a href="{{ email_url }}">{% trans "Change Email" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% url 'account_change_password' as change_password_url %}
|
||||
{% if change_password_url %}
|
||||
<li class="level-item button is-light">
|
||||
<a href="{{ change_password_url }}">{% trans "Change Password" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% url 'mfa_index' as mfa_url %}
|
||||
{% if mfa_url %}
|
||||
<li class="level-item button is-light">
|
||||
<a href="{{ mfa_url }}">{% trans "Two-Factor Authentication" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% url 'usersessions_list' as usersessions_list_url %}
|
||||
{% if usersessions_list_url %}
|
||||
<li class="level-item button is-light">
|
||||
<a href="{{ usersessions_list_url }}">{% trans "Sessions" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% url 'account_logout' as logout_url %}
|
||||
{% if logout_url %}
|
||||
<li class="level-item button is-light">
|
||||
<a href="{{ logout_url }}">{% trans "Sign Out" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% url 'account_login' as login_url %}
|
||||
{% if login_url %}
|
||||
<li class="level-item button is-light has-text-weight-bold">
|
||||
<a href="{{ login_url }}">{% trans "Sign In" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% url 'account_signup' as signup_url %}
|
||||
{% if signup_url %}
|
||||
<li class="level-item button is-light has-text-weight-bold">
|
||||
<a href="{{ signup_url }}">{% trans "Sign Up" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
<section class="section">
|
||||
<div id="notifications">
|
||||
{% for message in messages %}
|
||||
<article class="notification is-light has-text-centered {{ message.tags }}">
|
||||
<button class="delete"></button>
|
||||
{{ message|safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="content">
|
||||
<div class="content container">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
|
|
|
@ -7,13 +7,25 @@
|
|||
<meta name="description" content="Système d'information de la DGNum" />
|
||||
<title>DGNum</title>
|
||||
|
||||
{% block extra_head %}
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% include "_links.html" %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% include "_hero.html" %}
|
||||
|
||||
<section class="section">
|
||||
<section class="container is-max-widescreen py-6">
|
||||
<div id="notifications">
|
||||
{% for message in messages %}
|
||||
<article class="notification is-light has-text-centered {{ message.tags }}">
|
||||
<button class="delete"></button>
|
||||
{{ message|safe }}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</section>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}{% endblock %}
|
|
@ -1,29 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n socialaccount %}
|
||||
|
||||
{% block content %}
|
||||
<div class="fixed-grid">
|
||||
<div class="grid">
|
||||
<a href="{% provider_login_url 'kanidm' %}"
|
||||
class="cell has-background-primary-dark p-6 has-radius-normal has-text-centered has-text-white">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="ti ti-login"></i>
|
||||
</span>
|
||||
<span><b>{% trans "Connexion via la DGNum" %}</b></span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{% provider_login_url 'kanidm' %}"
|
||||
class="cell has-background-primary p-6 has-radius-normal has-text-centered has-text-white">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="ti ti-login"></i>
|
||||
</span>
|
||||
<span><b>{% trans "Connexion via l'ENS" %}</b></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
17
src/shared/templates/mail/credentials_reset.txt
Normal file
17
src/shared/templates/mail/credentials_reset.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
Bonjour,
|
||||
|
||||
Une demande de réinitialisation de votre mot de passe DGNum a été effectuée.
|
||||
|
||||
Pour mettre à jour vos moyens de connexion, merci de vous rendre à l'adresse : {{ link }}
|
||||
|
||||
--
|
||||
|
||||
Hello,
|
||||
|
||||
A request to reset your DGNum password has been made.
|
||||
|
||||
To update your login details, please go to: {{ link }}
|
||||
|
||||
|
||||
Bien cordialement,
|
||||
La Délégation Générale Numérique
|
13
src/shared/templates/socialaccount/authentication_error.html
Normal file
13
src/shared/templates/socialaccount/authentication_error.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load allauth %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="subtitle">{% trans "Erreur lors de la connexion" %}</h2>
|
||||
<hr>
|
||||
|
||||
<p class="notification is-danger is-light has-text-centered">
|
||||
{% trans "Une erreur est survenue lors de votre tentative de connexion avec un compte tiers." %}
|
||||
</p>
|
||||
{% endblock content %}
|
23
src/shared/templates/socialaccount/login.html
Normal file
23
src/shared/templates/socialaccount/login.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load allauth i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Connexion" %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="subtitle">
|
||||
{% blocktrans with provider.name as provider %}Se connecter via un compte <b>{{ provider }}</b>{% endblocktrans %}
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<p class="notification is-warning is-light has-text-centered">
|
||||
{% blocktrans with provider.name as provider %}Vous vous apprêtez à vous connecter à l'aide d'un compte tiers provenant de {{ provider }}.{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button is-fullwidth" type="submit">{% trans "Continuer" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
Loading…
Reference in a new issue