Add UserBan management views

This commit is contained in:
imperosol 2025-01-03 01:13:43 +01:00
parent af47587116
commit 4f35cc00bc
14 changed files with 266 additions and 45 deletions

View File

@ -29,7 +29,7 @@
align-items: center;
gap: 20px;
&:hover {
&.clickable:hover {
background-color: darken($primary-neutral-light-color, 5%);
}

View File

@ -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 {

View File

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

View File

@ -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

View File

@ -0,0 +1,7 @@
::: rootplace.forms
handler: python
options:
members:
- MergeForm
- SelectUserForm
- BanForm

View File

@ -1 +1,12 @@
::: rootplace.views
::: rootplace.views
handler: python
options:
members:
- merge_users
- delete_all_forum_user_messages
- MergeUsersView
- DeleteAllForumUserMessagesView
- OperationLogListView
- BanView
- BanCreateView
- BanDeleteView

View File

@ -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
View 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,
}

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

View File

View 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()

View File

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

View File

@ -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")