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
@@ -0,0 +1,23 @@
|
|
1
|
+
"""Template tags to transform Python types into human-readable strings."""
|
2
|
+
|
3
|
+
from django import template
|
4
|
+
|
5
|
+
register = template.Library()
|
6
|
+
|
7
|
+
|
8
|
+
@register.filter
|
9
|
+
def stringify(value: list, quote_style: str | None = None) -> str:
|
10
|
+
"""Convert a list of strings into a human-readable string with optional quoting."""
|
11
|
+
if not isinstance(value, list):
|
12
|
+
raise TypeError("The 'stringify' filter currently only supports lists.")
|
13
|
+
|
14
|
+
quote = "'" if quote_style == "single" else '"' if quote_style == "double" else ""
|
15
|
+
quoted_items = [f"{quote}{item!s}{quote}" for item in value]
|
16
|
+
|
17
|
+
if len(quoted_items) == 0:
|
18
|
+
return ""
|
19
|
+
if len(quoted_items) == 1:
|
20
|
+
return quoted_items[0]
|
21
|
+
if len(quoted_items) == 2: # noqa: PLR2004
|
22
|
+
return " and ".join(quoted_items)
|
23
|
+
return f"{', '.join(quoted_items[:-1])}, and {quoted_items[-1]}"
|
picata/transformers.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
"""Callables to transform the response."""
|
2
|
+
|
3
|
+
from lxml import etree
|
4
|
+
|
5
|
+
from hpk.helpers import ALPHANUMERIC_REGEX, get_full_text
|
6
|
+
|
7
|
+
|
8
|
+
def add_heading_ids(tree: etree._Element) -> None:
|
9
|
+
"""Add a unique id to any heading in <main> missing one, derived from its inner text."""
|
10
|
+
seen_ids = set()
|
11
|
+
main = tree.xpath("/html/body/main")
|
12
|
+
if not main:
|
13
|
+
return
|
14
|
+
|
15
|
+
for heading in main[0].xpath(".//h1|//h2|//h3|//h4|//h5|//h6"):
|
16
|
+
if heading.get("id"):
|
17
|
+
continue
|
18
|
+
heading_text = get_full_text(heading)
|
19
|
+
slug = heading_text.lower().replace(" ", "-")
|
20
|
+
unique_id = slug
|
21
|
+
count = 1
|
22
|
+
while unique_id in seen_ids:
|
23
|
+
unique_id = f"{slug}-{count}"
|
24
|
+
count += 1
|
25
|
+
seen_ids.add(unique_id)
|
26
|
+
heading.set("id", unique_id)
|
27
|
+
|
28
|
+
|
29
|
+
class AnchorInserter:
|
30
|
+
"""Transformer to insert anchored pilcrows into targeted elements in the document."""
|
31
|
+
|
32
|
+
def __init__(self, root: str, targets: str) -> None:
|
33
|
+
"""Remember the root paths we're to operate on."""
|
34
|
+
self.root_xpath = root
|
35
|
+
self.targets_xpath = targets
|
36
|
+
|
37
|
+
def __call__(self, tree: etree._Element) -> None:
|
38
|
+
"""Inserts anchors into targets within the specified roots."""
|
39
|
+
for root_element in tree.xpath(self.root_xpath):
|
40
|
+
self._process_targets(root_element, self.targets_xpath)
|
41
|
+
|
42
|
+
def _process_targets(self, root: etree._Element, targets: str) -> None:
|
43
|
+
"""Processes targets within a given root element, inserting anchors."""
|
44
|
+
for target in root.xpath(targets):
|
45
|
+
target_id = target.get("id")
|
46
|
+
if not target_id or target.xpath(".//a"):
|
47
|
+
continue
|
48
|
+
|
49
|
+
sanitized_id = self._sanitize_id(target_id)
|
50
|
+
if sanitized_id != target_id:
|
51
|
+
target.set("id", sanitized_id)
|
52
|
+
|
53
|
+
# Append an anchored pilcrow to the target element
|
54
|
+
anchor = etree.Element("a", href=f"#{target_id}", **{"class": "target-link"})
|
55
|
+
anchor.text = "¶"
|
56
|
+
target.append(anchor)
|
57
|
+
|
58
|
+
def _sanitize_id(self, id_value: str) -> str:
|
59
|
+
"""Sanitize the ID by removing non-alphanumeric characters."""
|
60
|
+
return ALPHANUMERIC_REGEX.sub("", id_value)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"""Reusable types for the hpk project."""
|
2
|
+
|
3
|
+
from typing import Any, TypedDict
|
4
|
+
|
5
|
+
from django.http import HttpRequest
|
6
|
+
|
7
|
+
# Generic arguments and keyword arguments
|
8
|
+
Args = tuple[Any, ...]
|
9
|
+
Kwargs = dict[str, Any]
|
10
|
+
|
11
|
+
|
12
|
+
class Context(TypedDict):
|
13
|
+
"""Base class for context dicts passed all around the system."""
|
14
|
+
|
15
|
+
request: HttpRequest
|
16
|
+
|
17
|
+
|
18
|
+
# Log arguments for structured logging
|
19
|
+
LogArg = str | int | float | bool
|
picata/typing/wagtail.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Types for Wagtail.
|
2
|
+
|
3
|
+
NB: This module split the hpk.typing module into a package in order to avoid
|
4
|
+
circular imports during program initialisation, with the `from wagtail` imports.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from wagtail.blocks import StructBlock
|
10
|
+
from wagtail.models import Page
|
11
|
+
|
12
|
+
from . import Context
|
13
|
+
|
14
|
+
|
15
|
+
# StructBlock types
|
16
|
+
class BlockRenderContextDict(Context):
|
17
|
+
"""Context dicts passed to render functions for Blocks."""
|
18
|
+
|
19
|
+
self: StructBlock
|
20
|
+
page: Page
|
21
|
+
|
22
|
+
|
23
|
+
BlockRenderContext = BlockRenderContextDict | None
|
24
|
+
BlockRenderValue = dict[str, Any]
|
25
|
+
|
26
|
+
|
27
|
+
class PageContext(Context):
|
28
|
+
"""Base class for Wagtail Page classes."""
|
29
|
+
|
30
|
+
self: Page
|
31
|
+
page: Page
|
picata/urls.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Top-level URL configuration for the site."""
|
2
|
+
|
3
|
+
from debug_toolbar.toolbar import debug_toolbar_urls
|
4
|
+
from django.conf import settings
|
5
|
+
from django.contrib import admin
|
6
|
+
from django.urls import include, path, re_path
|
7
|
+
from wagtail import urls as wagtail_urls
|
8
|
+
from wagtail.admin import urls as wagtailadmin_urls
|
9
|
+
from wagtail.contrib.sitemaps.views import sitemap
|
10
|
+
from wagtail.documents import urls as wagtaildocs_urls
|
11
|
+
from wagtail.images.views.serve import ServeView
|
12
|
+
|
13
|
+
from hpk.views import search
|
14
|
+
|
15
|
+
urlpatterns = [
|
16
|
+
path("django-admin/", admin.site.urls), # Django Admin
|
17
|
+
path("admin/", include(wagtailadmin_urls)), # Wagtail Admin
|
18
|
+
path("documents/", include(wagtaildocs_urls)), # Wagtail documents
|
19
|
+
re_path(
|
20
|
+
r"^images/([^/]*)/(\d*)/([^/]*)/[^/]*$", ServeView.as_view(), name="wagtailimages_serve"
|
21
|
+
),
|
22
|
+
path("sitemap.xml", sitemap),
|
23
|
+
path("search/", search, name="search"),
|
24
|
+
]
|
25
|
+
|
26
|
+
# Debug-mode-only URLs
|
27
|
+
if settings.DEBUG:
|
28
|
+
from django.conf.urls.static import static
|
29
|
+
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
30
|
+
from django.views.generic import RedirectView
|
31
|
+
|
32
|
+
from hpk.views import debug_shell, preview
|
33
|
+
|
34
|
+
# Serve static and media files from development server
|
35
|
+
urlpatterns += staticfiles_urlpatterns()
|
36
|
+
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
37
|
+
|
38
|
+
# Enable Django Debug Toolbar
|
39
|
+
urlpatterns += debug_toolbar_urls()
|
40
|
+
|
41
|
+
urlpatterns += [
|
42
|
+
path("favicon.ico", RedirectView.as_view(url=settings.STATIC_URL + "favicon.ico")),
|
43
|
+
path("shell/", debug_shell), # Just raises an exception (to invoke Werkzeug shell access)
|
44
|
+
path("preview/<slug:file>/", preview, name="debug_preview"), # templates/previews/<file>
|
45
|
+
]
|
46
|
+
|
47
|
+
# Let Wagtail take care of the rest
|
48
|
+
urlpatterns += [path("", include(wagtail_urls))]
|
picata/validators.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
"""Custom validators for Django fields."""
|
2
|
+
|
3
|
+
from django.core.exceptions import ValidationError
|
4
|
+
from django.core.validators import URLValidator
|
5
|
+
|
6
|
+
|
7
|
+
class HREFValidator:
|
8
|
+
"""Custom validator for href attributes in HTML anchor tags.
|
9
|
+
|
10
|
+
Basically it's `URLValidator` but with 'mailto:', 'tel:' and 'file:' added.
|
11
|
+
(Doesn't actually validate email addresses, telephone numbers, or file paths.)
|
12
|
+
"""
|
13
|
+
|
14
|
+
def __init__(
|
15
|
+
self, extra_schemes: list[str] | None = None, url_schemes: list[str] | None = None
|
16
|
+
) -> None:
|
17
|
+
"""Store schemes we check for and initialise a basic `URLValidator`."""
|
18
|
+
self.schemes: list[str] = extra_schemes or ["mailto", "tel", "file", "sms"]
|
19
|
+
self.base_validator = URLValidator(schemes=url_schemes)
|
20
|
+
|
21
|
+
def __call__(self, value: str) -> None:
|
22
|
+
"""Try validating a custom scheme (e.g. "mailto:…") if the `URLValidator` fails."""
|
23
|
+
try:
|
24
|
+
self.base_validator(value)
|
25
|
+
except ValidationError as err:
|
26
|
+
if ":" in value:
|
27
|
+
if not any(value.startswith(f"{scheme}:") for scheme in self.schemes):
|
28
|
+
raise ValidationError(
|
29
|
+
f"'{value}' is not a valid href.", code="invalid_href"
|
30
|
+
) from err
|
31
|
+
elif not value.startswith(("/", "#")): # Allow relative links or fragments
|
32
|
+
raise ValidationError(
|
33
|
+
f"'{value}' is not a valid href.", code="invalid_href"
|
34
|
+
) from err
|
35
|
+
else:
|
36
|
+
return
|
picata/views.py
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
"""Top-level views for the site."""
|
2
|
+
|
3
|
+
# NB: Django's meta-class shenanigans over-complicate type hinting when QuerySets get involved.
|
4
|
+
# pyright: reportAttributeAccessIssue=false, reportArgumentType=false
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import TYPE_CHECKING, NoReturn
|
8
|
+
|
9
|
+
from django.http import HttpRequest, HttpResponse
|
10
|
+
from django.shortcuts import render
|
11
|
+
from hpk.helpers.wagtail import (
|
12
|
+
filter_pages_by_tags,
|
13
|
+
filter_pages_by_type,
|
14
|
+
page_preview_data,
|
15
|
+
visible_pages_qs,
|
16
|
+
)
|
17
|
+
from hpk.models import ArticleType
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from wagtail.query import PageQuerySet
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
def debug_shell(request: HttpRequest) -> NoReturn:
|
26
|
+
"""Just `assert False`, to force an exception and get to the Werkzeug debug console."""
|
27
|
+
logger.info(
|
28
|
+
"Raising `assert False` in the `debug_shell` view. "
|
29
|
+
"Request details: method=%s, path=%s, user=%s",
|
30
|
+
request.method,
|
31
|
+
request.path,
|
32
|
+
request.user if request.user.is_authenticated else "Anonymous",
|
33
|
+
)
|
34
|
+
assert False # noqa: B011, PT015, S101
|
35
|
+
|
36
|
+
|
37
|
+
def preview(request: HttpRequest, file: str) -> HttpResponse:
|
38
|
+
"""Render a named template from the "templates/previews/" directory."""
|
39
|
+
return render(request, f"picata/previews/{file}.html")
|
40
|
+
|
41
|
+
|
42
|
+
def search(request: HttpRequest) -> HttpResponse:
|
43
|
+
"""Render search results from the `query` and `tags` GET parameters."""
|
44
|
+
results: dict[str, str | list[str] | set[str]] = {}
|
45
|
+
|
46
|
+
# Base QuerySet for all pages
|
47
|
+
pages: PageQuerySet = visible_pages_qs(request)
|
48
|
+
|
49
|
+
# Perform search by query
|
50
|
+
query_string = request.GET.get("query")
|
51
|
+
if query_string:
|
52
|
+
pages = pages.search(query_string)
|
53
|
+
results["query"] = query_string
|
54
|
+
|
55
|
+
# Resolve specific pages post-search
|
56
|
+
specific_pages = [page.specific for page in pages]
|
57
|
+
|
58
|
+
# Filter by page types
|
59
|
+
page_types_string = request.GET.get("page_types")
|
60
|
+
if page_types_string:
|
61
|
+
page_type_slugs = {slug.strip() for slug in page_types_string.split(",") if slug.strip()}
|
62
|
+
matching_page_types = ArticleType.objects.filter(slug__in=page_type_slugs)
|
63
|
+
specific_pages = filter_pages_by_type(specific_pages, page_type_slugs)
|
64
|
+
results["page_types"] = [page_type.name for page_type in matching_page_types]
|
65
|
+
|
66
|
+
# Filter by tags
|
67
|
+
tags_string = request.GET.get("tags")
|
68
|
+
if tags_string:
|
69
|
+
tags = {tag.strip() for tag in tags_string.split(",") if tag.strip()}
|
70
|
+
specific_pages = filter_pages_by_tags(specific_pages, tags)
|
71
|
+
results["tags"] = tags
|
72
|
+
|
73
|
+
# Handle empty cases
|
74
|
+
if not (query_string or tags_string or page_types_string):
|
75
|
+
specific_pages = []
|
76
|
+
|
77
|
+
# Enhance pages with preview and publication data
|
78
|
+
page_previews = [page_preview_data(request, page) for page in specific_pages]
|
79
|
+
|
80
|
+
return render(request, "picata/search_results.html", {**results, "pages": page_previews})
|
picata/wagtail_hooks.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Wagtail hooks, used to customise view-level behaviour of the Wagtail admin and front-end.
|
2
|
+
|
3
|
+
See: https://docs.wagtail.org/en/stable/reference/hooks.html
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import ClassVar
|
8
|
+
|
9
|
+
from django.db.models import QuerySet, Value
|
10
|
+
from django.db.models.functions import Coalesce
|
11
|
+
from django.http import HttpRequest
|
12
|
+
from wagtail import hooks
|
13
|
+
from wagtail.models import Page
|
14
|
+
from wagtail.snippets.views.snippets import SnippetViewSet
|
15
|
+
|
16
|
+
from hpk.models import PageTag
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
@hooks.register("construct_explorer_page_queryset") # type: ignore[reportOptionalCall]
|
22
|
+
def order_admin_menu_by_date(parent_page: Page, pages: QuerySet, request: HttpRequest) -> QuerySet: # noqa: ARG001
|
23
|
+
"""Order admin menus latest-at-top for pages with lots of children (like 'blog')."""
|
24
|
+
if parent_page.slug == "blog":
|
25
|
+
# Sort directly in order_by with Coalesce
|
26
|
+
return pages.order_by(
|
27
|
+
Coalesce("first_published_at", "latest_revision_created_at", Value("1970-01-01")).desc()
|
28
|
+
)
|
29
|
+
return pages
|
30
|
+
|
31
|
+
|
32
|
+
class PageTagViewSet(SnippetViewSet):
|
33
|
+
"""Viewset for managing `PageTag`s."""
|
34
|
+
|
35
|
+
icon: str = "tag"
|
36
|
+
list_display: ClassVar[list[str]] = ["name"]
|
37
|
+
search_fields: ClassVar[list[str]] = ["name"]
|
38
|
+
|
39
|
+
|
40
|
+
@hooks.register("register_admin_viewset") # type: ignore[reportOptionalCall]
|
41
|
+
def register_article_tag_viewset() -> SnippetViewSet:
|
42
|
+
"""Make `PageTag`s editable via the Wagtail admin."""
|
43
|
+
return PageTagViewSet(model=PageTag)
|
picata/wsgi.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
"""WSGI config for the hpk project.
|
2
|
+
|
3
|
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
4
|
+
|
5
|
+
For more information on this file, see
|
6
|
+
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
7
|
+
"""
|
8
|
+
|
9
|
+
import os
|
10
|
+
|
11
|
+
from django.core.wsgi import get_wsgi_application
|
12
|
+
|
13
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hpk.settings.prod")
|
14
|
+
|
15
|
+
application = get_wsgi_application()
|
@@ -0,0 +1,87 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: picata
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Ada's Wagtail-based CMS & blog
|
5
|
+
Project-URL: Documentation, https://github.com/hipikat/picata#readme
|
6
|
+
Project-URL: Issues, https://github.com/hipikat/picata/issues
|
7
|
+
Project-URL: Source, https://github.com/hipikat/picata
|
8
|
+
Author-email: Ada Wright <ada@hpk.io>
|
9
|
+
License-Expression: MIT
|
10
|
+
License-File: LICENSE.md
|
11
|
+
Keywords: blog,cms,django,wagtail
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
13
|
+
Classifier: Framework :: Django CMS
|
14
|
+
Classifier: Framework :: Wagtail :: 6
|
15
|
+
Classifier: Programming Language :: Python
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
18
|
+
Requires-Python: >=3.13
|
19
|
+
Requires-Dist: gunicorn~=23.0.0
|
20
|
+
Requires-Dist: lxml~=5.3.0
|
21
|
+
Requires-Dist: psutil~=6.1.0
|
22
|
+
Requires-Dist: psycopg~=3.2.3
|
23
|
+
Requires-Dist: pygments~=2.18.0
|
24
|
+
Requires-Dist: python-slugify~=8.0.4
|
25
|
+
Requires-Dist: wagtail-modeladmin~=2.1.0
|
26
|
+
Requires-Dist: wagtail~=6.2
|
27
|
+
Description-Content-Type: text/markdown
|
28
|
+
|
29
|
+
# Ada's website
|
30
|
+
|
31
|
+
Wherein I set up a little website, and learn a bunch of stuff as I go.
|
32
|
+
|
33
|
+
## What it's made of
|
34
|
+
|
35
|
+
### Inside the box
|
36
|
+
|
37
|
+
- [Wagtail](https://wagtail.org) (on [Django](https://www.djangoproject.com)) is the web framework
|
38
|
+
<!-- - [Tailwind CSS](https://tailwindcss.com) for styling -->
|
39
|
+
|
40
|
+
### Holding things together
|
41
|
+
|
42
|
+
- [UV](https://github.com/astral-sh/uv) for all Python project management
|
43
|
+
- [Just](https://just.systems) as a command runner
|
44
|
+
- [OpenTofu](https://opentofu.org) for DevOps
|
45
|
+
- [Postgres](https://www.postgresql.org) for the database
|
46
|
+
- [Docker](https://www.docker.com) for local development
|
47
|
+
|
48
|
+
## Quickstart
|
49
|
+
|
50
|
+
### Requirements
|
51
|
+
|
52
|
+
- On a Mac:
|
53
|
+
|
54
|
+
```shell
|
55
|
+
brew install colima docker
|
56
|
+
```
|
57
|
+
|
58
|
+
### Run a development server
|
59
|
+
|
60
|
+
```shell
|
61
|
+
just tofu workspace select dev
|
62
|
+
just tofu apply
|
63
|
+
```
|
64
|
+
|
65
|
+
This will spin up a box on DigitalOcean using the settings defined in
|
66
|
+
[infra/variables.tf](infra/variables.tf), and create a DNS A record at
|
67
|
+
(workspace).for.(tld), (i.e. dev.for.hpk.io) pointing to the box. The variables
|
68
|
+
`do_token` and `ssh_fingerprint` should be defined in
|
69
|
+
[infra/secrets.tfvars](infra/secrets.tfvars). Workspace-specific variables are
|
70
|
+
defined in infra/envs/(workspace).tfvars; e.g.
|
71
|
+
[infra/envs/dev.tfvars](infra/envs/dev.tfvars) defines the 'tags' list for the
|
72
|
+
box as `[development]` and sets `cloud_init_config` to point to the
|
73
|
+
[cloud-init](https://cloud-init.io) script
|
74
|
+
[config/cloud-init-dev.yml](config/cloud-init-dev.yml).
|
75
|
+
|
76
|
+
The development cloud-init script will:
|
77
|
+
|
78
|
+
- Install the system packages [`just`](https://just.systems), [`zsh`](https://www.zsh.org),
|
79
|
+
[`gunicorn`](https://gunicorn.org), and `tree`
|
80
|
+
- Create a 'wagtail' user, with UID 1500
|
81
|
+
- Create the 'ada' user, and:
|
82
|
+
- install their SSH public keys,
|
83
|
+
- install their dotfiles,
|
84
|
+
- add them to the 'sudo' and 'wagtail' groups
|
85
|
+
- Install [Node](http://nodejs.org) on the system, from the `TF_VAR_NODE_VERSION`
|
86
|
+
defined in [.env](.env)
|
87
|
+
- Checkout this repository into `/app`, setting the owner and group to 'wagtail'.
|
@@ -0,0 +1,94 @@
|
|
1
|
+
LICENSE.md,sha256=Bv8sMyZI5NI6DMrfiAvCwIFRLSfJkimLF2KVcUMteKU,1103
|
2
|
+
README.md,sha256=tPe8EjMhr2cMwsf-N-gQcTT-IpyE2kIOnwggeJnSPzM,1990
|
3
|
+
entrypoint.tsx,sha256=Tk6L2rCk0KrE5kaSL0GFuL3S19yG-BsUgvNAGe32uy8,8820
|
4
|
+
manage.py,sha256=v0zbaNjwEksTx39Z9dgoWh8LxZEH1tuCpjLVVmiXBF4,423
|
5
|
+
pygments.sass,sha256=zbDYpWda3EoGmjoC3JshZy-_CECNf6WU9abYPF8EHms,6369
|
6
|
+
styles.sass,sha256=4Jb-Gxn9AP09pZzwLlCGGfeacCyrncpJAOCYcy-dyB4,8486
|
7
|
+
components/HelloWorld.tsx,sha256=Kp7gvhGehfrX1mw0jgr2_D6AueFgqgfMYGkyQgvWekg,180
|
8
|
+
picata/__init__.py,sha256=_kL7fTKu02_hXVnOmF1K7llLgpdOeQyAVliL7I5o-Dg,62
|
9
|
+
picata/apps.py,sha256=P1qIVWRACiNUA-TY6aIRvt-MyA-3baG82Z_MEpr2fsw,1241
|
10
|
+
picata/blocks.py,sha256=iPJc4eQN7_J-KGMMw3Iln31I1-tBluqgaUF-xCxq2Mc,5279
|
11
|
+
picata/log_utils.py,sha256=BRdB3PqpFx1XAhIyAzIOyQKiqrjbT3PBmkhH6-wAWJg,1555
|
12
|
+
picata/middleware.py,sha256=ycKEVr2GKGmGFQNqFG4ijBmxFuVK11khtiiAiTsbbsk,2064
|
13
|
+
picata/models.py,sha256=dra7x1pbRrMpFydgER_j7zPEnrP9Dqqy1n3rDR1Jbgc,15868
|
14
|
+
picata/transformers.py,sha256=8VkJJC0Xm8ZMzKxOp07rSnqJA7b-TiL02e_y07Gh5AI,2209
|
15
|
+
picata/urls.py,sha256=kTSLS0-f9YaLmhzgg8mW4Oc9W04eXVf_0TZpEnYdnJo,1828
|
16
|
+
picata/validators.py,sha256=X4wdIxbCdmuU-gJv45ptTFB7kHR166jkSQBJiTzP3ZU,1517
|
17
|
+
picata/views.py,sha256=L_NXtqZogdwP7X1uBIPOB1qnYSNpCCThuI8O7YeS47E,2874
|
18
|
+
picata/wagtail_hooks.py,sha256=Y_50IVpV62TEHtg1h8MN0IDkCWAMTVzv0Hd3eiult9M,1497
|
19
|
+
picata/wsgi.py,sha256=XdUhwbLt-MKyr9he5PRphYrvKadC8yF0glx1xxQpCgE,392
|
20
|
+
picata/helpers/__init__.py,sha256=acN445qKCuRVfInCEyCtx5W1BggloOSrawzdQ-c9m7s,2427
|
21
|
+
picata/helpers/wagtail.py,sha256=p_0qw530UcaIBY3AzeTYY6fOUxmVnC9DaP_0_gByrUw,2108
|
22
|
+
picata/migrations/0001_initial.py,sha256=q15Mjii63qa397O7eXyM-BSgTDzf1MqzKAJn1To_huc,9721
|
23
|
+
picata/migrations/0002_alter_article_content_alter_basicpage_content.py,sha256=WlmmGgbAaBCZvR904W8gAeyxp30fOfi4a0D5Xpiojsk,4490
|
24
|
+
picata/migrations/0003_alter_article_content_alter_basicpage_content.py,sha256=i2KD213JKxzM7Kgxlfufxz405IICBoVFJVeNBZuaRlI,4300
|
25
|
+
picata/migrations/0004_alter_article_content_alter_basicpage_content.py,sha256=sZ_ky-xTZREr43y9Zrh1DaqUS5ZXi4r7ilL_EZf9L_E,4204
|
26
|
+
picata/migrations/0005_socialsettings.py,sha256=flfEf47418UX972SoL0nta2gKjambGFACXpFD_oeY0s,1541
|
27
|
+
picata/migrations/0006_alter_article_content.py,sha256=EVTuWtWf74mpD-YvZEzBEdmwHAixzn4R5PxEABl8n0Q,2690
|
28
|
+
picata/migrations/0007_splitviewpage.py,sha256=KK67Kw4kbxVm56BZfKj58mDajZfsCpLwspAFegKQgNA,2675
|
29
|
+
picata/migrations/0008_alter_splitviewpage_content.py,sha256=Y5aj5rTgclKxAgpPF7wCOxDpyFe4rrVkxa_YsEAoYCc,3829
|
30
|
+
picata/migrations/0009_alter_splitviewpage_content.py,sha256=PPu4Hqtm7PkbQYRB_CzqsS-G3-vkAPncPco8Zrn1IXY,4406
|
31
|
+
picata/migrations/0010_alter_splitviewpage_content.py,sha256=Re61nL4y6zHRGMhxADkqHNDkfoWQ8bHGbZK74uJXatg,4265
|
32
|
+
picata/migrations/0011_alter_splitviewpage_options_and_more.py,sha256=OPtQipM3COYRVf9RaP9G8ec0x3NlyR8wT1er3m11kg8,4527
|
33
|
+
picata/migrations/0012_alter_splitviewpage_content.py,sha256=pPERBPW4uduQ-sGops44p2gavJ2hYh5toZ67hmvjhLo,4357
|
34
|
+
picata/migrations/0013_alter_article_content.py,sha256=zVn-YC5jhXiG_c6hkD_uhAgu1BCdsqA1j1xfgJj5n1U,1571
|
35
|
+
picata/migrations/0014_alter_article_content_alter_article_summary.py,sha256=5RvNPNyjy0dnyOw9RfcuTB8pFlpmg6p3yK8kuR0zgMk,1186
|
36
|
+
picata/migrations/0015_alter_article_options_article_tagline_and_more.py,sha256=8ToGgChYwqSoxo5EgwM8SISbQhFt7yewH-fN3raNE_Y,849
|
37
|
+
picata/migrations/0016_alter_article_options_alter_articletag_options_and_more.py,sha256=yxDQhzCMOZCl3NPEyYnM1Yw1E5HAn4wN_BPV5PYrMxE,917
|
38
|
+
picata/migrations/0017_articletagrelation_alter_article_tags_and_more.py,sha256=3rPB6p0GxwTCvTVQIDKNFBtrf26q99mdmQwoCqpXyM8,1341
|
39
|
+
picata/migrations/0018_rename_articletag_pagetag_and_more.py,sha256=0wFB82uAlJZAA7BD1OVTJIWvY5by8fZss7TIVsVLnnY,485
|
40
|
+
picata/migrations/0019_rename_name_plural_articletype__name_plural.py,sha256=K58-B-V2ld2gJ7OuHnrQchC7zZvLP2Bnss3bQB5oS84,391
|
41
|
+
picata/migrations/0020_rename__name_plural_articletype__pluralised_name.py,sha256=X9zRPk5G6iMwitrr5BTUB9Nk9VqQQ951HfBrQ8_MXD4,405
|
42
|
+
picata/migrations/0021_rename_article_type_article_page_type.py,sha256=DtxWYyke-8RmoFqpW817rtrIzVMSV8wqCpS1e911Nxc,399
|
43
|
+
picata/migrations/0022_homepage.py,sha256=SYpqIlquJzLh4YozTcJVjR_0fW_v9GRrY75bnAUk0vE,3591
|
44
|
+
picata/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
+
picata/settings/__init__.py,sha256=5qI40E9aCWsFanUxAnruZi1wXrad3oAwnusglycfPsk,47
|
46
|
+
picata/settings/base.py,sha256=APK6EyBUu1C3Q8VX8JciKTQX-iZcARm2BMD54LDgAaY,9273
|
47
|
+
picata/settings/dev.py,sha256=6SXFuEH3NTq2L3iWcC49Ky3gxJnPflHE3TtjByzuEOg,2796
|
48
|
+
picata/settings/mypy.py,sha256=M-_KU9Qi4twjCHwwuhFbGz9hnIU3TXLH0Sg3XionVNY,148
|
49
|
+
picata/settings/prod.py,sha256=iPsqLOdinblT_ayqMW2_GqpNccsXJlWA9-kA015X7Vo,474
|
50
|
+
picata/settings/test.py,sha256=H_Wkn_gjsIV0r5HsU-s5HLhW_Oi63VQp4Afj2TIgRhI,244
|
51
|
+
picata/static/picata/ada-profile.jpg,sha256=HlewC8MPH_zSr--pUXV5JCEE0emOrn8BIKCQv41WidA,209953
|
52
|
+
picata/static/picata/ada-social-bear.jpg,sha256=3jH2kf57boRLS5AD7M-qW-xsBP84didfeM1KN5Xp4tc,128453
|
53
|
+
picata/static/picata/favicon.ico,sha256=MJLjMSsDpx9Mjb1v7y8V5oPMX3gLtD4cIDmNAdOgHC8,15406
|
54
|
+
picata/static/picata/icons.svg,sha256=LRLgy8bHyZKorDyPxkO2mMdbevKs40sCnhdWoO29pdY,11320
|
55
|
+
picata/static/picata/fonts/Bitter-Light.ttf,sha256=JCgJXY06RiEONk2htx2bitpL3zqeGGJ01Z81fwRTwAo,182212
|
56
|
+
picata/static/picata/fonts/Bitter-LightItalic.ttf,sha256=A6oI_l_kGd2F9-V0eOYMoydigDPRdahO6aUKMvQXddY,181412
|
57
|
+
picata/static/picata/fonts/FiraCode-Light.ttf,sha256=bKCGEsYc36CaTQ_0-STZkfU7PDJXZpyQQDfQD7oJyTQ,188708
|
58
|
+
picata/static/picata/fonts/FiraCode-SemiBold.ttf,sha256=BsEuLjCqZkqwFH0_iBxrWH1hrdhDZa9cNM61DkA5hiE,188848
|
59
|
+
picata/static/picata/fonts/Sacramento-Regular.ttf,sha256=pbneZ75FSoJrRthYUS4y7sjDAxK-tMT4ZK7iOQQU6cU,64808
|
60
|
+
picata/static/picata/fonts/ZillaSlab-Bold.ttf,sha256=7Fo-wbzMoTThzw3ivb2PiLNcB1atOZdGClX22mwHu_Q,248052
|
61
|
+
picata/static/picata/fonts/ZillaSlab-BoldItalic.ttf,sha256=xra8p0e0zYZ5gyu5w6BeR8fnOJbZkeoIRFZuRm-FpEI,255272
|
62
|
+
picata/static/picata/fonts/ZillaSlab-Light.ttf,sha256=x9c6KiQ-iPpyqCDb2yYgDqi0GW-DNM8YIyTXqehumJ8,239836
|
63
|
+
picata/static/picata/fonts/ZillaSlab-LightItalic.ttf,sha256=8pgf5nlQb5DBqfaNKDj2P7iDMRwaW80YjaC3hXb72PA,245200
|
64
|
+
picata/static/picata/fonts/ZillaSlabHighlight-Bold.ttf,sha256=MbNNCVxe1Kt-j8rJpognUmTZrh9R9je-35MgwoeL7eU,245180
|
65
|
+
picata/templates/picata/3_column.html,sha256=4SYfqTbmAtYICEulpx1RWocBCQUUn32EnZJrcxfVWwA,790
|
66
|
+
picata/templates/picata/404.html,sha256=4q8kKGGHWDk9Z8ThW2rP70T3SQrGNGRf153pvYcV7VE,230
|
67
|
+
picata/templates/picata/500.html,sha256=1xvvK2uoiZKkc2EuVVnbfg69KmrCJUdplR0vvFtWvKo,369
|
68
|
+
picata/templates/picata/_post_list.html,sha256=JpvNAOGmupLQIC6lTyjoyNOyRGMUQwOqEvEaZgSPrsM,915
|
69
|
+
picata/templates/picata/article.html,sha256=NBwXpicb_oL2xy3FN8_6KJNbrrQD2rTrNIu1ngOietk,794
|
70
|
+
picata/templates/picata/base.html,sha256=gE8pChABBlXUYfyQCkUdVZx9tFHS-cnMEWGNeaHGziI,8140
|
71
|
+
picata/templates/picata/basic_page.html,sha256=1FS_xdL6erskk9T-_TP3JWARczVstOLzs2ttvkbPbdk,173
|
72
|
+
picata/templates/picata/dl_view.html,sha256=epAV72HR2f0I3GWBEngUtYOQnHSsXs_SsNlhH7u-JbU,494
|
73
|
+
picata/templates/picata/home_page.html,sha256=nVDZwlsXfKtdSwQR80e0cHtiX7hf91ws5tYhu_ug_gU,635
|
74
|
+
picata/templates/picata/post_listing.html,sha256=3aSWYdPfwvYblE4s7b2P_dOLiiCWcrT1uvXRuDBak5Y,428
|
75
|
+
picata/templates/picata/search_results.html,sha256=P5HwdkLX2iQjZFEtD4YOp4Mun17btshjKBw__z6q8Hs,957
|
76
|
+
picata/templates/picata/split_view.html,sha256=DqRUKT5wc_-ByibQRZoyDU91omK6TgK7wC3Rp72bbgM,402
|
77
|
+
picata/templates/picata/blocks/icon_link_item.html,sha256=t-9LoA00Nv-H031yIL8es89zevbn315jlr5LTiAyM_o,301
|
78
|
+
picata/templates/picata/blocks/icon_link_list.html,sha256=7KU-hLOhAdKnoecLcugqWNzO5SgPnN9XfTDs1y60f0c,261
|
79
|
+
picata/templates/picata/blocks/icon_link_list_stream.html,sha256=B7yz5Ss_dT_5NIMsVwggnGFGmRgawNQrvnowaJUSUnw,69
|
80
|
+
picata/templates/picata/previews/3col.html,sha256=CwnWJpK5dvmVxqhBuu0QSFHWKhZlnc3xEowMQk3ltBo,4473
|
81
|
+
picata/templates/picata/previews/dl.html,sha256=ANOqlD3nlj5NF-4sPmya3eo0CNwmxgmUXswmz22Bh30,2095
|
82
|
+
picata/templates/picata/previews/split.html,sha256=hqv27u4wKTX8jJiNMazXioyRU9Uajpys_7LVQeI3NtE,2106
|
83
|
+
picata/templates/picata/previews/theme_gallery.html,sha256=f10610quPimz6xzpozvpZtdRrTpSP5KFIppuGeOZ308,8007
|
84
|
+
picata/templates/picata/tags/site_menu.html,sha256=Tb_BjV88ZH-nRb76alzSXPVUokPC9O2mVByKmeAgQWA,223
|
85
|
+
picata/templatetags/__init__.py,sha256=YNGfxmI00gewcJkkUlH219BHimHD_4p5Lz-zXPvgO-U,47
|
86
|
+
picata/templatetags/absolute_static.py,sha256=asY9uNFShD5iQCaU-ZjsgXQOJNlmLmkoOvRs2WWhEYE,454
|
87
|
+
picata/templatetags/menu_tags.py,sha256=WvsyNHyuD4oiueedWjqnej2PrAQW0FdlGeEYrYd_xbs,1267
|
88
|
+
picata/templatetags/stringify.py,sha256=QxStfcCgn29IGinH_bdsZoaiTLL7pz30ObDitlagecs,850
|
89
|
+
picata/typing/__init__.py,sha256=Nu2HZtspAQTzxMx6gmRKdDp9DfqmFc5urxfokXp2k-Y,402
|
90
|
+
picata/typing/wagtail.py,sha256=r84cf-8AzaWCMULF4HRjl5wmTFKIxw2DwvhxmIDurjA,661
|
91
|
+
picata-0.0.1.dist-info/METADATA,sha256=CzIkWSGSTfq45DELALkcS2ReZ-sxZ3sJ-vMpyfRWq7M,2996
|
92
|
+
picata-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
93
|
+
picata-0.0.1.dist-info/licenses/LICENSE.md,sha256=Bv8sMyZI5NI6DMrfiAvCwIFRLSfJkimLF2KVcUMteKU,1103
|
94
|
+
picata-0.0.1.dist-info/RECORD,,
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright © `2024` `Ada Wright <ada@hpk.io>`
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person
|
6
|
+
obtaining a copy of this software and associated documentation
|
7
|
+
files (the “Software”), to deal in the Software without
|
8
|
+
restriction, including without limitation the rights to use,
|
9
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the
|
11
|
+
Software is furnished to do so, subject to the following
|
12
|
+
conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
19
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
21
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
22
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
23
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
+
OTHER DEALINGS IN THE SOFTWARE.
|