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
@@ -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.
|