Pagination on UV guide

This commit is contained in:
Antoine Bartuccio 2024-07-22 18:40:32 +02:00 committed by thomas girod
parent 3046438cb1
commit 293369f165
4 changed files with 79 additions and 34 deletions

View File

@ -24,11 +24,6 @@ $black-color: hsl(0, 0%, 17%);
$faceblue: hsl(221, 44%, 41%); $faceblue: hsl(221, 44%, 41%);
$twitblue: hsl(206, 82%, 63%); $twitblue: hsl(206, 82%, 63%);
$pinktober: #ff5674;
$pinktober-secondary: #8a2536;
$pinktober-primary-text: white;
$pinktober-bar-closed: $pinktober-secondary;
$pinktober-bar-opened: #388e3c;
$shadow-color: rgb(223, 223, 223); $shadow-color: rgb(223, 223, 223);
@ -48,6 +43,18 @@ body {
font-family: sans-serif; font-family: sans-serif;
} }
button:disabled,
button:disabled:hover {
color: #fff;
background-color: #6c757d;
}
button.active,
button.active:hover {
color: #fff;
background-color: $secondary-color;
}
a.button, a.button,
button, button,
input[type="button"], input[type="button"],
@ -1510,6 +1517,10 @@ $pedagogy-light-blue: #caf0ff;
$pedagogy-white-text: #f0f0f0; $pedagogy-white-text: #f0f0f0;
.pedagogy { .pedagogy {
#pagination {
text-align: center;
}
&.star-not-checked { &.star-not-checked {
color: #f7f7f7; color: #f7f7f7;
margin-bottom: 0; margin-bottom: 0;

View File

@ -3,8 +3,9 @@ from typing import Annotated
from annotated_types import Ge from annotated_types import Ge
from django.conf import settings from django.conf import settings
from ninja import Query from ninja import Query
from ninja_extra import ControllerBase, api_controller, route from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.exceptions import NotFound from ninja_extra.exceptions import NotFound
from ninja_extra.pagination import PageNumberPaginationExtra, PaginatedResponseSchema
from core.api_permissions import IsInGroup, IsRoot, IsSubscriber from core.api_permissions import IsInGroup, IsRoot, IsSubscriber
from pedagogy.models import UV from pedagogy.models import UV
@ -29,8 +30,9 @@ class UvController(ControllerBase):
raise NotFound raise NotFound
return res return res
@route.get("", response=list[SimpleUvSchema], url_name="fetch_uvs") @route.get(
"", response=PaginatedResponseSchema[SimpleUvSchema], url_name="fetch_uvs"
)
@paginate(PageNumberPaginationExtra, page_size=100)
def fetch_uv_list(self, search: Query[UvFilterSchema]): def fetch_uv_list(self, search: Query[UvFilterSchema]):
# le `[:50]`, c'est de la pagination eco+ return search.filter(UV.objects.all())
# si quelqu'un est motivé, il peut faire une vraie pagination
return search.filter(UV.objects.all())[:50]

View File

@ -73,9 +73,9 @@
<div class="radio-semester"> <div class="radio-semester">
<div class="radio-guide"> <div class="radio-guide">
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/> <input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/>
<label for="radioAUTUMN"><i class="fa fa-sun-o"></i></label> <label for="radioAUTUMN"><i class="fa fa-leaf"></i></label>
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/> <input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/>
<label for="radioSPRING"><i class="fa fa-leaf"></i></label> <label for="radioSPRING"><i class="fa fa-sun-o"></i></label>
</div> </div>
</div> </div>
</div> </div>
@ -96,22 +96,29 @@
</tr> </tr>
</thead> </thead>
<tbody id="dynamic_view_content"> <tbody id="dynamic_view_content">
<template x-for="uv in uvs" :key="uv.id"> <template x-for="uv in uvs.results" :key="uv.id">
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`"> <tr @click="window.location.href = `/pedagogy/uv/${uv.id}`">
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td> <td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td>
<td x-text="uv.title"></td> <td x-text="uv.title"></td>
<td x-text="uv.department"></td> <td x-text="uv.department"></td>
<td x-text="uv.credit_type"></td> <td x-text="uv.credit_type"></td>
<td><i :class="[uv.semester].includes('AUTUMN') && 'fa fa-leaf'"></i></td> <td><i :class="uv.semester.includes('AUTUMN') && 'fa fa-leaf'"></i></td>
<td><i :class="[uv.semester].includes('SPRING') && 'fa fa-sun-o'"></i></td> <td><i :class="uv.semester.includes('SPRING') && 'fa fa-sun-o'"></i></td>
{% if can_create_uv -%} {% if can_create_uv -%}
<td><a :href="`/pedagogy/uv/${uv.id}/update`">{% trans %}Edit{% endtrans %}</a></td> <td><a :href="`/pedagogy/uv/${uv.id}/edit`">{% trans %}Edit{% endtrans %}</a></td>
<td><a :href="`/pedagogy/uv/${uv.id}/delete`">{% trans %}Delete{% endtrans %}</a></td> <td><a :href="`/pedagogy/uv/${uv.id}/delete`">{% trans %}Delete{% endtrans %}</a></td>
{%- endif -%} {%- endif -%}
</tr> </tr>
</template> </template>
</tbody> </tbody>
</table> </table>
<nav id="pagination" class="hidden" :style="max_page() == 0 && 'display: none;'">
<button @click="page--" :disabled="page == 1">{% trans %}Previous{% endtrans %}</button>
<template x-for="i in max_page()">
<button x-text="i" @click="page = i" :class="i == page && 'active'"></button>
</template>
<button @click="page++" :disabled="page == max_page()">{% trans %}Next{% endtrans %}</button>
</nav>
</div> </div>
<script> <script>
const initialUrlParams = new URLSearchParams(window.location.search); const initialUrlParams = new URLSearchParams(window.location.search);
@ -140,9 +147,13 @@
then fetch the corresponding data from the API. then fetch the corresponding data from the API.
This data will then be displayed on the result part of the page. This data will then be displayed on the result part of the page.
#} #}
const page_default = 1;
const page_size_default = 100;
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("uv_search", () => ({ Alpine.data("uv_search", () => ({
uvs: [], uvs: [],
page: initialUrlParams.get("page") || page_default,
page_size: initialUrlParams.get("page_size") || page_size_default,
search: initialUrlParams.get("search") || "", search: initialUrlParams.get("search") || "",
department: initialUrlParams.getAll("department"), department: initialUrlParams.getAll("department"),
credit_type: initialUrlParams.getAll("credit_type"), credit_type: initialUrlParams.getAll("credit_type"),
@ -153,18 +164,31 @@
initialUrlParams.get("semester").split("_AND_") : [], initialUrlParams.get("semester").split("_AND_") : [],
async init() { async init() {
["search", "department", "credit_type", "semester"].forEach((param) => { let search_params = ["search", "department", "credit_type", "semester"];
let pagination_params = ["semester", "page"];
search_params.forEach((param) => {
this.$watch(param, async () => {
{# Reset pagination on search #}
this.page = page_default;
this.page_size = page_size_default;
});
});
search_params.concat(pagination_params).forEach((param) => {
this.$watch(param, async (value) => { this.$watch(param, async (value) => {
update_query_string(param, value); update_query_string(param, value);
await this.fetch_data(); {# reload data on form change #} await this.fetch_data(); {# reload data on form change #}
}); });
}) });
await this.fetch_data(); {# load initial data #} await this.fetch_data(); {# load initial data #}
}, },
async fetch_data() { async fetch_data() {
const url = "{{ url("api:fetch_uvs") }}" + window.location.search; const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
this.uvs = await (await fetch(url)).json(); this.uvs = await (await fetch(url)).json();
},
max_page() {
return Math.round(this.uvs.count / this.page_size);
} }
})) }))
}) })

View File

@ -617,39 +617,47 @@ class UVSearchTest(TestCase):
res = self.client.get(self.url + "?search=PA00") res = self.client.get(self.url + "?search=PA00")
uv = UV.objects.get(code="PA00") uv = UV.objects.get(code="PA00")
assert res.status_code == 200 assert res.status_code == 200
assert json.loads(res.content) == [ assert json.loads(res.content) == {
{ "count": 1,
"id": uv.id, "next": None,
"title": uv.title, "previous": None,
"code": uv.code, "results": [
"credit_type": uv.credit_type, {
"semester": uv.semester, "id": uv.id,
"department": uv.department, "title": uv.title,
} "code": uv.code,
] "credit_type": uv.credit_type,
"semester": uv.semester,
"department": uv.department,
}
],
}
def test_search_by_code(self): def test_search_by_code(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
res = self.client.get(self.url + "?search=MT") res = self.client.get(self.url + "?search=MT")
assert res.status_code == 200 assert res.status_code == 200
assert {uv["code"] for uv in json.loads(res.content)} == {"MT01", "MT10"} assert {uv["code"] for uv in json.loads(res.content)["results"]} == {
"MT01",
"MT10",
}
def test_search_by_credit_type(self): def test_search_by_credit_type(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
res = self.client.get(self.url + "?credit_type=CS") res = self.client.get(self.url + "?credit_type=CS")
assert res.status_code == 200 assert res.status_code == 200
codes = [uv["code"] for uv in json.loads(res.content)] codes = [uv["code"] for uv in json.loads(res.content)["results"]]
assert codes == ["AP4A", "MT01", "PHYS11"] assert codes == ["AP4A", "MT01", "PHYS11"]
res = self.client.get(self.url + "?credit_type=CS&credit_type=OM") res = self.client.get(self.url + "?credit_type=CS&credit_type=OM")
assert res.status_code == 200 assert res.status_code == 200
codes = {uv["code"] for uv in json.loads(res.content)} codes = {uv["code"] for uv in json.loads(res.content)["results"]}
assert codes == {"AP4A", "MT01", "PHYS11", "PA00"} assert codes == {"AP4A", "MT01", "PHYS11", "PA00"}
def test_search_by_semester(self): def test_search_by_semester(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
res = self.client.get(self.url + "?semester=SPRING") res = self.client.get(self.url + "?semester=SPRING")
assert res.status_code == 200 assert res.status_code == 200
codes = {uv["code"] for uv in json.loads(res.content)} codes = {uv["code"] for uv in json.loads(res.content)["results"]}
assert codes == {"DA50", "TNEV", "PA00"} assert codes == {"DA50", "TNEV", "PA00"}
def test_search_multiple_filters(self): def test_search_multiple_filters(self):
@ -658,14 +666,14 @@ class UVSearchTest(TestCase):
self.url + "?semester=AUTUMN&credit_type=CS&department=TC" self.url + "?semester=AUTUMN&credit_type=CS&department=TC"
) )
assert res.status_code == 200 assert res.status_code == 200
codes = {uv["code"] for uv in json.loads(res.content)} codes = {uv["code"] for uv in json.loads(res.content)["results"]}
assert codes == {"MT01", "PHYS11"} assert codes == {"MT01", "PHYS11"}
def test_search_fails(self): def test_search_fails(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
res = self.client.get(self.url + "?credit_type=CS&search=DA") res = self.client.get(self.url + "?credit_type=CS&search=DA")
assert res.status_code == 200 assert res.status_code == 200
assert json.loads(res.content) == [] assert json.loads(res.content)["results"] == []
def test_search_pa00_fail(self): def test_search_pa00_fail(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)