Add pagination to blog post list
This commit is contained in:
parent
bf097b9bb7
commit
0fbd36ce3e
6 changed files with 112 additions and 24 deletions
|
@ -46,3 +46,27 @@
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav.pagination {
|
||||||
|
@include dark-mode {
|
||||||
|
a.pagination-link,
|
||||||
|
.pagination-next,
|
||||||
|
.pagination-previous {
|
||||||
|
background-color: $grey-darker;
|
||||||
|
border-color: $black;
|
||||||
|
color: $dark-mode-text;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $grey-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-current {
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-ellipsis {
|
||||||
|
color: $dark-mode-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.core.paginator import EmptyPage
|
||||||
|
from django.core.paginator import Page as PaginatorPage
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import TruncMonth
|
from django.db.models.functions import TruncMonth
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import Http404, HttpResponse, HttpResponseBadRequest
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from modelcluster.fields import ParentalManyToManyField
|
from modelcluster.fields import ParentalManyToManyField
|
||||||
|
@ -12,6 +15,7 @@ from wagtail.contrib.routable_page.models import RoutablePageMixin, route
|
||||||
from wagtail.query import PageQuerySet
|
from wagtail.query import PageQuerySet
|
||||||
|
|
||||||
from website.common.models import BaseContentPage
|
from website.common.models import BaseContentPage
|
||||||
|
from website.common.serializers import PaginationSerializer
|
||||||
from website.common.utils import TocEntry
|
from website.common.utils import TocEntry
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,31 +34,47 @@ class BlogPostListPage(RoutablePageMixin, BaseContentPage):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def table_of_contents(self) -> list[TocEntry]:
|
def table_of_contents(self) -> list[TocEntry]:
|
||||||
post_months = [
|
post_months = sorted(
|
||||||
dt.strftime("%Y-%m")
|
{
|
||||||
for dt in self.get_blog_posts()
|
dt.strftime("%Y-%m")
|
||||||
.annotate(post_month=TruncMonth("date", output_field=models.DateField()))
|
for dt in self.paginator_page.object_list.annotate(
|
||||||
.order_by("-post_month")
|
post_month=TruncMonth("date", output_field=models.DateField())
|
||||||
.values_list("post_month", flat=True)
|
).values_list("post_month", flat=True)
|
||||||
.distinct()
|
}
|
||||||
]
|
)
|
||||||
|
|
||||||
return [TocEntry(post_month, post_month, 0, []) for post_month in post_months]
|
return [TocEntry(post_month, post_month, 0, []) for post_month in post_months]
|
||||||
|
|
||||||
def get_blog_posts(self) -> PageQuerySet:
|
def get_blog_posts(self) -> PageQuerySet:
|
||||||
return BlogPostPage.objects.descendant_of(self).live()
|
return BlogPostPage.objects.descendant_of(self).live()
|
||||||
|
|
||||||
def get_context(self, request: HttpRequest) -> dict:
|
@cached_property
|
||||||
context = super().get_context(request)
|
def paginator_page(self) -> PaginatorPage:
|
||||||
context["child_pages"] = (
|
pages = (
|
||||||
self.get_blog_posts()
|
self.get_blog_posts()
|
||||||
.select_related("hero_image")
|
.select_related("hero_image")
|
||||||
.select_related("hero_unsplash_photo")
|
.select_related("hero_unsplash_photo")
|
||||||
.prefetch_related("tags")
|
.prefetch_related("tags")
|
||||||
.order_by("-date")
|
.order_by("-date", "title")
|
||||||
)
|
)
|
||||||
|
paginator = Paginator(pages, per_page=1)
|
||||||
|
try:
|
||||||
|
return paginator.page(self.serializer.validated_data["page"])
|
||||||
|
except EmptyPage:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
def get_context(self, request: HttpRequest) -> dict:
|
||||||
|
context = super().get_context(request)
|
||||||
|
context["pages"] = self.paginator_page
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@route(r"^$")
|
||||||
|
def index_route(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
self.serializer = PaginationSerializer(data=request.GET)
|
||||||
|
if not self.serializer.is_valid():
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
return super().index_route(request)
|
||||||
|
|
||||||
@route(r"^feed/$")
|
@route(r"^feed/$")
|
||||||
def feed(self, request: HttpRequest) -> HttpResponse:
|
def feed(self, request: HttpRequest) -> HttpResponse:
|
||||||
from .views import BlogPostPageFeed
|
from .views import BlogPostPageFeed
|
||||||
|
|
|
@ -13,16 +13,21 @@
|
||||||
|
|
||||||
{% block post_content %}
|
{% block post_content %}
|
||||||
<section class="container">
|
<section class="container">
|
||||||
{% for page in child_pages %}
|
{% for page in pages %}
|
||||||
{% if not filtering_by_tag %}
|
{% ifchanged %}
|
||||||
{% ifchanged %}
|
<time datetime="{{ page.date|date:'Y-m' }} title='{{ page.date|date:"F Y" }}'>
|
||||||
<time datetime="{{ page.date|date:'c' }}" title='{{ page.date|date:"F Y" }}'>
|
<h3 id="{{ page.date|date:'Y-m' }}" class="date-header">{{ page.date|date:"Y-m" }}</h3>
|
||||||
<h3 id="{{ page.date|date:'Y-m' }}" class="date-header">{{ page.date|date:"Y-m" }}</h3>
|
</time>
|
||||||
</time>
|
{% endifchanged %}
|
||||||
{% endifchanged %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% include "common/listing-item.html" %}
|
{% include "common/listing-item.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if pages.has_other_pages %}
|
||||||
|
<section class="container">
|
||||||
|
<hr class="my-5" />
|
||||||
|
{% include "common/pagination.html" %}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
5
website/common/serializers.py
Normal file
5
website/common/serializers.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class PaginationSerializer(serializers.Serializer):
|
||||||
|
page = serializers.IntegerField(min_value=1, default=1)
|
33
website/common/templates/common/pagination.html
Normal file
33
website/common/templates/common/pagination.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% load wagtailadmin_tags %}
|
||||||
|
|
||||||
|
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
||||||
|
{% if pages.has_previous %}
|
||||||
|
<a class="pagination-previous" href="{% querystring page=pages.previous_page_number %}">Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if pages.has_next %}
|
||||||
|
<a class="pagination-next" href="{% querystring page=pages.next_page_number %}">Next page</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="pagination-list">
|
||||||
|
{% if pages.has_previous and pages.previous_page_number != 1 %}
|
||||||
|
<li><a class="pagination-link" aria-label="Goto page 1" href="{% querystring page=1 %}">1</a></li>
|
||||||
|
<li><span class="pagination-ellipsis">…</span></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if pages.has_previous %}
|
||||||
|
<li><a class="pagination-link" aria-label="Goto page {{ pages.previous_page_number }}" href="{% querystring page=pages.previous_page_number %}">{{ pages.previous_page_number }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li><a class="pagination-link is-current" aria-label="Page {{ pages.number }}" aria-current="page">{{ pages.number }}</a></li>
|
||||||
|
|
||||||
|
{% if pages.has_next %}
|
||||||
|
<li><a class="pagination-link" aria-label="Goto page {{ pages.next_page_number }}" href="{% querystring page=pages.next_page_number %}">{{ pages.next_page_number }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if pages.has_next and pages.next_page_number != pages.paginator.num_pages %}
|
||||||
|
<li><span class="pagination-ellipsis">…</span></li>
|
||||||
|
<li><a class="pagination-link" aria-label="Goto page {{ pages.paginator.num_pages }}" href="{% querystring page=pages.paginator.num_pages %}">{{ pages.paginator.num_pages }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
|
@ -1,8 +1,9 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from website.common.serializers import PaginationSerializer
|
||||||
|
|
||||||
MIN_SEARCH_LENGTH = 3
|
MIN_SEARCH_LENGTH = 3
|
||||||
|
|
||||||
|
|
||||||
class SearchParamsSerializer(serializers.Serializer):
|
class SearchParamsSerializer(PaginationSerializer):
|
||||||
q = serializers.CharField(min_length=MIN_SEARCH_LENGTH)
|
q = serializers.CharField(min_length=MIN_SEARCH_LENGTH)
|
||||||
page = serializers.IntegerField(min_value=1, default=1)
|
|
||||||
|
|
Loading…
Reference in a new issue