1 Commits
eurock ... rgpd

Author SHA1 Message Date
imperosol
f08e343e17 feat: whitelist for user visibility 2026-03-14 16:43:13 +01:00
14 changed files with 86 additions and 128 deletions

View File

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

View File

@@ -63,6 +63,7 @@ class UserAdmin(admin.ModelAdmin):
"scrub_pict",
"user_permissions",
"groups",
"whitelisted_users",
)
inlines = (UserBanInline,)
search_fields = ["first_name", "last_name", "username"]

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

@@ -116,56 +116,6 @@
</span>
</div>
{% endif %}
<section>
<div class="category-header">
<h3 class="margin-bottom">{% trans %}Eurockéennes 2025 partnership{% endtrans %}</h3>
{% if user.is_subscribed %}
<div id="eurock-partner" style="
min-height: 600px;
background-color: lightgrey;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 10px;
">
<p style="text-align: center;">
{% trans trimmed %}
Our partner uses Weezevent to sell tickets.
Weezevent may collect user info according to
its own privacy policy.
By clicking the accept button you consent to
their terms of services.
{% endtrans %}
</p>
<a href="https://weezevent.com/fr/politique-de-confidentialite/">{% trans %}Privacy policy{% endtrans %}</a>
<button
hx-get="{{ url("eboutic:eurock") }}"
hx-target="#eurock-partner"
hx-swap="outerHTML"
hx-trigger="click, load[document.cookie.includes('weezevent_accept=true')]"
@htmx:after-request="document.cookie = 'weezevent_accept=true'"
>{% trans %}Accept{% endtrans %}
</button>
</div>
{% else %}
<p>
{%- trans trimmed %}
You must be subscribed to benefit from the partnership with the Eurockéennes.
{% endtrans -%}
</p>
<p>
{%- trans trimmed %}
This partnership offers a discount of up to 33%
on tickets for Friday, Saturday and Sunday,
as well as the 3-day package from Friday to Sunday.
{% endtrans -%}
</p>
{% endif %}
</div>
</section>
{% for priority_groups in products|groupby('order') %}
{% for category, items in priority_groups.list|groupby('category') %}
{% if items|count > 0 %}

View File

@@ -1,16 +0,0 @@
<a title="Logiciel billetterie en ligne"
href="https://www.weezevent.com?c=sys_widget"
class="weezevent-widget-integration"
target="_blank"
data-src="https://widget.weezevent.com/ticket/8aaba226-f7a3-4192-a64e-72ff8f5b35b7?id_evenement=1419869&locale=fr-FR&code=28747"
data-width="650"
data-height="600"
data-resize="1"
data-nopb="0"
data-type="neo"
data-width_auto="1"
data-noscroll="0"
data-id="1419869">
Billetterie Weezevent
</a>
<script type="text/javascript" src="https://widget.weezevent.com/weez.js" async defer></script>

View File

@@ -0,0 +1,17 @@
<a
title="Logiciel billetterie en ligne"
href="https://widget.weezevent.com/ticket/6ef65533-f5b0-4571-9d21-1f1bc63921f0?id_evenement=1211855&locale=fr-FR&code=34146"
class="weezevent-widget-integration"
target="_blank"
data-src="https://widget.weezevent.com/ticket/6ef65533-f5b0-4571-9d21-1f1bc63921f0?id_evenement=1211855&locale=fr-FR&code=34146"
data-width="650"
data-height="600"
data-resize="1"
data-nopb="0"
data-type="neo"
data-width_auto="1"
data-noscroll="0"
data-id="1211855">
Billetterie Weezevent
</a>
<script type="text/javascript" src="https://widget.weezevent.com/weez.js" async defer></script>

View File

@@ -31,7 +31,6 @@ from eboutic.views import (
EbouticMainView,
EbouticPayWithSith,
EtransactionAutoAnswer,
EurockPartnerFragment,
payment_result,
)
@@ -51,5 +50,4 @@ urlpatterns = [
EtransactionAutoAnswer.as_view(),
name="etransation_autoanswer",
),
path("eurock/", EurockPartnerFragment.as_view(), name="eurock"),
]

View File

@@ -42,11 +42,11 @@ from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_GET
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
from django.views.generic import DetailView, FormView, UpdateView, View
from django.views.generic.edit import SingleObjectMixin
from django_countries.fields import Country
from core.auth.mixins import CanViewMixin, IsSubscriberMixin
from core.auth.mixins import CanViewMixin
from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BaseBasketForm, BasketProductForm, BillingInfoForm
from counter.models import (
@@ -350,7 +350,3 @@ class EtransactionAutoAnswer(View):
return HttpResponse(
"Payment failed with error: " + request.GET["Error"], status=202
)
class EurockPartnerFragment(IsSubscriberMixin, TemplateView):
template_name = "eboutic/eurock_fragment.jinja"

View File

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

View File

@@ -5838,39 +5838,3 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
#, python-format
msgid "Maximum characters: %(max_length)s"
msgstr "Nombre de caractères max: %(max_length)s"
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid "Eurockéennes 2025 partnership"
msgstr "Partenariat Eurockéennes 2025"
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid ""
"Our partner uses Weezevent to sell tickets. Weezevent may collect user info "
"according to its own privacy policy. By clicking the accept button you "
"consent to their terms of services."
msgstr ""
"Notre partenaire utilises Wezevent pour vendre ses billets. Weezevent peut "
"collecter des informations utilisateur conformément à sa propre politique de "
"confidentialité. En cliquant sur le bouton d'acceptation vous consentez à "
"leurs termes de service."
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid "Privacy policy"
msgstr "Politique de confidentialité"
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid ""
"You must be subscribed to benefit from the partnership with the Eurockéennes."
msgstr ""
"Vous devez être cotisant pour bénéficier du partenariat avec les "
"Eurockéennes."
#: eboutic/templates/eboutic/eboutic_main.jinja
#, python-format
msgid ""
"This partnership offers a discount of up to 33%% on tickets for Friday, "
"Saturday and Sunday, as well as the 3-day package from Friday to Sunday."
msgstr ""
"Ce partenariat permet de profiter d'une réduction jusqu'à 33%% sur les "
"billets du vendredi, du samedi et du dimanche, ainsi qu'au forfait 3 jours, "
"du vendredi au dimanche."