diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de547dc2..0afbc963 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.13 + rev: v0.15.19 hooks: - id: ruff-check # just check the code, and print the errors - id: ruff-check # actually fix the fixable errors, but print nothing @@ -12,7 +12,7 @@ repos: rev: v0.6.1 hooks: - id: biome-check - additional_dependencies: ["@biomejs/biome@2.4.6"] + additional_dependencies: ["@biomejs/biome@2.5.1"] - repo: https://github.com/rtts/djhtml rev: 3.0.11 hooks: diff --git a/biome.json b/biome.json index bd41ee38..520f3241 100644 --- a/biome.json +++ b/biome.json @@ -17,7 +17,7 @@ "linter": { "enabled": true, "rules": { - "recommended": true, + "preset": "recommended", "style": { "useNamingConvention": "error" }, diff --git a/club/api.py b/club/api.py index 4a055e2c..cf1070d0 100644 --- a/club/api.py +++ b/club/api.py @@ -11,6 +11,7 @@ from club.models import Club, Membership from club.schemas import ( ClubSchema, ClubSearchFilterSchema, + MembershipFilterSchema, SimpleClubSchema, UserMembershipSchema, ) @@ -62,3 +63,43 @@ class UserClubController(ControllerBase): .filter(user=user) .select_related("club", "user", "role") ) + + +@api_controller("/clubs/members/") +class ClubMembershipController(ControllerBase): + @route.get( + "/new", + response=list[UserMembershipSchema], + auth=[ApiKeyAuth(), SessionAuth()], + permissions=[HasPerm("club.view_club")], + url_name="get_new_clubs_members_since_date", + ) + def fetch_new_club_members(self, filters: Query[MembershipFilterSchema]): + """give all the members of all clubs that have joined since a given date""" + memberships = ( + Membership.objects.ongoing() + .filter(start_date__gte=filters.since_date, end_date__isnull=True) + .select_related("user", "role", "club") + ) + if filters.clubs_id: + memberships = memberships.filter(club_id__in=filters.clubs_id) + + return memberships.order_by("start_date") + + @route.get( + "/former", + response=list[UserMembershipSchema], + auth=[ApiKeyAuth(), SessionAuth()], + permissions=[HasPerm("club.view_club")], + url_name="get_former_clubs_members_since_date", + ) + def fetch_former_club_members(self, filters: Query[MembershipFilterSchema]): + """give all the former members of all clubs that have left since a given date""" + memberships = Membership.objects.filter( + start_date__lt=filters.since_date, + end_date__gte=filters.since_date, + ).select_related("user", "role", "club") + if filters.clubs_id: + memberships = memberships.filter(club_id__in=filters.clubs_id) + + return memberships.order_by("start_date") diff --git a/club/schemas.py b/club/schemas.py index 99d05fc1..10ea2411 100644 --- a/club/schemas.py +++ b/club/schemas.py @@ -1,3 +1,4 @@ +from datetime import date from typing import Annotated from django.db.models import Q @@ -79,3 +80,9 @@ class UserMembershipSchema(ModelSchema): club: SimpleClubSchema role: ClubRoleSchema + user: SimpleUserSchema + + +class MembershipFilterSchema(FilterSchema): + since_date: Annotated[date, FilterLookup("date__lte")] + clubs_id: set[int] | None = None diff --git a/club/static/club/list.scss b/club/static/club/list.scss index 9fbf952f..030c7a56 100644 --- a/club/static/club/list.scss +++ b/club/static/club/list.scss @@ -45,3 +45,10 @@ } } } + +@media screen and (max-width: 575px){ + #club-list{ + padding-left: 0; + padding-right: 0; + } +} diff --git a/club/tests/test_club_membership_controller.py b/club/tests/test_club_membership_controller.py new file mode 100644 index 00000000..27aee901 --- /dev/null +++ b/club/tests/test_club_membership_controller.py @@ -0,0 +1,203 @@ +from datetime import timedelta + +from django.contrib.auth.models import Permission +from django.test import TestCase +from django.urls import reverse +from django.utils.timezone import localdate +from model_bakery import baker + +from club.models import Club, ClubRole, Membership +from core.baker_recipes import subscriber_user +from core.models import User + + +class TestMembershipAPI(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = baker.make(User) + perm = Permission.objects.get(codename="view_club") + cls.user.user_permissions.add(perm) + cls.clubs = baker.make(Club, _quantity=3, is_active=True) + cls.roles = baker.make(ClubRole, _quantity=3, is_active=True) + cls.expectedNumQueries = 5 + + # Clean existing data to avoid side effects + Membership.objects.all().delete() + + cls.memberships = [ + # on going + Membership.objects.create( + club=cls.clubs[0], + user=subscriber_user.make(), + role=cls.roles[0], + start_date=localdate() - timedelta(weeks=1), + ), + # on going + Membership.objects.create( + club=cls.clubs[1], + user=subscriber_user.make(), + role=cls.roles[1], + start_date=localdate() - timedelta(days=1), + ), + # former + Membership.objects.create( + club=cls.clubs[1], + user=subscriber_user.make(), + role=cls.roles[2], + start_date=localdate() - timedelta(weeks=2), + end_date=localdate() - timedelta(days=6), + ), + # on going + Membership.objects.create( + club=cls.clubs[2], + user=subscriber_user.make(), + role=cls.roles[0], + start_date=localdate() - timedelta(weeks=3), + ), + # former + Membership.objects.create( + club=cls.clubs[1], + user=subscriber_user.make(), + role=cls.roles[2], + start_date=localdate() - timedelta(days=4), + end_date=localdate() - timedelta(days=3), + ), + # on going + Membership.objects.create( + club=cls.clubs[2], + user=subscriber_user.make(), + role=cls.roles[0], + start_date=localdate() - timedelta(days=1), + ), + # former + Membership.objects.create( + club=cls.clubs[0], + user=subscriber_user.make(), + role=cls.roles[0], + start_date=localdate() - timedelta(weeks=6), + end_date=localdate() - timedelta(days=3), + ), + # former + Membership.objects.create( + club=cls.clubs[2], + user=subscriber_user.make(), + role=cls.roles[0], + start_date=localdate() - timedelta(weeks=8), + end_date=localdate() - timedelta(days=6), + ), + # former + Membership.objects.create( + club=cls.clubs[1], + user=subscriber_user.make(), + role=cls.roles[0], + start_date=localdate() - timedelta(weeks=8), + end_date=localdate() - timedelta(weeks=7, days=5), + ), + ] + + +class TestNewMembershipAPI(TestMembershipAPI): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.url = reverse("api:get_new_clubs_members_since_date") + + def test_new_membership_one_club(self): + self.client.force_login(self.user) + since_date = localdate() - timedelta(weeks=1) + arg = {"since_date": since_date, "clubs_id": self.clubs[0].id} + with self.assertNumQueries(self.expectedNumQueries): + response = self.client.get(self.url, query_params=arg) + assert response.status_code == 200 + data = response.json() + + membership_ids = [e["id"] for e in data] + expected_ids = [self.memberships[0].id] + assert membership_ids == expected_ids + + def test_new_membership_multiple_club(self): + self.client.force_login(self.user) + since_date = localdate() - timedelta(weeks=1) + arg = { + "since_date": since_date, + "clubs_id": [self.clubs[0].id, self.clubs[1].id], + } + with self.assertNumQueries(self.expectedNumQueries): + response = self.client.get(self.url, query_params=arg) + assert response.status_code == 200 + data = response.json() + + membership_ids = [e["id"] for e in data] + expected_ids = [self.memberships[0].id, self.memberships[1].id] + assert membership_ids == expected_ids + + def test_new_membership_all_clubs(self): + self.client.force_login(self.user) + since_date = localdate() - timedelta(weeks=1) + arg = {"since_date": since_date} + with self.assertNumQueries(self.expectedNumQueries): + response = self.client.get(self.url, query_params=arg) + assert response.status_code == 200 + data = response.json() + + membership_ids = [e["id"] for e in data] + expected_ids = [ + self.memberships[0].id, + self.memberships[1].id, + self.memberships[5].id, + ] + assert membership_ids == expected_ids + + +class TestFormerMembershipAPI(TestMembershipAPI): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.url = reverse("api:get_former_clubs_members_since_date") + + def test_former_membership_one_club(self): + self.client.force_login(self.user) + since_date = localdate() - timedelta(weeks=1) + arg = {"since_date": since_date, "clubs_id": self.clubs[1].id} + with self.assertNumQueries(self.expectedNumQueries): + response = self.client.get(self.url, query_params=arg) + + assert response.status_code == 200 + data = response.json() + + membership_ids = [e["id"] for e in data] + expected_ids = [self.memberships[2].id] + assert membership_ids == expected_ids + + def test_new_membership_multiple_club(self): + self.client.force_login(self.user) + since_date = localdate() - timedelta(weeks=1) + arg = { + "since_date": since_date, + "clubs_id": [self.clubs[1].id, self.clubs[0].id], + } + with self.assertNumQueries(self.expectedNumQueries): + response = self.client.get(self.url, query_params=arg) + assert response.status_code == 200 + data = response.json() + + membership_ids = [e["id"] for e in data] + expected_ids = [self.memberships[6].id, self.memberships[2].id] + assert membership_ids == expected_ids + + def test_new_membership_all_clubs(self): + self.client.force_login(self.user) + since_date = localdate() - timedelta(weeks=1) + arg = {"since_date": since_date} + with self.assertNumQueries(self.expectedNumQueries): + response = self.client.get(self.url, query_params=arg) + assert response.status_code == 200 + data = response.json() + + membership_ids = [e["id"] for e in data] + expected_ids = [ + self.memberships[7].id, + self.memberships[6].id, + self.memberships[2].id, + ] + assert membership_ids == expected_ids diff --git a/club/tests/test_page.py b/club/tests/test_page.py index 6567a690..aeefe068 100644 --- a/club/tests/test_page.py +++ b/club/tests/test_page.py @@ -1,4 +1,5 @@ import pytest +from aemark import markdown from bs4 import BeautifulSoup from django.test import Client from django.urls import reverse @@ -7,7 +8,6 @@ from pytest_django.asserts import assertHTMLEqual, assertRedirects from club.models import Club, ClubRole, Membership from core.baker_recipes import subscriber_user -from core.markdown import markdown from core.models import PageRev, User diff --git a/com/schemas.py b/com/schemas.py index efc01f01..9af51e18 100644 --- a/com/schemas.py +++ b/com/schemas.py @@ -1,13 +1,13 @@ from datetime import datetime from typing import Annotated +from aemark import markdown from ninja import FilterLookup, FilterSchema, ModelSchema from ninja_extra import service_resolver from ninja_extra.context import RouteContext from club.schemas import ClubProfileSchema from com.models import News, NewsDate -from core.markdown import markdown class NewsDateFilterSchema(FilterSchema): diff --git a/com/tests/test_api.py b/com/tests/test_api.py index ce747347..d8c98acf 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -2,6 +2,7 @@ from datetime import timedelta from pathlib import Path import pytest +from aemark import markdown from django.conf import settings from django.contrib.auth.models import Permission from django.http import HttpResponse @@ -13,7 +14,6 @@ from pytest_django.asserts import assertNumQueries from com.ics_calendar import IcsCalendar from com.models import News, NewsDate -from core.markdown import markdown from core.models import User diff --git a/core/fixtures/SYNTAX.html b/core/fixtures/SYNTAX.html index 7b41e07c..f5b9f6cc 100644 --- a/core/fixtures/SYNTAX.html +++ b/core/fixtures/SYNTAX.html @@ -2,12 +2,9 @@

Markdown-AE Documentation

Le Markdown le plus standard se trouve documenté ici: https://www.markdownguide.org/basic-syntax.
-Si cette page n'est pas exhaustive vis à vis de la syntaxe du site AE, +Si cette page n’est pas exhaustive vis à vis de la syntaxe du site AE, elle a au moins le mérite de bien documenter le Markdown original.

-

Le réel parseur du site AE est une version tunée de mistune.
-Les plus aventureux pourront aller lire ses tests -afin d'en connaître la syntaxe le plus finement possible.
-En pratique, cette page devrait déjà résumer une bonne partie.

+

Le réel parseur du site AE est une version tunée de comrak.

Basique

Liens

nom du lien

-

nom du lien

+

nom du lien