4 Commits

Author SHA1 Message Date
dependabot[bot]
0a6a212933 Bump picomatch from 4.0.3 to 4.0.4
Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-29 14:36:27 +00:00
Titouan
ca37996d6a Merge pull request #1332 from ae-utbm/taiste
Stats & Whitelist, Eurockéenne, fix pagination, Vite 8, delete unused settings
2026-03-29 16:35:16 +02:00
Titouan
173311c1d5 Merge pull request #1315 from ae-utbm/taiste
Product history, formula management, test election
2026-03-12 11:33:45 +01:00
thomas girod
2995823d6e Merge pull request #1293 from ae-utbm/taiste
Refactors, updates and db optimisations
2026-02-13 15:25:04 +01:00
11 changed files with 81 additions and 236 deletions

View File

@@ -6,7 +6,7 @@ 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 (
ClubSchema, ClubSchema,
@@ -22,11 +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( @route.get(
"/{int:club_id}", "/{int:club_id}",

View File

@@ -315,22 +315,3 @@ class JoinClubForm(ClubMemberForm):
_("You are already a member of this club"), code="invalid" _("You are already a member of this club"), code="invalid"
) )
return super().clean() return super().clean()
class ClubSearchForm(forms.ModelForm):
class Meta:
model = Club
fields = ["name"]
widgets = {"name": forms.SearchInput(attrs={"autocomplete": "off"})}
club_status = forms.NullBooleanField(
label=_("Club status"),
widget=forms.RadioSelect(
choices=[(True, _("Active")), (False, _("Inactive")), ("", _("All clubs"))],
),
initial=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["name"].required = False

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,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,75 +1,51 @@
{% if is_fragment %} {% extends "core/base.jinja" %}
{% extends "core/base_fragment.jinja" %}
{# Don't display tabs and errors #} {% block title -%}
{% block tabs %} {% trans %}Club list{% endtrans %}
{% endblock %} {%- endblock %}
{% block errors %}
{% endblock %}
{% else %}
{% extends "core/base.jinja" %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
{% endblock %}
{% block description -%}
{% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %}
{% block title -%}
{% trans %}Club list{% endtrans %}
{%- endblock %}
{% endif %}
{% from "core/macros.jinja" import paginate_htmx %} {% block description -%}
{% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %}
{% macro display_club(club) -%}
{% if club.is_active or user.is_root %}
<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 %}
{% if not is_fragment %} {% if user.is_root %}
<main> <p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p>
<h3>{% trans %}Filters{% endtrans %}</h3>
<form>
<div class="row gap-4x">
{{ form }}
</div>
<button type="submit" class="btn btn-blue margin-bottom">
<i class="fa fa-magnifying-glass"></i>{% trans %}Search{% endtrans %}
</button>
</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 %}
<div id="paginated">
{% endif %} {% endif %}
<section class="aria-busy-grow" id="club-list"> {% if club_list %}
{% for club in object_list %} <h3>{% trans %}Club list{% endtrans %}</h3>
<div class="card"> <ul>
{% set club_url = club.get_absolute_url() %} {%- for club in club_list %}
<a href="{{ club_url }}"> {{ display_club(club) }}
{% if club.logo %} {%- endfor %}
<img class="club-image" src="{{ club.logo.url }}" alt="logo {{ club.name }}"> </ul>
{% else %} {% else %}
<i class="fa-regular fa-image fa-4x club-image"></i> {% trans %}There is no club in this website.{% endtrans %}
{% endif %}
</a>
<div class="content">
<a href="{{ club_url }}">
<h4>
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
</h4>
</a>
{{ club.short_description|markdown }}
</div>
</div>
{% endfor %}
{% if is_paginated %}
{{ paginate_htmx(request, page_obj, paginator, htmx_target="#paginated") }}
{% endif %}
</section>
{% if not is_fragment %}
</div>
</main>
{% endif %} {% endif %}
{% 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,14 +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.parametrize("nb_additional_clubs", [10, 30])
@pytest.mark.parametrize("is_fragment", [True, False])
@pytest.mark.django_db
def test_club_list(client: Client, nb_additional_clubs: int, is_fragment):
client.force_login(baker.make(User))
baker.make(Club, _quantity=nb_additional_clubs)
headers = {"HX-Request": True} if is_fragment else {}
res = client.get(reverse("club:club_list"), headers=headers)
assert res.status_code == 200

View File

@@ -44,19 +44,13 @@ 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, ListView, 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 ( from django.views.generic.edit import CreateView, DeleteView, UpdateView
CreateView,
DeleteView,
FormMixin,
UpdateView,
)
from club.forms import ( from club.forms import (
ClubAddMemberForm, ClubAddMemberForm,
ClubAdminEditForm, ClubAdminEditForm,
ClubEditForm, ClubEditForm,
ClubOldMemberForm, ClubOldMemberForm,
ClubSearchForm,
JoinClubForm, JoinClubForm,
MailingForm, MailingForm,
SellingsForm, SellingsForm,
@@ -72,12 +66,7 @@ from com.views import (
from core.auth.mixins import CanEditMixin, PermissionOrClubBoardRequiredMixin from core.auth.mixins import CanEditMixin, PermissionOrClubBoardRequiredMixin
from core.models import Page, PageRev from core.models import Page, PageRev
from core.views import BasePageEditView, DetailFormView, UseFragmentsMixin from core.views import BasePageEditView, DetailFormView, UseFragmentsMixin
from core.views.mixins import ( from core.views.mixins import FragmentMixin, FragmentRenderer, TabedViewMixin
AllowFragment,
FragmentMixin,
FragmentRenderer,
TabedViewMixin,
)
from counter.models import Selling from counter.models import Selling
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -191,41 +180,15 @@ class ClubTabsMixin(TabedViewMixin):
return tab_list return tab_list
class ClubListView(AllowFragment, FormMixin, ListView): class ClubListView(ListView):
"""List the clubs of the AE, with a form to perform basic search. """List the Clubs."""
Notes:
This view is fully public, because we want to advertise as much as possible
the cultural life of the AE.
In accordance with that matter, searching and listing the clubs is done
entirely server-side (no AlpineJS involved) ;
this is done this way in order to be sure the page is the most accessible
and SEO-friendly possible, even if it makes the UX slightly less smooth.
"""
model = Club
template_name = "club/club_list.jinja" template_name = "club/club_list.jinja"
form_class = ClubSearchForm queryset = (
queryset = Club.objects.order_by("name") Club.objects.filter(parent=None).order_by("name").prefetch_related("children")
paginate_by = 1 )
context_object_name = "club_list"
def get_form_kwargs(self):
res = super().get_form_kwargs()
if self.request.GET:
res |= {"data": self.request.GET, "initial": self.request.GET}
return res
def get_queryset(self):
form: ClubSearchForm = self.get_form()
qs = self.queryset
if not form.is_bound:
return qs.filter(is_active=True)
if not form.is_valid():
return qs.none()
if name := form.cleaned_data.get("name"):
qs = qs.filter(name__icontains=name)
if (is_active := form.cleaned_data.get("club_status")) is not None:
qs = qs.filter(is_active=is_active)
return qs
class ClubView(ClubTabsMixin, DetailView): class ClubView(ClubTabsMixin, DetailView):

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

@@ -129,10 +129,10 @@
current_page (django.core.paginator.Page): the current page object current_page (django.core.paginator.Page): the current page object
paginator (django.core.paginator.Paginator): the paginator object paginator (django.core.paginator.Paginator): the paginator object
#} #}
{{ paginate_server_side(request, current_page, paginator, "") }} {{ paginate_server_side(request, current_page, paginator, False) }}
{% endmacro %} {% endmacro %}
{% macro paginate_htmx(request, current_page, paginator, htmx_target="#content") %} {% macro paginate_htmx(request, current_page, paginator) %}
{# Add pagination buttons for pages without Alpine but supporting fragments. {# Add pagination buttons for pages without Alpine but supporting fragments.
This must be coupled with a view that handles pagination This must be coupled with a view that handles pagination
@@ -144,19 +144,18 @@
request (django.http.request.HttpRequest): the current django request request (django.http.request.HttpRequest): the current django request
current_page (django.core.paginator.Page): the current page object current_page (django.core.paginator.Page): the current page object
paginator (django.core.paginator.Paginator): the paginator object paginator (django.core.paginator.Paginator): the paginator object
htmx_target (string): htmx target selector (default '#content')
#} #}
{{ paginate_server_side(request, current_page, paginator, htmx_target) }} {{ paginate_server_side(request, current_page, paginator, True) }}
{% endmacro %} {% endmacro %}
{% macro paginate_server_side(request, current_page, paginator, htmx_target) %} {% macro paginate_server_side(request, current_page, paginator, use_htmx) %}
<nav class="pagination"> <nav class="pagination">
{% if current_page.has_previous() %} {% if current_page.has_previous() %}
<a <a
{% if htmx_target -%} {% if use_htmx -%}
hx-get="?{{ querystring(request, page=current_page.previous_page_number()) }}" hx-get="?{{ querystring(request, page=current_page.previous_page_number()) }}"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-target="{{ htmx_target }}" hx-target="#content"
hx-push-url="true" hx-push-url="true"
hx-trigger="click, keyup[key=='ArrowLeft'] from:body" hx-trigger="click, keyup[key=='ArrowLeft'] from:body"
{%- else -%} {%- else -%}
@@ -177,10 +176,10 @@
<strong>{{ paginator.ELLIPSIS }}</strong> <strong>{{ paginator.ELLIPSIS }}</strong>
{% else %} {% else %}
<a <a
{% if htmx_target -%} {% if use_htmx -%}
hx-get="?{{ querystring(request, page=i) }}" hx-get="?{{ querystring(request, page=i) }}"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-target="{{ htmx_target }}" hx-target="#content"
hx-push-url="true" hx-push-url="true"
{%- else -%} {%- else -%}
href="?{{ querystring(request, page=i) }}" href="?{{ querystring(request, page=i) }}"
@@ -192,10 +191,10 @@
{% endfor %} {% endfor %}
{% if current_page.has_next() %} {% if current_page.has_next() %}
<a <a
{% if htmx_target -%} {% if use_htmx -%}
hx-get="?{{querystring(request, page=current_page.next_page_number())}}" hx-get="?{{querystring(request, page=current_page.next_page_number())}}"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-target="{{ htmx_target }}" hx-target="#content"
hx-push-url="true" hx-push-url="true"
hx-trigger="click, keyup[key=='ArrowRight'] from:body" hx-trigger="click, keyup[key=='ArrowRight'] from:body"
{%- else -%} {%- else -%}

View File

@@ -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"
@@ -1901,6 +1881,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"

6
package-lock.json generated
View File

@@ -4312,9 +4312,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {