exclude hidden users from ajax search

This commit is contained in:
imperosol
2025-11-09 21:19:15 +01:00
parent cb6fe18916
commit 59d8d73c88
6 changed files with 55 additions and 8 deletions

View File

@@ -240,7 +240,8 @@ class NewsListView(TemplateView):
if not self.request.user.has_perm("core.view_user"): if not self.request.user.has_perm("core.view_user"):
return [] return []
return itertools.groupby( return itertools.groupby(
User.objects.filter( User.objects.viewable_by(self.request.user)
.filter(
date_of_birth__month=localdate().month, date_of_birth__month=localdate().month,
date_of_birth__day=localdate().day, date_of_birth__day=localdate().day,
is_viewable=True, is_viewable=True,

View File

@@ -74,7 +74,7 @@ class MailingListController(ControllerBase):
class UserController(ControllerBase): class UserController(ControllerBase):
@route.get("", response=list[UserProfileSchema], permissions=[CanAccessLookup]) @route.get("", response=list[UserProfileSchema], permissions=[CanAccessLookup])
def fetch_profiles(self, pks: Query[set[int]]): def fetch_profiles(self, pks: Query[set[int]]):
return User.objects.filter(pk__in=pks) return User.objects.viewable_by(self.context.request.user).filter(pk__in=pks)
@route.get("/{int:user_id}", response=UserSchema, permissions=[CanView]) @route.get("/{int:user_id}", response=UserSchema, permissions=[CanView])
def fetch_user(self, user_id: int): def fetch_user(self, user_id: int):
@@ -90,7 +90,9 @@ class UserController(ControllerBase):
@paginate(PageNumberPaginationExtra, page_size=20) @paginate(PageNumberPaginationExtra, page_size=20)
def search_users(self, filters: Query[UserFilterSchema]): def search_users(self, filters: Query[UserFilterSchema]):
return filters.filter( return filters.filter(
User.objects.order_by(F("last_login").desc(nulls_last=True)) User.objects.viewable_by(self.context.request.user).order_by(
F("last_login").desc(nulls_last=True)
)
) )

View File

@@ -150,7 +150,8 @@ class Command(BaseCommand):
Weekmail().save() Weekmail().save()
# Here we add a lot of test datas, that are not necessary for the Sith, but that provide a basic development environment # Here we add a lot of test datas, that are not necessary for the Sith,
# but that provide a basic development environment
self.now = timezone.now().replace(hour=12, second=0) self.now = timezone.now().replace(hour=12, second=0)
skia = User.objects.create_user( skia = User.objects.create_user(

View File

@@ -180,6 +180,15 @@ class UserQuerySet(models.QuerySet):
Q(Exists(subscriptions)) | Q(Exists(refills)) | Q(Exists(purchases)) Q(Exists(subscriptions)) | Q(Exists(refills)) | Q(Exists(purchases))
) )
def viewable_by(self, user: User) -> Self:
if user.has_perm("core.view_hidden_user"):
return self
if user.has_perm("core.view_user"):
return self.filter(is_viewable=True)
if user.is_anonymous:
return self.none()
return self.filter(id=user.id)
class CustomUserManager(UserManager.from_queryset(UserQuerySet)): class CustomUserManager(UserManager.from_queryset(UserQuerySet)):
# see https://docs.djangoproject.com/fr/stable/topics/migrations/#model-managers # see https://docs.djangoproject.com/fr/stable/topics/migrations/#model-managers

View File

@@ -3,6 +3,7 @@ from datetime import timedelta
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.contrib import auth from django.contrib import auth
from django.contrib.auth.models import Permission
from django.core.management import call_command from django.core.management import call_command
from django.test import Client, RequestFactory, TestCase from django.test import Client, RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
@@ -18,7 +19,7 @@ from core.baker_recipes import (
subscriber_user, subscriber_user,
very_old_subscriber_user, very_old_subscriber_user,
) )
from core.models import Group, User from core.models import AnonymousUser, Group, User
from core.views import UserTabsMixin from core.views import UserTabsMixin
from counter.baker_recipes import sale_recipe from counter.baker_recipes import sale_recipe
from counter.models import Counter, Customer, Refilling, Selling from counter.models import Counter, Customer, Refilling, Selling
@@ -368,3 +369,38 @@ class TestRedirectMe:
def test_promo_has_logo(promo): def test_promo_has_logo(promo):
user = baker.make(User, promo=promo) user = baker.make(User, promo=promo)
assert user.promo_has_logo() assert user.promo_has_logo()
@pytest.mark.django_db
class TestUserQuerySetViewableBy:
@pytest.fixture
def users(self) -> list[User]:
return [
baker.make(User),
subscriber_user.make(),
subscriber_user.make(is_viewable=False),
]
def test_admin_user(self, users: list[User]):
user = baker.make(
User,
user_permissions=[Permission.objects.get(codename="view_hidden_user")],
)
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
assert set(viewable) == set(users)
@pytest.mark.parametrize(
"user_factory", [old_subscriber_user.make, subscriber_user.make]
)
def test_subscriber(self, users: list[User], user_factory):
user = user_factory()
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
assert set(viewable) == {users[0], users[1]}
@pytest.mark.parametrize(
"user_factory", [lambda: baker.make(User), lambda: AnonymousUser()]
)
def test_not_subscriber(self, users: list[User], user_factory):
user = user_factory()
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
assert not viewable.exists()

View File

@@ -103,9 +103,7 @@ def password_root_change(request, user_id):
"""Allows a root user to change someone's password.""" """Allows a root user to change someone's password."""
if not request.user.is_root: if not request.user.is_root:
raise PermissionDenied raise PermissionDenied
user = User.objects.filter(id=user_id).first() user = get_object_or_404(User, id=user_id)
if not user:
raise Http404("User not found")
if request.method == "POST": if request.method == "POST":
form = views.SetPasswordForm(user=user, data=request.POST) form = views.SetPasswordForm(user=user, data=request.POST)
if form.is_valid(): if form.is_valid():