diff --git a/club/forms.py b/club/forms.py index dcd270e7..56dfd726 100644 --- a/club/forms.py +++ b/club/forms.py @@ -315,3 +315,22 @@ class JoinClubForm(ClubMemberForm): _("You are already a member of this club"), code="invalid" ) 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 diff --git a/club/static/bundled/club/club-list-index.ts b/club/static/bundled/club/club-list-index.ts deleted file mode 100644 index 84adedda..00000000 --- a/club/static/bundled/club/club-list-index.ts +++ /dev/null @@ -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 = { - 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 !== ""); - }, - })); -}); diff --git a/club/templates/club/club_list.jinja b/club/templates/club/club_list.jinja index 15b9a54d..3cc7205a 100644 --- a/club/templates/club/club_list.jinja +++ b/club/templates/club/club_list.jinja @@ -1,5 +1,5 @@ {% extends "core/base.jinja" %} -{% from "core/macros.jinja" import paginate_alpine %} +{% from "core/macros.jinja" import paginate_jinja %} {% block title -%} {% trans %}Club list{% endtrans %} @@ -9,44 +9,18 @@ {% trans %}The list of all clubs existing at UTBM.{% endtrans %} {%- endblock %} -{% block additional_js %} - -{% endblock %} - {% block additional_css %} {% endblock %} {% block content %} -
+

{% trans %}Filters{% endtrans %}

-
+
-
- - -
-
- {% trans %}Club state{% endtrans %} -
- - -
-
- - -
-
- - -
-
+ {{ form }}
+

{% trans %}Club list{% endtrans %}

{% if user.has_perm("club.add_club") %} @@ -55,29 +29,29 @@ {% trans %}New club{% endtrans %} {% endif %} -
- + {% endfor %}
- {{ paginate_alpine("currentPage", "nbPages") }} + {{ paginate_jinja(page_obj, paginator) }}
{% endblock %} diff --git a/club/views.py b/club/views.py index 2a3c8f87..7704c1dd 100644 --- a/club/views.py +++ b/club/views.py @@ -42,15 +42,21 @@ from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext 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.edit import CreateView, DeleteView, UpdateView +from django.views.generic.edit import ( + CreateView, + DeleteView, + FormMixin, + UpdateView, +) from club.forms import ( ClubAddMemberForm, ClubAdminEditForm, ClubEditForm, ClubOldMemberForm, + ClubSearchForm, JoinClubForm, MailingForm, SellingsForm, @@ -180,10 +186,41 @@ class ClubTabsMixin(TabedViewMixin): return tab_list -class ClubListView(TemplateView): - """List the Clubs.""" +class ClubListView(FormMixin, ListView): + """List the clubs of the AE, with a form to perform basic search. + + 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. + """ template_name = "club/club_list.jinja" + form_class = ClubSearchForm + queryset = Club.objects.order_by("name") + paginate_by = 20 + + 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 + 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):