mirror of
https://github.com/ae-utbm/sith.git
synced 2025-01-24 16:01:11 +00:00
commit
7f4cc5fb0f
@ -17,7 +17,7 @@ from django.contrib import admin
|
|||||||
from django.contrib.auth.models import Group as AuthGroup
|
from django.contrib.auth.models import Group as AuthGroup
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
|
||||||
from core.models import Group, OperationLog, Page, SithFile, User
|
from core.models import BanGroup, Group, OperationLog, Page, SithFile, User, UserBan
|
||||||
|
|
||||||
admin.site.unregister(AuthGroup)
|
admin.site.unregister(AuthGroup)
|
||||||
|
|
||||||
@ -30,6 +30,19 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
autocomplete_fields = ("permissions",)
|
autocomplete_fields = ("permissions",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(BanGroup)
|
||||||
|
class BanGroupAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "description")
|
||||||
|
search_fields = ("name",)
|
||||||
|
autocomplete_fields = ("permissions",)
|
||||||
|
|
||||||
|
|
||||||
|
class UserBanInline(admin.TabularInline):
|
||||||
|
model = UserBan
|
||||||
|
extra = 0
|
||||||
|
autocomplete_fields = ("ban_group",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
list_display = ("first_name", "last_name", "username", "email", "nick_name")
|
list_display = ("first_name", "last_name", "username", "email", "nick_name")
|
||||||
@ -42,9 +55,16 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
"user_permissions",
|
"user_permissions",
|
||||||
"groups",
|
"groups",
|
||||||
)
|
)
|
||||||
|
inlines = (UserBanInline,)
|
||||||
search_fields = ["first_name", "last_name", "username"]
|
search_fields = ["first_name", "last_name", "username"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserBan)
|
||||||
|
class UserBanAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("user", "ban_group", "created_at", "expires_at")
|
||||||
|
autocomplete_fields = ("user", "ban_group")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Permission)
|
@admin.register(Permission)
|
||||||
class PermissionAdmin(admin.ModelAdmin):
|
class PermissionAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("codename",)
|
search_fields = ("codename",)
|
||||||
|
@ -48,7 +48,7 @@ from accounting.models import (
|
|||||||
from club.models import Club, Membership
|
from club.models import Club, Membership
|
||||||
from com.calendar import IcsCalendar
|
from com.calendar import IcsCalendar
|
||||||
from com.models import News, NewsDate, Sith, Weekmail
|
from com.models import News, NewsDate, Sith, Weekmail
|
||||||
from core.models import Group, Page, PageRev, SithFile, User
|
from core.models import BanGroup, Group, Page, PageRev, SithFile, User
|
||||||
from core.utils import resize_image
|
from core.utils import resize_image
|
||||||
from counter.models import Counter, Product, ProductType, StudentCard
|
from counter.models import Counter, Product, ProductType, StudentCard
|
||||||
from election.models import Candidature, Election, ElectionList, Role
|
from election.models import Candidature, Election, ElectionList, Role
|
||||||
@ -94,6 +94,7 @@ class Command(BaseCommand):
|
|||||||
Sith.objects.create(weekmail_destinations="etudiants@git.an personnel@git.an")
|
Sith.objects.create(weekmail_destinations="etudiants@git.an personnel@git.an")
|
||||||
Site.objects.create(domain=settings.SITH_URL, name=settings.SITH_NAME)
|
Site.objects.create(domain=settings.SITH_URL, name=settings.SITH_NAME)
|
||||||
groups = self._create_groups()
|
groups = self._create_groups()
|
||||||
|
self._create_ban_groups()
|
||||||
|
|
||||||
root = User.objects.create_superuser(
|
root = User.objects.create_superuser(
|
||||||
id=0,
|
id=0,
|
||||||
@ -951,11 +952,6 @@ Welcome to the wiki page!
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Group.objects.create(
|
|
||||||
name="Banned from buying alcohol", is_manually_manageable=True
|
|
||||||
)
|
|
||||||
Group.objects.create(name="Banned from counters", is_manually_manageable=True)
|
|
||||||
Group.objects.create(name="Banned to subscribe", is_manually_manageable=True)
|
|
||||||
sas_admin = Group.objects.create(name="SAS admin", is_manually_manageable=True)
|
sas_admin = Group.objects.create(name="SAS admin", is_manually_manageable=True)
|
||||||
sas_admin.permissions.add(
|
sas_admin.permissions.add(
|
||||||
*list(
|
*list(
|
||||||
@ -995,3 +991,8 @@ Welcome to the wiki page!
|
|||||||
sas_admin=sas_admin,
|
sas_admin=sas_admin,
|
||||||
pedagogy_admin=pedagogy_admin,
|
pedagogy_admin=pedagogy_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_ban_groups(self):
|
||||||
|
BanGroup.objects.create(name="Banned from buying alcohol", description="")
|
||||||
|
BanGroup.objects.create(name="Banned from counters", description="")
|
||||||
|
BanGroup.objects.create(name="Banned to subscribe", description="")
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
# Generated by Django 4.2.17 on 2024-12-31 13:30
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.migrations.state import StateApps
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_ban_groups(apps: StateApps, schema_editor):
|
||||||
|
Group = apps.get_model("core", "Group")
|
||||||
|
BanGroup = apps.get_model("core", "BanGroup")
|
||||||
|
ban_group_ids = [
|
||||||
|
settings.SITH_GROUP_BANNED_ALCOHOL_ID,
|
||||||
|
settings.SITH_GROUP_BANNED_COUNTER_ID,
|
||||||
|
settings.SITH_GROUP_BANNED_SUBSCRIPTION_ID,
|
||||||
|
]
|
||||||
|
# this is a N+1 Queries, but the prod database has a grand total of 3 ban groups
|
||||||
|
for group in Group.objects.filter(id__in=ban_group_ids):
|
||||||
|
# auth_group, which both Group and BanGroup inherit,
|
||||||
|
# is unique by name.
|
||||||
|
# If we tried give the exact same name to the migrated BanGroup
|
||||||
|
# before deleting the corresponding Group,
|
||||||
|
# we would have an IntegrityError.
|
||||||
|
# So we append a space to the name, in order to create a name
|
||||||
|
# that will look the same, but that isn't really the same.
|
||||||
|
ban_group = BanGroup.objects.create(
|
||||||
|
name=f"{group.name} ",
|
||||||
|
description=group.description,
|
||||||
|
)
|
||||||
|
perms = list(group.permissions.values_list("id", flat=True))
|
||||||
|
if perms:
|
||||||
|
ban_group.permissions.add(*perms)
|
||||||
|
ban_group.users.add(
|
||||||
|
*group.users.values_list("id", flat=True), through_defaults={"reason": ""}
|
||||||
|
)
|
||||||
|
group.delete()
|
||||||
|
# now that the original group is no longer there,
|
||||||
|
# we can remove the appended space
|
||||||
|
ban_group.name = ban_group.name.strip()
|
||||||
|
ban_group.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
("core", "0042_invert_is_manually_manageable_20250104_1742"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BanGroup",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"group_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="auth.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("description", models.TextField(verbose_name="description")),
|
||||||
|
],
|
||||||
|
bases=("auth.group",),
|
||||||
|
managers=[
|
||||||
|
("objects", django.contrib.auth.models.GroupManager()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "ban group",
|
||||||
|
"verbose_name_plural": "ban groups",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="group",
|
||||||
|
name="description",
|
||||||
|
field=models.TextField(verbose_name="description"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="groups",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="users",
|
||||||
|
to="core.group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserBan",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"expires_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="When the ban should be removed. Currently, there is no automatic removal, so this is purely indicative. Automatic ban removal may be implemented later on.",
|
||||||
|
null=True,
|
||||||
|
verbose_name="expires at",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("reason", models.TextField(verbose_name="reason")),
|
||||||
|
(
|
||||||
|
"ban_group",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="user_bans",
|
||||||
|
to="core.bangroup",
|
||||||
|
verbose_name="ban type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="bans",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="ban_groups",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
help_text="The bans this user has received.",
|
||||||
|
related_name="users",
|
||||||
|
through="core.UserBan",
|
||||||
|
to="core.bangroup",
|
||||||
|
verbose_name="ban groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="userban",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("ban_group", "user"), name="unique_ban_type_per_user"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="userban",
|
||||||
|
constraint=models.CheckConstraint(
|
||||||
|
check=models.Q(("expires_at__gte", models.F("created_at"))),
|
||||||
|
name="user_ban_end_after_start",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_ban_groups, reverse_code=migrations.RunPython.noop, elidable=True
|
||||||
|
),
|
||||||
|
]
|
@ -42,7 +42,7 @@ from django.core.cache import cache
|
|||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, F, OuterRef, Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@ -65,8 +65,7 @@ class Group(AuthGroup):
|
|||||||
default=False,
|
default=False,
|
||||||
help_text=_("If False, this shouldn't be shown on group management pages"),
|
help_text=_("If False, this shouldn't be shown on group management pages"),
|
||||||
)
|
)
|
||||||
#: Description of the group
|
description = models.TextField(_("description"))
|
||||||
description = models.CharField(_("description"), max_length=60)
|
|
||||||
|
|
||||||
def get_absolute_url(self) -> str:
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("core:group_list")
|
return reverse("core:group_list")
|
||||||
@ -134,6 +133,28 @@ def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None
|
|||||||
return group
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
class BanGroup(AuthGroup):
|
||||||
|
"""An anti-group, that removes permissions instead of giving them.
|
||||||
|
|
||||||
|
Users are linked to BanGroups through UserBan objects.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
user = User.objects.get(username="...")
|
||||||
|
ban_group = BanGroup.objects.first()
|
||||||
|
UserBan.objects.create(user=user, ban_group=ban_group, reason="...")
|
||||||
|
|
||||||
|
assert user.ban_groups.contains(ban_group)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
description = models.TextField(_("description"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("ban group")
|
||||||
|
verbose_name_plural = _("ban groups")
|
||||||
|
|
||||||
|
|
||||||
class UserQuerySet(models.QuerySet):
|
class UserQuerySet(models.QuerySet):
|
||||||
def filter_inactive(self) -> Self:
|
def filter_inactive(self) -> Self:
|
||||||
from counter.models import Refilling, Selling
|
from counter.models import Refilling, Selling
|
||||||
@ -184,7 +205,13 @@ class User(AbstractUser):
|
|||||||
"granted to each of their groups."
|
"granted to each of their groups."
|
||||||
),
|
),
|
||||||
related_name="users",
|
related_name="users",
|
||||||
blank=True,
|
)
|
||||||
|
ban_groups = models.ManyToManyField(
|
||||||
|
BanGroup,
|
||||||
|
verbose_name=_("ban groups"),
|
||||||
|
through="UserBan",
|
||||||
|
help_text=_("The bans this user has received."),
|
||||||
|
related_name="users",
|
||||||
)
|
)
|
||||||
home = models.OneToOneField(
|
home = models.OneToOneField(
|
||||||
"SithFile",
|
"SithFile",
|
||||||
@ -424,12 +451,12 @@ class User(AbstractUser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_banned_alcohol(self):
|
def is_banned_alcohol(self) -> bool:
|
||||||
return self.is_in_group(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID)
|
return self.ban_groups.filter(id=settings.SITH_GROUP_BANNED_ALCOHOL_ID).exists()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_banned_counter(self):
|
def is_banned_counter(self) -> bool:
|
||||||
return self.is_in_group(pk=settings.SITH_GROUP_BANNED_COUNTER_ID)
|
return self.ban_groups.filter(id=settings.SITH_GROUP_BANNED_COUNTER_ID).exists()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def age(self) -> int:
|
def age(self) -> int:
|
||||||
@ -731,6 +758,52 @@ class AnonymousUser(AuthAnonymousUser):
|
|||||||
return _("Visitor")
|
return _("Visitor")
|
||||||
|
|
||||||
|
|
||||||
|
class UserBan(models.Model):
|
||||||
|
"""A ban of a user.
|
||||||
|
|
||||||
|
A user can be banned for a specific reason, for a specific duration.
|
||||||
|
The expiration date is indicative, and the ban should be removed manually.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ban_group = models.ForeignKey(
|
||||||
|
BanGroup,
|
||||||
|
verbose_name=_("ban type"),
|
||||||
|
related_name="user_bans",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User, verbose_name=_("user"), related_name="bans", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
|
expires_at = models.DateTimeField(
|
||||||
|
_("expires at"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_(
|
||||||
|
"When the ban should be removed. "
|
||||||
|
"Currently, there is no automatic removal, so this is purely indicative. "
|
||||||
|
"Automatic ban removal may be implemented later on."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
reason = models.TextField(_("reason"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("user ban")
|
||||||
|
verbose_name_plural = _("user bans")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["ban_group", "user"], name="unique_ban_type_per_user"
|
||||||
|
),
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=Q(expires_at__gte=F("created_at")),
|
||||||
|
name="user_ban_end_after_start",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Ban of user {self.user.id}"
|
||||||
|
|
||||||
|
|
||||||
class Preferences(models.Model):
|
class Preferences(models.Model):
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
User, related_name="_preferences", on_delete=models.CASCADE
|
User, related_name="_preferences", on_delete=models.CASCADE
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
&:hover {
|
&.clickable:hover {
|
||||||
background-color: darken($primary-neutral-light-color, 5%);
|
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,
|
.form-group,
|
||||||
> p,
|
> p,
|
||||||
> div {
|
> div {
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
<li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
|
<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>
|
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if user.can_create_subscription or user.is_root %}
|
||||||
<li><a href="{{ url('subscription:subscription') }}">{% trans %}Subscriptions{% endtrans %}</a></li>
|
<li><a href="{{ url('subscription:subscription') }}">{% trans %}Subscriptions{% endtrans %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -358,9 +358,6 @@ class TestUserIsInGroup(TestCase):
|
|||||||
cls.accounting_admin = Group.objects.get(name="Accounting admin")
|
cls.accounting_admin = Group.objects.get(name="Accounting admin")
|
||||||
cls.com_admin = Group.objects.get(name="Communication admin")
|
cls.com_admin = Group.objects.get(name="Communication admin")
|
||||||
cls.counter_admin = Group.objects.get(name="Counter admin")
|
cls.counter_admin = Group.objects.get(name="Counter admin")
|
||||||
cls.banned_alcohol = Group.objects.get(name="Banned from buying alcohol")
|
|
||||||
cls.banned_counters = Group.objects.get(name="Banned from counters")
|
|
||||||
cls.banned_subscription = Group.objects.get(name="Banned to subscribe")
|
|
||||||
cls.sas_admin = Group.objects.get(name="SAS admin")
|
cls.sas_admin = Group.objects.get(name="SAS admin")
|
||||||
cls.club = baker.make(Club)
|
cls.club = baker.make(Club)
|
||||||
cls.main_club = Club.objects.get(id=1)
|
cls.main_club = Club.objects.get(id=1)
|
||||||
@ -373,7 +370,6 @@ class TestUserIsInGroup(TestCase):
|
|||||||
self.assert_in_public_group(user)
|
self.assert_in_public_group(user)
|
||||||
for group in (
|
for group in (
|
||||||
self.root_group,
|
self.root_group,
|
||||||
self.banned_counters,
|
|
||||||
self.accounting_admin,
|
self.accounting_admin,
|
||||||
self.sas_admin,
|
self.sas_admin,
|
||||||
self.subscribers,
|
self.subscribers,
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
|
from datetime import date, datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from captcha.fields import CaptchaField
|
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.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
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
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
@ -125,6 +133,23 @@ class SelectUser(TextInput):
|
|||||||
return output
|
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
|
# Forms
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ from model_bakery import baker
|
|||||||
|
|
||||||
from club.models import Club, Membership
|
from club.models import Club, Membership
|
||||||
from core.baker_recipes import board_user, subscriber_user, very_old_subscriber_user
|
from core.baker_recipes import board_user, subscriber_user, very_old_subscriber_user
|
||||||
from core.models import Group, User
|
from core.models import BanGroup, User
|
||||||
from counter.baker_recipes import product_recipe
|
from counter.baker_recipes import product_recipe
|
||||||
from counter.models import (
|
from counter.models import (
|
||||||
Counter,
|
Counter,
|
||||||
@ -229,11 +229,11 @@ class TestCounterClick(TestFullClickBase):
|
|||||||
cls.set_age(cls.banned_alcohol_customer, 20)
|
cls.set_age(cls.banned_alcohol_customer, 20)
|
||||||
cls.set_age(cls.underage_customer, 17)
|
cls.set_age(cls.underage_customer, 17)
|
||||||
|
|
||||||
cls.banned_alcohol_customer.groups.add(
|
cls.banned_alcohol_customer.ban_groups.add(
|
||||||
Group.objects.get(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID)
|
BanGroup.objects.get(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID)
|
||||||
)
|
)
|
||||||
cls.banned_counter_customer.groups.add(
|
cls.banned_counter_customer.ban_groups.add(
|
||||||
Group.objects.get(pk=settings.SITH_GROUP_BANNED_COUNTER_ID)
|
BanGroup.objects.get(pk=settings.SITH_GROUP_BANNED_COUNTER_ID)
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.beer = product_recipe.make(
|
cls.beer = product_recipe.make(
|
||||||
|
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
|
@ -921,8 +921,8 @@ msgstr "home"
|
|||||||
msgid "You can not make loops in clubs"
|
msgid "You can not make loops in clubs"
|
||||||
msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
|
msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
|
||||||
|
|
||||||
#: club/models.py counter/models.py eboutic/models.py election/models.py
|
#: club/models.py core/models.py counter/models.py eboutic/models.py
|
||||||
#: launderette/models.py sas/models.py trombi/models.py
|
#: election/models.py launderette/models.py sas/models.py trombi/models.py
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "utilisateur"
|
msgstr "utilisateur"
|
||||||
|
|
||||||
@ -1009,6 +1009,7 @@ msgstr "Description"
|
|||||||
|
|
||||||
#: club/templates/club/club_members.jinja core/templates/core/user_clubs.jinja
|
#: club/templates/club/club_members.jinja core/templates/core/user_clubs.jinja
|
||||||
#: launderette/templates/launderette/launderette_admin.jinja
|
#: launderette/templates/launderette/launderette_admin.jinja
|
||||||
|
#: rootplace/templates/rootplace/userban.jinja
|
||||||
msgid "Since"
|
msgid "Since"
|
||||||
msgstr "Depuis"
|
msgstr "Depuis"
|
||||||
|
|
||||||
@ -1787,7 +1788,7 @@ msgstr "Message d'alerte"
|
|||||||
msgid "Screens list"
|
msgid "Screens list"
|
||||||
msgstr "Liste d'écrans"
|
msgstr "Liste d'écrans"
|
||||||
|
|
||||||
#: com/views.py
|
#: com/views.py rootplace/templates/rootplace/userban.jinja
|
||||||
msgid "Until"
|
msgid "Until"
|
||||||
msgstr "Jusqu'à"
|
msgstr "Jusqu'à"
|
||||||
|
|
||||||
@ -1832,6 +1833,14 @@ msgstr ""
|
|||||||
msgid "%(value)s is not a valid promo (between 0 and %(end)s)"
|
msgid "%(value)s is not a valid promo (between 0 and %(end)s)"
|
||||||
msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)"
|
msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid "ban group"
|
||||||
|
msgstr "groupe de ban"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid "ban groups"
|
||||||
|
msgstr "groupes de ban"
|
||||||
|
|
||||||
#: core/models.py
|
#: core/models.py
|
||||||
msgid "first name"
|
msgid "first name"
|
||||||
msgstr "Prénom"
|
msgstr "Prénom"
|
||||||
@ -1868,6 +1877,10 @@ msgstr ""
|
|||||||
"Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes "
|
"Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes "
|
||||||
"les permissions de chacun de ses groupes."
|
"les permissions de chacun de ses groupes."
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid "The bans this user has received."
|
||||||
|
msgstr "Les bans que cet utilisateur a reçus."
|
||||||
|
|
||||||
#: core/models.py
|
#: core/models.py
|
||||||
msgid "profile"
|
msgid "profile"
|
||||||
msgstr "profil"
|
msgstr "profil"
|
||||||
@ -2019,6 +2032,42 @@ msgstr "Profil"
|
|||||||
msgid "Visitor"
|
msgid "Visitor"
|
||||||
msgstr "Visiteur"
|
msgstr "Visiteur"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid "ban type"
|
||||||
|
msgstr "type de ban"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid "created at"
|
||||||
|
msgstr "créé le"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid "expires at"
|
||||||
|
msgstr "expire le"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
msgid ""
|
||||||
|
"When the ban should be removed. Currently, there is no automatic removal, so "
|
||||||
|
"this is purely indicative. Automatic ban removal may be implemented later on."
|
||||||
|
msgstr ""
|
||||||
|
"Quand le ban devrait être retiré. Actuellement, il n'y a pas de retrait automatique, "
|
||||||
|
"donc ceci est purement indicatif. Le retrait automatique pourra être implémenté plus tard."
|
||||||
|
|
||||||
|
#: core/models.py pedagogy/models.py
|
||||||
|
msgid "reason"
|
||||||
|
msgstr "raison"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "user"
|
||||||
|
msgid "user ban"
|
||||||
|
msgstr "utilisateur"
|
||||||
|
|
||||||
|
#: core/models.py
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "user"
|
||||||
|
msgid "user bans"
|
||||||
|
msgstr "utilisateur"
|
||||||
|
|
||||||
#: core/models.py
|
#: core/models.py
|
||||||
msgid "receive the Weekmail"
|
msgid "receive the Weekmail"
|
||||||
msgstr "recevoir le Weekmail"
|
msgstr "recevoir le Weekmail"
|
||||||
@ -3117,6 +3166,10 @@ msgstr "Journal d'opérations"
|
|||||||
msgid "Delete user's forum messages"
|
msgid "Delete user's forum messages"
|
||||||
msgstr "Supprimer les messages forum d'un utilisateur"
|
msgstr "Supprimer les messages forum d'un utilisateur"
|
||||||
|
|
||||||
|
#: core/templates/core/user_tools.jinja
|
||||||
|
msgid "Bans"
|
||||||
|
msgstr "Bans"
|
||||||
|
|
||||||
#: core/templates/core/user_tools.jinja
|
#: core/templates/core/user_tools.jinja
|
||||||
msgid "Subscriptions"
|
msgid "Subscriptions"
|
||||||
msgstr "Cotisations"
|
msgstr "Cotisations"
|
||||||
@ -3260,6 +3313,10 @@ msgstr "Choisir un fichier"
|
|||||||
msgid "Choose user"
|
msgid "Choose user"
|
||||||
msgstr "Choisir un utilisateur"
|
msgstr "Choisir un utilisateur"
|
||||||
|
|
||||||
|
#: core/views/forms.py
|
||||||
|
msgid "Ensure this timestamp is set in the future"
|
||||||
|
msgstr "Assurez-vous que cet horodatage est dans le futur"
|
||||||
|
|
||||||
#: core/views/forms.py
|
#: core/views/forms.py
|
||||||
msgid "Username, email, or account number"
|
msgid "Username, email, or account number"
|
||||||
msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
|
msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
|
||||||
@ -4882,10 +4939,6 @@ msgstr "signaler"
|
|||||||
msgid "reporter"
|
msgid "reporter"
|
||||||
msgstr "signalant"
|
msgstr "signalant"
|
||||||
|
|
||||||
#: pedagogy/models.py
|
|
||||||
msgid "reason"
|
|
||||||
msgstr "raison"
|
|
||||||
|
|
||||||
#: pedagogy/templates/pedagogy/guide.jinja
|
#: pedagogy/templates/pedagogy/guide.jinja
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(display_name)s"
|
msgid "%(display_name)s"
|
||||||
@ -4917,7 +4970,8 @@ msgstr "non noté"
|
|||||||
msgid "UV comment moderation"
|
msgid "UV comment moderation"
|
||||||
msgstr "Modération des commentaires d'UV"
|
msgstr "Modération des commentaires d'UV"
|
||||||
|
|
||||||
#: pedagogy/templates/pedagogy/moderation.jinja sas/models.py
|
#: pedagogy/templates/pedagogy/moderation.jinja
|
||||||
|
#: rootplace/templates/rootplace/userban.jinja sas/models.py
|
||||||
msgid "Reason"
|
msgid "Reason"
|
||||||
msgstr "Raison"
|
msgstr "Raison"
|
||||||
|
|
||||||
@ -5038,6 +5092,18 @@ msgstr "Autocomplétion réussite"
|
|||||||
msgid "An error occurred: "
|
msgid "An error occurred: "
|
||||||
msgstr "Une erreur est survenue : "
|
msgstr "Une erreur est survenue : "
|
||||||
|
|
||||||
|
#: rootplace/forms.py
|
||||||
|
msgid "User that will be kept"
|
||||||
|
msgstr "Utilisateur qui sera conservé"
|
||||||
|
|
||||||
|
#: rootplace/forms.py
|
||||||
|
msgid "User that will be deleted"
|
||||||
|
msgstr "Utilisateur qui sera supprimé"
|
||||||
|
|
||||||
|
#: rootplace/forms.py
|
||||||
|
msgid "User to be selected"
|
||||||
|
msgstr "Utilisateur à sélectionner"
|
||||||
|
|
||||||
#: rootplace/templates/rootplace/delete_user_messages.jinja
|
#: rootplace/templates/rootplace/delete_user_messages.jinja
|
||||||
msgid "Delete all forum messages from an user"
|
msgid "Delete all forum messages from an user"
|
||||||
msgstr "Supprimer tous les messages forum d'un utilisateur"
|
msgstr "Supprimer tous les messages forum d'un utilisateur"
|
||||||
@ -5067,17 +5133,21 @@ msgstr "Fusionner deux utilisateurs"
|
|||||||
msgid "Merge"
|
msgid "Merge"
|
||||||
msgstr "Fusion"
|
msgstr "Fusion"
|
||||||
|
|
||||||
#: rootplace/views.py
|
#: rootplace/templates/rootplace/userban.jinja
|
||||||
msgid "User that will be kept"
|
msgid "Ban a user"
|
||||||
msgstr "Utilisateur qui sera conservé"
|
msgstr "Bannir un utilisateur"
|
||||||
|
|
||||||
#: rootplace/views.py
|
#: rootplace/templates/rootplace/userban.jinja
|
||||||
msgid "User that will be deleted"
|
msgid "not specified"
|
||||||
msgstr "Utilisateur qui sera supprimé"
|
msgstr "non spécifié"
|
||||||
|
|
||||||
#: rootplace/views.py
|
#: rootplace/templates/rootplace/userban.jinja
|
||||||
msgid "User to be selected"
|
msgid "Remove ban"
|
||||||
msgstr "Utilisateur à sélectionner"
|
msgstr "Retirer le ban"
|
||||||
|
|
||||||
|
#: rootplace/templates/rootplace/userban.jinja
|
||||||
|
msgid "No active ban."
|
||||||
|
msgstr "Pas de ban actif"
|
||||||
|
|
||||||
#: sas/forms.py
|
#: sas/forms.py
|
||||||
msgid "Add a new album"
|
msgid "Add a new album"
|
||||||
|
@ -127,6 +127,7 @@ nav:
|
|||||||
- reference/pedagogy/schemas.md
|
- reference/pedagogy/schemas.md
|
||||||
- rootplace:
|
- rootplace:
|
||||||
- reference/rootplace/models.md
|
- reference/rootplace/models.md
|
||||||
|
- reference/rootplace/forms.md
|
||||||
- reference/rootplace/views.md
|
- reference/rootplace/views.md
|
||||||
- sas:
|
- sas:
|
||||||
- reference/sas/models.md
|
- 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 django.urls import path
|
||||||
|
|
||||||
from rootplace.views import (
|
from rootplace.views import (
|
||||||
|
BanCreateView,
|
||||||
|
BanDeleteView,
|
||||||
|
BanView,
|
||||||
DeleteAllForumUserMessagesView,
|
DeleteAllForumUserMessagesView,
|
||||||
MergeUsersView,
|
MergeUsersView,
|
||||||
OperationLogListView,
|
OperationLogListView,
|
||||||
@ -38,4 +41,7 @@ urlpatterns = [
|
|||||||
name="delete_forum_messages",
|
name="delete_forum_messages",
|
||||||
),
|
),
|
||||||
path("logs/", OperationLogListView.as_view(), name="operation_logs"),
|
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
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
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 import timezone
|
||||||
from django.utils.timezone import localdate
|
from django.utils.timezone import localdate
|
||||||
from django.utils.translation import gettext as _
|
from django.views.generic import DeleteView, ListView
|
||||||
from django.views.generic import ListView
|
from django.views.generic.edit import CreateView, FormView
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
|
|
||||||
from core.models import OperationLog, SithFile, User
|
from core.models import OperationLog, SithFile, User, UserBan
|
||||||
from core.views import CanEditPropMixin
|
from core.views import CanEditPropMixin
|
||||||
from core.views.widgets.select import AutoCompleteSelectUser
|
|
||||||
from counter.models import Customer
|
from counter.models import Customer
|
||||||
from forum.models import ForumMessageMeta
|
from forum.models import ForumMessageMeta
|
||||||
|
from rootplace.forms import BanForm, MergeForm, SelectUserForm
|
||||||
|
|
||||||
|
|
||||||
def __merge_subscriptions(u1: User, u2: User):
|
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()
|
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):
|
class MergeUsersView(FormView):
|
||||||
template_name = "rootplace/merge.jinja"
|
template_name = "rootplace/merge.jinja"
|
||||||
form_class = MergeForm
|
form_class = MergeForm
|
||||||
@ -233,3 +205,39 @@ class OperationLogListView(ListView, CanEditPropMixin):
|
|||||||
template_name = "rootplace/logs.jinja"
|
template_name = "rootplace/logs.jinja"
|
||||||
ordering = ["-date"]
|
ordering = ["-date"]
|
||||||
paginate_by = 100
|
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")
|
||||||
|
@ -363,12 +363,13 @@ SITH_GROUP_OLD_SUBSCRIBERS_ID = 4
|
|||||||
SITH_GROUP_ACCOUNTING_ADMIN_ID = 5
|
SITH_GROUP_ACCOUNTING_ADMIN_ID = 5
|
||||||
SITH_GROUP_COM_ADMIN_ID = 6
|
SITH_GROUP_COM_ADMIN_ID = 6
|
||||||
SITH_GROUP_COUNTER_ADMIN_ID = 7
|
SITH_GROUP_COUNTER_ADMIN_ID = 7
|
||||||
SITH_GROUP_BANNED_ALCOHOL_ID = 8
|
SITH_GROUP_SAS_ADMIN_ID = 8
|
||||||
SITH_GROUP_BANNED_COUNTER_ID = 9
|
SITH_GROUP_FORUM_ADMIN_ID = 9
|
||||||
SITH_GROUP_BANNED_SUBSCRIPTION_ID = 10
|
SITH_GROUP_PEDAGOGY_ADMIN_ID = 10
|
||||||
SITH_GROUP_SAS_ADMIN_ID = 11
|
|
||||||
SITH_GROUP_FORUM_ADMIN_ID = 12
|
SITH_GROUP_BANNED_ALCOHOL_ID = 11
|
||||||
SITH_GROUP_PEDAGOGY_ADMIN_ID = 13
|
SITH_GROUP_BANNED_COUNTER_ID = 12
|
||||||
|
SITH_GROUP_BANNED_SUBSCRIPTION_ID = 13
|
||||||
|
|
||||||
SITH_CLUB_REFOUND_ID = 89
|
SITH_CLUB_REFOUND_ID = 89
|
||||||
SITH_COUNTER_REFOUND_ID = 38
|
SITH_COUNTER_REFOUND_ID = 38
|
||||||
|
Loading…
Reference in New Issue
Block a user