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">
|
<script setup lang="ts">
|
||||||
import { FilmsByMonth } from "~/composables/types"
|
import { FilmsByMonth } from "~/composables/types"
|
||||||
|
import { PropType } from "@vue/runtime-core"
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
past: { type: Boolean, default: false },
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<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
|
projectionMonth: string
|
||||||
films: Film[]
|
films: Film[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaginatedResponse<T> = {
|
||||||
|
results: T[]
|
||||||
|
currentPage: number
|
||||||
|
totalResults: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
// thanks @cityssm/bulma-a11y
|
// thanks @cityssm/bulma-a11y
|
||||||
|
|
||||||
a:focus,
|
a, button, .pagination-link, .pagination-next, .pagination-previous
|
||||||
button:focus
|
&:focus
|
||||||
outline-color: $black
|
outline-color: $black
|
||||||
outline-offset: 4px
|
outline-offset: 4px
|
||||||
outline-style: dotted !important
|
outline-style: dotted !important
|
||||||
|
@ -16,33 +16,24 @@ button:focus
|
||||||
outline-color: $white
|
outline-color: $white
|
||||||
|
|
||||||
a:focus
|
a:focus
|
||||||
|
|
||||||
.has-text-white
|
.has-text-white
|
||||||
outline-color: $white
|
outline-color: $white
|
||||||
|
|
||||||
.button:focus
|
.button:focus
|
||||||
|
|
||||||
&.is-black
|
&.is-black
|
||||||
outline-color: $black
|
outline-color: $black
|
||||||
|
|
||||||
&.is-danger
|
&.is-danger
|
||||||
outline-color: $danger-dark
|
outline-color: $danger-dark
|
||||||
|
|
||||||
&.is-dark
|
&.is-dark
|
||||||
outline-color: $dark
|
outline-color: $dark
|
||||||
|
|
||||||
&.is-info
|
&.is-info
|
||||||
outline-color: $info-dark
|
outline-color: $info-dark
|
||||||
|
|
||||||
&.is-link
|
&.is-link
|
||||||
outline-color: $link-dark
|
outline-color: $link-dark
|
||||||
|
|
||||||
&.is-primary
|
&.is-primary
|
||||||
outline-color: $primary-dark
|
outline-color: $primary-dark
|
||||||
|
|
||||||
&.is-success
|
&.is-success
|
||||||
outline-color: $success-dark
|
outline-color: $success-dark
|
||||||
|
|
||||||
&.is-warning
|
&.is-warning
|
||||||
outline-color: $warning-dark
|
outline-color: $warning-dark
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
@import "bulma/sass/components/card"
|
@import "bulma/sass/components/card"
|
||||||
@import "bulma/sass/components/modal"
|
@import "bulma/sass/components/modal"
|
||||||
@import "bulma/sass/components/navbar"
|
@import "bulma/sass/components/navbar"
|
||||||
|
@import "bulma/sass/components/pagination"
|
||||||
@import "bulma/sass/components/tabs"
|
@import "bulma/sass/components/tabs"
|
||||||
@import "bulma/sass/elements/button"
|
@import "bulma/sass/elements/button"
|
||||||
@import "bulma/sass/elements/container"
|
@import "bulma/sass/elements/container"
|
||||||
|
|
|
@ -18,18 +18,44 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<Pagination
|
||||||
|
:model-value="currentPage"
|
||||||
|
:max="totalPages"
|
||||||
|
@update:model-value="changePage"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ShortFilm } from "~/composables/types"
|
import { ShortFilm, PaginatedResponse } from "~/composables/types"
|
||||||
|
|
||||||
definePageMeta({ layout: "admin" })
|
definePageMeta({ layout: "admin" })
|
||||||
useHead({ title: "Liste des séances" })
|
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[]>([])
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
|
|
|
@ -6,39 +6,65 @@
|
||||||
<div class="tabs is-toggle is-small">
|
<div class="tabs is-toggle is-small">
|
||||||
<ul>
|
<ul>
|
||||||
<li :class="isPast ? 'is-active' : undefined">
|
<li :class="isPast ? 'is-active' : undefined">
|
||||||
<a @click="view = 'past'">
|
<a @click="changeView('past')">
|
||||||
<span>passées</span>
|
<span>passées</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li :class="isPast ? undefined : 'is-active'">
|
<li :class="isPast ? undefined : 'is-active'">
|
||||||
<a @click="view = 'future'">
|
<a @click="changeView('future')">
|
||||||
<span>à venir</span>
|
<span>à venir</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<KeepAlive>
|
<MovieCardListPerMonth :films-by-month="filmsByMonth" />
|
||||||
<MovieCardListPerMonth v-if="isPast" past />
|
<Pagination
|
||||||
</KeepAlive>
|
:model-value="currentPage"
|
||||||
<KeepAlive>
|
:max="totalPages"
|
||||||
<MovieCardListPerMonth v-if="!isPast" />
|
@update:model-value="changePage"
|
||||||
</KeepAlive>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { FilmsByMonth, PaginatedResponse } from "~/composables/types"
|
||||||
|
|
||||||
useHead({ title: "Calendrier" })
|
useHead({ title: "Calendrier" })
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const view = computed<string>({
|
const currentPage = ref<number>(parseInt(route.query?.page as string) || 1)
|
||||||
get: () => (route.query?.view as string) || "future",
|
const totalPages = ref<number>(1)
|
||||||
set(value) {
|
const currentView = ref<string>((route.query?.view as string) || "future")
|
||||||
router.replace({ query: { ...route.query, view: value } })
|
const isPast = computed<boolean>(() => currentView.value === "past")
|
||||||
},
|
|
||||||
})
|
const filmsByMonth = ref<FilmsByMonth[]>([])
|
||||||
const isPast = computed<boolean>(() => view.value === "past")
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass">
|
||||||
|
|
|
@ -38,7 +38,7 @@ useHead({ title: "Accueil" })
|
||||||
const modules = ref<SwiperModule[]>([Navigation])
|
const modules = ref<SwiperModule[]>([Navigation])
|
||||||
|
|
||||||
const films = ref<Film[]>()
|
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 firstFilm = computed(() => films.value?.[0])
|
||||||
const nextFilms = computed(() => films.value?.slice(1))
|
const nextFilms = computed(() => films.value?.slice(1))
|
||||||
</script>
|
</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)
|
return self.general_com_view(com.ics)
|
||||||
|
|
||||||
|
|
||||||
class FilmViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet):
|
class FilmViewSet(mixins.RetrieveModelMixin, GenericViewSet):
|
||||||
serializer_class = FilmSerializer
|
serializer_class = FilmSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -65,12 +65,19 @@ class FilmViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewS
|
||||||
ordering = f"{'-' if past else ''}projection_date"
|
ordering = f"{'-' if past else ''}projection_date"
|
||||||
return queryset.filter(date_filter).order_by(ordering)
|
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"])
|
@action(detail=False, methods=["GET"])
|
||||||
def calendar(self, request):
|
def calendar(self, request):
|
||||||
qs = self.get_queryset().annotate(
|
qs = self.get_queryset().annotate(
|
||||||
projection_month=TruncMonth("projection_date")
|
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 = [
|
data = [
|
||||||
{
|
{
|
||||||
"projection_month": month.strftime("%B %Y"),
|
"projection_month": month.strftime("%B %Y"),
|
||||||
|
@ -78,4 +85,4 @@ class FilmViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewS
|
||||||
}
|
}
|
||||||
for (month, monthly_data) in grouped
|
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.CamelCaseMultiPartParser",
|
||||||
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
|
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
|
||||||
),
|
),
|
||||||
|
'DEFAULT_PAGINATION_CLASS': 'myapi.pagination.PagePaginationWithPageCount.PagePaginationWithPageCount',
|
||||||
|
'PAGE_SIZE': 2
|
||||||
}
|
}
|
||||||
|
|
||||||
TMDB_API_KEY = config.getstr("tmdb.api_key")
|
TMDB_API_KEY = config.getstr("tmdb.api_key")
|
||||||
|
|
Loading…
Add table
Reference in a new issue