mirror of
https://github.com/ae-utbm/sith.git
synced 2025-01-08 16:11:17 +00:00
Add UserBan management views
This commit is contained in:
parent
af47587116
commit
4f35cc00bc
@ -29,7 +29,7 @@
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
&:hover {
|
||||
&.clickable:hover {
|
||||
background-color: darken($primary-neutral-light-color, 5%);
|
||||
}
|
||||
|
||||
|
@ -199,14 +199,6 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------- LEGEND
|
||||
|
||||
legend {
|
||||
font-weight: var(--nf-label-font-weight);
|
||||
display: block;
|
||||
margin-bottom: calc(var(--nf-input-size) / 5);
|
||||
}
|
||||
|
||||
.form-group,
|
||||
> p,
|
||||
> div {
|
||||
|
@ -23,6 +23,9 @@
|
||||
<li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
|
||||
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
|
||||
{% endif %}
|
||||
{% if user.has_perm("core:view_userban") %}
|
||||
<li><a href="{{ url("rootplace:ban_list") }}">{% trans %}Bans{% endtrans %}</a></li>
|
||||
{% endif %}
|
||||
{% if user.can_create_subscription or user.is_root %}
|
||||
<li><a href="{{ url('subscription:subscription') }}">{% trans %}Subscriptions{% endtrans %}</a></li>
|
||||
{% endif %}
|
||||
|
@ -21,6 +21,7 @@
|
||||
#
|
||||
#
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
from io import BytesIO
|
||||
|
||||
from captcha.fields import CaptchaField
|
||||
@ -32,7 +33,14 @@ from django.contrib.staticfiles.management.commands.collectstatic import (
|
||||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.forms import CheckboxSelectMultiple, DateInput, DateTimeInput, TextInput
|
||||
from django.forms import (
|
||||
CheckboxSelectMultiple,
|
||||
DateInput,
|
||||
DateTimeInput,
|
||||
TextInput,
|
||||
Widget,
|
||||
)
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
@ -125,6 +133,23 @@ class SelectUser(TextInput):
|
||||
return output
|
||||
|
||||
|
||||
# Fields
|
||||
|
||||
|
||||
def validate_future_timestamp(value: date | datetime):
|
||||
if value <= now():
|
||||
raise ValueError(_("Ensure this timestamp is set in the future"))
|
||||
|
||||
|
||||
class FutureDateTimeField(forms.DateTimeField):
|
||||
"""A datetime field that accepts only future timestamps."""
|
||||
|
||||
default_validators = [validate_future_timestamp]
|
||||
|
||||
def widget_attrs(self, widget: Widget) -> dict[str, str]:
|
||||
return {"min": widget.format_value(now())}
|
||||
|
||||
|
||||
# Forms
|
||||
|
||||
|
||||
|
7
docs/reference/rootplace/forms.md
Normal file
7
docs/reference/rootplace/forms.md
Normal file
@ -0,0 +1,7 @@
|
||||
::: rootplace.forms
|
||||
handler: python
|
||||
options:
|
||||
members:
|
||||
- MergeForm
|
||||
- SelectUserForm
|
||||
- BanForm
|
@ -1 +1,12 @@
|
||||
::: rootplace.views
|
||||
::: rootplace.views
|
||||
handler: python
|
||||
options:
|
||||
members:
|
||||
- merge_users
|
||||
- delete_all_forum_user_messages
|
||||
- MergeUsersView
|
||||
- DeleteAllForumUserMessagesView
|
||||
- OperationLogListView
|
||||
- BanView
|
||||
- BanCreateView
|
||||
- BanDeleteView
|
@ -127,6 +127,7 @@ nav:
|
||||
- reference/pedagogy/schemas.md
|
||||
- rootplace:
|
||||
- reference/rootplace/models.md
|
||||
- reference/rootplace/forms.md
|
||||
- reference/rootplace/views.md
|
||||
- sas:
|
||||
- reference/sas/models.md
|
||||
|
49
rootplace/forms.py
Normal file
49
rootplace/forms.py
Normal file
@ -0,0 +1,49 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import User, UserBan
|
||||
from core.views.forms import FutureDateTimeField, SelectDateTime
|
||||
from core.views.widgets.select import AutoCompleteSelectUser
|
||||
|
||||
|
||||
class MergeForm(forms.Form):
|
||||
user1 = forms.ModelChoiceField(
|
||||
label=_("User that will be kept"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
user2 = forms.ModelChoiceField(
|
||||
label=_("User that will be deleted"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
|
||||
class SelectUserForm(forms.Form):
|
||||
user = forms.ModelChoiceField(
|
||||
label=_("User to be selected"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
|
||||
class BanForm(forms.ModelForm):
|
||||
"""Form to ban a user."""
|
||||
|
||||
required_css_class = "required"
|
||||
|
||||
class Meta:
|
||||
model = UserBan
|
||||
fields = ["user", "ban_group", "reason", "expires_at"]
|
||||
field_classes = {"expires_at": FutureDateTimeField}
|
||||
widgets = {
|
||||
"user": AutoCompleteSelectUser,
|
||||
"ban_group": forms.RadioSelect,
|
||||
"expires_at": SelectDateTime,
|
||||
}
|
62
rootplace/templates/rootplace/userban.jinja
Normal file
62
rootplace/templates/rootplace/userban.jinja
Normal file
@ -0,0 +1,62 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
|
||||
{% block additional_css %}
|
||||
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% if user.has_perm("core:add_userban") %}
|
||||
<a href="{{ url("rootplace:ban_create") }}" class="btn btn-red margin-bottom">
|
||||
<i class="fa fa-person-circle-xmark"></i>
|
||||
{% trans %}Ban a user{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% for user_ban in user_bans %}
|
||||
<div class="card card-row margin-bottom">
|
||||
<img
|
||||
class="card-image"
|
||||
alt="profil de {{ user_ban.user.get_short_name() }}"
|
||||
{%- if user_ban.user.profile_pict -%}
|
||||
src="{{ user_ban.user.profile_pict.get_download_url() }}"
|
||||
{%- else -%}
|
||||
src="{{ static("core/img/unknown.jpg") }}"
|
||||
{%- endif -%}
|
||||
/>
|
||||
<div class="card-content">
|
||||
<strong>
|
||||
<a href="{{ user_ban.user.get_absolute_url() }}">
|
||||
{{ user_ban.user.get_full_name() }}
|
||||
</a>
|
||||
</strong>
|
||||
<em>{{ user_ban.ban_group.name }}</em>
|
||||
<p>{% trans %}Since{% endtrans %} : {{ user_ban.created_at|date }}</p>
|
||||
<p>
|
||||
{% trans %}Until{% endtrans %} :
|
||||
{% if user_ban.expires_at %}
|
||||
{{ user_ban.expires_at|date }} {{ user_ban.expires_at|time }}
|
||||
{% else %}
|
||||
{% trans %}not specified{% endtrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<details>
|
||||
<summary class="clickable">{% trans %}Reason{% endtrans %}</summary>
|
||||
<p>{{ user_ban.reason }}</p>
|
||||
</details>
|
||||
{% if user.has_perm("core:delete_userban") %}
|
||||
<span>
|
||||
<a
|
||||
href="{{ url("rootplace:ban_remove", ban_id=user_ban.id) }}"
|
||||
class="btn btn-blue"
|
||||
>
|
||||
{% trans %}Remove ban{% endtrans %}
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{% trans %}No active ban.{% endtrans %}</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
0
rootplace/tests/__init__.py
Normal file
0
rootplace/tests/__init__.py
Normal file
57
rootplace/tests/test_ban.py
Normal file
57
rootplace/tests/test_ban.py
Normal file
@ -0,0 +1,57 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import localtime
|
||||
from model_bakery import baker
|
||||
from pytest_django.asserts import assertRedirects
|
||||
|
||||
from core.models import BanGroup, User, UserBan
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def operator(db) -> User:
|
||||
return baker.make(
|
||||
User,
|
||||
user_permissions=Permission.objects.filter(
|
||||
codename__in=["view_userban", "add_userban", "delete_userban"]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"expires_at",
|
||||
[None, localtime().replace(second=0, microsecond=0) + timedelta(days=7)],
|
||||
)
|
||||
def test_ban_user(client: Client, operator: User, expires_at: datetime):
|
||||
client.force_login(operator)
|
||||
user = baker.make(User)
|
||||
ban_group = BanGroup.objects.first()
|
||||
data = {
|
||||
"user": user.id,
|
||||
"ban_group": ban_group.id,
|
||||
"reason": "Being naughty",
|
||||
}
|
||||
if expires_at is not None:
|
||||
data["expires_at"] = expires_at.strftime("%Y-%m-%d %H:%M")
|
||||
response = client.post(reverse("rootplace:ban_create"), data)
|
||||
assertRedirects(response, expected_url=reverse("rootplace:ban_list"))
|
||||
bans = list(user.bans.all())
|
||||
assert len(bans) == 1
|
||||
assert bans[0].expires_at == expires_at
|
||||
assert bans[0].reason == "Being naughty"
|
||||
assert bans[0].ban_group == ban_group
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_remove_ban(client: Client, operator: User):
|
||||
client.force_login(operator)
|
||||
user = baker.make(User)
|
||||
ban = baker.make(UserBan, user=user)
|
||||
assert user.bans.exists()
|
||||
response = client.post(reverse("rootplace:ban_remove", kwargs={"ban_id": ban.id}))
|
||||
assertRedirects(response, expected_url=reverse("rootplace:ban_list"))
|
||||
assert not user.bans.exists()
|
@ -25,6 +25,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from rootplace.views import (
|
||||
BanCreateView,
|
||||
BanDeleteView,
|
||||
BanView,
|
||||
DeleteAllForumUserMessagesView,
|
||||
MergeUsersView,
|
||||
OperationLogListView,
|
||||
@ -38,4 +41,7 @@ urlpatterns = [
|
||||
name="delete_forum_messages",
|
||||
),
|
||||
path("logs/", OperationLogListView.as_view(), name="operation_logs"),
|
||||
path("ban/", BanView.as_view(), name="ban_list"),
|
||||
path("ban/new", BanCreateView.as_view(), name="ban_create"),
|
||||
path("ban/<int:ban_id>/remove/", BanDeleteView.as_view(), name="ban_remove"),
|
||||
]
|
||||
|
@ -23,20 +23,19 @@
|
||||
#
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import localdate
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import FormView
|
||||
from django.views.generic import DeleteView, ListView
|
||||
from django.views.generic.edit import CreateView, FormView
|
||||
|
||||
from core.models import OperationLog, SithFile, User
|
||||
from core.models import OperationLog, SithFile, User, UserBan
|
||||
from core.views import CanEditPropMixin
|
||||
from core.views.widgets.select import AutoCompleteSelectUser
|
||||
from counter.models import Customer
|
||||
from forum.models import ForumMessageMeta
|
||||
from rootplace.forms import BanForm, MergeForm, SelectUserForm
|
||||
|
||||
|
||||
def __merge_subscriptions(u1: User, u2: User):
|
||||
@ -155,33 +154,6 @@ def delete_all_forum_user_messages(
|
||||
ForumMessageMeta(message=message, user=moderator, action="DELETE").save()
|
||||
|
||||
|
||||
class MergeForm(forms.Form):
|
||||
user1 = forms.ModelChoiceField(
|
||||
label=_("User that will be kept"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
user2 = forms.ModelChoiceField(
|
||||
label=_("User that will be deleted"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
|
||||
class SelectUserForm(forms.Form):
|
||||
user = forms.ModelChoiceField(
|
||||
label=_("User to be selected"),
|
||||
help_text=None,
|
||||
required=True,
|
||||
widget=AutoCompleteSelectUser,
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
|
||||
class MergeUsersView(FormView):
|
||||
template_name = "rootplace/merge.jinja"
|
||||
form_class = MergeForm
|
||||
@ -233,3 +205,39 @@ class OperationLogListView(ListView, CanEditPropMixin):
|
||||
template_name = "rootplace/logs.jinja"
|
||||
ordering = ["-date"]
|
||||
paginate_by = 100
|
||||
|
||||
|
||||
class BanView(PermissionRequiredMixin, ListView):
|
||||
"""[UserBan][core.models.UserBan] management view.
|
||||
|
||||
Displays :
|
||||
|
||||
- the list of active bans with their main information,
|
||||
with a link to [BanDeleteView][rootplace.views.BanDeleteView] for each one
|
||||
- a link which redirects to [BanCreateView][rootplace.views.BanCreateView]
|
||||
"""
|
||||
|
||||
permission_required = "core.view_userban"
|
||||
template_name = "rootplace/userban.jinja"
|
||||
queryset = UserBan.objects.select_related("user", "user__profile_pict", "ban_group")
|
||||
ordering = "created_at"
|
||||
context_object_name = "user_bans"
|
||||
|
||||
|
||||
class BanCreateView(PermissionRequiredMixin, CreateView):
|
||||
"""[UserBan][core.models.UserBan] creation view."""
|
||||
|
||||
permission_required = "core.add_userban"
|
||||
form_class = BanForm
|
||||
template_name = "core/create.jinja"
|
||||
success_url = reverse_lazy("rootplace:ban_list")
|
||||
|
||||
|
||||
class BanDeleteView(PermissionRequiredMixin, DeleteView):
|
||||
"""[UserBan][core.models.UserBan] deletion view."""
|
||||
|
||||
permission_required = "core.delete_userban"
|
||||
pk_url_kwarg = "ban_id"
|
||||
model = UserBan
|
||||
template_name = "core/delete_confirm.jinja"
|
||||
success_url = reverse_lazy("rootplace:ban_list")
|
||||
|
Loading…
Reference in New Issue
Block a user