mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-14 15:45:02 +00:00
feat: whitelist for user visibility
This commit is contained in:
@@ -244,9 +244,8 @@ class NewsListView(TemplateView):
|
||||
.filter(
|
||||
date_of_birth__month=localdate().month,
|
||||
date_of_birth__day=localdate().day,
|
||||
is_viewable=True,
|
||||
role__in=["STUDENT", "FORMER STUDENT"],
|
||||
)
|
||||
.filter(role__in=["STUDENT", "FORMER STUDENT"])
|
||||
.order_by("-date_of_birth"),
|
||||
key=lambda u: u.date_of_birth.year,
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ class UserAdmin(admin.ModelAdmin):
|
||||
"scrub_pict",
|
||||
"user_permissions",
|
||||
"groups",
|
||||
"whitelisted_users",
|
||||
)
|
||||
inlines = (UserBanInline,)
|
||||
search_fields = ["first_name", "last_name", "username"]
|
||||
|
||||
24
core/migrations/0049_user_whitelisted_users.py
Normal file
24
core/migrations/0049_user_whitelisted_users.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-14 08:39
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("core", "0048_alter_user_options")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="whitelisted_users",
|
||||
field=models.ManyToManyField(
|
||||
help_text=(
|
||||
"If this profile is hidden, "
|
||||
"the users in this list will still be able to see it."
|
||||
),
|
||||
related_name="visible_by_whitelist",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="whitelisted users",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -131,7 +131,7 @@ class UserQuerySet(models.QuerySet):
|
||||
if user.has_perm("core.view_hidden_user"):
|
||||
return self
|
||||
if user.has_perm("core.view_user"):
|
||||
return self.filter(is_viewable=True)
|
||||
return self.filter(Q(is_viewable=True) | Q(whitelisted_users=user))
|
||||
if user.is_anonymous:
|
||||
return self.none()
|
||||
return self.filter(id=user.id)
|
||||
@@ -279,6 +279,15 @@ class User(AbstractUser):
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
whitelisted_users = models.ManyToManyField(
|
||||
"User",
|
||||
related_name="visible_by_whitelist",
|
||||
verbose_name=_("whitelisted users"),
|
||||
help_text=_(
|
||||
"If this profile is hidden, "
|
||||
"the users in this list will still be able to see it."
|
||||
),
|
||||
)
|
||||
godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True)
|
||||
|
||||
objects = CustomUserManager()
|
||||
@@ -567,10 +576,31 @@ class User(AbstractUser):
|
||||
return user.is_root or user.is_board_member
|
||||
|
||||
def can_be_viewed_by(self, user: User) -> bool:
|
||||
"""Check if the given user can be viewed by this user.
|
||||
|
||||
Given users A and B. A can be viewed by B if :
|
||||
|
||||
- A and B are the same user
|
||||
- or B has the permission to view hidden users
|
||||
- or B can view users in general and A didn't hide its profile
|
||||
- or B is in A's whitelist.
|
||||
"""
|
||||
|
||||
def is_in_whitelist(u: User):
|
||||
if (
|
||||
hasattr(self, "_prefetched_objects_cache")
|
||||
and "whitelisted_users" in self._prefetched_objects_cache
|
||||
):
|
||||
return u in self.whitelisted_users.all()
|
||||
return self.whitelisted_users.contains(u)
|
||||
|
||||
return (
|
||||
user.id == self.id
|
||||
or user.has_perm("core.view_hidden_user")
|
||||
or (user.has_perm("core.view_user") and self.is_viewable)
|
||||
or (
|
||||
user.has_perm("core.view_user")
|
||||
and (self.is_viewable or is_in_whitelist(user))
|
||||
)
|
||||
)
|
||||
|
||||
def get_mini_item(self):
|
||||
|
||||
@@ -399,13 +399,12 @@ class TestUserQuerySetViewableBy:
|
||||
return [
|
||||
baker.make(User),
|
||||
subscriber_user.make(),
|
||||
subscriber_user.make(is_viewable=False),
|
||||
*subscriber_user.make(is_viewable=False, _quantity=2),
|
||||
]
|
||||
|
||||
def test_admin_user(self, users: list[User]):
|
||||
user = baker.make(
|
||||
User,
|
||||
user_permissions=[Permission.objects.get(codename="view_hidden_user")],
|
||||
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)
|
||||
@@ -418,6 +417,12 @@ class TestUserQuerySetViewableBy:
|
||||
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
|
||||
assert set(viewable) == {users[0], users[1]}
|
||||
|
||||
def test_whitelist(self, users: list[User]):
|
||||
user = subscriber_user.make()
|
||||
users[3].whitelisted_users.add(user)
|
||||
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
|
||||
assert set(viewable) == {users[0], users[1], users[3]}
|
||||
|
||||
@pytest.mark.parametrize("user_factory", [lambda: baker.make(User), AnonymousUser])
|
||||
def test_not_subscriber(self, users: list[User], user_factory):
|
||||
user = user_factory()
|
||||
|
||||
@@ -69,7 +69,6 @@ from core.views import (
|
||||
UserCreationView,
|
||||
UserGodfathersTreeView,
|
||||
UserGodfathersView,
|
||||
UserListView,
|
||||
UserMeRedirect,
|
||||
UserMiniView,
|
||||
UserPreferencesView,
|
||||
@@ -136,7 +135,6 @@ urlpatterns = [
|
||||
"group/<int:group_id>/detail/", GroupTemplateView.as_view(), name="group_detail"
|
||||
),
|
||||
# User views
|
||||
path("user/", UserListView.as_view(), name="user_list"),
|
||||
path(
|
||||
"user/me/<path:remaining_path>/",
|
||||
UserMeRedirect.as_view(),
|
||||
|
||||
@@ -48,7 +48,6 @@ from django.views.generic import (
|
||||
CreateView,
|
||||
DeleteView,
|
||||
DetailView,
|
||||
ListView,
|
||||
RedirectView,
|
||||
TemplateView,
|
||||
)
|
||||
@@ -404,13 +403,6 @@ class UserMiniView(CanViewMixin, DetailView):
|
||||
template_name = "core/user_mini.jinja"
|
||||
|
||||
|
||||
class UserListView(ListView, CanEditPropMixin):
|
||||
"""Displays the user list."""
|
||||
|
||||
model = User
|
||||
template_name = "core/user_list.jinja"
|
||||
|
||||
|
||||
# FIXME: the edit_once fields aren't displayed to the user (as expected).
|
||||
# However, if the user re-add them manually in the form, they are saved.
|
||||
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
<label for="{{ input_id }}">
|
||||
{%- endif %}
|
||||
<figure>
|
||||
{%- if user.is_viewable %}
|
||||
{%- if user.can_view(candidature.user) %}
|
||||
{% if candidature.user.profile_pict %}
|
||||
<img class="candidate__picture" src="{{ candidature.user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}">
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user