1 Commits

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

View File

@@ -6,10 +6,9 @@ from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema from ninja_extra.schemas import PaginatedResponseSchema
from api.auth import ApiKeyAuth from api.auth import ApiKeyAuth
from api.permissions import CanView, HasPerm from api.permissions import CanAccessLookup, CanView, HasPerm
from club.models import Club, Membership from club.models import Club, Membership
from club.schemas import ( from club.schemas import (
ClubProfileSchema,
ClubSchema, ClubSchema,
ClubSearchFilterSchema, ClubSearchFilterSchema,
SimpleClubSchema, SimpleClubSchema,
@@ -23,21 +22,13 @@ class ClubController(ControllerBase):
@route.get( @route.get(
"/search", "/search",
response=PaginatedResponseSchema[SimpleClubSchema], response=PaginatedResponseSchema[SimpleClubSchema],
auth=[ApiKeyAuth(), SessionAuth()],
permissions=[CanAccessLookup],
url_name="search_club", url_name="search_club",
) )
@paginate(PageNumberPaginationExtra, page_size=50) @paginate(PageNumberPaginationExtra, page_size=50)
def search_club(self, filters: Query[ClubSearchFilterSchema]): def search_club(self, filters: Query[ClubSearchFilterSchema]):
return filters.filter(Club.objects.order_by("name")).values() return filters.filter(Club.objects.all())
@route.get(
"/search-profile",
response=PaginatedResponseSchema[ClubProfileSchema],
url_name="search_club_profile",
)
@paginate(PageNumberPaginationExtra, page_size=50)
def search_club_profile(self, filters: Query[ClubSearchFilterSchema]):
"""Same as /api/club/search, but with more returned data"""
return filters.filter(Club.objects.order_by("name"))
@route.get( @route.get(
"/{int:club_id}", "/{int:club_id}",

View File

@@ -30,7 +30,7 @@ class ClubProfileSchema(ModelSchema):
class Meta: class Meta:
model = Club model = Club
fields = ["id", "name", "logo", "is_active", "short_description"] fields = ["id", "name", "logo"]
url: str url: str

View File

@@ -1,63 +0,0 @@
import { History, updateQueryString } from "#core:utils/history";
import {
type ClubProfileSchema,
type ClubSearchClubData,
clubSearchClubProfile,
type Options,
} from "#openapi";
type ClubStatus = "active" | "inactive" | "both";
const PAGE_SIZE = 50;
document.addEventListener("alpine:init", () => {
Alpine.data("clubList", () => ({
clubName: "",
clubStatus: "active" as ClubStatus,
currentPage: 1,
nbPages: 1,
clubs: [] as ClubProfileSchema[],
loading: false,
async init() {
const urlParams = new URLSearchParams(window.location.search);
this.clubName = urlParams.get("clubName") || "";
this.clubStatus = urlParams.get("clubStatus") || "active";
this.currentPage = urlParams.get("currentPage") || 1;
for (const param of ["clubName", "clubStatus", "currentPage"]) {
this.$watch(param, async (value: number | string) => {
updateQueryString(param, value.toString(), History.Replace);
await this.loadClubs();
});
}
await this.loadClubs();
},
async loadClubs() {
this.loading = true;
const searchParams: Options<ClubSearchClubData> = {
query: { page: this.currentPage },
};
if (this.clubName) {
searchParams.query.search = this.clubName;
}
if (this.clubStatus === "active") {
searchParams.query.is_active = true;
} else if (this.clubStatus === "inactive") {
searchParams.query.is_active = false;
}
const res = await clubSearchClubProfile(searchParams);
this.nbPages = Math.ceil(res.data.count / PAGE_SIZE);
this.clubs = res.data.results;
this.loading = false;
},
getParagraphs(s: string) {
if (!s) {
return [];
}
return s
.split("\n")
.map((s) => s.trim())
.filter((s) => s !== "");
},
}));
});

View File

@@ -1,47 +0,0 @@
#club-list {
display: flex;
flex-direction: column;
gap: 2em;
padding: 2em;
.card {
display: block;
background-color: unset;
.club-image {
float: left;
margin-right: 2rem;
margin-bottom: .5rem;
width: 150px;
height: 150px;
border-radius: 10%;
background-color: rgba(173, 173, 173, 0.2);
@media screen and (max-width: 500px) {
width: 100px;
height: 100px;
}
}
i.club-image {
display: flex;
flex-direction: column;
justify-content: center;
color: black;
}
.content {
display: block;
text-align: justify;
h4 {
margin-top: 0;
margin-right: .5rem;
}
p {
font-size: 100%;
}
}
}
}

View File

@@ -1,5 +1,4 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import paginate_alpine %}
{% block title -%} {% block title -%}
{% trans %}Club list{% endtrans %} {% trans %}Club list{% endtrans %}
@@ -9,76 +8,45 @@
{% trans %}The list of all clubs existing at UTBM.{% endtrans %} {% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %} {%- endblock %}
{% block additional_js %} {% macro display_club(club) -%}
<script type="module" src="{{ static("bundled/club/club-list-index.ts") }}"></script>
{% endblock %}
{% block additional_css %} {% if club.is_active or user.is_root %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
{% endblock %} <li><a href="{{ url('club:club_view', club_id=club.id) }}">{{ club.name }}</a>
{% if not club.is_active %}
({% trans %}inactive{% endtrans %})
{% endif %}
{% if club.president %} - <a href="{{ url('core:user_profile', user_id=club.president.user.id) }}">{{ club.president.user }}</a>{% endif %}
{% if club.short_description %}<p>{{ club.short_description|markdown }}</p>{% endif %}
{% endif %}
{%- if club.children.all()|length != 0 %}
<ul>
{%- for c in club.children.order_by('name').prefetch_related("children") %}
{{ display_club(c) }}
{%- endfor %}
</ul>
{%- endif -%}
</li>
{%- endmacro %}
{% block content %} {% block content %}
<main x-data="clubList"> {% if user.is_root %}
<h3>{% trans %}Filters{% endtrans %}</h3> <p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p>
<form id="club-list-filters"> {% endif %}
<div class="row gap-4x"> {% if club_list %}
<fieldset> <h3>{% trans %}Club list{% endtrans %}</h3>
<label for="club-name-input">{% trans %}Name{% endtrans %}</label> <ul>
<input {%- for club in club_list %}
id="club-name-input" {{ display_club(club) }}
type="text" {%- endfor %}
name="club-name" </ul>
x-model.debounce.500ms="clubName" {% else %}
/> {% trans %}There is no club in this website.{% endtrans %}
</fieldset>
<fieldset class="grow">
<legend>{% trans %}Club state{% endtrans %}</legend>
<div class="row">
<input type="radio" id="filter-active-clubs" x-model="clubStatus" value="active">
<label for="filter-active-clubs">{% trans %}Active{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-inactive-clubs" x-model="clubStatus" value="inactive">
<label for="filter-inactive-clubs">{% trans %}Inactive{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-all-clubs" x-model="clubStatus" value="both">
<label for="filter-all-clubs">{% trans %}All clubs{% endtrans %}</label>
</div>
</fieldset>
</div>
</form>
<h3>{% trans %}Club list{% endtrans %}</h3>
{% if user.has_perm("club.add_club") %}
<br>
<a href="{{ url('club:club_new') }}" class="btn btn-blue">
<i class="fa fa-plus"></i> {% trans %}New club{% endtrans %}
</a>
{% endif %} {% endif %}
<section class="aria-busy-grow" :aria-busy="loading" id="club-list">
<template x-for="club of clubs" :key="club.id">
<div class="card">
<a :href="club.url">
<template x-if="club.logo">
<img class="club-image" :src="club.logo" :alt="`logo ${club.name}`">
</template>
<template x-if="!club.logo">
<i class="fa-regular fa-image fa-4x club-image"></i>
</template>
</a>
<div class="content">
<a :href="club.url">
<h4 x-text="`${club.name} ${club.is_active ? '' : '({% trans %}inactive{% endtrans %})'}`"></h4>
</a>
<template x-for="paragraph of getParagraphs(club.short_description)">
<p x-text="paragraph"></p>
</template>
</div>
</div>
</template>
</section>
{{ paginate_alpine("currentPage", "nbPages") }}
</main>
{% endblock %} {% endblock %}

View File

@@ -1,15 +1,12 @@
from datetime import timedelta from datetime import timedelta
import pytest import pytest
from django.test import Client
from django.urls import reverse
from django.utils.timezone import localdate from django.utils.timezone import localdate
from model_bakery import baker from model_bakery import baker
from model_bakery.recipe import Recipe from model_bakery.recipe import Recipe
from club.models import Club, Membership from club.models import Club, Membership
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
from core.models import User
@pytest.mark.django_db @pytest.mark.django_db
@@ -28,10 +25,3 @@ def test_club_queryset_having_board_member():
club_ids = Club.objects.having_board_member(user).values_list("id", flat=True) club_ids = Club.objects.having_board_member(user).values_list("id", flat=True)
assert set(club_ids) == {clubs[1].id, clubs[2].id} assert set(club_ids) == {clubs[1].id, clubs[2].id}
@pytest.mark.django_db
def test_club_list(client: Client):
client.force_login(baker.make(User))
res = client.get(reverse("club:club_list"))
assert res.status_code == 200

View File

@@ -59,14 +59,6 @@ class TestClubSearch(TestCase):
ids = {d["id"] for d in response.json()["results"]} ids = {d["id"] for d in response.json()["results"]}
assert ids == {c.id for c in [self.clubs[0], self.clubs[1], self.clubs[3]]} assert ids == {c.id for c in [self.clubs[0], self.clubs[1], self.clubs[3]]}
def test_club_search_profile(self):
self.client.force_login(self.user)
url = reverse("api:search_club_profile")
response = self.client.get(url, {"search": "AE"})
assert response.status_code == 200
ids = {d["id"] for d in response.json()["results"]}
assert ids == {c.id for c in [self.clubs[0], self.clubs[1], self.clubs[3]]}
@pytest.mark.django_db @pytest.mark.django_db
class TestFetchClub: class TestFetchClub:

View File

@@ -42,7 +42,7 @@ from django.utils.functional import cached_property
from django.utils.timezone import now 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 django.views.generic import DetailView, TemplateView, View from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
@@ -180,10 +180,15 @@ class ClubTabsMixin(TabedViewMixin):
return tab_list return tab_list
class ClubListView(TemplateView): class ClubListView(ListView):
"""List the Clubs.""" """List the Clubs."""
model = Club
template_name = "club/club_list.jinja" template_name = "club/club_list.jinja"
queryset = (
Club.objects.filter(parent=None).order_by("name").prefetch_related("children")
)
context_object_name = "club_list"
class ClubView(ClubTabsMixin, DetailView): class ClubView(ClubTabsMixin, DetailView):

View File

@@ -244,9 +244,8 @@ class NewsListView(TemplateView):
.filter( .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, role__in=["STUDENT", "FORMER STUDENT"],
) )
.filter(role__in=["STUDENT", "FORMER STUDENT"])
.order_by("-date_of_birth"), .order_by("-date_of_birth"),
key=lambda u: u.date_of_birth.year, key=lambda u: u.date_of_birth.year,
) )

View File

@@ -63,6 +63,7 @@ class UserAdmin(admin.ModelAdmin):
"scrub_pict", "scrub_pict",
"user_permissions", "user_permissions",
"groups", "groups",
"whitelisted_users",
) )
inlines = (UserBanInline,) inlines = (UserBanInline,)
search_fields = ["first_name", "last_name", "username"] 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"): if user.has_perm("core.view_hidden_user"):
return self return self
if user.has_perm("core.view_user"): 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: if user.is_anonymous:
return self.none() return self.none()
return self.filter(id=user.id) return self.filter(id=user.id)
@@ -279,6 +279,15 @@ class User(AbstractUser):
), ),
default=True, 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) godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True)
objects = CustomUserManager() objects = CustomUserManager()
@@ -567,10 +576,31 @@ class User(AbstractUser):
return user.is_root or user.is_board_member return user.is_root or user.is_board_member
def can_be_viewed_by(self, user: User) -> bool: 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 ( return (
user.id == self.id user.id == self.id
or user.has_perm("core.view_hidden_user") 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): def get_mini_item(self):

View File

@@ -5,8 +5,9 @@
<details name="navbar" class="menu"> <details name="navbar" class="menu">
<summary class="head">{% trans %}Associations & Clubs{% endtrans %}</summary> <summary class="head">{% trans %}Associations & Clubs{% endtrans %}</summary>
<ul class="content"> <ul class="content">
<li><a href="{{ url("core:page", page_name="ae") }}">{% trans %}AE{% endtrans %}</a></li> <li><a href="{{ url('core:page', page_name='ae') }}">{% trans %}AE{% endtrans %}</a></li>
<li><a href="{{ url("club:club_list") }}">{% trans %}AE's clubs{% endtrans %}</a></li> <li><a href="{{ url('core:page', page_name='clubs') }}">{% trans %}AE's clubs{% endtrans %}</a></li>
<li><a href="{{ url('core:page', page_name='utbm-associations') }}">{% trans %}Others UTBM's Associations{% endtrans %}</a></li>
</ul> </ul>
</details> </details>
<details name="navbar" class="menu"> <details name="navbar" class="menu">

View File

@@ -399,13 +399,12 @@ class TestUserQuerySetViewableBy:
return [ return [
baker.make(User), baker.make(User),
subscriber_user.make(), 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]): def test_admin_user(self, users: list[User]):
user = baker.make( user = baker.make(
User, User, user_permissions=[Permission.objects.get(codename="view_hidden_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) viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
assert set(viewable) == set(users) 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) viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
assert set(viewable) == {users[0], users[1]} 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]) @pytest.mark.parametrize("user_factory", [lambda: baker.make(User), AnonymousUser])
def test_not_subscriber(self, users: list[User], user_factory): def test_not_subscriber(self, users: list[User], user_factory):
user = user_factory() user = user_factory()

View File

@@ -69,7 +69,6 @@ from core.views import (
UserCreationView, UserCreationView,
UserGodfathersTreeView, UserGodfathersTreeView,
UserGodfathersView, UserGodfathersView,
UserListView,
UserMeRedirect, UserMeRedirect,
UserMiniView, UserMiniView,
UserPreferencesView, UserPreferencesView,
@@ -136,7 +135,6 @@ urlpatterns = [
"group/<int:group_id>/detail/", GroupTemplateView.as_view(), name="group_detail" "group/<int:group_id>/detail/", GroupTemplateView.as_view(), name="group_detail"
), ),
# User views # User views
path("user/", UserListView.as_view(), name="user_list"),
path( path(
"user/me/<path:remaining_path>/", "user/me/<path:remaining_path>/",
UserMeRedirect.as_view(), UserMeRedirect.as_view(),

View File

@@ -48,7 +48,6 @@ from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
DetailView, DetailView,
ListView,
RedirectView, RedirectView,
TemplateView, TemplateView,
) )
@@ -248,15 +247,14 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Groups"), "name": _("Groups"),
} }
) )
can_view_account = ( if (
hasattr(user, "customer") hasattr(user, "customer")
and user.customer and user.customer
and ( and (
user == self.request.user user == self.request.user
or self.request.user.has_perm("counter.view_customer") or self.request.user.has_perm("counter.view_customer")
) )
) ):
if can_view_account or user.preferences.show_my_stats:
tab_list.append( tab_list.append(
{ {
"url": reverse("core:user_stats", kwargs={"user_id": user.id}), "url": reverse("core:user_stats", kwargs={"user_id": user.id}),
@@ -264,7 +262,6 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Stats"), "name": _("Stats"),
} }
) )
if can_view_account:
tab_list.append( tab_list.append(
{ {
"url": reverse("core:user_account", kwargs={"user_id": user.id}), "url": reverse("core:user_account", kwargs={"user_id": user.id}),
@@ -351,7 +348,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
return kwargs return kwargs
class UserStatsView(UserTabsMixin, UserPassesTestMixin, DetailView): class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
"""Display a user's stats.""" """Display a user's stats."""
model = User model = User
@@ -359,20 +356,15 @@ class UserStatsView(UserTabsMixin, UserPassesTestMixin, DetailView):
context_object_name = "profile" context_object_name = "profile"
template_name = "core/user_stats.jinja" template_name = "core/user_stats.jinja"
current_tab = "stats" current_tab = "stats"
queryset = User.objects.exclude(customer=None).select_related( queryset = User.objects.exclude(customer=None).select_related("customer")
"customer", "_preferences"
)
def test_func(self): def dispatch(self, request, *arg, **kwargs):
profile: User = self.get_object() profile = self.get_object()
return ( if not (
profile == self.request.user profile == request.user or request.user.has_perm("counter.view_customer")
or self.request.user.has_perm("counter.view_customer") ):
or ( raise PermissionDenied
self.request.user.can_view(profile) return super().dispatch(request, *arg, **kwargs)
and profile.preferences.show_my_stats
)
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
@@ -411,13 +403,6 @@ class UserMiniView(CanViewMixin, DetailView):
template_name = "core/user_mini.jinja" 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). # 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. # However, if the user re-add them manually in the form, they are saved.
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):

View File

@@ -146,7 +146,7 @@
<label for="{{ input_id }}"> <label for="{{ input_id }}">
{%- endif %} {%- endif %}
<figure> <figure>
{%- if user.is_viewable %} {%- if user.can_view(candidature.user) %}
{% if candidature.user.profile_pict %} {% if candidature.user.profile_pict %}
<img class="candidate__picture" src="{{ candidature.user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}"> <img class="candidate__picture" src="{{ candidature.user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}">
{% else %} {% else %}

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-20 16:24+0100\n" "POT-Creation-Date: 2026-03-10 10:28+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -310,36 +310,16 @@ msgid "The list of all clubs existing at UTBM."
msgstr "La liste de tous les clubs existants à l'UTBM" msgstr "La liste de tous les clubs existants à l'UTBM"
#: club/templates/club/club_list.jinja #: club/templates/club/club_list.jinja
msgid "Filters" msgid "inactive"
msgstr "Filtres" msgstr "inactif"
#: club/templates/club/club_list.jinja
msgid "Name"
msgstr "Nom"
#: club/templates/club/club_list.jinja
msgid "Club state"
msgstr "Etat du club"
#: club/templates/club/club_list.jinja
msgid "Active"
msgstr "Actif"
#: club/templates/club/club_list.jinja
msgid "Inactive"
msgstr "Inactif"
#: club/templates/club/club_list.jinja
msgid "All clubs"
msgstr "Tous les clubs"
#: club/templates/club/club_list.jinja core/templates/core/user_tools.jinja #: club/templates/club/club_list.jinja core/templates/core/user_tools.jinja
msgid "New club" msgid "New club"
msgstr "Nouveau club" msgstr "Nouveau club"
#: club/templates/club/club_list.jinja #: club/templates/club/club_list.jinja
msgid "inactive" msgid "There is no club in this website."
msgstr "inactif" msgstr "Il n'y a pas de club dans ce site web."
#: club/templates/club/club_members.jinja #: club/templates/club/club_members.jinja
msgid "Club members" msgid "Club members"
@@ -1880,6 +1860,10 @@ msgstr "L'AE"
msgid "AE's clubs" msgid "AE's clubs"
msgstr "Les clubs de L'AE" msgstr "Les clubs de L'AE"
#: core/templates/core/base/navbar.jinja
msgid "Others UTBM's Associations"
msgstr "Les autres associations de l'UTBM"
#: core/templates/core/base/navbar.jinja #: core/templates/core/base/navbar.jinja
msgid "Big event" msgid "Big event"
msgstr "Grandes Activités" msgstr "Grandes Activités"

View File

@@ -355,6 +355,7 @@ SITH_TWITTER = "@ae_utbm"
# AE configuration # AE configuration
SITH_MAIN_CLUB_ID = env.int("SITH_MAIN_CLUB_ID", default=1) 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_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 # Main root for club pages
SITH_CLUB_ROOT_PAGE = "clubs" SITH_CLUB_ROOT_PAGE = "clubs"
@@ -482,6 +483,13 @@ SITH_LOG_OPERATION_TYPE = [
SITH_PEDAGOGY_UTBM_API = "https://extranet1.utbm.fr/gpedago/api/guide" 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 # Defines pagination for cash summary
SITH_COUNTER_CASH_SUMMARY_LENGTH = 50 SITH_COUNTER_CASH_SUMMARY_LENGTH = 50
@@ -504,6 +512,7 @@ SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER = env.int(
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = env.int( SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = env.int(
"SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS", default=2 "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 # Number of weeks before the end of a subscription when the subscriber can resubscribe
SITH_SUBSCRIPTION_END = 10 SITH_SUBSCRIPTION_END = 10