diff --git a/cdhweb/blog/models.py b/cdhweb/blog/models.py index e4592e05..0d9f8d81 100644 --- a/cdhweb/blog/models.py +++ b/cdhweb/blog/models.py @@ -1,7 +1,9 @@ from datetime import date +from django.core.paginator import Paginator from django.db import models from django.db.models.fields.related import RelatedField +from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.dateformat import format from django.utils.functional import cached_property @@ -12,6 +14,7 @@ from modelcluster.models import ClusterableModel from taggit.models import TaggedItemBase from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel +from wagtail.contrib.routable_page.models import RoutablePageMixin, path, re_path from wagtail.fields import RichTextField from wagtail.models import Orderable, Page, PageManager, PageQuerySet from wagtail.search import index @@ -186,6 +189,25 @@ def author_list(self): """Comma-separated list of author names.""" return ", ".join(str(author.person) for author in self.authors.all()) + def get_url_parts(self, *args, **kwargs): + """Custom blog post URLs of the form /updates/2014/03/01/my-post.""" + url_parts = super().get_url_parts(*args, **kwargs) + # NOTE evidently these can sometimes be None; unclear why – perhaps it + # gets called in a context where the request is unavailable. Seems to + # happen immediately on page creation; the creation still succeeds. + if url_parts and self.first_published_at: + site_id, root_url, _ = url_parts + page_path = self.get_parent().specific.reverse_subpage( + "dated_child", + kwargs={ + "year": self.first_published_at.year, + # force two-digit month and day + "month": "%02d" % self.first_published_at.month, + "day": "%02d" % self.first_published_at.day, + "slug": self.slug, + }, + ) + return site_id, root_url, page_path def get_sitemap_urls(self, request): """Override sitemap listings to add priority for featured posts.""" @@ -208,9 +230,12 @@ class BlogLinkPageArchived(LinkPage): subpage_types = [BlogPost] -class BlogLandingPage(StandardHeroMixinNoImage, Page): +class BlogLandingPage(StandardHeroMixinNoImage, RoutablePageMixin, Page): """Container page that defines where Event pages can be created.""" + page_size = 15 + template_name = "blog/blog_landing_page.html" + content_panels = StandardHeroMixinNoImage.content_panels search_fields = StandardHeroMixinNoImage.search_fields @@ -219,26 +244,60 @@ class BlogLandingPage(StandardHeroMixinNoImage, Page): subpage_types = [BlogPost, ContentPage] - def get_posts_for_year_and_month(self, month, year): - # get blogs by year and month - return ( - self.get_children() - .live() - .filter(first_published_at__year=year, first_published_at__month=month) - .order_by("-first_published_at") + def get_context(self, request, year=None, month=None): + context = super().get_context(request) + + if year: + posts = self.get_posts_for_year_and_month(year=year, month=month) + else: + posts = self.get_latest_posts() + + page_number = request.GET.get("page") or 1 + paginator = Paginator(posts, self.page_size) + page = paginator.page(page_number) + context.update( + { + "paginator": paginator, + "is_paginated": page.has_other_pages(), + "page_obj": page, # Used in pagination template + "posts": page, # Used in page template + } ) - def get_posts_for_year(self, year): - # get blogs by year - return ( + context["date_list"] = self.get_list_of_dates() + + return context + + @path("/", name="by-year") + @path("//", name="by-month") + def by_date(self, request, year=None, month=None): + context = self.get_context(request, year=year, month=month) + return self.render(request, context_overrides=context) + + @path("////", name="dated_child") + def dated_child(self, request, year=None, month=None, day=None, slug=None): + child = get_object_or_404( self.get_children() .live() - .filter(first_published_at__year=year) - .order_by("-first_published_at") + .public() + .filter( + first_published_at__year=year, + first_published_at__month=month, + first_published_at__day=day, + ), + slug=slug, ) + return child.specific.serve(request) + + def get_posts_for_year_and_month(self, year=None, month=None): + # get blogs by year and month + child_qs = self.get_latest_posts().filter(first_published_at__year=year) + if month: + child_qs = child_qs.filter(first_published_at__month=month) + return child_qs def get_latest_posts(self): - child_pages = self.get_children().live() + child_pages = self.get_children().live().public().specific() # Fetch all posts ordered by most recently published return child_pages.order_by("-first_published_at") diff --git a/cdhweb/blog/urls.py b/cdhweb/blog/urls.py index c4d2d95d..ed500769 100644 --- a/cdhweb/blog/urls.py +++ b/cdhweb/blog/urls.py @@ -4,17 +4,6 @@ app_name = "blog" urlpatterns = [ - re_path(r"^(?P[\w-]+)/$", views.BlogLandingPageView.as_view(), name="list"), - re_path( - r"^(?P[\w-]+)/(?P\d{4})/$", - views.BlogLandingPageView.as_view(), - name="by-year", - ), - re_path( - r"^(?P[\w-]+)/(?P\d{4})/(?P\d{2})/$", - views.BlogLandingPageView.as_view(), - name="by-month", - ), path("rss/", views.RssBlogPostFeed(), name="rss"), path("atom/", views.AtomBlogPostFeed(), name="atom"), ] diff --git a/cdhweb/blog/views.py b/cdhweb/blog/views.py index bc7676f7..6c4758b8 100644 --- a/cdhweb/blog/views.py +++ b/cdhweb/blog/views.py @@ -1,44 +1,7 @@ from django.contrib.syndication.views import Feed from django.utils.feedgenerator import Atom1Feed -from django.views.generic import ListView -from django.views.generic.detail import DetailView - -from cdhweb.blog.models import BlogLandingPage, BlogPost -from cdhweb.pages.views import LastModifiedMixin - - -class BlogLandingPageView(ListView): - model = BlogLandingPage - template_name = "blog/blog_landing_page.html" - context_object_name = "posts" - paginate_by = 15 - make_object_list = True - - def get_object(self): - slug = self.kwargs.get("slug") - return BlogLandingPage.objects.get(slug=slug) - - def get_queryset(self, **kwargs): - month = self.kwargs.get("month") - year = self.kwargs.get("year") - - if month and year: - posts = self.get_object().get_posts_for_year_and_month( - int(month), int(year) - ) - elif year: - posts = self.get_object().get_posts_for_year(int(year)) - else: - # if month and year are not supplied then supply all posts from newest to oldest - posts = self.get_object().get_latest_posts() - - return posts - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["date_list"] = self.get_object().get_list_of_dates() - context["self"] = self.get_object() - return context + +from cdhweb.blog.models import BlogPost class RssBlogPostFeed(Feed): diff --git a/cdhweb/urls.py b/cdhweb/urls.py index 3ec339c1..e0cc3842 100644 --- a/cdhweb/urls.py +++ b/cdhweb/urls.py @@ -48,7 +48,7 @@ path("_500/", lambda _: 1 / 0), # for testing 500 error page # main apps path("people/", include("cdhweb.people.urls", namespace="people")), - path("", include("cdhweb.blog.urls", namespace="blog")), + path("blog/", include("cdhweb.blog.urls", namespace="blog")), path("events/", include("cdhweb.events.urls", namespace="event")), path("projects/", include("cdhweb.projects.urls", namespace="projects")), # search