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:
parent
478b4895db
commit
f6bf4ce618
11 changed files with 203 additions and 47 deletions
|
@ -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">
|
||||
|
|
88
front/components/pagination.vue
Normal file
88
front/components/pagination.vue
Normal 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">…</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 = "…"
|
||||
|
||||
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>
|
|
@ -40,3 +40,10 @@ export type FilmsByMonth = {
|
|||
projectionMonth: string
|
||||
films: Film[]
|
||||
}
|
||||
|
||||
export type PaginatedResponse<T> = {
|
||||
results: T[]
|
||||
currentPage: number
|
||||
totalResults: number
|
||||
totalPages: number
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
// thanks @cityssm/bulma-a11y
|
||||
|
||||
a:focus,
|
||||
button:focus
|
||||
a, button, .pagination-link, .pagination-next, .pagination-previous
|
||||
&:focus
|
||||
outline-color: $black
|
||||
outline-offset: 4px
|
||||
outline-style: dotted !important
|
||||
|
@ -16,33 +16,24 @@ button:focus
|
|||
outline-color: $white
|
||||
|
||||
a:focus
|
||||
|
||||
.has-text-white
|
||||
outline-color: $white
|
||||
|
||||
.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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
13
server/myapi/pagination/PagePaginationWithPageCount.py
Normal file
13
server/myapi/pagination/PagePaginationWithPageCount.py
Normal 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
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue