Compare commits
18 commits
thubrecht/
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
25a1b880e5 | ||
d98d8d013c | |||
|
dbffa735a0 | ||
4262f0e7a5 | |||
|
9ce3238000 | ||
29c5abbc1a | |||
|
cc50d540c3 | ||
|
ef00b63053 | ||
c771f1ad49 | |||
66cb88721f | |||
e8830540ec | |||
93792f5f46 | |||
d5c1e2225a | |||
3da477729a | |||
9c02e1e902 | |||
f257209f8f | |||
0fa182cb4a | |||
|
20d0f1fba9 |
12 changed files with 695 additions and 102 deletions
|
@ -109,10 +109,11 @@ DATABASES = {
|
|||
"NAME": DBNAME,
|
||||
"USER": DBUSER,
|
||||
"PASSWORD": DBPASSWD,
|
||||
"HOST": "localhost",
|
||||
"HOST": "",
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Django==2.2.*
|
||||
Django==3.2.*
|
||||
git+https://git.eleves.ens.fr/klub-dev-ens/django-allauth-ens.git@1.1.3
|
||||
wiki==0.7
|
||||
wiki==0.7.*
|
||||
|
|
|
@ -1,55 +1,61 @@
|
|||
{% extends "wiki/base_site.html" %}
|
||||
{% load i18n staticfiles %}
|
||||
{% load sekizai_tags %}
|
||||
{% load i18n static sekizai_tags %}
|
||||
|
||||
{% block wiki_site_title %} - WikiENS{% endblock %}
|
||||
|
||||
{% block wiki_header_branding %}
|
||||
<a id="navbar-title" class="navbar-brand" href="/">
|
||||
Wiki <img src="{% static 'img/logoENS-small.png' %}" style="display:inline-block; height:100%;" /> ENS
|
||||
Wiki <img src="{% static 'img/logoENS-small.png' %}" style="display:inline-block; height:100%;" /> ENS
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block wiki_header_navlinks %}
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-primary" href="{% url 'wiki:root' %}">Accueil</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-primary" href="{% url 'wiki:root' %}">Accueil</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-right">
|
||||
<ul class="navbar-nav">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
{% trans "Paramètres de compte" %}
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{% url "account_email" %}">
|
||||
<i class="fa fa-envelope"></i>
|
||||
{% trans "Email" %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url "account_change_password" %}">
|
||||
<i class="fa fa-lock"></i>
|
||||
{% trans "Mot de passe" %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url "socialaccount_connections" %}" title="Clipper…">
|
||||
<i class="fa fa-sign-in"></i>
|
||||
{% trans "Connexions par tiers" %}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url "account_logout" %}">
|
||||
<i class="fa fa-power-off"></i>
|
||||
{% trans "Déconnexion" %}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url "account_signup" %}">{% trans "Sign Up" %}</a>
|
||||
</li>
|
||||
<ul class="navbar-nav">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
{% trans "Paramètres de compte" %}
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{% url 'account_email' %}">
|
||||
<i class="fa fa-envelope"></i>
|
||||
{% trans "Email" %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url 'account_change_password' %}">
|
||||
<i class="fa fa-lock"></i>
|
||||
{% trans "Mot de passe" %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url 'socialaccount_connections' %}" title="Clipper…">
|
||||
<i class="fa fa-sign-in-alt"></i>
|
||||
{% trans "Connexions par tiers" %}
|
||||
</a>
|
||||
{% if request.user.is_staff or request.user.managed_groups.exists %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'wiki_groups:managed-groups' %}">
|
||||
<i class="fa fa-cog"></i>
|
||||
{% trans "Liste des groupes gérés" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'account_logout' %}">
|
||||
<i class="fa fa-power-off"></i>
|
||||
{% trans "Déconnexion" %}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
52
wiki_groups/forms.py
Normal file
52
wiki_groups/forms.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from wiki_groups.models import WikiGroup
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SelectUserForm(forms.Form):
|
||||
user = forms.CharField(max_length=150)
|
||||
|
||||
def clean_user(self):
|
||||
user = User.objects.filter(username=self.cleaned_data["user"]).first()
|
||||
|
||||
if user is None:
|
||||
self.add_error(
|
||||
"user", "Aucune utilisatrice ou utilisateur avec ce login n'existe."
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class SelectGroupForm(forms.Form):
|
||||
group = forms.CharField(max_length=150)
|
||||
|
||||
def clean_group(self):
|
||||
group = WikiGroup.objects.filter(
|
||||
django_group__name=self.cleaned_data["group"]
|
||||
).first()
|
||||
|
||||
if group is None:
|
||||
self.add_error("group", "Aucun groupe avec ce nom n'existe.")
|
||||
|
||||
return group
|
||||
|
||||
|
||||
class CreateGroupForm(forms.Form):
|
||||
group = forms.CharField(max_length=150)
|
||||
|
||||
def clean_group(self):
|
||||
name = self.cleaned_data["group"]
|
||||
|
||||
django_group, created = Group.objects.get_or_create(name=name)
|
||||
|
||||
if hasattr(django_group, "wikigroup"):
|
||||
self.add_error("group", "Un groupe avec ce nom existe déjà.")
|
||||
return None
|
||||
|
||||
group = WikiGroup.objects.create(django_group=django_group)
|
||||
|
||||
return group
|
20
wiki_groups/migrations/0002_wikigroup_managers.py
Normal file
20
wiki_groups/migrations/0002_wikigroup_managers.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.19 on 2021-07-23 09:01
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wiki_groups', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='wikigroup',
|
||||
name='managers',
|
||||
field=models.ManyToManyField(blank=True, related_name='managed_groups', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -1,12 +1,14 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import Group as DjangoGroup
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_save, m2m_changed
|
||||
from django.contrib.auth.models import Group as DjangoGroup
|
||||
from django.db import models
|
||||
from django.db.models.signals import m2m_changed, post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class WikiGroup(models.Model):
|
||||
""" A structured user group, used to grant permissions on sub-wikis
|
||||
"""A structured user group, used to grant permissions on sub-wikis
|
||||
|
||||
This model contains a structured group of users, in the sense that a group contains
|
||||
both users and other groups, allowing a DAG group structure.
|
||||
|
@ -20,8 +22,8 @@ class WikiGroup(models.Model):
|
|||
"""
|
||||
|
||||
class CyclicStructureException(Exception):
|
||||
""" Exception raised when a new edge introduces a cycle in the groups'
|
||||
structure """
|
||||
"""Exception raised when a new edge introduces a cycle in the groups'
|
||||
structure"""
|
||||
|
||||
def __init__(self, from_group, to_group):
|
||||
self.from_group = from_group
|
||||
|
@ -40,22 +42,54 @@ class WikiGroup(models.Model):
|
|||
related_name="included_in_groups",
|
||||
blank=True,
|
||||
)
|
||||
users = models.ManyToManyField(get_user_model(), blank=True)
|
||||
users = models.ManyToManyField(User, blank=True)
|
||||
managers = models.ManyToManyField(User, related_name="managed_groups", blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.django_group)
|
||||
|
||||
def get_all_users(self):
|
||||
""" Get the queryset of all the users in this group, including recursively
|
||||
included users """
|
||||
"""Get the queryset of all the users in this group, including recursively
|
||||
included users"""
|
||||
users_set = self.users.all()
|
||||
for subgroup in self.includes_groups.all():
|
||||
users_set = users_set.union(subgroup.get_all_users())
|
||||
return users_set
|
||||
|
||||
def is_manager(self, user):
|
||||
"""Checks wether the user is a manager of this group or any subgroup"""
|
||||
|
||||
# Base case: the user is an explicit manager
|
||||
if user in self.managers.all():
|
||||
return True
|
||||
|
||||
for subgroup in self.includes_groups.all():
|
||||
# If the user is a manager of a subgroup
|
||||
if subgroup.is_manager(user):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_all_groups(self, already_notified=None):
|
||||
"""Returns the set of metagroups i.e. self plus the list of groups including
|
||||
this group recursively"""
|
||||
|
||||
if already_notified is None:
|
||||
already_notified = set()
|
||||
elif self.pk in already_notified:
|
||||
return set()
|
||||
already_notified.add(self.pk)
|
||||
|
||||
groups = {self}
|
||||
|
||||
for metagroup in self.included_in_groups.all():
|
||||
groups |= metagroup.get_all_groups(already_notified=already_notified)
|
||||
|
||||
return groups
|
||||
|
||||
def propagate_update(self, already_notified=None):
|
||||
""" Commits itself to the Django group, and calls this method on every group in
|
||||
`included_in_groups` """
|
||||
"""Commits itself to the Django group, and calls this method on every group in
|
||||
`included_in_groups`"""
|
||||
|
||||
# Check that we did not already propagate the update signal to this group
|
||||
if already_notified is None:
|
||||
|
@ -69,20 +103,20 @@ class WikiGroup(models.Model):
|
|||
metagroup.propagate_update(already_notified=already_notified)
|
||||
|
||||
def commit_to_django_group(self):
|
||||
""" Writes this model's data to the related Django group """
|
||||
"""Writes this model's data to the related Django group"""
|
||||
|
||||
self.django_group.user_set.set(self.get_all_users())
|
||||
|
||||
def group_in_cycle(self, with_children):
|
||||
""" Checks whether this group would be in a group cycle if it had
|
||||
"""Checks whether this group would be in a group cycle if it had
|
||||
`with_children` as child nodes. This assumes that the graph currently stored is
|
||||
acyclic.
|
||||
|
||||
Returns `None` if no cycle is found, else retuns a child from `with_children`
|
||||
causing the cycle to appear. """
|
||||
causing the cycle to appear."""
|
||||
|
||||
def do_dfs(cur_node, visited_nodes):
|
||||
""" DFS to check whether we find `self` again """
|
||||
"""DFS to check whether we find `self` again"""
|
||||
if cur_node.pk in visited_nodes:
|
||||
return False
|
||||
if cur_node.pk == self.pk:
|
||||
|
@ -103,7 +137,7 @@ class WikiGroup(models.Model):
|
|||
|
||||
@receiver(post_save, sender=WikiGroup, dispatch_uid="on_wiki_group_changed")
|
||||
def on_wiki_group_changed(sender, instance, **kwargs):
|
||||
""" Commit the related WikiGroups to Django Group upon model change """
|
||||
"""Commit the related WikiGroups to Django Group upon model change"""
|
||||
instance.propagate_update()
|
||||
|
||||
|
||||
|
@ -113,8 +147,8 @@ def on_wiki_group_changed(sender, instance, **kwargs):
|
|||
dispatch_uid="on_wiki_group_includes_changed",
|
||||
)
|
||||
def on_wiki_group_includes_changed(sender, instance, action, **kwargs):
|
||||
""" Commit the related WikiGroups to Django Group upon change of the set of
|
||||
included other groups """
|
||||
"""Commit the related WikiGroups to Django Group upon change of the set of
|
||||
included other groups"""
|
||||
if action in ["post_add", "post_remove", "post_clear"]:
|
||||
instance.propagate_update()
|
||||
|
||||
|
@ -125,7 +159,7 @@ def on_wiki_group_includes_changed(sender, instance, action, **kwargs):
|
|||
dispatch_uid="on_wiki_group_users_changed",
|
||||
)
|
||||
def on_wiki_group_users_changed(sender, instance, action, **kwargs):
|
||||
""" Commit the related WikiGroups to Django Group upon change of included users """
|
||||
"""Commit the related WikiGroups to Django Group upon change of included users"""
|
||||
if action in ["post_add", "post_remove", "post_clear"]:
|
||||
instance.propagate_update()
|
||||
|
||||
|
@ -136,7 +170,7 @@ def on_wiki_group_users_changed(sender, instance, action, **kwargs):
|
|||
dispatch_uid="on_wiki_group_includes_check_acyclic",
|
||||
)
|
||||
def on_wiki_group_includes_check_acyclic(sender, instance, action, pk_set, **kwargs):
|
||||
""" Checks the acyclicity of the groups' graph before committing new edges.
|
||||
"""Checks the acyclicity of the groups' graph before committing new edges.
|
||||
|
||||
PLEASE NOTE that this check is only a fallback, and that forms should validate
|
||||
the acyclicity before committing anything.
|
||||
|
|
207
wiki_groups/templates/wiki_groups/admin.html
Normal file
207
wiki_groups/templates/wiki_groups/admin.html
Normal file
|
@ -0,0 +1,207 @@
|
|||
{% extends "wiki/base.html" %}
|
||||
{% load sekizai_tags %}
|
||||
|
||||
{% block wiki_site_title %}Groupes administrés - WikiENS{% endblock %}
|
||||
|
||||
{% block wiki_contents %}
|
||||
<h2>Gestion du groupe « {{ wikigroup }} »</h2>
|
||||
<hr>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col">
|
||||
<h4>Liste des membres</h4>
|
||||
<br>
|
||||
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<form action="{% url 'wiki_groups:add-user' wikigroup.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control {% if errors.user %}is-invalid{% endif %}" name="user" value="{{ errors.user.value }}" placeholder="Ajouter un membre">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary rounded-right">Enregistrer</button>
|
||||
</div>
|
||||
{% if errors.user %}
|
||||
{% for msg in errors.user.msg %}
|
||||
<div class="invalid-feedback">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Entrer le login (clipper) de la personne à ajouter</small>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for user in wikigroup.users.all %}
|
||||
<div class="list-group-item pb-2">
|
||||
<span class="font-italic">{{ user }}</span>
|
||||
<button type="button" class="btn btn-danger btn-sm float-right" data-toggle="modal" data-target="#modal-confirm" data-href="{% url 'wiki_groups:remove-user' wikigroup.pk user.pk %}" data-name="{{ user }}" data-kind="membre">Enlever</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<h4>Liste des groupes inclus</h4>
|
||||
<br>
|
||||
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<form action="{% url 'wiki_groups:add-group' wikigroup.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control {% if errors.group_add %}is-invalid{% endif %}" name="group" value="{{ errors.group_add.value }}" placeholder="Ajouter un groupe existant">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary rounded-right">Enregistrer</button>
|
||||
</div>
|
||||
{% if errors.group_add %}
|
||||
{% for msg in errors.group_add.msg %}
|
||||
<div class="invalid-feedback">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Entrer le nom du groupe à ajouter</small>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for group in wikigroup.includes_groups.all %}
|
||||
<div class="list-group-item pb-2">
|
||||
<span class="font-italic">{{ group }}</span>
|
||||
<button type="button" class="btn btn-danger btn-sm float-right" data-toggle="modal" data-target="#modal-confirm" data-href="{% url 'wiki_groups:remove-group' wikigroup.pk group.pk %}" data-name="{{ group }}" data-kind="groupe">Enlever</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="list-group-item">
|
||||
<form action="{% url 'wiki_groups:create-group' wikigroup.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control {% if errors.group_create %}is-invalid{% endif %}" name="group" value="{{ errors.group_create.value }}" placeholder="Créer et ajouter un groupe">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary rounded-right">Enregistrer</button>
|
||||
</div>
|
||||
{% if errors.group_create %}
|
||||
{% for msg in errors.group_create.msg %}
|
||||
<div class="invalid-feedback">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Entrer le nom du groupe à créer</small>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h4>Liste des gestionnaires</h4>
|
||||
<br>
|
||||
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<form action="{% url 'wiki_groups:add-manager' wikigroup.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control {% if errors.manager %}is-invalid{% endif %}" name="user" value="{{ errors.manager.value }}" placeholder="Ajouter un·e gestionnaire">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary rounded-right">Enregistrer</button>
|
||||
</div>
|
||||
{% if errors.manager %}
|
||||
{% for msg in errors.manager.msg %}
|
||||
<div class="invalid-feedback">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Entrer le login (clipper) de la personne à ajouter</small>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for user in wikigroup.managers.all %}
|
||||
<div class="list-group-item pb-2">
|
||||
<span class="font-italic">{{ user }}</span>
|
||||
<button type="button" class="btn btn-danger btn-sm float-right" data-toggle="modal" data-target="#modal-confirm" data-href="{% url 'wiki_groups:remove-manager' wikigroup.pk user.pk %}" data-name="{{ user }}" data-kind="gestionnaire">Enlever</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<div class="col">
|
||||
<h4>Supprimer ce groupe</h4>
|
||||
<br>
|
||||
|
||||
<div class="jumbotron">
|
||||
<p class="text-danger">Attention, cette action est irréversible.</p>
|
||||
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#modal-delete">Supprimer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modal-delete">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Supprimer le groupe {{ wikigroup }} ?</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
La suppression du groupe « {{ wikigroup }} » est irréversible et tous ses membres seront enlevés des groupes suivants :
|
||||
<ul>
|
||||
{% for g in wikigroup.get_all_groups %}
|
||||
<li>{{ g }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
|
||||
<a class="btn btn-danger" href="{% url 'wiki_groups:delete-group' wikigroup.pk %}">Supprimer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Confirmation modal #}
|
||||
<div class="modal fade" id="modal-confirm">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
|
||||
<a class="btn btn-danger">Enlever</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% addtoblock "js" %}
|
||||
<script>
|
||||
$('#modal-confirm').on('show.bs.modal', function(event) {
|
||||
const b = $(event.relatedTarget);
|
||||
|
||||
const modal = $(this);
|
||||
modal.find('.modal-title').text(`Enlever un ${b.data('kind')}`);
|
||||
modal.find('.modal-body').html(`Enlever <span class="font-weight-bold font-italic">${b.data('name')}</span> en tant que ${b.data('kind')} du groupe {{ wikigroup }} ?`);
|
||||
modal.find('.modal-footer a').prop('href', b.data('href'));
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endaddtoblock %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,37 +0,0 @@
|
|||
{% extends "wiki/base.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block wiki_site_title %}Groupes - WikiENS{% endblock %}
|
||||
|
||||
{% block wiki_contents %}
|
||||
<h2>Graphe des groupes du wiki</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id="svg-graph"></div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
Les flèches représentent l'inclusion : <code>A -> B</code> signifie que
|
||||
le groupe <code>A</code> est contenu dans le groupe <code>B</code>.
|
||||
</p>
|
||||
|
||||
<script src="{% static 'wiki_groups/js/vendor/viz.js' %}"></script>
|
||||
<script src="{% static 'wiki_groups/js/vendor/lite.render.js' %}"></script>
|
||||
<script>
|
||||
var viz = new Viz();
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
viz.renderSVGElement(this.responseText)
|
||||
.then(element => {
|
||||
document.getElementById("svg-graph").appendChild(element);
|
||||
})
|
||||
}
|
||||
};
|
||||
xhttp.open("GET", "{% url 'wiki_groups:dot_graph' %}", true);
|
||||
xhttp.send();
|
||||
</script>
|
||||
{% endblock %}
|
52
wiki_groups/templates/wiki_groups/list.html
Normal file
52
wiki_groups/templates/wiki_groups/list.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% extends "wiki/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block wiki_site_title %}Groupes - WikiENS{% endblock %}
|
||||
|
||||
{% block wiki_contents %}
|
||||
<h2>
|
||||
Liste des groupes du wiki
|
||||
{% if request.user.is_staff or request.user.managed_groups.exists %}
|
||||
<a class="btn btn-primary float-right" href="{% url 'wiki_groups:managed-groups' %}">
|
||||
Liste des groupes gérés
|
||||
</a>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<ul>
|
||||
{% for group in wikigroup_list %}
|
||||
<li>{{ group.django_group.name }} {% if group.managers.exists %}(Géré par {% for m in group.managers.all %}{{ m }}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Graphe des groupes du wiki</h2>
|
||||
<hr>
|
||||
|
||||
<div id="svg-graph" class="overflow-auto"></div>
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
Les flèches représentent l'inclusion : <code>A -> B</code> signifie que
|
||||
le groupe <code>A</code> est contenu dans le groupe <code>B</code>.
|
||||
</p>
|
||||
|
||||
<script src="{% static 'wiki_groups/js/vendor/viz.js' %}"></script>
|
||||
<script src="{% static 'wiki_groups/js/vendor/lite.render.js' %}"></script>
|
||||
<script>
|
||||
var viz = new Viz();
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
viz.renderSVGElement(this.responseText)
|
||||
.then(element => {
|
||||
document.getElementById("svg-graph").appendChild(element);
|
||||
})
|
||||
}
|
||||
};
|
||||
xhttp.open('GET', '{% url 'wiki_groups:dot_graph' %}', true);
|
||||
xhttp.send();
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
18
wiki_groups/templates/wiki_groups/managed.html
Normal file
18
wiki_groups/templates/wiki_groups/managed.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "wiki/base.html" %}
|
||||
|
||||
{% block wiki_site_title %}Groupes administrés - WikiENS{% endblock %}
|
||||
|
||||
{% block wiki_contents %}
|
||||
<h2>Liste des groupes administrés</h2>
|
||||
<hr>
|
||||
|
||||
{% if wikigroup_list %}
|
||||
<div class="list-group">
|
||||
{% for group in wikigroup_list %}
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'wiki_groups:admin-group' group.pk %}">{{ group }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -1,9 +1,31 @@
|
|||
from django.urls import path
|
||||
from wiki_groups import views
|
||||
|
||||
from wiki_groups import views
|
||||
|
||||
app_name = "wiki_groups"
|
||||
urlpatterns = [
|
||||
path("", views.GroupList.as_view(), name="list"),
|
||||
path("dot_graph", views.dot_graph, name="dot_graph"),
|
||||
path("graph", views.graph, name="graph"),
|
||||
path("managed", views.ManagedGroupsView.as_view(), name="managed-groups"),
|
||||
path("<int:pk>", views.WikiGroupView.as_view(), name="admin-group"),
|
||||
path(
|
||||
"<int:pk>/rm-user/<int:user_pk>",
|
||||
views.RemoveUserView.as_view(),
|
||||
name="remove-user",
|
||||
),
|
||||
path(
|
||||
"<int:pk>/rm-manager/<int:user_pk>",
|
||||
views.RemoveManagerView.as_view(),
|
||||
name="remove-manager",
|
||||
),
|
||||
path(
|
||||
"<int:pk>/rm-group/<int:group_pk>",
|
||||
views.RemoveGroupView.as_view(),
|
||||
name="remove-group",
|
||||
),
|
||||
path("<int:pk>/add-user", views.AddUserView.as_view(), name="add-user"),
|
||||
path("<int:pk>/add-manager", views.AddManagerView.as_view(), name="add-manager"),
|
||||
path("<int:pk>/add-group", views.AddGroupView.as_view(), name="add-group"),
|
||||
path("<int:pk>/create-group", views.CreateGroupView.as_view(), name="create-group"),
|
||||
path("<int:pk>/delete", views.DeleteGroupView.as_view(), name="delete-group"),
|
||||
]
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
from django.http import HttpResponse
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views.generic import DetailView, ListView, RedirectView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.edit import BaseFormView
|
||||
|
||||
from wiki_groups.forms import CreateGroupForm, SelectGroupForm, SelectUserForm
|
||||
from wiki_groups.models import WikiGroup
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def dot_graph(request):
|
||||
response = HttpResponse(content_type="text/vnd.graphviz")
|
||||
|
@ -22,4 +30,214 @@ def dot_graph(request):
|
|||
return response
|
||||
|
||||
|
||||
graph = TemplateView.as_view(template_name="wiki_groups/graph.html")
|
||||
class GroupList(ListView):
|
||||
template_name = "wiki_groups/list.html"
|
||||
model = WikiGroup
|
||||
|
||||
def get_queryset(self):
|
||||
return WikiGroup.objects.select_related("django_group").order_by(
|
||||
"django_group__name"
|
||||
)
|
||||
|
||||
|
||||
class WikiGroupMixin(SingleObjectMixin, UserPassesTestMixin):
|
||||
"""
|
||||
Restricts the view to a manager of the wikigroup, and selects automatically
|
||||
the required WikiGroup with its Django group
|
||||
"""
|
||||
|
||||
model = WikiGroup
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related("django_group")
|
||||
|
||||
def test_func(self):
|
||||
self.object = self.get_object()
|
||||
return self.request.user.is_staff or self.object.is_manager(self.request.user)
|
||||
|
||||
|
||||
class StaffMixin(UserPassesTestMixin):
|
||||
"""Restricts the view to a staff member"""
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_staff
|
||||
|
||||
|
||||
class ManagedGroupsView(LoginRequiredMixin, ListView):
|
||||
model = WikiGroup
|
||||
template_name = "wiki_groups/managed.html"
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_staff:
|
||||
return WikiGroup.objects.select_related("django_group")
|
||||
return self.request.user.managed_groups.select_related("django_group")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
wikigroups = set()
|
||||
|
||||
for group in ctx["wikigroup_list"]:
|
||||
wikigroups |= group.get_all_groups()
|
||||
|
||||
ctx["wikigroup_list"] = wikigroups
|
||||
ctx["object_list"] = wikigroups
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class WikiGroupView(WikiGroupMixin, DetailView):
|
||||
template_name = "wiki_groups/admin.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update({"errors": self.request.session.pop("wiki_form_errors", None)})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class RemoveUserView(WikiGroupMixin, RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return reverse("wiki_groups:admin-group", args=[kwargs["pk"]])
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = User.objects.filter(pk=kwargs["user_pk"]).first()
|
||||
|
||||
if user is not None:
|
||||
self.object.users.remove(user)
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RemoveManagerView(WikiGroupMixin, RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return reverse("wiki_groups:admin-group", args=[kwargs["pk"]])
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = User.objects.filter(pk=kwargs["user_pk"]).first()
|
||||
|
||||
if user is not None:
|
||||
self.object.users.remove(user)
|
||||
|
||||
self.object.managers.remove(user)
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RemoveGroupView(WikiGroupMixin, RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return reverse("wiki_groups:admin-group", args=[kwargs["pk"]])
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
group = WikiGroup.objects.filter(pk=kwargs["group_pk"]).first()
|
||||
|
||||
if group is not None:
|
||||
self.object.includes_groups.remove(group)
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AddUserView(WikiGroupMixin, BaseFormView):
|
||||
form_class = SelectUserForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("wiki_groups:admin-group", args=[self.object.pk])
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object.users.add(form.cleaned_data["user"])
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
self.request.session["wiki_form_errors"] = {
|
||||
"user": {"value": form["user"].value(), "msg": form.errors["user"]}
|
||||
}
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class AddManagerView(WikiGroupMixin, BaseFormView):
|
||||
form_class = SelectUserForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("wiki_groups:admin-group", args=[self.object.pk])
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object.managers.add(form.cleaned_data["user"])
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
self.request.session["wiki_form_errors"] = {
|
||||
"manager": {"value": form["user"].value(), "msg": form.errors["user"]}
|
||||
}
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class AddGroupView(WikiGroupMixin, BaseFormView):
|
||||
"""Adds an existing metagroup to this group"""
|
||||
|
||||
form_class = SelectGroupForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("wiki_groups:admin-group", args=[self.object.pk])
|
||||
|
||||
def form_valid(self, form):
|
||||
subgroup = form.cleaned_data["group"]
|
||||
group = self.object
|
||||
|
||||
if group.group_in_cycle(list(group.includes_groups.all()) + [subgroup]):
|
||||
form.add_error(
|
||||
"group",
|
||||
(
|
||||
"Ajout impossible sous peine de créer un cycle "
|
||||
f"({group} est inclus dans {subgroup})"
|
||||
),
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
group.includes_groups.add(subgroup)
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
self.request.session["wiki_form_errors"] = {
|
||||
"group_add": {"value": form["group"].value(), "msg": form.errors["group"]}
|
||||
}
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class CreateGroupView(WikiGroupMixin, BaseFormView):
|
||||
"""Creates a metagroup included in this group"""
|
||||
|
||||
form_class = CreateGroupForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("wiki_groups:admin-group", args=[self.object.pk])
|
||||
|
||||
def form_valid(self, form):
|
||||
new_group = form.cleaned_data["group"]
|
||||
|
||||
self.object.includes_groups.add(new_group)
|
||||
new_group.managers.add(self.request.user)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
self.request.session["wiki_form_errors"] = {
|
||||
"group_create": {
|
||||
"value": form["group"].value(),
|
||||
"msg": form.errors["group"],
|
||||
}
|
||||
}
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class DeleteGroupView(SingleObjectMixin, StaffMixin, RedirectView):
|
||||
model = WikiGroup
|
||||
url = reverse_lazy("wiki_groups:managed-groups")
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
group = self.get_object()
|
||||
# On enlève les membres pour répercuter les changements
|
||||
group.users.clear()
|
||||
|
||||
# On utilise la propagation de la suppression django_group -> wiki_group
|
||||
group.django_group.delete()
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
|
Loading…
Reference in a new issue