Rajout de la gestion admin
This commit is contained in:
parent
287716276d
commit
f56cd87358
18 changed files with 409 additions and 18 deletions
20
elections/migrations/0031_alter_election_options.py
Normal file
20
elections/migrations/0031_alter_election_options.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-12 16:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0030_timestamps"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="election",
|
||||
options={
|
||||
"ordering": ["-start_date", "-end_date"],
|
||||
"permissions": [("election_admin", "Peut administrer des élections")],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -10,7 +10,7 @@ from .models import Election, Option, Question
|
|||
class AdminOnlyMixin(PermissionRequiredMixin):
|
||||
"""Restreint l'accès aux admins"""
|
||||
|
||||
permission_required = "elections.is_admin"
|
||||
permission_required = "elections.election_admin"
|
||||
|
||||
|
||||
class SelectElectionMixin:
|
||||
|
|
|
@ -6,12 +6,12 @@ from django.db import models, transaction
|
|||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from shared.auth import CONNECTION_METHODS
|
||||
from shared.utils import choices_length
|
||||
|
||||
from .staticdefs import (
|
||||
BALLOT_TYPE,
|
||||
CAST_FUNCTIONS,
|
||||
CONNECTION_METHODS,
|
||||
QUESTION_TYPES,
|
||||
TALLY_FUNCTIONS,
|
||||
VALIDATE_FUNCTIONS,
|
||||
|
@ -81,7 +81,7 @@ class Election(models.Model):
|
|||
|
||||
class Meta:
|
||||
permissions = [
|
||||
("is_admin", _("Peut administrer des élections")),
|
||||
("election_admin", _("Peut administrer des élections")),
|
||||
]
|
||||
ordering = ["-start_date", "-end_date"]
|
||||
|
||||
|
|
|
@ -22,11 +22,6 @@ MAIL_VOTE_DELETED = (
|
|||
"Kadenios"
|
||||
)
|
||||
|
||||
CONNECTION_METHODS = {
|
||||
"pwd": _("mot de passe"),
|
||||
"cas": _("CAS"),
|
||||
}
|
||||
|
||||
QUESTION_TYPES = [
|
||||
("assentiment", _("Assentiment")),
|
||||
("uninominal", _("Uninominal")),
|
||||
|
|
17
faqs/migrations/0002_alter_faq_options.py
Normal file
17
faqs/migrations/0002_alter_faq_options.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-12 17:29
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("faqs", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="faq",
|
||||
options={"permissions": [("faq_admin", "Can create faqs")]},
|
||||
),
|
||||
]
|
|
@ -4,7 +4,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||
class AdminOnlyMixin(PermissionRequiredMixin):
|
||||
"""Restreint l'accès aux admins"""
|
||||
|
||||
permission_required = "faqs.is_author"
|
||||
permission_required = "faqs.faq_admin"
|
||||
|
||||
|
||||
class CreatorOnlyMixin(AdminOnlyMixin):
|
||||
|
|
|
@ -25,7 +25,7 @@ class Faq(models.Model):
|
|||
|
||||
class Meta:
|
||||
permissions = [
|
||||
("is_author", "Can create faqs"),
|
||||
("faq_admin", "Can create faqs"),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["anchor"], name="unique_faq_anchor")
|
||||
|
|
3
shared/auth/__init__.py
Normal file
3
shared/auth/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .staticdefs import CONNECTION_METHODS
|
||||
|
||||
__all__ = [CONNECTION_METHODS]
|
|
@ -82,3 +82,38 @@ class PwdUserForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "full_name", "email"]
|
||||
|
||||
|
||||
class UserAdminForm(forms.Form):
|
||||
"""
|
||||
Allows to select an user and give them some admin permissions
|
||||
"""
|
||||
|
||||
username = forms.CharField(label=_("Nom d'utilisateur"), max_length=150)
|
||||
|
||||
full_admin = forms.BooleanField(
|
||||
label=_("Passer administrateur de Kadenios"), required=False
|
||||
)
|
||||
faq_admin = forms.BooleanField(
|
||||
label=_("Autoriser à créer des FAQs"), required=False
|
||||
)
|
||||
election_admin = forms.BooleanField(
|
||||
label=_("Autoriser à créer des élections"), required=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
username = cleaned_data["username"]
|
||||
|
||||
if not username[:5] in ["cas__", "pwd__"]:
|
||||
self.add_error(
|
||||
"username",
|
||||
_(
|
||||
"Format de login invalide, seuls les comptes CAS ou avec "
|
||||
"mot de passe sont modifiables"
|
||||
),
|
||||
)
|
||||
elif not User.objects.filter(username=username).exists():
|
||||
self.add_error("username", _("Pas d'utilisateur·rice avec ce login"))
|
||||
|
||||
return cleaned_data
|
||||
|
|
6
shared/auth/staticdefs.py
Normal file
6
shared/auth/staticdefs.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
CONNECTION_METHODS = {
|
||||
"pwd": _("mot de passe"),
|
||||
"cas": _("CAS"),
|
||||
}
|
|
@ -9,4 +9,9 @@ urlpatterns = [
|
|||
name="auth.election",
|
||||
),
|
||||
path("pwd-create", views.CreatePwdAccount.as_view(), name="auth.create-account"),
|
||||
path("admin", views.AdminPanelView.as_view(), name="auth.admin"),
|
||||
path(
|
||||
"permissions", views.PermissionManagementView.as_view(), name="auth.permissions"
|
||||
),
|
||||
path("accounts", views.AccountListView.as_view(), name="auth.accounts"),
|
||||
]
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, FormView, ListView, TemplateView
|
||||
|
||||
from .forms import ElectionAuthForm, PwdUserForm
|
||||
from .forms import ElectionAuthForm, PwdUserForm, UserAdminForm
|
||||
from .utils import generate_password
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Mixin to restrict access to staff members
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class StaffMemberMixin(UserPassesTestMixin):
|
||||
"""
|
||||
Mixin permettant de restreindre l'accès aux membres `staff`, si la personne
|
||||
n'est pas connectée, renvoie sur la page d'authentification
|
||||
"""
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_active and self.request.user.is_staff
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Election Specific Login
|
||||
# #############################################################################
|
||||
|
@ -28,13 +46,21 @@ class ElectionLoginView(auth_views.LoginView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Admin Panel
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class AdminPanelView(StaffMemberMixin, TemplateView):
|
||||
template_name = "auth/admin-panel.html"
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Creation of Password Accounts
|
||||
# #############################################################################
|
||||
|
||||
|
||||
@method_decorator(staff_member_required, name="dispatch")
|
||||
class CreatePwdAccount(CreateView):
|
||||
class CreatePwdAccount(StaffMemberMixin, CreateView):
|
||||
model = User
|
||||
form_class = PwdUserForm
|
||||
template_name = "auth/create-user.html"
|
||||
|
@ -46,3 +72,79 @@ class CreatePwdAccount(CreateView):
|
|||
|
||||
# On envoie un mail pour réinitialiser le mot de passe
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# List of password and CAS users
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class AccountListView(StaffMemberMixin, ListView):
|
||||
model = User
|
||||
template_name = "auth/account-list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
qs = self.get_queryset()
|
||||
|
||||
ctx["cas_users"] = qs.filter(username__startswith="cas__")
|
||||
ctx["pwd_users"] = qs.filter(username__startswith="pwd__")
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Permission management
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class PermissionManagementView(StaffMemberMixin, SuccessMessageMixin, FormView):
|
||||
form_class = UserAdminForm
|
||||
template_name = "auth/permission-management.html"
|
||||
success_message = _("Permissions modifiées avec succès !")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update({"username": self.request.GET.get("user", None)})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_initial(self):
|
||||
username = self.request.GET.get("user", None)
|
||||
if username is not None:
|
||||
user = User.objects.filter(username=username).first()
|
||||
|
||||
if user is not None:
|
||||
return {
|
||||
"username": username,
|
||||
"full_admin": user.is_staff,
|
||||
"election_admin": user.has_perm("elections.election_admin"),
|
||||
"faq_admin": user.has_perm("faqs.faq_admin"),
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("auth.permissions") + f"?user={self.user}"
|
||||
|
||||
def form_valid(self, form):
|
||||
user = User.objects.get(username=form.cleaned_data["username"])
|
||||
self.user = user.username
|
||||
|
||||
# Kadenios admin
|
||||
user.is_staff = form.cleaned_data["full_admin"]
|
||||
|
||||
# Election admin
|
||||
perm_election = Permission.objects.get(codename="election_admin")
|
||||
if form.cleaned_data["election_admin"]:
|
||||
perm_election.user_set.add(user)
|
||||
else:
|
||||
perm_election.user_set.remove(user)
|
||||
|
||||
# FAQ admin
|
||||
perm_faq = Permission.objects.get(codename="faq_admin")
|
||||
if form.cleaned_data["faq_admin"]:
|
||||
perm_faq.user_set.add(user)
|
||||
else:
|
||||
perm_faq.user_set.remove(user)
|
||||
|
||||
user.save()
|
||||
return super().form_valid(form)
|
||||
|
|
94
shared/templates/auth/account-list.html
Normal file
94
shared/templates/auth/account-list.html
Normal file
|
@ -0,0 +1,94 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
function initSearch(input) {
|
||||
const $search = document.getElementById(input);
|
||||
const $users = $search.closest('div.panel').querySelectorAll('a.panel-block') || [];
|
||||
|
||||
$search.addEventListener('input', () => {
|
||||
const username = $search.value.toLowerCase();
|
||||
|
||||
$users.forEach(user => {
|
||||
if (user.id.includes(username)) {
|
||||
user.classList.remove('is-hidden');
|
||||
} else {
|
||||
user.classList.add('is-hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSearch('pwd_search');
|
||||
initSearch('cas_search');
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Liste des comptes" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns">
|
||||
{# Password Accounts #}
|
||||
<div class="column is-half">
|
||||
<div class="panel">
|
||||
<p class="panel-heading is-radiusless">{% trans "Comptes avec mot de passe" %}</p>
|
||||
|
||||
{# Search bar #}
|
||||
<div class="panel-block">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="text" id="pwd_search" placeholder="{% trans "Search" %}">
|
||||
<span class="icon is-left">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# List of users #}
|
||||
{% for u in pwd_users %}
|
||||
<a class="panel-block" href="{% url 'auth.permissions' %}?user={{ u.username }}" id={{ u.base_username|lower }}>
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-user-cog"></i>
|
||||
</span>
|
||||
<span class="ml-2">{{ u.full_name }} ({{ u.base_username }})</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# CAS Accounts #}
|
||||
<div class="column is-half">
|
||||
<div class="panel">
|
||||
<p class="panel-heading is-radiusless">{% trans "Comptes CAS" %}</p>
|
||||
|
||||
{# Search bar #}
|
||||
<div class="panel-block">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="text" id="cas_search" placeholder="{% trans "Search" %}">
|
||||
<span class="icon is-left">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# List of users #}
|
||||
{% for u in cas_users %}
|
||||
<a class="panel-block" href="{% url 'auth.permissions' %}?user={{ u.username }}" id={{ u.base_username|lower }}>
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-user-cog"></i>
|
||||
</span>
|
||||
<span class="ml-2">{{ u.full_name }} ({{ u.base_username }})</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
38
shared/templates/auth/admin-panel.html
Normal file
38
shared/templates/auth/admin-panel.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Gestion de Kadenios" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent">
|
||||
<a class="tile is-child notification is-light" href="{% url 'auth.create-account' %}">
|
||||
<div class="subtitle has-text-centered">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Créer un nouveau compte" %}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tile is-parent">
|
||||
<a class="tile is-child notification is-light" href="{% url 'auth.accounts' %}">
|
||||
<div class="subtitle has-text-centered">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fas fa-stream"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Liste des comptes" %}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -23,6 +23,15 @@
|
|||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'auth.admin' %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
56
shared/templates/auth/permission-management.html
Normal file
56
shared/templates/auth/permission-management.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
const cas_users = {{ cas_users|safe }};
|
||||
const pwd_users = {{ pwd_users|safe }};
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Gestion des permissions" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="message is-primary">
|
||||
<p class="message-body">
|
||||
{% trans "Pour modifier un compte CAS, le nom d'utilisateur doit commencer par <code>cas__</code>, pour un compte avec mot de passe, <code>pwd__</code>." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light" type="submit">
|
||||
<span class="icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'auth.accounts' %}#{{ username }}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -37,5 +37,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -137,6 +137,17 @@
|
|||
<div class="level-item">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="level is-mobile">
|
||||
{# Paramètres de Kadenios #}
|
||||
{% if user.is_staff %}
|
||||
<div class="level-item has-tooltip-light has-tooltip-bottom" data-tooltip="Administration">
|
||||
<a class="button is-primary" href="{% url 'auth.admin' %}">
|
||||
<span class="icon is-size-3">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="level-item">
|
||||
<div class="tag">
|
||||
{% blocktrans with name=user.base_username connection=user.connection_method %}Connecté·e en tant que {{ name }} par {{ connection }}{% endblocktrans %}
|
||||
|
@ -208,6 +219,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% block layout %}
|
||||
<div class="main-content">
|
||||
<div class="columns is-centered">
|
||||
|
|
Loading…
Reference in a new issue