Move listing functionality to generic base page
This commit is contained in:
parent
655d3a484e
commit
e19a2456e7
12 changed files with 153 additions and 200 deletions
|
@ -1,25 +1,18 @@
|
|||
from typing import Any
|
||||
from typing import Any, Type
|
||||
|
||||
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 Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from modelcluster.fields import ParentalManyToManyField
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
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.models import BaseContentPage, BaseListingPage
|
||||
from website.common.utils import TocEntry
|
||||
from website.common.views import ContentPageFeed
|
||||
|
||||
|
||||
class BlogPostListPage(RoutablePageMixin, BaseContentPage):
|
||||
class BlogPostListPage(BaseListingPage):
|
||||
max_count = 1
|
||||
subpage_types = [
|
||||
"blog.BlogPostPage",
|
||||
|
@ -28,60 +21,36 @@ class BlogPostListPage(RoutablePageMixin, BaseContentPage):
|
|||
"blog.BlogPostCollectionPage",
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def show_reading_time(self) -> bool:
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def table_of_contents(self) -> list[TocEntry]:
|
||||
post_months = sorted(
|
||||
{
|
||||
dt.strftime("%Y-%m")
|
||||
for dt in self.paginator_page.object_list.annotate(
|
||||
for dt in self.get_paginator_page()
|
||||
.object_list.annotate(
|
||||
post_month=TruncMonth("date", output_field=models.DateField())
|
||||
).values_list("post_month", flat=True)
|
||||
)
|
||||
.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()
|
||||
|
||||
@cached_property
|
||||
def paginator_page(self) -> PaginatorPage:
|
||||
pages = (
|
||||
self.get_blog_posts()
|
||||
def get_listing_pages(self) -> models.QuerySet:
|
||||
return (
|
||||
BlogPostPage.objects.descendant_of(self)
|
||||
.live()
|
||||
.select_related("hero_image")
|
||||
.select_related("hero_unsplash_photo")
|
||||
.prefetch_related("tags")
|
||||
.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:
|
||||
@property
|
||||
def feed_class(self) -> Type[ContentPageFeed]:
|
||||
from .views import BlogPostPageFeed
|
||||
|
||||
return BlogPostPageFeed(
|
||||
self.get_blog_posts().order_by("-date"), self.get_url(), self.title
|
||||
)(request)
|
||||
return BlogPostPageFeed
|
||||
|
||||
|
||||
class BlogPostPage(BaseContentPage):
|
||||
|
@ -97,66 +66,34 @@ class BlogPostPage(BaseContentPage):
|
|||
]
|
||||
|
||||
|
||||
class BlogPostTagListPage(BaseContentPage):
|
||||
class BlogPostTagListPage(BaseListingPage):
|
||||
max_count = 1
|
||||
parent_page_types = [BlogPostListPage]
|
||||
subpage_types = ["blog.BlogPostTagPage"]
|
||||
|
||||
@cached_property
|
||||
def table_of_contents(self) -> list[TocEntry]:
|
||||
return [TocEntry(page.title, page.slug, 0, []) for page in self.get_tags()]
|
||||
|
||||
def get_tags(self) -> PageQuerySet:
|
||||
return (
|
||||
self.get_children()
|
||||
.specific()
|
||||
.live()
|
||||
.order_by("title")
|
||||
.select_related("hero_image")
|
||||
.select_related("hero_unsplash_photo")
|
||||
)
|
||||
|
||||
def get_context(self, request: HttpRequest) -> dict:
|
||||
context = super().get_context(request)
|
||||
context["tags"] = self.get_children().specific().live().order_by("title")
|
||||
return context
|
||||
return [
|
||||
TocEntry(page.title, page.slug, 0, []) for page in self.get_listing_pages()
|
||||
]
|
||||
|
||||
|
||||
class BlogPostTagPage(RoutablePageMixin, BaseContentPage):
|
||||
class BlogPostTagPage(BaseListingPage):
|
||||
subpage_types: list[Any] = []
|
||||
parent_page_types = [BlogPostTagListPage]
|
||||
|
||||
@cached_property
|
||||
def table_of_contents(self) -> list[TocEntry]:
|
||||
return [
|
||||
TocEntry(page.title, page.slug, 0, []) for page in self.get_blog_posts()
|
||||
]
|
||||
|
||||
def get_blog_posts(self) -> PageQuerySet:
|
||||
def get_listing_pages(self) -> models.QuerySet:
|
||||
blog_list_page = BlogPostListPage.objects.all().live().get()
|
||||
return (
|
||||
blog_list_page.get_blog_posts()
|
||||
.filter(tags=self)
|
||||
.order_by("-date")
|
||||
.select_related("hero_image")
|
||||
.select_related("hero_unsplash_photo")
|
||||
)
|
||||
return blog_list_page.get_listing_pages().filter(tags=self)
|
||||
|
||||
def get_context(self, request: HttpRequest) -> dict:
|
||||
context = super().get_context(request)
|
||||
context["pages"] = self.get_blog_posts()
|
||||
return context
|
||||
|
||||
@route(r"^feed/$")
|
||||
def feed(self, request: HttpRequest) -> HttpResponse:
|
||||
@property
|
||||
def feed_class(self) -> Type[ContentPageFeed]:
|
||||
from .views import BlogPostPageFeed
|
||||
|
||||
return BlogPostPageFeed(
|
||||
self.get_blog_posts().order_by("-date"), self.get_url(), self.title
|
||||
)(request)
|
||||
return BlogPostPageFeed
|
||||
|
||||
|
||||
class BlogPostCollectionListPage(BaseContentPage):
|
||||
class BlogPostCollectionListPage(BaseListingPage):
|
||||
subpage_types: list[Any] = []
|
||||
parent_page_types = [BlogPostListPage]
|
||||
max_count = 1
|
||||
|
@ -164,38 +101,23 @@ class BlogPostCollectionListPage(BaseContentPage):
|
|||
@cached_property
|
||||
def table_of_contents(self) -> list[TocEntry]:
|
||||
return [
|
||||
TocEntry(page.title, page.slug, 0, []) for page in self.get_collections()
|
||||
TocEntry(page.title, page.slug, 0, []) for page in self.get_listing_pages()
|
||||
]
|
||||
|
||||
def get_collections(self) -> PageQuerySet:
|
||||
def get_listing_pages(self) -> models.QuerySet:
|
||||
blog_list_page = BlogPostListPage.objects.all().live().get()
|
||||
return BlogPostCollectionPage.objects.child_of(blog_list_page).live()
|
||||
|
||||
def get_context(self, request: HttpRequest) -> dict:
|
||||
context = super().get_context(request)
|
||||
context["collections"] = self.get_collections()
|
||||
return context
|
||||
|
||||
|
||||
class BlogPostCollectionPage(BaseContentPage):
|
||||
class BlogPostCollectionPage(BaseListingPage):
|
||||
parent_page_types = [BlogPostListPage]
|
||||
subpage_types = [BlogPostPage]
|
||||
|
||||
@cached_property
|
||||
def table_of_contents(self) -> list[TocEntry]:
|
||||
return [
|
||||
TocEntry(page.title, page.slug, 0, []) for page in self.get_blog_posts()
|
||||
]
|
||||
def get_listing_pages(self) -> models.QuerySet:
|
||||
return super().get_listing_pages().order_by("-date")
|
||||
|
||||
def get_blog_posts(self) -> PageQuerySet:
|
||||
return (
|
||||
BlogPostPage.objects.child_of(self)
|
||||
.order_by("-date")
|
||||
.select_related("hero_image")
|
||||
.select_related("hero_unsplash_photo")
|
||||
)
|
||||
@property
|
||||
def feed_class(self) -> Type[ContentPageFeed]:
|
||||
from .views import BlogPostPageFeed
|
||||
|
||||
def get_context(self, request: HttpRequest) -> dict:
|
||||
context = super().get_context(request)
|
||||
context["pages"] = self.get_blog_posts()
|
||||
return context
|
||||
return BlogPostPageFeed
|
||||
|
|
|
@ -1,9 +1 @@
|
|||
{% extends "common/content_page.html" %}
|
||||
|
||||
{% block post_content %}
|
||||
<section class="container">
|
||||
{% for collection in collections %}
|
||||
{% include "common/listing-item.html" with page=collection %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% extends "common/listing_page.html" %}
|
||||
|
|
|
@ -1,9 +1 @@
|
|||
{% extends "common/content_page.html" %}
|
||||
|
||||
{% block post_content %}
|
||||
<section class="container">
|
||||
{% for page in pages %}
|
||||
{% include "common/listing-item.html" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% extends "common/listing_page.html" %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
{% block post_content %}
|
||||
<section class="container">
|
||||
{% for page in pages %}
|
||||
{% for page in listing_pages %}
|
||||
{% ifchanged %}
|
||||
<time datetime="{{ page.date|date:'Y-m' }}" title='{{ page.date|date:"F Y" }}'>
|
||||
<h3 id="{{ page.date|date:'Y-m' }}" class="date-header">{{ page.date|date:"Y-m" }}</h3>
|
||||
|
@ -24,10 +24,10 @@
|
|||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{% if pages.has_other_pages %}
|
||||
{% if listing_pages.has_other_pages %}
|
||||
<section class="container">
|
||||
<hr class="my-5" />
|
||||
{% include "common/pagination.html" %}
|
||||
{% include "common/pagination.html" with pages=listing_pages %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1 @@
|
|||
{% extends "common/content_page.html" %}
|
||||
|
||||
{% block post_content %}
|
||||
<section class="container">
|
||||
{% for tag in tags %}
|
||||
{% include "common/listing-item.html" with page=tag %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% extends "common/listing_page.html" %}
|
||||
|
|
|
@ -1,15 +1 @@
|
|||
{% extends "common/content_page.html" %}
|
||||
|
||||
{% load wagtailroutablepage_tags %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="alternate" type="application/rss+xml" href="{% routablepageurl page 'feed' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block post_content %}
|
||||
<section class="container">
|
||||
{% for page in pages %}
|
||||
{% include "common/listing-item.html" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% extends "common/listing_page.html" %}
|
||||
|
|
|
@ -1,40 +1,10 @@
|
|||
from datetime import datetime, time
|
||||
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from wagtail.query import PageQuerySet
|
||||
from website.common.views import ContentPageFeed
|
||||
|
||||
from .models import BlogPostPage
|
||||
|
||||
|
||||
class BlogPostPageFeed(Feed):
|
||||
def __init__(self, posts: PageQuerySet, link: str, title: str):
|
||||
self.posts = posts
|
||||
self.link = link
|
||||
self.title = title
|
||||
super().__init__()
|
||||
|
||||
def __call__(
|
||||
self, request: HttpRequest, *args: list, **kwargs: dict
|
||||
) -> HttpResponse:
|
||||
self.request = request
|
||||
return super().__call__(request, *args, **kwargs)
|
||||
|
||||
def items(self) -> PageQuerySet:
|
||||
return self.posts
|
||||
|
||||
def item_title(self, item: BlogPostPage) -> str:
|
||||
return item.title
|
||||
|
||||
def item_link(self, item: BlogPostPage) -> str:
|
||||
return item.get_full_url(request=self.request)
|
||||
|
||||
def item_description(self, item: BlogPostPage) -> str:
|
||||
return item.summary
|
||||
|
||||
class BlogPostPageFeed(ContentPageFeed):
|
||||
def item_pubdate(self, item: BlogPostPage) -> datetime:
|
||||
return datetime.combine(item.date, time())
|
||||
|
||||
def item_updateddate(self, item: BlogPostPage) -> datetime:
|
||||
return item.last_published_at
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
from datetime import timedelta
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.core.cache import cache
|
||||
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.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.utils.functional import cached_property, classproperty
|
||||
from django.utils.text import slugify
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.images import get_image_model_string
|
||||
from wagtail.images.views.serve import generate_image_url
|
||||
|
@ -19,6 +25,7 @@ from wagtailmetadata.models import MetadataMixin
|
|||
from website.common.utils import count_words
|
||||
from website.contrib.unsplash.widgets import UnsplashPhotoChooser
|
||||
|
||||
from .serializers import PaginationSerializer
|
||||
from .streamfield import add_heading_anchors, get_blocks, get_content_html
|
||||
from .utils import TocEntry, extract_text, get_table_of_contents, truncate_string
|
||||
|
||||
|
@ -161,18 +168,67 @@ class ContentPage(BaseContentPage):
|
|||
subpage_types: list[Any] = []
|
||||
|
||||
|
||||
class ListingPage(BaseContentPage):
|
||||
def get_context(self, request: HttpRequest) -> dict:
|
||||
context = super().get_context(request)
|
||||
context["child_pages"] = (
|
||||
class BaseListingPage(RoutablePageMixin, BaseContentPage):
|
||||
PAGE_SIZE = 20
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_listing_pages(self) -> models.QuerySet:
|
||||
return (
|
||||
self.get_children()
|
||||
.live()
|
||||
.specific()
|
||||
.select_related("hero_image")
|
||||
.select_related("hero_unsplash_photo")
|
||||
.order_by("title")
|
||||
)
|
||||
|
||||
def get_paginator_page(self) -> PaginatorPage:
|
||||
paginator = Paginator(self.get_listing_pages(), per_page=self.PAGE_SIZE)
|
||||
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["listing_pages"] = self.get_paginator_page()
|
||||
return context
|
||||
|
||||
@cached_property
|
||||
def table_of_contents(self) -> list[TocEntry]:
|
||||
return []
|
||||
|
||||
@cached_property
|
||||
def show_reading_time(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def feed_class(self) -> Type[Feed]:
|
||||
from .views import ContentPageFeed
|
||||
|
||||
return ContentPageFeed
|
||||
|
||||
@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:
|
||||
return self.feed_class(
|
||||
self.get_listing_pages(),
|
||||
self.get_full_url(request),
|
||||
self.title,
|
||||
)(request)
|
||||
|
||||
|
||||
class ListingPage(BaseListingPage):
|
||||
pass
|
||||
|
||||
|
||||
@register_snippet
|
||||
class ReferralLink(models.Model, index.Indexed):
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
{% extends "common/content_page.html" %}
|
||||
|
||||
{% load wagtailroutablepage_tags %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="alternate" type="application/rss+xml" href="{% routablepageurl page 'feed' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block post_content %}
|
||||
<section class="container">
|
||||
{% for page in child_pages %}
|
||||
{% for page in listing_pages %}
|
||||
{% include "common/listing-item.html" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
|
|
@ -54,8 +54,13 @@ class ListingPageTestCase(TestCase):
|
|||
def test_accessible(self) -> None:
|
||||
response = self.client.get(self.page.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.context["child_pages"]), 2)
|
||||
self.assertEqual(len(response.context["listing_pages"]), 2)
|
||||
|
||||
def test_feed_accessible(self) -> None:
|
||||
response = self.client.get(self.page.url + self.page.reverse_subpage("feed"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8")
|
||||
|
||||
def test_queries(self) -> None:
|
||||
with self.assertNumQueries(24):
|
||||
with self.assertNumQueries(25):
|
||||
self.client.get(self.page.url)
|
||||
|
|
|
@ -12,7 +12,7 @@ from wagtail.query import PageQuerySet
|
|||
|
||||
from website.home.models import HomePage
|
||||
|
||||
from .models import BasePage
|
||||
from .models import BaseContentPage, BasePage
|
||||
|
||||
|
||||
class Error404View(TemplateView):
|
||||
|
@ -71,3 +71,35 @@ class AllPagesFeed(Feed):
|
|||
|
||||
def item_updateddate(self, item: BasePage) -> datetime:
|
||||
return item.last_published_at
|
||||
|
||||
|
||||
class ContentPageFeed(Feed):
|
||||
def __init__(self, posts: PageQuerySet, link: str, title: str):
|
||||
self.posts = posts
|
||||
self.link = link
|
||||
self.title = title
|
||||
super().__init__()
|
||||
|
||||
def __call__(
|
||||
self, request: HttpRequest, *args: list, **kwargs: dict
|
||||
) -> HttpResponse:
|
||||
self.request = request
|
||||
return super().__call__(request, *args, **kwargs)
|
||||
|
||||
def items(self) -> PageQuerySet:
|
||||
return self.posts
|
||||
|
||||
def item_title(self, item: BaseContentPage) -> str:
|
||||
return item.title
|
||||
|
||||
def item_link(self, item: BaseContentPage) -> str:
|
||||
return item.get_full_url(request=self.request)
|
||||
|
||||
def item_description(self, item: BaseContentPage) -> str:
|
||||
return item.summary
|
||||
|
||||
def item_pubdate(self, item: BaseContentPage) -> datetime:
|
||||
return item.first_published_at
|
||||
|
||||
def item_updateddate(self, item: BaseContentPage) -> datetime:
|
||||
return item.last_published_at
|
||||
|
|
|
@ -5,7 +5,6 @@ from wagtail.images import get_image_model_string
|
|||
from wagtail.images.models import Image
|
||||
from wagtailmetadata.models import WagtailImageMetadataMixin
|
||||
|
||||
from website.blog.models import BlogPostPage
|
||||
from website.common.models import BasePage
|
||||
|
||||
|
||||
|
@ -38,6 +37,7 @@ class HomePage(BasePage, WagtailImageMetadataMixin):
|
|||
return ""
|
||||
|
||||
def get_context(self, request: HttpRequest) -> dict:
|
||||
from website.blog.models import BlogPostPage
|
||||
from website.search.models import SearchPage
|
||||
|
||||
context = super().get_context(request)
|
||||
|
|
Loading…
Reference in a new issue