Rajout de la gestion admin

This commit is contained in:
Tom Hubrecht 2021-07-13 02:41:32 +02:00
parent 287716276d
commit f56cd87358
18 changed files with 409 additions and 18 deletions

View 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")],
},
),
]

View file

@ -10,7 +10,7 @@ from .models import Election, Option, Question
class AdminOnlyMixin(PermissionRequiredMixin): class AdminOnlyMixin(PermissionRequiredMixin):
"""Restreint l'accès aux admins""" """Restreint l'accès aux admins"""
permission_required = "elections.is_admin" permission_required = "elections.election_admin"
class SelectElectionMixin: class SelectElectionMixin:

View file

@ -6,12 +6,12 @@ from django.db import models, transaction
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from shared.auth import CONNECTION_METHODS
from shared.utils import choices_length from shared.utils import choices_length
from .staticdefs import ( from .staticdefs import (
BALLOT_TYPE, BALLOT_TYPE,
CAST_FUNCTIONS, CAST_FUNCTIONS,
CONNECTION_METHODS,
QUESTION_TYPES, QUESTION_TYPES,
TALLY_FUNCTIONS, TALLY_FUNCTIONS,
VALIDATE_FUNCTIONS, VALIDATE_FUNCTIONS,
@ -81,7 +81,7 @@ class Election(models.Model):
class Meta: class Meta:
permissions = [ permissions = [
("is_admin", _("Peut administrer des élections")), ("election_admin", _("Peut administrer des élections")),
] ]
ordering = ["-start_date", "-end_date"] ordering = ["-start_date", "-end_date"]

View file

@ -22,11 +22,6 @@ MAIL_VOTE_DELETED = (
"Kadenios" "Kadenios"
) )
CONNECTION_METHODS = {
"pwd": _("mot de passe"),
"cas": _("CAS"),
}
QUESTION_TYPES = [ QUESTION_TYPES = [
("assentiment", _("Assentiment")), ("assentiment", _("Assentiment")),
("uninominal", _("Uninominal")), ("uninominal", _("Uninominal")),

View 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")]},
),
]

View file

@ -4,7 +4,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
class AdminOnlyMixin(PermissionRequiredMixin): class AdminOnlyMixin(PermissionRequiredMixin):
"""Restreint l'accès aux admins""" """Restreint l'accès aux admins"""
permission_required = "faqs.is_author" permission_required = "faqs.faq_admin"
class CreatorOnlyMixin(AdminOnlyMixin): class CreatorOnlyMixin(AdminOnlyMixin):

View file

@ -25,7 +25,7 @@ class Faq(models.Model):
class Meta: class Meta:
permissions = [ permissions = [
("is_author", "Can create faqs"), ("faq_admin", "Can create faqs"),
] ]
constraints = [ constraints = [
models.UniqueConstraint(fields=["anchor"], name="unique_faq_anchor") models.UniqueConstraint(fields=["anchor"], name="unique_faq_anchor")

3
shared/auth/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from .staticdefs import CONNECTION_METHODS
__all__ = [CONNECTION_METHODS]

View file

@ -82,3 +82,38 @@ class PwdUserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ["username", "full_name", "email"] 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

View file

@ -0,0 +1,6 @@
from django.utils.translation import gettext_lazy as _
CONNECTION_METHODS = {
"pwd": _("mot de passe"),
"cas": _("CAS"),
}

View file

@ -9,4 +9,9 @@ urlpatterns = [
name="auth.election", name="auth.election",
), ),
path("pwd-create", views.CreatePwdAccount.as_view(), name="auth.create-account"), 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"),
] ]

View file

@ -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 get_user_model
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.urls import reverse_lazy from django.contrib.auth.mixins import UserPassesTestMixin
from django.utils.decorators import method_decorator from django.contrib.auth.models import Permission
from django.views.generic.edit import CreateView 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 from .utils import generate_password
User = get_user_model() 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 # Election Specific Login
# ############################################################################# # #############################################################################
@ -28,13 +46,21 @@ class ElectionLoginView(auth_views.LoginView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
# #############################################################################
# Admin Panel
# #############################################################################
class AdminPanelView(StaffMemberMixin, TemplateView):
template_name = "auth/admin-panel.html"
# ############################################################################# # #############################################################################
# Creation of Password Accounts # Creation of Password Accounts
# ############################################################################# # #############################################################################
@method_decorator(staff_member_required, name="dispatch") class CreatePwdAccount(StaffMemberMixin, CreateView):
class CreatePwdAccount(CreateView):
model = User model = User
form_class = PwdUserForm form_class = PwdUserForm
template_name = "auth/create-user.html" template_name = "auth/create-user.html"
@ -46,3 +72,79 @@ class CreatePwdAccount(CreateView):
# On envoie un mail pour réinitialiser le mot de passe # On envoie un mail pour réinitialiser le mot de passe
return super().form_valid(form) 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)

View 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 %}

View 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 %}

View file

@ -23,6 +23,15 @@
<span>{% trans "Enregistrer" %}</span> <span>{% trans "Enregistrer" %}</span>
</button> </button>
</div> </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> </div>
</form> </form>
</div> </div>

View 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 %}

View file

@ -37,5 +37,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -137,6 +137,17 @@
<div class="level-item"> <div class="level-item">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="level is-mobile"> <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="level-item">
<div class="tag"> <div class="tag">
{% blocktrans with name=user.base_username connection=user.connection_method %}Connecté·e en tant que {{ name }} par {{ connection }}{% endblocktrans %} {% 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>
</div> </div>
</nav> </nav>
{% block layout %} {% block layout %}
<div class="main-content"> <div class="main-content">
<div class="columns is-centered"> <div class="columns is-centered">