12 Commits

Author SHA1 Message Date
thomas girod
f9910c3360 Merge pull request #1320 from ae-utbm/user-whitelist
feat: whitelist for user visibility
2026-03-23 23:21:30 +01:00
imperosol
f0f8cc5604 add permission to AE board to see hidden users in populate 2026-03-23 23:03:53 +01:00
imperosol
2a8e810ad0 always show the show_my_stats input 2026-03-23 23:03:53 +01:00
imperosol
739a1bba47 Use whitelist for picture identifications 2026-03-23 23:03:53 +01:00
imperosol
180852a598 add explanation comment 2026-03-23 23:03:53 +01:00
imperosol
c3989a0016 add translations 2026-03-23 23:03:53 +01:00
imperosol
435c8f9612 include visibility settings in the user preferences page 2026-03-23 23:03:53 +01:00
imperosol
3d7f57b8da feat: whitelist for user visibility 2026-03-23 23:03:53 +01:00
thomas girod
ffa0b94408 Merge pull request #1319 from ae-utbm/show-my-stats
show user stats to subscribers if show_my_stats is enabled
2026-03-20 13:49:48 +01:00
thomas girod
22a1f4ba07 Merge pull request #1317 from ae-utbm/remove-settings
remove unused settings
2026-03-20 13:47:22 +01:00
imperosol
1c0b89bfc7 show user stats to subscribers if show_my_stats is enabled 2026-03-14 16:23:56 +01:00
imperosol
f1a60e589a remove unused settings 2026-03-12 10:26:40 +01:00
20 changed files with 315 additions and 140 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

@@ -116,7 +116,11 @@ class Command(BaseCommand):
)
main_club.board_group.permissions.add(
*Permission.objects.filter(
codename__in=["view_subscription", "add_subscription"]
codename__in=[
"view_subscription",
"add_subscription",
"view_hidden_user",
]
)
)
bar_club = Club.objects.create(

View File

@@ -0,0 +1,37 @@
# 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(
blank=True,
help_text=(
"Even 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",
),
),
migrations.AlterField(
model_name="preferences",
name="show_my_stats",
field=models.BooleanField(
default=False,
help_text=(
"Allow subscribers (or whitelisted users "
"if your profile is hidden) to access your AE account stats."
),
verbose_name="show your stats to others",
),
),
]

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,16 @@ class User(AbstractUser):
),
default=True,
)
whitelisted_users = models.ManyToManyField(
"User",
related_name="visible_by_whitelist",
verbose_name=_("whitelisted users"),
help_text=_(
"Even if this profile is hidden, "
"the users in this list will still be able to see it."
),
blank=True,
)
godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True)
objects = CustomUserManager()
@@ -518,7 +528,7 @@ class User(AbstractUser):
self.username = user_name
return user_name
def is_owner(self, obj):
def is_owner(self, obj: models.Model):
"""Determine if the object is owned by the user."""
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
return True
@@ -526,7 +536,7 @@ class User(AbstractUser):
return True
return self.is_root
def can_edit(self, obj):
def can_edit(self, obj: models.Model):
"""Determine if the object can be edited by the user."""
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
return True
@@ -540,11 +550,9 @@ class User(AbstractUser):
pks = list(obj.edit_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True
if isinstance(obj, User) and obj == self:
return True
return self.is_owner(obj)
def can_view(self, obj):
def can_view(self, obj: models.Model):
"""Determine if the object can be viewed by the user."""
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True
@@ -563,14 +571,35 @@ class User(AbstractUser):
return True
return self.can_edit(obj)
def can_be_edited_by(self, user):
return user.is_root or user.is_board_member
def can_be_edited_by(self, user: User):
return user == self or 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):
@@ -750,7 +779,14 @@ class Preferences(models.Model):
User, related_name="_preferences", on_delete=models.CASCADE
)
receive_weekmail = models.BooleanField(_("receive the Weekmail"), default=False)
show_my_stats = models.BooleanField(_("show your stats to others"), default=False)
show_my_stats = models.BooleanField(
_("show your stats to others"),
help_text=_(
"Allow subscribers (or whitelisted users "
"if your profile is hidden) to access your AE account stats."
),
default=False,
)
notify_on_click = models.BooleanField(
_("get a notification for every click"), default=False
)

View File

