diff --git a/static/src/scss/_listing.scss b/static/src/scss/_listing.scss
index acc4471..278e73f 100644
--- a/static/src/scss/_listing.scss
+++ b/static/src/scss/_listing.scss
@@ -46,3 +46,27 @@
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;
+ }
+ }
+}
diff --git a/website/blog/models.py b/website/blog/models.py
index 05419fa..44ef1ce 100644
--- a/website/blog/models.py
+++ b/website/blog/models.py
@@ -1,9 +1,12 @@
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.models.functions import TruncMonth
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.functional import cached_property
from modelcluster.fields import ParentalManyToManyField
@@ -12,6 +15,7 @@ from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.query import PageQuerySet
from website.common.models import BaseContentPage
+from website.common.serializers import PaginationSerializer
from website.common.utils import TocEntry
@@ -30,31 +34,47 @@ class BlogPostListPage(RoutablePageMixin, BaseContentPage):
@cached_property
def table_of_contents(self) -> list[TocEntry]:
- post_months = [
- dt.strftime("%Y-%m")
- for dt in self.get_blog_posts()
- .annotate(post_month=TruncMonth("date", output_field=models.DateField()))
- .order_by("-post_month")
- .values_list("post_month", flat=True)
- .distinct()
- ]
+ post_months = sorted(
+ {
+ dt.strftime("%Y-%m")
+ for dt in self.paginator_page.object_list.annotate(
+ post_month=TruncMonth("date", output_field=models.DateField())
+ ).values_list("post_month", flat=True)
+ }
+ )
return [TocEntry(post_month, post_month, 0, []) for post_month in post_months]
def get_blog_posts(self) -> PageQuerySet:
return BlogPostPage.objects.descendant_of(self).live()
- def get_context(self, request: HttpRequest) -> dict:
- context = super().get_context(request)
- context["child_pages"] = (
+ @cached_property
+ def paginator_page(self) -> PaginatorPage:
+ pages = (
self.get_blog_posts()
.select_related("hero_image")
.select_related("hero_unsplash_photo")
.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
+ @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/$")
def feed(self, request: HttpRequest) -> HttpResponse:
from .views import BlogPostPageFeed
diff --git a/website/blog/templates/blog/blog_post_list_page.html b/website/blog/templates/blog/blog_post_list_page.html
index 194b7c0..770bc99 100644
--- a/website/blog/templates/blog/blog_post_list_page.html
+++ b/website/blog/templates/blog/blog_post_list_page.html
@@ -13,16 +13,21 @@
{% block post_content %}
- {% for page in child_pages %}
- {% if not filtering_by_tag %}
- {% ifchanged %}
-
- {% endifchanged %}
- {% endif %}
+ {% for page in pages %}
+ {% ifchanged %}
+
+ {% endifchanged %}
{% include "common/listing-item.html" %}
{% endfor %}
-
+
+
+ {% if pages.has_other_pages %}
+
+
+ {% include "common/pagination.html" %}
+
+ {% endif %}
{% endblock %}
diff --git a/website/common/serializers.py b/website/common/serializers.py
new file mode 100644
index 0000000..b576791
--- /dev/null
+++ b/website/common/serializers.py
@@ -0,0 +1,5 @@
+from rest_framework import serializers
+
+
+class PaginationSerializer(serializers.Serializer):
+ page = serializers.IntegerField(min_value=1, default=1)
diff --git a/website/common/templates/common/pagination.html b/website/common/templates/common/pagination.html
new file mode 100644
index 0000000..4384c7f
--- /dev/null
+++ b/website/common/templates/common/pagination.html
@@ -0,0 +1,33 @@
+{% load wagtailadmin_tags %}
+
+
diff --git a/website/search/serializers.py b/website/search/serializers.py
index 95664a4..4935125 100644
--- a/website/search/serializers.py
+++ b/website/search/serializers.py
@@ -1,8 +1,9 @@
from rest_framework import serializers
+from website.common.serializers import PaginationSerializer
+
MIN_SEARCH_LENGTH = 3
-class SearchParamsSerializer(serializers.Serializer):
+class SearchParamsSerializer(PaginationSerializer):
q = serializers.CharField(min_length=MIN_SEARCH_LENGTH)
- page = serializers.IntegerField(min_value=1, default=1)