feat [front,api]: pagination calendar and admin list

- extract list for home in action to not paginate

- compute calendar list from parent to paginate
This commit is contained in:
Alice 2022-12-31 03:59:18 +01:00
parent 478b4895db
commit f6bf4ce618
11 changed files with 203 additions and 47 deletions

View file

@ -22,16 +22,11 @@
<script setup lang="ts">
import { FilmsByMonth } from "~/composables/types"
import { PropType } from "@vue/runtime-core"
const props = defineProps({
past: { type: Boolean, default: false },
defineProps({
filmsByMonth: { type: Array as PropType<FilmsByMonth[]>, default: [] },
})
const filmsByMonth = ref<FilmsByMonth[]>()
filmsByMonth.value = (
await apiGet<FilmsByMonth[]>(`films/calendar/`, {
period: props.past ? "past" : "future",
})
).data.value
</script>
<style scoped lang="sass">

View file

@ -0,0 +1,88 @@
<template>
<nav class="pagination" role="navigation" aria-label="pagination">
<nuxt-link
class="pagination-previous"
:disabled="!prev ? 'disabled' : undefined"
:tabindex="!prev ? -1 : undefined"
:to="genRouteQuery(prev)"
@click="checkAndUpdate(prev)"
>
Précédent
</nuxt-link>
<nuxt-link
class="pagination-next"
:disabled="!next ? 'disabled' : undefined"
:tabindex="!next ? -1 : undefined"
:to="genRouteQuery(next)"
@click="checkAndUpdate(next)"
>
Suivant
</nuxt-link>
<ul class="pagination-list">
<li v-for="(i, index) in items" :key="index">
<span v-if="i === ELLIPSIS" class="pagination-ellipsis">&hellip;</span>
<nuxt-link
v-else
class="pagination-link"
:class="{ 'is-current': current === i }"
:aria-label="current === i ? `Page ${i}` : `Aller à la page ${i}`"
:aria-current="current === i ? 'page' : undefined"
:to="genRouteQuery(i)"
@click="checkAndUpdate(i)"
>
{{ i }}
</nuxt-link>
</li>
</ul>
</nav>
</template>
<script setup lang="ts">
const router = useRouter()
const props = defineProps({
nbToShow: { type: Number, default: 2 },
max: { type: Number, required: true },
modelValue: { type: Number, required: true },
})
const current = useModel<number>("modelValue")
const ELLIPSIS = "&hellip;"
const prev = computed<number | null>(() =>
current.value === 1 ? null : current.value - 1
)
const next = computed<number | null>(() =>
current.value === props.max ? null : current.value + 1
)
const items = reactive<(number | string)[]>([1])
watch(current, () => paginate)
paginate()
function paginate() {
items.splice(1)
if (current.value === 1 && props.max === 1) return
if (current.value > 4) items.push(ELLIPSIS)
const r = props.nbToShow,
rLeft = current.value - r,
rRight = current.value + r
for (let i = rLeft > r ? rLeft : r; i <= Math.min(props.max, rRight); i++)
items.push(i)
if (rRight + 1 < props.max) items.push(ELLIPSIS)
if (rRight < props.max) items.push(props.max)
}
function checkAndUpdate(page: number | null) {
if (page) current.value = page
paginate()
}
function genRouteQuery(page: number) {
if (!page) return router.currentRoute
return { query: { ...router.currentRoute.value.query, page } }
}
</script>
<style scoped lang="sass"></style>

View file

@ -40,3 +40,10 @@ export type FilmsByMonth = {
projectionMonth: string
films: Film[]
}
export type PaginatedResponse<T> = {
results: T[]
currentPage: number
totalResults: number
totalPages: number
}

View file

@ -4,45 +4,36 @@
// thanks @cityssm/bulma-a11y
a:focus,
button:focus
outline-color: $black
outline-offset: 4px
outline-style: dotted !important
outline-width: 2px !important
a, button, .pagination-link, .pagination-next, .pagination-previous
&:focus
outline-color: $black
outline-offset: 4px
outline-style: dotted !important
outline-width: 2px !important
.has-background-black &,
.has-background-dark &
.has-background-dark &
outline-color: $white
a:focus
a:focus
.has-text-white
outline-color: $white
.button:focus
.button:focus
&.is-black
outline-color: $black
&.is-danger
outline-color: $danger-dark
&.is-dark
outline-color: $dark
&.is-info
outline-color: $info-dark
&.is-link
outline-color: $link-dark
&.is-primary
outline-color: $primary-dark
&.is-success
outline-color: $success-dark
&.is-warning
outline-color: $warning-dark

View file

@ -8,6 +8,7 @@
@import "bulma/sass/components/card"
@import "bulma/sass/components/modal"
@import "bulma/sass/components/navbar"
@import "bulma/sass/components/pagination"
@import "bulma/sass/components/tabs"
@import "bulma/sass/elements/button"
@import "bulma/sass/elements/container"

View file

@ -18,18 +18,44 @@
</div>
</li>
</ul>
<Pagination
:model-value="currentPage"
:max="totalPages"
@update:model-value="changePage"
/>
</main>
</template>
<script setup lang="ts">
import { ShortFilm } from "~/composables/types"
import { ShortFilm, PaginatedResponse } from "~/composables/types"
definePageMeta({ layout: "admin" })
useHead({ title: "Liste des séances" })
const route = useRoute()
const router = useRouter()
const currentPage = ref<number>(parseInt(route.query?.page as string) || 1)
const totalPages = ref<number>(1)
const films = reactive<ShortFilm[]>([])
Object.assign(films, (await apiGet("admin/films/")).data.value)
async function changePage(newPage: number) {
await loadFilms(newPage)
router.replace({ query: { ...route.query, page: currentPage.value } })
}
await loadFilms(currentPage.value)
async function loadFilms(page = 1) {
const res = (
await apiGet<PaginatedResponse<ShortFilm>>(`admin/films/`, {
page: page,
})
).data.value
Object.assign(films, res?.results || [])
totalPages.value = res?.totalPages || 1
currentPage.value = res?.currentPage || 1
}
</script>
<style lang="sass">

View file

@ -6,39 +6,65 @@
<div class="tabs is-toggle is-small">
<ul>
<li :class="isPast ? 'is-active' : undefined">
<a @click="view = 'past'">
<a @click="changeView('past')">
<span>passées</span>
</a>
</li>
<li :class="isPast ? undefined : 'is-active'">
<a @click="view = 'future'">
<a @click="changeView('future')">
<span>à venir</span>
</a>
</li>
</ul>
</div>
</header>
<KeepAlive>
<MovieCardListPerMonth v-if="isPast" past />
</KeepAlive>
<KeepAlive>
<MovieCardListPerMonth v-if="!isPast" />
</KeepAlive>
<MovieCardListPerMonth :films-by-month="filmsByMonth" />
<Pagination
:model-value="currentPage"
:max="totalPages"
@update:model-value="changePage"
/>
</section>
</template>
<script setup lang="ts">
import { FilmsByMonth, PaginatedResponse } from "~/composables/types"
useHead({ title: "Calendrier" })
const router = useRouter()
const route = useRoute()
const view = computed<string>({
get: () => (route.query?.view as string) || "future",
set(value) {
router.replace({ query: { ...route.query, view: value } })
},
})
const isPast = computed<boolean>(() => view.value === "past")
const currentPage = ref<number>(parseInt(route.query?.page as string) || 1)
const totalPages = ref<number>(1)
const currentView = ref<string>((route.query?.view as string) || "future")
const isPast = computed<boolean>(() => currentView.value === "past")
const filmsByMonth = ref<FilmsByMonth[]>([])
async function changePage(newPage: number) {
await loadFilms(newPage, isPast.value)
router.replace({ query: { ...route.query, page: currentPage.value } })
}
async function changeView(newView: string) {
await loadFilms(1, newView === "past")
currentView.value = newView
router.replace({ query: { view: newView } })
}
await loadFilms(currentPage.value, isPast.value)
async function loadFilms(page = 1, isPast = false) {
const res = (
await apiGet<PaginatedResponse<FilmsByMonth>>(`films/calendar/`, {
period: isPast ? "past" : "future",
page: page,
})
).data.value
filmsByMonth.value = res?.results || []
totalPages.value = res?.totalPages || 1
currentPage.value = res?.currentPage || 1
}
</script>
<style scoped lang="sass">

View file

@ -38,7 +38,7 @@ useHead({ title: "Accueil" })
const modules = ref<SwiperModule[]>([Navigation])
const films = ref<Film[]>()
films.value = ((await apiGet<Film[]>(`films/`)).data.value || []) as Film[]
films.value = ((await apiGet<Film[]>(`films/home/`)).data.value || []) as Film[]
const firstFilm = computed(() => films.value?.[0])
const nextFilms = computed(() => films.value?.slice(1))
</script>

View file

@ -0,0 +1,13 @@
from rest_framework import pagination
from rest_framework.response import Response
class PagePaginationWithPageCount(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'current_page': self.page.number,
'total_results': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'results': data
})

View file

@ -51,7 +51,7 @@ class AdminFilmViewSet(viewsets.ModelViewSet):
return self.general_com_view(com.ics)
class FilmViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet):
class FilmViewSet(mixins.RetrieveModelMixin, GenericViewSet):
serializer_class = FilmSerializer
def get_queryset(self):
@ -65,12 +65,19 @@ class FilmViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewS
ordering = f"{'-' if past else ''}projection_date"
return queryset.filter(date_filter).order_by(ordering)
@action(detail=False, methods=["GET"])
def home(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())[:5]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=["GET"])
def calendar(self, request):
qs = self.get_queryset().annotate(
projection_month=TruncMonth("projection_date")
)
grouped = groupby(qs, lambda f: f.projection_month)
page = self.paginate_queryset(qs) or qs
grouped = groupby(page, lambda f: f.projection_month)
data = [
{
"projection_month": month.strftime("%B %Y"),
@ -78,4 +85,4 @@ class FilmViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewS
}
for (month, monthly_data) in grouped
]
return Response(data)
return self.get_paginated_response(data)

View file

@ -126,6 +126,8 @@ REST_FRAMEWORK = {
"djangorestframework_camel_case.parser.CamelCaseMultiPartParser",
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
),
'DEFAULT_PAGINATION_CLASS': 'myapi.pagination.PagePaginationWithPageCount.PagePaginationWithPageCount',
'PAGE_SIZE': 2
}
TMDB_API_KEY = config.getstr("tmdb.api_key")