@@ -157,6 +157,7 @@ form {
margin-bottom: .25rem;
font-size: 80%;
display: block;
max-width: calc(100% - calc(var(--nf-input-size) * 2))
}
fieldset {

View File

@@ -5,17 +5,6 @@
}
.profile {
&-visible {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding-top: 10px;
input[type="checkbox"]+label {
max-width: unset;
}
}
&-pictures {
box-sizing: border-box;
display: flex;

View File

@@ -19,28 +19,6 @@
}
}
}
&-cards,
&-trombi {
>p {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: justify;
gap: 5px;
margin: 0;
>input,
>select {
min-width: 300px;
}
}
}
&-submit-btn {
margin-top: 10px !important;
max-width: 100px;
}
}
.justify {

View File

@@ -1,14 +1,11 @@
<div id="quick-notifications"
x-data="{
messages: [
{% if messages %}
{% for message in messages %}
{
tag: '{{ message.tags }}',
text: '{{ message }}',
},
{% endfor %}
{% endif %}
{%- for message in messages -%}
{%- if not message.extra_tags -%}
{ tag: '{{ message.tags }}', text: '{{ message }}' },
{%- endif -%}
{%- endfor -%}
]
}"
@quick-notification-add="(e) => messages.push(e?.detail)"

View File

@@ -0,0 +1,33 @@
<form
hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }"
>
{% for message in messages %}
{% if message.extra_tags=="visibility" %}
<div class="alert alert-success">
{{ message }}
</div>
{% endif %}
{% endfor %}
{% csrf_token %}
{{ form.non_field_errors() }}
<fieldset class="form-group">
{{ form.is_viewable|add_attr("x-model=isViewable") }}
{{ form.is_viewable.label_tag() }}
<span class="helptext">{{ form.is_viewable.help_text }}</span>
{{ form.is_viewable.errors }}
</fieldset>
<fieldset class="form-group" x-show="!isViewable">
{{ form.whitelisted_users.as_field_group() }}
</fieldset>
<fieldset class="form-group">
{{ form.show_my_stats }}
{{ form.show_my_stats.label_tag() }}
<span class="helptext">
{{ form.show_my_stats.help_text }}
</span>
{{ form.show_my_stats.errors }}
</fieldset>
<input type="submit" class="btn btn-blue" value="{% trans %}Save{% endtrans %}">
</form>

View File

