2022-06-26 19:25:30 +01:00
|
|
|
import math
|
2022-06-19 13:23:41 +01:00
|
|
|
from typing import Any
|
|
|
|
|
2022-06-14 20:57:43 +01:00
|
|
|
from django.db import models
|
2022-06-19 16:35:56 +01:00
|
|
|
from django.http.request import HttpRequest
|
2022-06-19 20:13:19 +01:00
|
|
|
from django.utils.functional import cached_property, classproperty
|
2022-06-14 20:57:43 +01:00
|
|
|
from wagtail.admin.panels import FieldPanel
|
2022-06-26 18:37:04 +01:00
|
|
|
from wagtail.fields import StreamField
|
2022-06-15 09:27:20 +01:00
|
|
|
from wagtail.images import get_image_model_string
|
2022-06-10 15:48:07 +01:00
|
|
|
from wagtail.models import Page
|
|
|
|
|
2022-07-03 23:10:57 +01:00
|
|
|
from website.common.utils import count_words
|
|
|
|
|
|
|
|
from .streamfield import add_heading_anchors, get_blocks, get_content_html
|
|
|
|
from .utils import TocEntry, extract_text, get_table_of_contents, truncate_string
|
2022-06-19 20:13:19 +01:00
|
|
|
|
2022-06-10 15:48:07 +01:00
|
|
|
|
|
|
|
class BasePage(Page):
|
2022-06-17 14:03:43 +01:00
|
|
|
show_in_menus_default = True
|
|
|
|
|
2022-06-17 15:58:23 +01:00
|
|
|
HERO_IMAGE_SIZE = "width-1200"
|
|
|
|
|
2022-06-10 15:48:07 +01:00
|
|
|
class Meta:
|
|
|
|
abstract = True
|
2022-06-10 15:54:31 +01:00
|
|
|
|
2022-06-14 22:30:39 +01:00
|
|
|
@classproperty
|
2022-06-12 15:17:28 +01:00
|
|
|
def body_class(cls) -> str:
|
2022-06-10 16:22:20 +01:00
|
|
|
return "page-" + cls._meta.db_table.replace("_", "-")
|
2022-06-14 20:57:43 +01:00
|
|
|
|
2022-06-19 16:56:47 +01:00
|
|
|
def get_parent_pages(self) -> models.QuerySet[Page]:
|
|
|
|
"""
|
|
|
|
Shim over the fact everything is in 1 tree
|
|
|
|
"""
|
|
|
|
return self.get_ancestors().reverse().exclude(depth__lte=2)
|
|
|
|
|
2022-06-14 20:57:43 +01:00
|
|
|
|
2022-06-19 13:23:41 +01:00
|
|
|
class BaseContentMixin(models.Model):
|
2022-06-14 20:57:43 +01:00
|
|
|
subtitle = models.CharField(max_length=255, blank=True)
|
2022-06-15 09:27:20 +01:00
|
|
|
hero_image = models.ForeignKey(
|
2022-06-19 11:36:15 +01:00
|
|
|
get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL
|
2022-06-15 09:27:20 +01:00
|
|
|
)
|
2022-06-26 18:37:04 +01:00
|
|
|
body = StreamField(get_blocks(), blank=True, use_json_field=True)
|
2022-06-14 20:57:43 +01:00
|
|
|
|
2022-06-19 13:23:41 +01:00
|
|
|
content_panels = [
|
2022-06-15 09:27:20 +01:00
|
|
|
FieldPanel("subtitle"),
|
|
|
|
FieldPanel("hero_image"),
|
2022-06-26 18:37:04 +01:00
|
|
|
FieldPanel("body"),
|
2022-06-15 09:27:20 +01:00
|
|
|
]
|
2022-06-19 13:23:41 +01:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
2022-06-19 21:16:03 +01:00
|
|
|
@cached_property
|
|
|
|
def table_of_contents(self) -> list[TocEntry]:
|
2022-07-03 23:10:57 +01:00
|
|
|
return get_table_of_contents(self.content_html)
|
2022-06-19 21:16:03 +01:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def reading_time(self) -> int:
|
2022-06-26 19:25:30 +01:00
|
|
|
"""
|
|
|
|
https://help.medium.com/hc/en-us/articles/214991667-Read-time
|
|
|
|
"""
|
|
|
|
return int(math.ceil(self.word_count / 265))
|
2022-06-19 21:16:03 +01:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def word_count(self) -> int:
|
2022-07-03 23:10:57 +01:00
|
|
|
return count_words(self.plain_text)
|
2022-06-19 21:16:03 +01:00
|
|
|
|
2022-06-26 19:52:20 +01:00
|
|
|
@cached_property
|
|
|
|
def summary(self) -> str:
|
2022-07-03 23:10:57 +01:00
|
|
|
return truncate_string(self.plain_text, 50)
|
2022-06-26 19:52:20 +01:00
|
|
|
|
2022-07-01 09:25:57 +01:00
|
|
|
@cached_property
|
2022-07-03 22:00:52 +01:00
|
|
|
def body_html(self) -> str:
|
2022-07-01 09:25:57 +01:00
|
|
|
return add_heading_anchors(str(self.body))
|
|
|
|
|
2022-07-03 23:10:57 +01:00
|
|
|
@cached_property
|
|
|
|
def content_html(self) -> str:
|
2022-07-04 18:55:18 +01:00
|
|
|
return get_content_html(self.body_html)
|
2022-07-03 23:10:57 +01:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def plain_text(self) -> str:
|
|
|
|
return extract_text(self.content_html)
|
|
|
|
|
2022-06-19 13:23:41 +01:00
|
|
|
|
|
|
|
class ContentPage(BasePage, BaseContentMixin): # type: ignore[misc]
|
|
|
|
subpage_types: list[Any] = []
|
|
|
|
content_panels = BasePage.content_panels + BaseContentMixin.content_panels
|
|
|
|
|
|
|
|
|
|
|
|
class ListingPage(BasePage, BaseContentMixin): # type: ignore[misc]
|
|
|
|
content_panels = BasePage.content_panels + BaseContentMixin.content_panels
|
2022-06-19 16:35:56 +01:00
|
|
|
|
|
|
|
def get_context(self, request: HttpRequest) -> dict:
|
|
|
|
context = super().get_context(request)
|
|
|
|
context["child_pages"] = (
|
|
|
|
self.get_children().live().specific().select_related("hero_image")
|
|
|
|
)
|
|
|
|
return context
|