mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-22 03:25:05 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4027f5d1d6 | ||
|
|
efea12cf1b | ||
|
|
41eb7cde3e |
17
club/api.py
17
club/api.py
@@ -6,9 +6,10 @@ from ninja_extra.pagination import PageNumberPaginationExtra
|
||||
from ninja_extra.schemas import PaginatedResponseSchema
|
||||
|
||||
from api.auth import ApiKeyAuth
|
||||
from api.permissions import CanAccessLookup, CanView, HasPerm
|
||||
from api.permissions import CanView, HasPerm
|
||||
from club.models import Club, Membership
|
||||
from club.schemas import (
|
||||
ClubProfileSchema,
|
||||
ClubSchema,
|
||||
ClubSearchFilterSchema,
|
||||
SimpleClubSchema,
|
||||
@@ -22,13 +23,21 @@ class ClubController(ControllerBase):
|
||||
@route.get(
|
||||
"/search",
|
||||
response=PaginatedResponseSchema[SimpleClubSchema],
|
||||
auth=[ApiKeyAuth(), SessionAuth()],
|
||||
permissions=[CanAccessLookup],
|
||||
url_name="search_club",
|
||||
)
|
||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||
def search_club(self, filters: Query[ClubSearchFilterSchema]):
|
||||
return filters.filter(Club.objects.all())
|
||||
return filters.filter(Club.objects.order_by("name")).values()
|
||||
|
||||
@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(
|
||||
"/{int:club_id}",
|
||||
|
||||
@@ -30,7 +30,7 @@ class ClubProfileSchema(ModelSchema):
|
||||
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = ["id", "name", "logo"]
|
||||
fields = ["id", "name", "logo", "is_active", "short_description"]
|
||||
|
||||
url: str
|
||||
|
||||
|
||||
63
club/static/bundled/club/club-list-index.ts
Normal file
63
club/static/bundled/club/club-list-index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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 !== "");
|
||||
},
|
||||
}));
|
||||
});
|
||||
47
club/static/club/list.scss
Normal file
47
club/static/club/list.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
#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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
{% from "core/macros.jinja" import paginate_alpine %}
|
||||
|
||||
{% block title -%}
|
||||
{% trans %}Club list{% endtrans %}
|
||||
@@ -8,45 +9,76 @@
|
||||
{% trans %}The list of all clubs existing at UTBM.{% endtrans %}
|
||||
{%- endblock %}
|
||||
|
||||
{% macro display_club(club) -%}
|
||||
{% block additional_js %}
|
||||
<script type="module" src="{{ static("bundled/club/club-list-index.ts") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% 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 additional_css %}
|
||||
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if user.is_root %}
|
||||
<p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p>
|
||||
{% endif %}
|
||||
{% if club_list %}
|
||||
<main x-data="clubList">
|
||||
<h3>{% trans %}Filters{% endtrans %}</h3>
|
||||
<form id="club-list-filters">
|
||||
<div class="row gap-4x">
|
||||
<fieldset>
|
||||
<label for="club-name-input">{% trans %}Name{% endtrans %}</label>
|
||||
<input
|
||||
id="club-name-input"
|
||||
type="text"
|
||||
name="club-name"
|
||||
x-model.debounce.500ms="clubName"
|
||||
/>
|
||||
</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>
|
||||
<ul>
|
||||
{%- for club in club_list %}
|
||||
{{ display_club(club) }}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% trans %}There is no club in this website.{% endtrans %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import localdate
|
||||
from model_bakery import baker
|
||||
from model_bakery.recipe import Recipe
|
||||
|
||||
from club.models import Club, Membership
|
||||
from core.baker_recipes import subscriber_user
|
||||
from core.models import User
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -25,3 +28,10 @@ def test_club_queryset_having_board_member():
|
||||
|
||||
club_ids = Club.objects.having_board_member(user).values_list("id", flat=True)
|
||||
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
|
||||
|
||||
@@ -59,6 +59,14 @@ class TestClubSearch(TestCase):
|
||||
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]]}
|
||||
|
||||
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
|
||||
class TestFetchClub:
|
||||
|
||||
@@ -42,7 +42,7 @@ 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, ListView, View
|
||||
from django.views.generic import DetailView, TemplateView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
|
||||
@@ -180,15 +180,10 @@ class ClubTabsMixin(TabedViewMixin):
|
||||
return tab_list
|
||||
|
||||
|
||||
class ClubListView(ListView):
|
||||
class ClubListView(TemplateView):
|
||||
"""List the Clubs."""
|
||||
|
||||
model = Club
|
||||
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):
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
<details name="navbar" class="menu">
|
||||
<summary class="head">{% trans %}Associations & Clubs{% endtrans %}</summary>
|
||||
<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='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>
|
||||
<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>
|
||||
</ul>
|
||||
</details>
|
||||
<details name="navbar" class="menu">
|
||||
|
||||
@@ -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-20 16:24+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"
|
||||
@@ -310,16 +310,36 @@ msgid "The list of all clubs existing at UTBM."
|
||||
msgstr "La liste de tous les clubs existants à l'UTBM"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "inactive"
|
||||
msgstr "inactif"
|
||||
msgid "Filters"
|
||||
msgstr "Filtres"
|
||||
|
||||
#: 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
|
||||
msgid "New club"
|
||||
msgstr "Nouveau club"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "There is no club in this website."
|
||||
msgstr "Il n'y a pas de club dans ce site web."
|
||||
msgid "inactive"
|
||||
msgstr "inactif"
|
||||
|
||||
#: club/templates/club/club_members.jinja
|
||||
msgid "Club members"
|
||||
@@ -1860,10 +1880,6 @@ msgstr "L'AE"
|
||||
msgid "AE's clubs"
|
||||
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
|
||||
msgid "Big event"
|
||||
msgstr "Grandes Activités"
|
||||
|
||||
Reference in New Issue
Block a user