Merge pull request #1469 from ae-utbm/pedagogy-style

Add a switch to filter open UEs
This commit is contained in:
thomas girod
2026-09-13 22:53:33 +02:00
committed by GitHub
5 changed files with 68 additions and 52 deletions
+2 -5
View File
@@ -29,14 +29,11 @@ class UeController(ControllerBase):
response=UeSchema,
)
def fetch_from_utbm_api(
self,
code: str,
lang: Query[str] = "fr",
year: Query[Annotated[int, Ge(2010)] | None] = None,
self, code: str, year: Query[Annotated[int, Ge(2010)] | None] = None
):
"""Fetch UE data from the UTBM API and returns it after some parsing."""
with UtbmApiClient() as client:
res = client.find_ue(lang, code, year)
res = client.find_ue(code, year)
if res is None:
raise NotFound
return res
+9
View File
@@ -153,6 +153,7 @@ class UeFilterSchema(FilterSchema):
set[Literal["CS", "TM", "EC", "OM", "QC"]] | None,
FilterLookup("credit_type__in"),
] = None
is_open: bool | None = None
language: str = "FR"
department: Annotated[set[str] | None, FilterLookup("department__in")] = None
@@ -187,3 +188,11 @@ class UeFilterSchema(FilterSchema):
return Q()
value.add("AUTUMN_AND_SPRING")
return Q(semester__in=value)
def filter_is_open(self, value: bool | None) -> Q: # noqa: FBT001
"""Filter by open/closed status."""
if value is None:
return Q()
if not value:
return Q(semester="CLOSED")
return ~Q(semester="CLOSED")
@@ -20,6 +20,7 @@ document.addEventListener("alpine:init", () => {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: pageSizeDefault,
search: "",
hideClosedUes: true,
department: [] as string[],
// biome-ignore lint/style/useNamingConvention: api is in snake_case
credit_type: [] as CreditType[],
@@ -41,6 +42,7 @@ document.addEventListener("alpine:init", () => {
10,
);
this.search = url.get("search") || "";
this.hideClosedUes = [null, "true"].includes(url.get("hideClosed"));
this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type") as CreditType[];
/* The semester is easier to use on the backend as an enum (spring/autumn/both/none)
@@ -61,7 +63,13 @@ document.addEventListener("alpine:init", () => {
this.to_change = [];
}, 50);
const searchParams = ["search", "department", "credit_type", "semester"];
const searchParams = [
"search",
"hideClosedUes",
"department",
"credit_type",
"semester",
];
const paginationParams = ["page", "page_size"];
for (const param of searchParams) {
this.$watch(param, () => {
@@ -93,6 +101,8 @@ document.addEventListener("alpine:init", () => {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
credit_type: this.credit_type.length > 0 ? this.credit_type : undefined,
semester: this.semester.length > 0 ? this.semester : undefined,
// biome-ignore lint/style/useNamingConvention: api is snake_case
is_open: this.hideClosedUes ? true : undefined,
department: this.department.length > 0 ? this.department : undefined,
search: this.search || undefined,
},
+4
View File
@@ -44,6 +44,10 @@
x-model.debounce.500ms="search"
/>
</fieldset>
<fieldset>
<input type="checkbox" class="switch" x-model="hideClosedUes" id="hide-closed-ues" name="hide-closed-ues">
<label for="hide-closed-ues">{% trans %}Hide closed UEs{% endtrans %}</label>
</fieldset>
<div class="row gap-3x margin-bottom radio-guide">
<fieldset>
{% set departments = [
+42 -46
View File
@@ -1,8 +1,6 @@
import json
from django.conf import settings
from django.core.management import call_command
from django.test import TestCase
from django.test.testcases import call_command
from django.urls import reverse
from model_bakery import baker
from model_bakery.recipe import Recipe
@@ -17,7 +15,7 @@ class TestUESearch(TestCase):
@classmethod
def setUpTestData(cls):
cls.root = User.objects.get(username="root")
cls.root = baker.make(User, is_superuser=True)
cls.url = reverse("api:fetch_ues")
ue_recipe = Recipe(UE, author=cls.root)
ues = [
@@ -92,10 +90,10 @@ class TestUESearch(TestCase):
def test_format(self):
"""Test that the return data format is correct"""
self.client.force_login(self.root)
res = self.client.get(self.url + "?search=PA00")
res = self.client.get(self.url, query_params={"search": "PA00"})
ue = UE.objects.get(code="PA00")
assert res.status_code == 200
assert json.loads(res.content) == {
assert res.json() == {
"count": 1,
"next": None,
"previous": None,
@@ -121,79 +119,77 @@ class TestUESearch(TestCase):
def test_search_by_text(self):
self.client.force_login(self.root)
for query, expected in (
# UE code search case insensitive
# UE code search case-insensitive
("m", {"MT01", "MT10"}),
("M", {"MT01", "MT10"}),
("mt", {"MT01", "MT10"}),
("MT", {"MT01", "MT10"}),
("algèbre", {"MT01"}), # Title search case insensitive
("algèbre", {"MT01"}), # Title search case-insensitive
# Manager search
("moss", {"TNEV"}),
("francky", {"DA50", "AP4A"}),
):
res = self.client.get(self.url + f"?search={query}")
res = self.client.get(self.url, query_params={"search": query})
assert res.status_code == 200
assert {ue["code"] for ue in json.loads(res.content)["results"]} == expected
assert {ue["code"] for ue in res.json()["results"]} == expected
def test_search_by_credit_type(self):
self.client.force_login(self.root)
res = self.client.get(self.url + "?credit_type=CS")
res = self.client.get(self.url, query_params={"credit_type": "CS"})
assert res.status_code == 200
codes = [ue["code"] for ue in json.loads(res.content)["results"]]
codes = [ue["code"] for ue in res.json()["results"]]
assert codes == ["AP4A", "MT01", "PHYS11"]
res = self.client.get(self.url + "?credit_type=CS&credit_type=OM")
res = self.client.get(self.url, query_params={"credit_type": ["CS", "OM"]})
assert res.status_code == 200
codes = {ue["code"] for ue in json.loads(res.content)["results"]}
codes = {ue["code"] for ue in res.json()["results"]}
assert codes == {"AP4A", "MT01", "PHYS11", "PA00"}
def test_search_by_semester(self):
self.client.force_login(self.root)
res = self.client.get(self.url + "?semester=SPRING")
res = self.client.get(self.url, query_params={"semester": "SPRING"})
assert res.status_code == 200
codes = {ue["code"] for ue in json.loads(res.content)["results"]}
codes = {ue["code"] for ue in res.json()["results"]}
assert codes == {"DA50", "TNEV", "PA00"}
def test_search_multiple_filters(self):
self.client.force_login(self.root)
res = self.client.get(
self.url + "?semester=AUTUMN&credit_type=CS&department=TC"
self.url,
query_params={
"semester": "AUTUMN",
"credit_type": "CS",
"department": "TC",
},
)
assert res.status_code == 200
codes = {ue["code"] for ue in json.loads(res.content)["results"]}
codes = {ue["code"] for ue in res.json()["results"]}
assert codes == {"MT01", "PHYS11"}
def test_search_fails(self):
self.client.force_login(self.root)
res = self.client.get(self.url + "?credit_type=CS&search=DA")
res = self.client.get(
self.url, query_params={"search": "DA", "credit_type": "CS"}
)
assert res.status_code == 200
assert json.loads(res.content)["results"] == []
assert res.json()["results"] == []
def test_search_closed_status(self):
# Simple heuristic : take a UE, and mark everything before it as closed
closed_ue = UE.objects.order_by("id")[2]
UE.objects.filter(id__lte=closed_ue.id).update(semester="CLOSED")
def test_search_pa00_fail(self):
self.client.force_login(self.root)
# Search with UE code
response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"})
self.assertNotContains(response, text="PA00")
# Search with first letter of UE code
response = self.client.get(reverse("pedagogy:guide"), {"search": "I"})
self.assertNotContains(response, text="PA00")
res = self.client.get(self.url, query_params={"is_open": False})
assert res.status_code == 200
ids = {ue["id"] for ue in res.json()["results"]}
assert ids == set(
UE.objects.filter(semester="CLOSED").values_list("id", flat=True)
)
# Search with UE manager
response = self.client.get(reverse("pedagogy:guide"), {"search": "GILLES"})
self.assertNotContains(response, text="PA00")
# Search with department
response = self.client.get(reverse("pedagogy:guide"), {"department": "TC"})
self.assertNotContains(response, text="PA00")
# Search with semester
response = self.client.get(reverse("pedagogy:guide"), {"semester": "CLOSED"})
self.assertNotContains(response, text="PA00")
# Search with language
response = self.client.get(reverse("pedagogy:guide"), {"language": "EN"})
self.assertNotContains(response, text="PA00")
# Search with credit type
response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "TM"})
self.assertNotContains(response, text="PA00")
res = self.client.get(self.url, query_params={"is_open": True})
assert res.status_code == 200
ids = {ue["id"] for ue in res.json()["results"]}
assert ids == set(
UE.objects.exclude(semester="CLOSED").values_list("id", flat=True)
)