picata 0.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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."""
|