picata 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LICENSE.md +24 -0
- README.md +59 -0
- components/HelloWorld.tsx +11 -0
- entrypoint.tsx +268 -0
- manage.py +15 -0
- picata/__init__.py +1 -0
- picata/apps.py +33 -0
- picata/blocks.py +175 -0
- picata/helpers/__init__.py +70 -0
- picata/helpers/wagtail.py +61 -0
- picata/log_utils.py +47 -0
- picata/middleware.py +54 -0
- picata/migrations/0001_initial.py +264 -0
- picata/migrations/0002_alter_article_content_alter_basicpage_content.py +112 -0
- picata/migrations/0003_alter_article_content_alter_basicpage_content.py +104 -0
- picata/migrations/0004_alter_article_content_alter_basicpage_content.py +105 -0
- picata/migrations/0005_socialsettings.py +48 -0
- picata/migrations/0006_alter_article_content.py +71 -0
- picata/migrations/0007_splitviewpage.py +69 -0
- picata/migrations/0008_alter_splitviewpage_content.py +96 -0
- picata/migrations/0009_alter_splitviewpage_content.py +111 -0
- picata/migrations/0010_alter_splitviewpage_content.py +105 -0
- picata/migrations/0011_alter_splitviewpage_options_and_more.py +113 -0
- picata/migrations/0012_alter_splitviewpage_content.py +109 -0
- picata/migrations/0013_alter_article_content.py +43 -0
- picata/migrations/0014_alter_article_content_alter_article_summary.py +24 -0
- picata/migrations/0015_alter_article_options_article_tagline_and_more.py +28 -0
- picata/migrations/0016_alter_article_options_alter_articletag_options_and_more.py +33 -0
- picata/migrations/0017_articletagrelation_alter_article_tags_and_more.py +35 -0
- picata/migrations/0018_rename_articletag_pagetag_and_more.py +21 -0
- picata/migrations/0019_rename_name_plural_articletype__name_plural.py +18 -0
- picata/migrations/0020_rename__name_plural_articletype__pluralised_name.py +18 -0
- picata/migrations/0021_rename_article_type_article_page_type.py +18 -0
- picata/migrations/0022_homepage.py +28 -0
- picata/migrations/__init__.py +0 -0
- picata/models.py +486 -0
- picata/settings/__init__.py +1 -0
- picata/settings/base.py +345 -0
- picata/settings/dev.py +94 -0
- picata/settings/mypy.py +7 -0
- picata/settings/prod.py +12 -0
- picata/settings/test.py +6 -0
- picata/static/picata/ada-profile.jpg +0 -0
- picata/static/picata/ada-social-bear.jpg +0 -0
- picata/static/picata/favicon.ico +0 -0
- picata/static/picata/fonts/Bitter-Light.ttf +0 -0
- picata/static/picata/fonts/Bitter-LightItalic.ttf +0 -0
- picata/static/picata/fonts/FiraCode-Light.ttf +0 -0
- picata/static/picata/fonts/FiraCode-SemiBold.ttf +0 -0
- picata/static/picata/fonts/Sacramento-Regular.ttf +0 -0
- picata/static/picata/fonts/ZillaSlab-Bold.ttf +0 -0
- picata/static/picata/fonts/ZillaSlab-BoldItalic.ttf +0 -0
- picata/static/picata/fonts/ZillaSlab-Light.ttf +0 -0
- picata/static/picata/fonts/ZillaSlab-LightItalic.ttf +0 -0
- picata/static/picata/fonts/ZillaSlabHighlight-Bold.ttf +0 -0
- picata/static/picata/icons.svg +56 -0
- picata/templates/picata/3_column.html +28 -0
- picata/templates/picata/404.html +11 -0
- picata/templates/picata/500.html +13 -0
- picata/templates/picata/_post_list.html +24 -0
- picata/templates/picata/article.html +20 -0
- picata/templates/picata/base.html +135 -0
- picata/templates/picata/basic_page.html +10 -0
- picata/templates/picata/blocks/icon_link_item.html +7 -0
- picata/templates/picata/blocks/icon_link_list.html +4 -0
- picata/templates/picata/blocks/icon_link_list_stream.html +3 -0
- picata/templates/picata/dl_view.html +18 -0
- picata/templates/picata/home_page.html +21 -0
- picata/templates/picata/post_listing.html +17 -0
- picata/templates/picata/previews/3col.html +73 -0
- picata/templates/picata/previews/dl.html +10 -0
- picata/templates/picata/previews/split.html +10 -0
- picata/templates/picata/previews/theme_gallery.html +158 -0
- picata/templates/picata/search_results.html +28 -0
- picata/templates/picata/split_view.html +15 -0
- picata/templates/picata/tags/site_menu.html +8 -0
- picata/templatetags/__init__.py +1 -0
- picata/templatetags/absolute_static.py +15 -0
- picata/templatetags/menu_tags.py +42 -0
- picata/templatetags/stringify.py +23 -0
- picata/transformers.py +60 -0
- picata/typing/__init__.py +19 -0
- picata/typing/wagtail.py +31 -0
- picata/urls.py +48 -0
- picata/validators.py +36 -0
- picata/views.py +80 -0
- picata/wagtail_hooks.py +43 -0
- picata/wsgi.py +15 -0
- picata-0.0.1.dist-info/METADATA +87 -0
- picata-0.0.1.dist-info/RECORD +94 -0
- picata-0.0.1.dist-info/WHEEL +4 -0
- picata-0.0.1.dist-info/licenses/LICENSE.md +24 -0
- pygments.sass +382 -0
- styles.sass +300 -0
picata/models.py
ADDED
@@ -0,0 +1,486 @@
|
|
1
|
+
"""Django models; mostly subclassed Wagtail classes."""
|
2
|
+
|
3
|
+
# NB: Django's meta-class shenanigans over-complicate type hinting when QuerySets get involved.
|
4
|
+
# pyright: reportAttributeAccessIssue=false
|
5
|
+
|
6
|
+
from collections import OrderedDict
|
7
|
+
from datetime import timedelta
|
8
|
+
from typing import Any, ClassVar, cast
|
9
|
+
|
10
|
+
from django.contrib.auth.models import AnonymousUser, User
|
11
|
+
from django.db.models import (
|
12
|
+
CASCADE,
|
13
|
+
SET_NULL,
|
14
|
+
CharField,
|
15
|
+
ForeignKey,
|
16
|
+
Model,
|
17
|
+
SlugField,
|
18
|
+
TextField,
|
19
|
+
)
|
20
|
+
from django.db.models.functions import Coalesce, ExtractYear
|
21
|
+
from django.http import HttpRequest
|
22
|
+
from django.urls import reverse
|
23
|
+
from hpk.typing import Args, Kwargs
|
24
|
+
from hpk.typing.wagtail import PageContext
|
25
|
+
from modelcluster.contrib.taggit import ClusterTaggableManager
|
26
|
+
from modelcluster.fields import ParentalKey
|
27
|
+
from taggit.models import TagBase, TaggedItemBase
|
28
|
+
from wagtail.admin.panels import FieldPanel, Panel
|
29
|
+
from wagtail.blocks import RichTextBlock
|
30
|
+
from wagtail.contrib.routable_page.models import RoutablePageMixin
|
31
|
+
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
|
32
|
+
from wagtail.fields import RichTextField, StreamField
|
33
|
+
from wagtail.images.blocks import ImageChooserBlock
|
34
|
+
from wagtail.images.models import Image
|
35
|
+
from wagtail.models import Page, PageManager
|
36
|
+
from wagtail.query import PageQuerySet
|
37
|
+
from wagtail.search import index
|
38
|
+
from wagtail_modeladmin.options import ModelAdmin
|
39
|
+
|
40
|
+
from .blocks import (
|
41
|
+
CodeBlock,
|
42
|
+
StaticIconLinkListsBlock,
|
43
|
+
WrappedImageChooserBlock,
|
44
|
+
)
|
45
|
+
|
46
|
+
|
47
|
+
class BasePageContext(PageContext, total=False):
|
48
|
+
"""Return-type for an `Article`'s context dictionary."""
|
49
|
+
|
50
|
+
url: str
|
51
|
+
published: bool | str
|
52
|
+
updated: bool | str
|
53
|
+
latest_draft: str
|
54
|
+
draft_url: str
|
55
|
+
title: str
|
56
|
+
|
57
|
+
|
58
|
+
class BasePage(Page):
|
59
|
+
"""Mixin for `Page`-types offering previews of themselves on other `Page`s."""
|
60
|
+
|
61
|
+
@property
|
62
|
+
def preview_data(self) -> dict[str, Any]:
|
63
|
+
"""Return a dictionary of data used in previewing this page type."""
|
64
|
+
return {"title": self.seo_title or self.title, "summary": self.search_description}
|
65
|
+
|
66
|
+
def get_publication_data(self, request: HttpRequest | None = None) -> dict[str, str]:
|
67
|
+
"""Helper method to calculate and format relevant dates for previews."""
|
68
|
+
site = self.get_site()
|
69
|
+
last_edited = self.latest_revision.created_at
|
70
|
+
year = self.first_published_at.year if self.first_published_at else last_edited.year
|
71
|
+
published, updated = self.first_published_at, self.last_published_at
|
72
|
+
|
73
|
+
# Convert datetime objects to strings like "3 Jan, '25", or False, and
|
74
|
+
# give a grace-period of one week for edits before marking the post as "updated"
|
75
|
+
published_str = f"{published.day} {published:%b '%y}" if published else False
|
76
|
+
updated_str = (
|
77
|
+
f"{updated.day} {updated:%b '%y}"
|
78
|
+
if published and updated and (updated >= published + timedelta(weeks=1))
|
79
|
+
else False
|
80
|
+
)
|
81
|
+
|
82
|
+
data = {
|
83
|
+
"year": year,
|
84
|
+
"url": self.relative_url(site),
|
85
|
+
"published": published_str,
|
86
|
+
"updated": updated_str,
|
87
|
+
}
|
88
|
+
|
89
|
+
# Add last draft date & preview URL if there's an unpublished draft, for logged-in users
|
90
|
+
if (
|
91
|
+
(request and request.user.is_authenticated)
|
92
|
+
and (not published or (updated and last_edited > updated))
|
93
|
+
and hasattr(self, "id")
|
94
|
+
):
|
95
|
+
data.update(
|
96
|
+
{
|
97
|
+
"latest_draft": f"{last_edited.day} {last_edited:%b '%y}",
|
98
|
+
"draft_url": reverse("wagtailadmin_pages:preview_on_edit", args=[self.id]),
|
99
|
+
}
|
100
|
+
)
|
101
|
+
|
102
|
+
return data
|
103
|
+
|
104
|
+
def get_context(self, request: HttpRequest, *args: Args, **kwargs: Kwargs) -> BasePageContext:
|
105
|
+
"""Gather any publication and preview data available for the page into the context."""
|
106
|
+
from hpk.helpers.wagtail import page_preview_data
|
107
|
+
|
108
|
+
context = super().get_context(request, *args, **kwargs)
|
109
|
+
context.update(page_preview_data(request, self))
|
110
|
+
return cast(BasePageContext, {**context})
|
111
|
+
|
112
|
+
class Meta:
|
113
|
+
"""Declare `BasePage` as an abstract `Page` class."""
|
114
|
+
|
115
|
+
abstract = True
|
116
|
+
|
117
|
+
|
118
|
+
# @register_snippet
|
119
|
+
class PageTag(TagBase):
|
120
|
+
"""Custom tag model for articles."""
|
121
|
+
|
122
|
+
def __str__(self) -> str:
|
123
|
+
"""String representation of the tag."""
|
124
|
+
return self.name
|
125
|
+
|
126
|
+
|
127
|
+
class PageTagRelation(TaggedItemBase):
|
128
|
+
"""Associates an PageTag with an Page."""
|
129
|
+
|
130
|
+
tag: ForeignKey[PageTag] = ForeignKey(
|
131
|
+
PageTag,
|
132
|
+
related_name="tagged_items",
|
133
|
+
on_delete=CASCADE,
|
134
|
+
)
|
135
|
+
content_object = ParentalKey(
|
136
|
+
"Article",
|
137
|
+
on_delete=CASCADE,
|
138
|
+
related_name="tagged_items",
|
139
|
+
)
|
140
|
+
|
141
|
+
|
142
|
+
class TaggedPage(BasePage):
|
143
|
+
"""Abstract base for a `Page` type supporting tags."""
|
144
|
+
|
145
|
+
tags = ClusterTaggableManager(
|
146
|
+
through=PageTagRelation,
|
147
|
+
blank=True,
|
148
|
+
help_text="Tags for the article.",
|
149
|
+
)
|
150
|
+
|
151
|
+
content_panels: ClassVar[list[Panel]] = [
|
152
|
+
*BasePage.content_panels,
|
153
|
+
FieldPanel("tags"),
|
154
|
+
]
|
155
|
+
|
156
|
+
class Meta:
|
157
|
+
"""Declare `BasePage` as an abstract `Page` class."""
|
158
|
+
|
159
|
+
abstract = True
|
160
|
+
|
161
|
+
|
162
|
+
class BasicPage(BasePage):
|
163
|
+
"""A basic page model for static content."""
|
164
|
+
|
165
|
+
template = "picata/basic_page.html"
|
166
|
+
|
167
|
+
content = StreamField(
|
168
|
+
[
|
169
|
+
("rich_text", RichTextBlock()),
|
170
|
+
("code", CodeBlock()),
|
171
|
+
("image", ImageChooserBlock()),
|
172
|
+
],
|
173
|
+
use_json_field=True,
|
174
|
+
blank=True,
|
175
|
+
help_text="Main content for the page.",
|
176
|
+
)
|
177
|
+
|
178
|
+
content_panels: ClassVar[list[FieldPanel]] = [
|
179
|
+
*BasePage.content_panels,
|
180
|
+
FieldPanel("content"),
|
181
|
+
]
|
182
|
+
|
183
|
+
search_fields: ClassVar[list[index.SearchField]] = [
|
184
|
+
*Page.search_fields,
|
185
|
+
index.SearchField("content"),
|
186
|
+
]
|
187
|
+
|
188
|
+
|
189
|
+
class SplitViewPage(BasePage):
|
190
|
+
"""A page with 50%-width divs, split down the middle."""
|
191
|
+
|
192
|
+
template = "picata/split_view.html"
|
193
|
+
|
194
|
+
content = StreamField(
|
195
|
+
[
|
196
|
+
("rich_text", RichTextBlock()),
|
197
|
+
("code", CodeBlock()),
|
198
|
+
("image", WrappedImageChooserBlock()),
|
199
|
+
("icon_link_lists", StaticIconLinkListsBlock()),
|
200
|
+
],
|
201
|
+
use_json_field=True,
|
202
|
+
blank=True,
|
203
|
+
help_text="Main content for the split-view page.",
|
204
|
+
)
|
205
|
+
|
206
|
+
content_panels: ClassVar[list[FieldPanel]] = [
|
207
|
+
*BasePage.content_panels,
|
208
|
+
FieldPanel("content"),
|
209
|
+
]
|
210
|
+
|
211
|
+
search_fields: ClassVar[list[index.SearchField]] = [
|
212
|
+
*Page.search_fields,
|
213
|
+
index.SearchField("content"),
|
214
|
+
]
|
215
|
+
|
216
|
+
class Meta:
|
217
|
+
"""Declare explicit human-readable names for the page type."""
|
218
|
+
|
219
|
+
verbose_name = "split-view page"
|
220
|
+
verbose_name_plural = "split-view pages"
|
221
|
+
|
222
|
+
|
223
|
+
class ArticleType(Model): # type: ignore[django-manager-missing]
|
224
|
+
"""Defines a type of article, like Blog Post, Review, or Guide."""
|
225
|
+
|
226
|
+
name = CharField(max_length=100, unique=True, help_text="Name of the article type.")
|
227
|
+
_Pluralised_name = CharField(
|
228
|
+
max_length=100,
|
229
|
+
blank=True,
|
230
|
+
help_text="Plural form of the article type name (optional). Defaults to appending 's'.",
|
231
|
+
)
|
232
|
+
slug = SlugField(unique=True, max_length=100)
|
233
|
+
description = TextField(blank=True, help_text="Optional description of this type.")
|
234
|
+
|
235
|
+
def __str__(self) -> str:
|
236
|
+
"""Return the name of the ArticleType."""
|
237
|
+
return self.name
|
238
|
+
|
239
|
+
@property
|
240
|
+
def name_plural(self) -> str:
|
241
|
+
"""Return the plural name of the article type."""
|
242
|
+
return self._Pluralised_name or f"{self.name}s"
|
243
|
+
|
244
|
+
@property
|
245
|
+
def indefinite_article(self) -> str:
|
246
|
+
"""Return a string like 'a guide' or 'an article'."""
|
247
|
+
name_lower = self.name.lower()
|
248
|
+
return f"{'an' if name_lower[0] in 'aeiou' else 'a'} {name_lower}"
|
249
|
+
|
250
|
+
|
251
|
+
class ArticleTypeAdmin(ModelAdmin):
|
252
|
+
"""Wagtail admin integration for managing article types."""
|
253
|
+
|
254
|
+
model = ArticleType
|
255
|
+
menu_label = "Article Types" # Label for the menu item
|
256
|
+
menu_icon = "tag" # Icon for the menu item (from Wagtail icon set)
|
257
|
+
add_to_settings_menu = True # Whether to add to the "Settings" menu
|
258
|
+
list_display = ("name", "slug") # Fields to display in the listing
|
259
|
+
search_fields = ("name", "slug") # Fields to include in the search bar
|
260
|
+
|
261
|
+
|
262
|
+
class ArticleContext(BasePageContext):
|
263
|
+
"""Return-type for an `Article`'s context dictionary."""
|
264
|
+
|
265
|
+
content: str
|
266
|
+
|
267
|
+
|
268
|
+
class ArticleQuerySet(PageQuerySet):
|
269
|
+
"""Default `QuerySet` for all `Article`-type pages."""
|
270
|
+
|
271
|
+
def with_effective_date(self) -> PageQuerySet:
|
272
|
+
"""Annotate with 'effective_date' to allow date-ordering to consider recent drafts."""
|
273
|
+
return self.annotate(
|
274
|
+
effective_date=Coalesce("first_published_at", "latest_revision_created_at")
|
275
|
+
)
|
276
|
+
|
277
|
+
def by_date(self) -> PageQuerySet:
|
278
|
+
"""Return all `Article` pages, ordered by decending "effective" date."""
|
279
|
+
return self.with_effective_date().order_by("-effective_date")
|
280
|
+
|
281
|
+
def live_for_user(self, user: AnonymousUser | User) -> PageQuerySet:
|
282
|
+
"""Filter out non-live pages for non-authenticated users."""
|
283
|
+
return self if user.is_authenticated else self.live()
|
284
|
+
|
285
|
+
|
286
|
+
class Article(TaggedPage):
|
287
|
+
"""Class for article-like pages."""
|
288
|
+
|
289
|
+
template = "picata/article.html"
|
290
|
+
objects = PageManager.from_queryset(ArticleQuerySet)()
|
291
|
+
|
292
|
+
tagline: CharField = CharField(blank=True, help_text="A short tagline for the article.")
|
293
|
+
summary = RichTextField(blank=True, help_text="A summary to be displayed in previews.")
|
294
|
+
content = StreamField(
|
295
|
+
[
|
296
|
+
("rich_text", RichTextBlock()),
|
297
|
+
("code", CodeBlock()),
|
298
|
+
("image", ImageChooserBlock()),
|
299
|
+
],
|
300
|
+
use_json_field=True,
|
301
|
+
blank=True,
|
302
|
+
help_text="Main content for the article.",
|
303
|
+
)
|
304
|
+
|
305
|
+
page_type: ForeignKey[ArticleType | None] = ForeignKey(
|
306
|
+
ArticleType,
|
307
|
+
null=True,
|
308
|
+
blank=True,
|
309
|
+
on_delete=SET_NULL,
|
310
|
+
related_name="articles",
|
311
|
+
help_text="Select the type of article.",
|
312
|
+
)
|
313
|
+
|
314
|
+
content_panels: ClassVar[list[Panel]] = [
|
315
|
+
*TaggedPage.content_panels,
|
316
|
+
FieldPanel("tagline"),
|
317
|
+
FieldPanel("summary"),
|
318
|
+
FieldPanel("content"),
|
319
|
+
FieldPanel("page_type"),
|
320
|
+
]
|
321
|
+
|
322
|
+
search_fields: ClassVar[list[index.SearchField]] = [
|
323
|
+
*TaggedPage.search_fields,
|
324
|
+
index.SearchField("tagline"),
|
325
|
+
index.SearchField("summary"),
|
326
|
+
index.SearchField("content"),
|
327
|
+
index.SearchField("tags"),
|
328
|
+
index.SearchField("page_type"),
|
329
|
+
]
|
330
|
+
|
331
|
+
@property
|
332
|
+
def preview_data(self) -> dict[str, Any]:
|
333
|
+
"""Return data required to render a preview of this article."""
|
334
|
+
return {
|
335
|
+
**super().preview_data,
|
336
|
+
"tagline": self.tagline,
|
337
|
+
"summary": self.summary,
|
338
|
+
"page_type": self.page_type,
|
339
|
+
"tags": list(self.tags.all()),
|
340
|
+
}
|
341
|
+
|
342
|
+
def get_context(self, request: HttpRequest, *args: Args, **kwargs: Kwargs) -> ArticleContext:
|
343
|
+
"""Provide extra context needed for the `Article` to render itself."""
|
344
|
+
context = dict(super().get_context(request, *args, **kwargs))
|
345
|
+
context.update({"content": self.content})
|
346
|
+
return cast(ArticleContext, context)
|
347
|
+
|
348
|
+
|
349
|
+
class PostGroupePageContext(PageContext):
|
350
|
+
"""Return-type for a `PostGroupPage`'s context dictionary."""
|
351
|
+
|
352
|
+
posts: OrderedDict[int, list[dict[str, str]]]
|
353
|
+
|
354
|
+
|
355
|
+
class PostGroupPage(RoutablePageMixin, Page):
|
356
|
+
"""A top-level page for grouping various types of posts or articles."""
|
357
|
+
|
358
|
+
template = "picata/post_listing.html"
|
359
|
+
subpage_types: ClassVar[list[str]] = ["hpk.Article"]
|
360
|
+
|
361
|
+
intro = RichTextField(blank=True, help_text="An optional introduction to this group.")
|
362
|
+
|
363
|
+
content_panels: ClassVar[list[Panel]] = [*BasePage.content_panels, FieldPanel("intro")]
|
364
|
+
|
365
|
+
def get_context(
|
366
|
+
self, request: HttpRequest, *args: Args, **kwargs: Kwargs
|
367
|
+
) -> PostGroupePageContext:
|
368
|
+
"""Add a dictionary of posts grouped by year to the context dict."""
|
369
|
+
children = self.get_children()
|
370
|
+
if not request.user.is_authenticated:
|
371
|
+
children = children.live()
|
372
|
+
children = children.specific()
|
373
|
+
children = children.annotate(
|
374
|
+
effective_date=Coalesce("first_published_at", "latest_revision_created_at"),
|
375
|
+
year_published=ExtractYear("first_published_at"),
|
376
|
+
)
|
377
|
+
|
378
|
+
# Create an OrderedDict grouping posts by year in reverse chronological order
|
379
|
+
posts_by_year: OrderedDict = OrderedDict()
|
380
|
+
for child in children.order_by("-effective_date"):
|
381
|
+
post_data = getattr(child, "preview_data", {}).copy()
|
382
|
+
post_data.update(**child.get_publication_data(request))
|
383
|
+
|
384
|
+
# Group posts by year, defaulting to last-draft year if unpublished
|
385
|
+
if post_data["year"] not in posts_by_year:
|
386
|
+
posts_by_year[post_data["year"]] = []
|
387
|
+
posts_by_year[post_data["year"]].append(post_data)
|
388
|
+
|
389
|
+
return cast(
|
390
|
+
PostGroupePageContext,
|
391
|
+
{**super().get_context(request, *args, **kwargs), "posts_by_year": posts_by_year},
|
392
|
+
)
|
393
|
+
|
394
|
+
class Meta:
|
395
|
+
"""Declare more human-friendly names for the page type."""
|
396
|
+
|
397
|
+
verbose_name: str = "post listing"
|
398
|
+
verbose_name_plural: str = "post listings"
|
399
|
+
|
400
|
+
|
401
|
+
@register_setting
|
402
|
+
class SocialSettings(BaseSiteSetting):
|
403
|
+
"""Site-wide social media configuration."""
|
404
|
+
|
405
|
+
default_social_image: ForeignKey[Image] = ForeignKey(
|
406
|
+
Image,
|
407
|
+
null=True,
|
408
|
+
blank=True,
|
409
|
+
on_delete=SET_NULL,
|
410
|
+
help_text="Default image for social media previews.",
|
411
|
+
related_name="+",
|
412
|
+
)
|
413
|
+
|
414
|
+
panels: ClassVar[list[Panel]] = [
|
415
|
+
FieldPanel("default_social_image"),
|
416
|
+
]
|
417
|
+
|
418
|
+
|
419
|
+
class HomePageContext(BasePageContext):
|
420
|
+
"""Return-type for the `HomePage`'s context dictionary."""
|
421
|
+
|
422
|
+
top_content: str
|
423
|
+
bottom_content: str
|
424
|
+
recent_posts: list[BasePage]
|
425
|
+
|
426
|
+
|
427
|
+
class HomePage(BasePage):
|
428
|
+
"""Single-use specialised page for the root of the site."""
|
429
|
+
|
430
|
+
template = "picata/home_page.html"
|
431
|
+
|
432
|
+
top_content = StreamField(
|
433
|
+
[
|
434
|
+
("rich_text", RichTextBlock()),
|
435
|
+
("image", WrappedImageChooserBlock()),
|
436
|
+
("icon_link_lists", StaticIconLinkListsBlock()),
|
437
|
+
],
|
438
|
+
use_json_field=True,
|
439
|
+
blank=True,
|
440
|
+
help_text="Content stream above 'Recent posts'",
|
441
|
+
)
|
442
|
+
|
443
|
+
bottom_content = StreamField(
|
444
|
+
[
|
445
|
+
("rich_text", RichTextBlock()),
|
446
|
+
("image", WrappedImageChooserBlock()),
|
447
|
+
("icon_link_lists", StaticIconLinkListsBlock()),
|
448
|
+
],
|
449
|
+
use_json_field=True,
|
450
|
+
blank=True,
|
451
|
+
help_text="Content stream rendered under 'Recent posts'",
|
452
|
+
)
|
453
|
+
|
454
|
+
content_panels: ClassVar[list[FieldPanel]] = [
|
455
|
+
*BasePage.content_panels,
|
456
|
+
FieldPanel("top_content"),
|
457
|
+
FieldPanel("bottom_content"),
|
458
|
+
]
|
459
|
+
|
460
|
+
search_fields: ClassVar[list[index.SearchField]] = [
|
461
|
+
*Page.search_fields,
|
462
|
+
index.SearchField("top_content"),
|
463
|
+
index.SearchField("bottom_content"),
|
464
|
+
]
|
465
|
+
|
466
|
+
def get_context(self, request: HttpRequest, *args: Args, **kwargs: Kwargs) -> HomePageContext:
|
467
|
+
"""Add content streams and a recent posts list to the context."""
|
468
|
+
from hpk.helpers.wagtail import page_preview_data
|
469
|
+
|
470
|
+
recent_posts = Article.objects.live_for_user(request.user).by_date()
|
471
|
+
recent_posts = [page_preview_data(request, post) for post in recent_posts]
|
472
|
+
|
473
|
+
return cast(
|
474
|
+
HomePageContext,
|
475
|
+
{
|
476
|
+
**dict(super().get_context(request, *args, **kwargs)),
|
477
|
+
"top_content": self.top_content,
|
478
|
+
"bottom_content": self.bottom_content,
|
479
|
+
"recent_posts": recent_posts,
|
480
|
+
},
|
481
|
+
)
|
482
|
+
|
483
|
+
class Meta:
|
484
|
+
"""Declare explicit human-readable names for the page type."""
|
485
|
+
|
486
|
+
verbose_name = "home page"
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Django settings package for the project."""
|