diff --git a/pedagogy/api.py b/pedagogy/api.py index 24f5919b..91acb817 100644 --- a/pedagogy/api.py +++ b/pedagogy/api.py @@ -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 diff --git a/pedagogy/schemas.py b/pedagogy/schemas.py index c25df71f..592c134a 100644 --- a/pedagogy/schemas.py +++ b/pedagogy/schemas.py @@ -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") diff --git a/pedagogy/static/bundled/pedagogy/guide-index.ts b/pedagogy/static/bundled/pedagogy/guide-index.ts index 9a3165a4..22987c4b 100644 --- a/pedagogy/static/bundled/pedagogy/guide-index.ts +++ b/pedagogy/static/bundled/pedagogy/guide-index.ts @@ -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, }, diff --git a/pedagogy/templates/pedagogy/guide.jinja b/pedagogy/templates/pedagogy/guide.jinja index 66e0f3b4..8cbfe58d 100644 --- a/pedagogy/templates/pedagogy/guide.jinja +++ b/pedagogy/templates/pedagogy/guide.jinja @@ -44,6 +44,10 @@ x-model.debounce.500ms="search" /> +
+ + +
{% set departments = [ diff --git a/pedagogy/tests/test_api.py b/pedagogy/tests/test_api.py index 2726252d..2b5cda44 100644 --- a/pedagogy/tests/test_api.py +++ b/pedagogy/tests/test_api.py @@ -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) + )