@@ -147,18 +147,7 @@
{%- endfor -%}
</div>
{# Checkboxes #}
<div class="profile-visible">
<div class="row">
{{ form.is_viewable }}
{{ form.is_viewable.label_tag() }}
</div>
<span class="helptext">
{{ form.is_viewable.help_text }}
</span>
</div>
<div class="final-actions">
{%- if form.instance == user -%}
<p>
<a href="{{ url('core:password_change') }}">{%- trans -%}Change my password{%- endtrans -%}</a>
@@ -170,7 +159,6 @@
</a>
</p>
{%- endif -%}
<p>
<input type="submit" value="{%- trans -%}Update{%- endtrans -%}" />
</p>

View File

@@ -1,7 +1,14 @@
{% extends "core/base.jinja" %}
{%- block additional_js -%}
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
{%- endblock -%}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ static('user/user_preferences.scss') }}">
{# importing ajax-select-index is necessary for it to be applied after HTMX reload #}
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
{%- endblock -%}
{% block title %}
@@ -11,30 +18,22 @@
{% block content %}
<div class="main">
<h2>{% trans %}Preferences{% endtrans %}</h2>
<h3>{% trans %}General{% endtrans %}</h3>
<form class="form form-general" action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p() }}
<input class="form-submit-btn" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
<h3>{% trans %}Trombi{% endtrans %}</h3>
{% if trombi_form %}
<form class="form form-trombi" action="{{ url('trombi:user_tools') }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ trombi_form.as_p() }}
<input class="form-submit-btn" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
{% else %}
<p>{% trans trombi=profile.trombi_user.trombi %}You already choose to be in that Trombi: {{ trombi }}.{% endtrans %}
<br />
<a href="{{ url('trombi:user_tools') }}">{% trans %}Go to my Trombi tools{% endtrans %}</a>
</p>
{% endif %}
<h3>{% trans %}Notifications{% endtrans %}</h3>
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form form-general">
{{ form.as_p() }}
</div>
<input class="btn btn-blue" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
<br />
<h3>{% trans %}Visibility{% endtrans %}</h3>
{{ user_visibility_fragment }}
<br />
{% if student_card_fragment %}
<h3>{% trans %}Student card{% endtrans %}</h3>
{{ student_card_fragment }}
@@ -43,5 +42,21 @@
add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}
</p>
{% endif %}
<br />
<h3>{% trans %}Trombi{% endtrans %}</h3>
{% if trombi_form %}
<form action="{{ url('trombi:user_tools') }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ trombi_form.as_p() }}
<input class="btn btn-blue" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
{% else %}
<p>{% trans trombi=profile.trombi_user.trombi %}You already choose to be in that Trombi: {{ trombi }}.{% endtrans %}
<br />
<a href="{{ url('trombi:user_tools') }}">{% trans %}Go to my Trombi tools{% endtrans %}</a>
</p>
{% endif %}
</div>
{% endblock %}

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,
@@ -78,6 +77,7 @@ from core.views import (
UserUpdateGroupView,
UserUpdateProfileView,
UserView,
UserVisibilityFormFragment,
delete_user_godfather,
logout,
notification,
@@ -136,7 +136,11 @@ urlpatterns = [
"group/<int:group_id>/detail/", GroupTemplateView.as_view(), name="group_detail"
),
# User views
path("user/", UserListView.as_view(), name="user_list"),
path(
"fragment/user/<int:user_id>/",
UserVisibilityFormFragment.as_view(),
name="user_visibility_fragment",
),
path(
"user/me/<path:remaining_path>/",
UserMeRedirect.as_view(),

View File

@@ -48,12 +48,13 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
from PIL import Image
from antispam.forms import AntiSpamEmailField
from core.models import Gift, Group, Page, PageRev, SithFile, User
from core.models import Gift, Group, Page, PageRev, Preferences, SithFile, User
from core.utils import resize_image
from core.views.widgets.ajax_select import (
AutoCompleteSelect,
AutoCompleteSelectGroup,
AutoCompleteSelectMultipleGroup,
AutoCompleteSelectMultipleUser,
AutoCompleteSelectUser,
)
from core.views.widgets.markdown import MarkdownInput
@@ -179,7 +180,6 @@ class UserProfileForm(forms.ModelForm):
"school",
"promo",
"forum_signature",
"is_viewable",
]
widgets = {
"date_of_birth": SelectDate,
@@ -264,6 +264,38 @@ class UserProfileForm(forms.ModelForm):
self._post_clean()
class UserVisibilityForm(forms.ModelForm):
class Meta:
model = User
fields = ["is_viewable", "whitelisted_users"]
widgets = {
"is_viewable": forms.CheckboxInput(attrs={"class": "switch"}),
"whitelisted_users": AutoCompleteSelectMultipleUser,
}
__preferences_fields = forms.fields_for_model(
Preferences,
["show_my_stats"],
widgets={"show_my_stats": forms.CheckboxInput(attrs={"class": "switch"})},
)
show_my_stats = __preferences_fields["show_my_stats"]
def __init__(
self, *args, initial: dict | None = None, instance: User | None = None, **kwargs
):
if instance:
initial = initial or {}
initial["show_my_stats"] = instance.preferences.show_my_stats
super().__init__(*args, initial=initial, instance=instance, **kwargs)
def save(self, commit=True) -> User: # noqa: FBT002
instance = super().save(commit=commit)
if commit:
instance.preferences.show_my_stats = self.cleaned_data["show_my_stats"]
instance.preferences.save()
return instance
class UserGroupsForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"

View File

@@ -28,10 +28,12 @@ from datetime import timedelta
from operator import itemgetter
from smtplib import SMTPException
from django.contrib import messages
from django.contrib.auth import login, views
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied
from django.db.models import DateField, F, QuerySet, Sum
from django.db.models.functions import Trunc
@@ -48,7 +50,6 @@ from django.views.generic import (
CreateView,
DeleteView,
DetailView,
ListView,
RedirectView,
TemplateView,
)
@@ -65,8 +66,9 @@ from core.views.forms import (
UserGodfathersForm,
UserGroupsForm,
UserProfileForm,
UserVisibilityForm,
)
from core.views.mixins import TabedViewMixin, UseFragmentsMixin
from core.views.mixins import FragmentMixin, TabedViewMixin, UseFragmentsMixin
from counter.models import Refilling, Selling
from eboutic.models import Invoice
from trombi.views import UserTrombiForm
@@ -248,14 +250,15 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Groups"),
}
)
if (
can_view_account = (
hasattr(user, "customer")
and user.customer
and (
user == self.request.user
or self.request.user.has_perm("counter.view_customer")
)
):
)
if can_view_account or user.preferences.show_my_stats:
tab_list.append(
{
"url": reverse("core:user_stats", kwargs={"user_id": user.id}),
@@ -263,6 +266,7 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Stats"),
}
)
if can_view_account:
tab_list.append(
{
"url": reverse("core:user_account", kwargs={"user_id": user.id}),
@@ -349,7 +353,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
return kwargs
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
class UserStatsView(UserTabsMixin, UserPassesTestMixin, DetailView):
"""Display a user's stats."""
model = User
@@ -357,15 +361,20 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
context_object_name = "profile"
template_name = "core/user_stats.jinja"
current_tab = "stats"
queryset = User.objects.exclude(customer=None).select_related("customer")
queryset = User.objects.exclude(customer=None).select_related(
"customer", "_preferences"
)
def dispatch(self, request, *arg, **kwargs):
profile = self.get_object()
if not (
profile == request.user or request.user.has_perm("counter.view_customer")
):
raise PermissionDenied
return super().dispatch(request, *arg, **kwargs)
def test_func(self):
profile: User = self.get_object()
return (
profile == self.request.user
or self.request.user.has_perm("counter.view_customer")
or (
self.request.user.can_view(profile)
and profile.preferences.show_my_stats
)
)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
@@ -404,13 +413,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):
@@ -468,6 +470,30 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
current_tab = "clubs"
class UserVisibilityFormFragment(FragmentMixin, SuccessMessageMixin, UpdateView):
model = User
form_class = UserVisibilityForm
template_name = "core/fragment/user_visibility.jinja"
pk_url_kwarg = "user_id"
def get_form_kwargs(self):
return super().get_form_kwargs() | {"label_suffix": ""}
def form_valid(self, form):
response = super().form_valid(form)
messages.success(
self.request, _("Visibility parameters updated."), extra_tags="visibility"
)
return response
def render_fragment(self, request, **kwargs) -> SafeString:
self.object = kwargs.get("user")
return super().render_fragment(request, **kwargs)
def get_success_url(self, **kwargs):
return self.request.path
class UserPreferencesView(UserTabsMixin, UseFragmentsMixin, CanEditMixin, UpdateView):
"""Edit a user's preferences."""
@@ -481,7 +507,10 @@ class UserPreferencesView(UserTabsMixin, UseFragmentsMixin, CanEditMixin, Update
current_tab = "prefs"
def get_form_kwargs(self):
return super().get_form_kwargs() | {"instance": self.object.preferences}
return super().get_form_kwargs() | {
"instance": self.object.preferences,
"label_suffix": "",
}
def get_success_url(self):
return self.request.path
@@ -491,6 +520,9 @@ class UserPreferencesView(UserTabsMixin, UseFragmentsMixin, CanEditMixin, Update
from counter.views.student_card import StudentCardFormFragment
res = super().get_fragment_context_data()
res["user_visibility_fragment"] = UserVisibilityFormFragment.as_fragment()(
self.request, user=self.object
)
if hasattr(self.object, "customer"):
res["student_card_fragment"] = StudentCardFormFragment.as_fragment()(
self.request, customer=self.object.customer

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

@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-10 10:28+0100\n"
"POT-Creation-Date: 2026-03-23 22:21+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -551,8 +551,9 @@ msgstr ""
#: com/templates/com/news_edit.jinja com/templates/com/poster_edit.jinja
#: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja
#: core/templates/core/create.jinja core/templates/core/edit.jinja
#: core/templates/core/file_edit.jinja core/templates/core/page/edit.jinja
#: core/templates/core/page/prop.jinja
#: core/templates/core/file_edit.jinja
#: core/templates/core/fragment/user_visibility.jinja
#: core/templates/core/page/edit.jinja core/templates/core/page/prop.jinja
#: core/templates/core/user_godfathers.jinja
#: core/templates/core/user_godfathers_tree.jinja
#: core/templates/core/user_preferences.jinja
@@ -1547,6 +1548,18 @@ msgid ""
msgstr ""
"Si vous désactivez cette option, seuls les admins pourront voir votre profil."
#: core/models.py
msgid "whitelisted users"
msgstr "utilisateurs whitelistés"
#: core/models.py
msgid ""
"Even if this profile is hidden, the users in this list will still be able to "
"see it."
msgstr ""
"Même si ce profil est caché, les utilisateurs sur cette liste pourront "
"toujours le voir."
#: core/models.py
msgid "A user with that username already exists"
msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
@@ -1603,6 +1616,14 @@ msgstr "recevoir le Weekmail"
msgid "show your stats to others"
msgstr "montrez vos statistiques aux autres"
#: core/models.py
msgid ""
"Allow subscribers (or whitelisted users if your profile is hidden) to access "
"your AE account stats."
msgstr ""
"Autorise les cotisants (ou les personnes whitelistées, si votre profil est "
"caché) à accéder aux statistiques de votre compte AE"
#: core/models.py
msgid "get a notification for every click"
msgstr "avoir une notification pour chaque click"
@@ -2612,21 +2633,12 @@ msgid "Preferences"
msgstr "Préférences"
#: core/templates/core/user_preferences.jinja
msgid "General"
msgstr "Général"
#: core/templates/core/user_preferences.jinja trombi/views.py
msgid "Trombi"
msgstr "Trombi"
msgid "Notifications"
msgstr "Notifications"
#: core/templates/core/user_preferences.jinja
#, python-format
msgid "You already choose to be in that Trombi: %(trombi)s."
msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s."
#: core/templates/core/user_preferences.jinja
msgid "Go to my Trombi tools"
msgstr "Allez à mes outils de Trombi"
msgid "Visibility"
msgstr "Visibilité"
#: core/templates/core/user_preferences.jinja
#: counter/templates/counter/counter_click.jinja
@@ -2645,6 +2657,19 @@ msgstr ""
"aurez besoin d'un lecteur NFC. Nous enregistrons l'UID de la carte qui fait "
"14 caractères de long."
#: core/templates/core/user_preferences.jinja trombi/views.py
msgid "Trombi"
msgstr "Trombi"
#: core/templates/core/user_preferences.jinja
#, python-format
msgid "You already choose to be in that Trombi: %(trombi)s."
msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s."
#: core/templates/core/user_preferences.jinja
msgid "Go to my Trombi tools"
msgstr "Allez à mes outils de Trombi"
#: core/templates/core/user_stats.jinja
#, python-format
msgid "%(user_name)s's stats"
@@ -2925,6 +2950,10 @@ msgstr "Photos"
msgid "Account"
msgstr "Compte"
#: core/views/user.py
msgid "Visibility parameters updated."
msgstr "Paramètres de visibilité mis à jour."
#: counter/apps.py counter/models.py
msgid "counter"
msgstr "comptoir"

View File

@@ -270,7 +270,11 @@ class PeoplePictureRelationQuerySet(models.QuerySet):
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
return self
if user.was_subscribed:
return self.filter(Q(user_id=user.id) | Q(user__is_viewable=True))
return self.filter(
Q(user_id=user.id)
| Q(user__is_viewable=True)
| Q(user__whitelisted_users=user)
)
return self.filter(user_id=user.id)

View File

@@ -355,7 +355,6 @@ SITH_TWITTER = "@ae_utbm"
# AE configuration
SITH_MAIN_CLUB_ID = env.int("SITH_MAIN_CLUB_ID", default=1)
SITH_PDF_CLUB_ID = env.int("SITH_PDF_CLUB_ID", default=2)
SITH_LAUNDERETTE_CLUB_ID = env.int("SITH_LAUNDERETTE_CLUB_ID", default=84)
# Main root for club pages
SITH_CLUB_ROOT_PAGE = "clubs"
@@ -483,13 +482,6 @@ SITH_LOG_OPERATION_TYPE = [
SITH_PEDAGOGY_UTBM_API = "https://extranet1.utbm.fr/gpedago/api/guide"
SITH_ECOCUP_CONS = env.int("SITH_ECOCUP_CONS", default=1151)
SITH_ECOCUP_DECO = env.int("SITH_ECOCUP_DECO", default=1152)
# The limit is the maximum difference between cons and deco possible for a customer
SITH_ECOCUP_LIMIT = 3
# Defines pagination for cash summary
SITH_COUNTER_CASH_SUMMARY_LENGTH = 50
@@ -512,7 +504,6 @@ SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER = env.int(
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = env.int(
"SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS", default=2
)
SITH_PRODUCTTYPE_SUBSCRIPTION = env.int("SITH_PRODUCTTYPE_SUBSCRIPTION", default=2)
# Number of weeks before the end of a subscription when the subscriber can resubscribe
SITH_SUBSCRIPTION_END = 10