picata 0.0.1__py3-none-any.whl → 0.0.2__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.
- README.md +7 -57
- manage.py +1 -1
- picata/__init__.py +1 -1
- picata/apps.py +6 -6
- picata/blocks.py +3 -2
- picata/helpers/wagtail.py +1 -1
- picata/middleware.py +1 -1
- picata/migrations/0001_initial.py +77 -211
- picata/models.py +6 -5
- picata/settings/base.py +15 -15
- picata/settings/dev.py +4 -2
- picata/templates/picata/base.html +3 -3
- picata/templates/picata/previews/theme_gallery.html +1 -1
- picata/templatetags/absolute_static.py +1 -1
- picata/templatetags/menu_tags.py +26 -12
- picata/transformers.py +1 -1
- picata/typing/__init__.py +1 -1
- picata/typing/wagtail.py +1 -1
- picata/urls.py +2 -2
- picata/views.py +3 -2
- picata/wagtail_hooks.py +1 -1
- picata/wsgi.py +2 -2
- picata-0.0.2.dist-info/METADATA +37 -0
- picata-0.0.2.dist-info/RECORD +73 -0
- picata/migrations/0002_alter_article_content_alter_basicpage_content.py +0 -112
- picata/migrations/0003_alter_article_content_alter_basicpage_content.py +0 -104
- picata/migrations/0004_alter_article_content_alter_basicpage_content.py +0 -105
- picata/migrations/0005_socialsettings.py +0 -48
- picata/migrations/0006_alter_article_content.py +0 -71
- picata/migrations/0007_splitviewpage.py +0 -69
- picata/migrations/0008_alter_splitviewpage_content.py +0 -96
- picata/migrations/0009_alter_splitviewpage_content.py +0 -111
- picata/migrations/0010_alter_splitviewpage_content.py +0 -105
- picata/migrations/0011_alter_splitviewpage_options_and_more.py +0 -113
- picata/migrations/0012_alter_splitviewpage_content.py +0 -109
- picata/migrations/0013_alter_article_content.py +0 -43
- picata/migrations/0014_alter_article_content_alter_article_summary.py +0 -24
- picata/migrations/0015_alter_article_options_article_tagline_and_more.py +0 -28
- picata/migrations/0016_alter_article_options_alter_articletag_options_and_more.py +0 -33
- picata/migrations/0017_articletagrelation_alter_article_tags_and_more.py +0 -35
- picata/migrations/0018_rename_articletag_pagetag_and_more.py +0 -21
- picata/migrations/0019_rename_name_plural_articletype__name_plural.py +0 -18
- picata/migrations/0020_rename__name_plural_articletype__pluralised_name.py +0 -18
- picata/migrations/0021_rename_article_type_article_page_type.py +0 -18
- picata/migrations/0022_homepage.py +0 -28
- picata-0.0.1.dist-info/METADATA +0 -87
- picata-0.0.1.dist-info/RECORD +0 -94
- {picata-0.0.1.dist-info → picata-0.0.2.dist-info}/WHEEL +0 -0
- {picata-0.0.1.dist-info → picata-0.0.2.dist-info}/licenses/LICENSE.md +0 -0
README.md
CHANGED
@@ -1,59 +1,9 @@
|
|
1
|
-
#
|
1
|
+
# Picata
|
2
2
|
|
3
|
-
|
3
|
+
**This project is very much pre-alpha**
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<!-- - [Tailwind CSS](https://tailwindcss.com) for styling -->
|
11
|
-
|
12
|
-
### Holding things together
|
13
|
-
|
14
|
-
- [UV](https://github.com/astral-sh/uv) for all Python project management
|
15
|
-
- [Just](https://just.systems) as a command runner
|
16
|
-
- [OpenTofu](https://opentofu.org) for DevOps
|
17
|
-
- [Postgres](https://www.postgresql.org) for the database
|
18
|
-
- [Docker](https://www.docker.com) for local development
|
19
|
-
|
20
|
-
## Quickstart
|
21
|
-
|
22
|
-
### Requirements
|
23
|
-
|
24
|
-
- On a Mac:
|
25
|
-
|
26
|
-
```shell
|
27
|
-
brew install colima docker
|
28
|
-
```
|
29
|
-
|
30
|
-
### Run a development server
|
31
|
-
|
32
|
-
```shell
|
33
|
-
just tofu workspace select dev
|
34
|
-
just tofu apply
|
35
|
-
```
|
36
|
-
|
37
|
-
This will spin up a box on DigitalOcean using the settings defined in
|
38
|
-
[infra/variables.tf](infra/variables.tf), and create a DNS A record at
|
39
|
-
(workspace).for.(tld), (i.e. dev.for.hpk.io) pointing to the box. The variables
|
40
|
-
`do_token` and `ssh_fingerprint` should be defined in
|
41
|
-
[infra/secrets.tfvars](infra/secrets.tfvars). Workspace-specific variables are
|
42
|
-
defined in infra/envs/(workspace).tfvars; e.g.
|
43
|
-
[infra/envs/dev.tfvars](infra/envs/dev.tfvars) defines the 'tags' list for the
|
44
|
-
box as `[development]` and sets `cloud_init_config` to point to the
|
45
|
-
[cloud-init](https://cloud-init.io) script
|
46
|
-
[config/cloud-init-dev.yml](config/cloud-init-dev.yml).
|
47
|
-
|
48
|
-
The development cloud-init script will:
|
49
|
-
|
50
|
-
- Install the system packages [`just`](https://just.systems), [`zsh`](https://www.zsh.org),
|
51
|
-
[`gunicorn`](https://gunicorn.org), and `tree`
|
52
|
-
- Create a 'wagtail' user, with UID 1500
|
53
|
-
- Create the 'ada' user, and:
|
54
|
-
- install their SSH public keys,
|
55
|
-
- install their dotfiles,
|
56
|
-
- add them to the 'sudo' and 'wagtail' groups
|
57
|
-
- Install [Node](http://nodejs.org) on the system, from the `TF_VAR_NODE_VERSION`
|
58
|
-
defined in [.env](.env)
|
59
|
-
- Checkout this repository into `/app`, setting the owner and group to 'wagtail'.
|
5
|
+
Picata is the CMS & blog application I've forked off from my personal website.
|
6
|
+
It's effectively a ton of pre-made Wagtail "stuff" (models, views, templatetags,
|
7
|
+
middleware, hooks, etc.), made generic enough that you can include this app
|
8
|
+
in your `INSTALLED_APPS` and have a CMS/blog up-and-running without having to
|
9
|
+
spend weeks or months tailoring Wagtail to your needs.
|
manage.py
CHANGED
@@ -5,7 +5,7 @@ from os import environ
|
|
5
5
|
from sys import argv
|
6
6
|
|
7
7
|
if __name__ == "__main__":
|
8
|
-
environ.setdefault("DJANGO_SETTINGS_MODULE", "
|
8
|
+
environ.setdefault("DJANGO_SETTINGS_MODULE", "picata.settings.dev")
|
9
9
|
|
10
10
|
if len(argv) >= 2: # noqa: PLR2004
|
11
11
|
environ.setdefault("DJANGO_MANAGEMENT_COMMAND", argv[1])
|
picata/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
"""
|
1
|
+
"""Top-level package for the Picata project."""
|
picata/apps.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
"""Application configuration for the
|
1
|
+
"""Application configuration for the Picata Django app."""
|
2
2
|
|
3
3
|
from django.apps import AppConfig
|
4
4
|
|
5
5
|
|
6
6
|
class Config(AppConfig):
|
7
|
-
"""Configuration class for the
|
7
|
+
"""Configuration class for the Picata Django application."""
|
8
8
|
|
9
9
|
default_auto_field = "django.db.models.BigAutoField"
|
10
|
-
name = "
|
10
|
+
name = "picata"
|
11
11
|
|
12
12
|
def ready(self) -> None:
|
13
13
|
"""Configure Wagtail admin with custom models, and register document transformers."""
|
@@ -15,13 +15,13 @@ class Config(AppConfig):
|
|
15
15
|
# Register the 'custom article type' model with the Wagtail admin
|
16
16
|
from wagtail_modeladmin.options import modeladmin_register
|
17
17
|
|
18
|
-
from
|
18
|
+
from picata.models import ArticleTypeAdmin
|
19
19
|
|
20
20
|
modeladmin_register(ArticleTypeAdmin)
|
21
21
|
|
22
22
|
# Add document transformers to the HTMLProcessingMiddleware
|
23
|
-
from
|
24
|
-
from
|
23
|
+
from picata.middleware import HTMLProcessingMiddleware
|
24
|
+
from picata.transformers import AnchorInserter, add_heading_ids
|
25
25
|
|
26
26
|
## Add ids to all headings missing them within html > body > main
|
27
27
|
HTMLProcessingMiddleware.add_transformer(add_heading_ids)
|
picata/blocks.py
CHANGED
@@ -3,8 +3,6 @@
|
|
3
3
|
import pygments
|
4
4
|
from django.forms import CharField
|
5
5
|
from django.utils.html import format_html
|
6
|
-
from hpk.typing.wagtail import BlockRenderContext, BlockRenderValue
|
7
|
-
from hpk.validators import HREFValidator
|
8
6
|
from pygments import formatters, lexers
|
9
7
|
from pygments.util import ClassNotFound
|
10
8
|
from wagtail.blocks import (
|
@@ -20,6 +18,9 @@ from wagtail.blocks import (
|
|
20
18
|
)
|
21
19
|
from wagtail.images.blocks import ImageChooserBlock
|
22
20
|
|
21
|
+
from picata.typing.wagtail import BlockRenderContext, BlockRenderValue
|
22
|
+
from picata.validators import HREFValidator
|
23
|
+
|
23
24
|
|
24
25
|
class HREFField(CharField):
|
25
26
|
"""Custom field for href attributes (i.e. URLs but also schemes like 'mailto:')."""
|
picata/helpers/wagtail.py
CHANGED
picata/middleware.py
CHANGED
@@ -1,264 +1,130 @@
|
|
1
|
-
# Generated by Django 5.1.
|
1
|
+
# Generated by Django 5.1.5 on 2025-01-24 00:18
|
2
2
|
|
3
3
|
import django.db.models.deletion
|
4
4
|
import modelcluster.contrib.taggit
|
5
5
|
import modelcluster.fields
|
6
|
+
import wagtail.contrib.routable_page.models
|
6
7
|
import wagtail.fields
|
7
8
|
from django.db import migrations, models
|
8
9
|
|
9
10
|
|
10
11
|
class Migration(migrations.Migration):
|
12
|
+
|
11
13
|
initial = True
|
12
14
|
|
13
15
|
dependencies = [
|
14
|
-
(
|
15
|
-
(
|
16
|
+
('wagtailcore', '0094_alter_page_locale'),
|
17
|
+
('wagtailimages', '0027_image_description'),
|
16
18
|
]
|
17
19
|
|
18
20
|
operations = [
|
19
21
|
migrations.CreateModel(
|
20
|
-
name=
|
22
|
+
name='ArticleType',
|
23
|
+
fields=[
|
24
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
25
|
+
('name', models.CharField(help_text='Name of the article type.', max_length=100, unique=True)),
|
26
|
+
('_Pluralised_name', models.CharField(blank=True, help_text="Plural form of the article type name (optional). Defaults to appending 's'.", max_length=100)),
|
27
|
+
('slug', models.SlugField(max_length=100, unique=True)),
|
28
|
+
('description', models.TextField(blank=True, help_text='Optional description of this type.')),
|
29
|
+
],
|
30
|
+
),
|
31
|
+
migrations.CreateModel(
|
32
|
+
name='BasicPage',
|
21
33
|
fields=[
|
22
|
-
(
|
23
|
-
|
24
|
-
models.OneToOneField(
|
25
|
-
auto_created=True,
|
26
|
-
on_delete=django.db.models.deletion.CASCADE,
|
27
|
-
parent_link=True,
|
28
|
-
primary_key=True,
|
29
|
-
serialize=False,
|
30
|
-
to="wagtailcore.page",
|
31
|
-
),
|
32
|
-
),
|
33
|
-
(
|
34
|
-
"summary",
|
35
|
-
wagtail.fields.RichTextField(
|
36
|
-
blank=True, help_text="A short summary, or tagline for the article."
|
37
|
-
),
|
38
|
-
),
|
39
|
-
(
|
40
|
-
"content",
|
41
|
-
wagtail.fields.StreamField(
|
42
|
-
[("section", 5), ("image", 3)],
|
43
|
-
blank=True,
|
44
|
-
block_lookup={
|
45
|
-
0: (
|
46
|
-
"wagtail.blocks.CharBlock",
|
47
|
-
(),
|
48
|
-
{"help_text": "Heading for this section.", "required": True},
|
49
|
-
),
|
50
|
-
1: (
|
51
|
-
"wagtail.blocks.IntegerBlock",
|
52
|
-
(),
|
53
|
-
{
|
54
|
-
"help_text": "Heading level",
|
55
|
-
"max_value": 6,
|
56
|
-
"min_value": 1,
|
57
|
-
"required": True,
|
58
|
-
},
|
59
|
-
),
|
60
|
-
2: (
|
61
|
-
"wagtail.blocks.RichTextBlock",
|
62
|
-
(),
|
63
|
-
{
|
64
|
-
"features": [
|
65
|
-
"bold",
|
66
|
-
"italic",
|
67
|
-
"link",
|
68
|
-
"ul",
|
69
|
-
"ol",
|
70
|
-
"document-link",
|
71
|
-
]
|
72
|
-
},
|
73
|
-
),
|
74
|
-
3: ("wagtail.images.blocks.ImageChooserBlock", (), {}),
|
75
|
-
4: (
|
76
|
-
"wagtail.blocks.StreamBlock",
|
77
|
-
[[("rich_text", 2), ("image", 3)]],
|
78
|
-
{
|
79
|
-
"help_text": "Content blocks for this section.",
|
80
|
-
"required": False,
|
81
|
-
},
|
82
|
-
),
|
83
|
-
5: (
|
84
|
-
"wagtail.blocks.StructBlock",
|
85
|
-
[[("heading", 0), ("level", 1), ("content", 4)]],
|
86
|
-
{},
|
87
|
-
),
|
88
|
-
},
|
89
|
-
help_text="Main content for the article.",
|
90
|
-
),
|
91
|
-
),
|
34
|
+
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
35
|
+
('content', wagtail.fields.StreamField([('rich_text', 0), ('code', 3), ('image', 4)], blank=True, block_lookup={0: ('wagtail.blocks.RichTextBlock', (), {}), 1: ('wagtail.blocks.TextBlock', (), {'help_text': None, 'required': True}), 2: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('python', 'Python'), ('javascript', 'JavaScript'), ('html', 'HTML'), ('css', 'CSS'), ('bash', 'Bash'), ('plaintext', 'Plain Text')], 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('code', 1), ('language', 2)]], {}), 4: ('wagtail.images.blocks.ImageChooserBlock', (), {})}, help_text='Main content for the page.')),
|
92
36
|
],
|
93
37
|
options={
|
94
|
-
|
95
|
-
"verbose_name_plural": "Articles",
|
38
|
+
'abstract': False,
|
96
39
|
},
|
97
|
-
bases=(
|
40
|
+
bases=('wagtailcore.page',),
|
98
41
|
),
|
99
42
|
migrations.CreateModel(
|
100
|
-
name=
|
43
|
+
name='HomePage',
|
101
44
|
fields=[
|
102
|
-
(
|
103
|
-
|
104
|
-
|
105
|
-
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
106
|
-
),
|
107
|
-
),
|
108
|
-
("name", models.CharField(max_length=100, unique=True, verbose_name="name")),
|
109
|
-
(
|
110
|
-
"slug",
|
111
|
-
models.SlugField(
|
112
|
-
allow_unicode=True, max_length=100, unique=True, verbose_name="slug"
|
113
|
-
),
|
114
|
-
),
|
45
|
+
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
46
|
+
('top_content', wagtail.fields.StreamField([('rich_text', 0), ('image', 1), ('icon_link_lists', 11)], blank=True, block_lookup={0: ('wagtail.blocks.RichTextBlock', (), {}), 1: ('picata.blocks.WrappedImageChooserBlock', (), {}), 2: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional heading for this list (e.g., Social Links).', 'required': False}), 3: ('wagtail.blocks.IntegerBlock', (), {'default': 2, 'help_text': 'Heading level for the list (1-6).', 'max_value': 6, 'min_value': 1, 'required': False}), 4: ('picata.blocks.HREFBlock', (), {'help_text': 'An optional link field', 'max_length': 255, 'required': False}), 5: ('wagtail.blocks.CharBlock', (), {'help_text': 'The title for the list item.', 'max_length': 50, 'required': True}), 6: ('wagtail.blocks.CharBlock', (), {'help_text': 'Id of the icon in the static/icons.svg file.', 'max_length': 255, 'required': False}), 7: ('wagtail.blocks.StructBlock', [[('href', 4), ('label', 5), ('icon', 6)]], {}), 8: ('wagtail.blocks.ListBlock', (7,), {'help_text': 'The list of items.'}), 9: ('wagtail.blocks.StructBlock', [[('heading', 2), ('heading_level', 3), ('items', 8)]], {}), 10: ('wagtail.blocks.StreamBlock', [[('link_list', 9)]], {'help_text': 'Add one or more heading-and-link-list blocks.', 'required': False}), 11: ('wagtail.blocks.StructBlock', [[('lists', 10)]], {})}, help_text="Content stream above 'Recent posts'")),
|
47
|
+
('bottom_content', wagtail.fields.StreamField([('rich_text', 0), ('image', 1), ('icon_link_lists', 11)], blank=True, block_lookup={0: ('wagtail.blocks.RichTextBlock', (), {}), 1: ('picata.blocks.WrappedImageChooserBlock', (), {}), 2: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional heading for this list (e.g., Social Links).', 'required': False}), 3: ('wagtail.blocks.IntegerBlock', (), {'default': 2, 'help_text': 'Heading level for the list (1-6).', 'max_value': 6, 'min_value': 1, 'required': False}), 4: ('picata.blocks.HREFBlock', (), {'help_text': 'An optional link field', 'max_length': 255, 'required': False}), 5: ('wagtail.blocks.CharBlock', (), {'help_text': 'The title for the list item.', 'max_length': 50, 'required': True}), 6: ('wagtail.blocks.CharBlock', (), {'help_text': 'Id of the icon in the static/icons.svg file.', 'max_length': 255, 'required': False}), 7: ('wagtail.blocks.StructBlock', [[('href', 4), ('label', 5), ('icon', 6)]], {}), 8: ('wagtail.blocks.ListBlock', (7,), {'help_text': 'The list of items.'}), 9: ('wagtail.blocks.StructBlock', [[('heading', 2), ('heading_level', 3), ('items', 8)]], {}), 10: ('wagtail.blocks.StreamBlock', [[('link_list', 9)]], {'help_text': 'Add one or more heading-and-link-list blocks.', 'required': False}), 11: ('wagtail.blocks.StructBlock', [[('lists', 10)]], {})}, help_text="Content stream rendered under 'Recent posts'")),
|
115
48
|
],
|
116
49
|
options={
|
117
|
-
|
118
|
-
"verbose_name_plural": "Article Tags",
|
50
|
+
'verbose_name': 'home page',
|
119
51
|
},
|
52
|
+
bases=('wagtailcore.page',),
|
120
53
|
),
|
121
54
|
migrations.CreateModel(
|
122
|
-
name=
|
55
|
+
name='PageTag',
|
123
56
|
fields=[
|
124
|
-
(
|
125
|
-
|
126
|
-
|
127
|
-
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
128
|
-
),
|
129
|
-
),
|
130
|
-
(
|
131
|
-
"name",
|
132
|
-
models.CharField(
|
133
|
-
help_text="Name of the article type.", max_length=100, unique=True
|
134
|
-
),
|
135
|
-
),
|
136
|
-
(
|
137
|
-
"name_plural",
|
138
|
-
models.CharField(
|
139
|
-
blank=True,
|
140
|
-
help_text="Plural form of the article type name (optional). Defaults to appending 's'.", # noqa: E501
|
141
|
-
max_length=100,
|
142
|
-
),
|
143
|
-
),
|
144
|
-
("slug", models.SlugField(max_length=100, unique=True)),
|
145
|
-
(
|
146
|
-
"description",
|
147
|
-
models.TextField(blank=True, help_text="Optional description of this type."),
|
148
|
-
),
|
57
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
58
|
+
('name', models.CharField(max_length=100, unique=True, verbose_name='name')),
|
59
|
+
('slug', models.SlugField(allow_unicode=True, max_length=100, unique=True, verbose_name='slug')),
|
149
60
|
],
|
61
|
+
options={
|
62
|
+
'abstract': False,
|
63
|
+
},
|
150
64
|
),
|
151
65
|
migrations.CreateModel(
|
152
|
-
name=
|
66
|
+
name='PostGroupPage',
|
153
67
|
fields=[
|
154
|
-
(
|
155
|
-
|
156
|
-
models.OneToOneField(
|
157
|
-
auto_created=True,
|
158
|
-
on_delete=django.db.models.deletion.CASCADE,
|
159
|
-
parent_link=True,
|
160
|
-
primary_key=True,
|
161
|
-
serialize=False,
|
162
|
-
to="wagtailcore.page",
|
163
|
-
),
|
164
|
-
),
|
165
|
-
(
|
166
|
-
"content",
|
167
|
-
wagtail.fields.StreamField(
|
168
|
-
[("rich_text", 0), ("image", 1)],
|
169
|
-
blank=True,
|
170
|
-
block_lookup={
|
171
|
-
0: ("wagtail.blocks.RichTextBlock", (), {}),
|
172
|
-
1: ("wagtail.images.blocks.ImageChooserBlock", (), {}),
|
173
|
-
},
|
174
|
-
help_text="Main content for the page.",
|
175
|
-
),
|
176
|
-
),
|
68
|
+
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
69
|
+
('intro', wagtail.fields.RichTextField(blank=True, help_text='An optional introduction to this group.')),
|
177
70
|
],
|
178
71
|
options={
|
179
|
-
|
180
|
-
|
72
|
+
'verbose_name': 'post listing',
|
73
|
+
'verbose_name_plural': 'post listings',
|
181
74
|
},
|
182
|
-
bases=(
|
75
|
+
bases=(wagtail.contrib.routable_page.models.RoutablePageMixin, 'wagtailcore.page'),
|
183
76
|
),
|
184
77
|
migrations.CreateModel(
|
185
|
-
name=
|
78
|
+
name='SplitViewPage',
|
186
79
|
fields=[
|
187
|
-
(
|
188
|
-
|
189
|
-
models.OneToOneField(
|
190
|
-
auto_created=True,
|
191
|
-
on_delete=django.db.models.deletion.CASCADE,
|
192
|
-
parent_link=True,
|
193
|
-
primary_key=True,
|
194
|
-
serialize=False,
|
195
|
-
to="wagtailcore.page",
|
196
|
-
),
|
197
|
-
),
|
198
|
-
(
|
199
|
-
"intro",
|
200
|
-
wagtail.fields.RichTextField(
|
201
|
-
blank=True, help_text="An optional introduction to this group."
|
202
|
-
),
|
203
|
-
),
|
80
|
+
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
81
|
+
('content', wagtail.fields.StreamField([('rich_text', 0), ('code', 3), ('image', 4), ('icon_link_lists', 14)], blank=True, block_lookup={0: ('wagtail.blocks.RichTextBlock', (), {}), 1: ('wagtail.blocks.TextBlock', (), {'help_text': None, 'required': True}), 2: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('python', 'Python'), ('javascript', 'JavaScript'), ('html', 'HTML'), ('css', 'CSS'), ('bash', 'Bash'), ('plaintext', 'Plain Text')], 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('code', 1), ('language', 2)]], {}), 4: ('picata.blocks.WrappedImageChooserBlock', (), {}), 5: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional heading for this list (e.g., Social Links).', 'required': False}), 6: ('wagtail.blocks.IntegerBlock', (), {'default': 2, 'help_text': 'Heading level for the list (1-6).', 'max_value': 6, 'min_value': 1, 'required': False}), 7: ('picata.blocks.HREFBlock', (), {'help_text': 'An optional link field', 'max_length': 255, 'required': False}), 8: ('wagtail.blocks.CharBlock', (), {'help_text': 'The title for the list item.', 'max_length': 50, 'required': True}), 9: ('wagtail.blocks.CharBlock', (), {'help_text': 'Id of the icon in the static/icons.svg file.', 'max_length': 255, 'required': False}), 10: ('wagtail.blocks.StructBlock', [[('href', 7), ('label', 8), ('icon', 9)]], {}), 11: ('wagtail.blocks.ListBlock', (10,), {'help_text': 'The list of items.'}), 12: ('wagtail.blocks.StructBlock', [[('heading', 5), ('heading_level', 6), ('items', 11)]], {}), 13: ('wagtail.blocks.StreamBlock', [[('link_list', 12)]], {'help_text': 'Add one or more heading-and-link-list blocks.', 'required': False}), 14: ('wagtail.blocks.StructBlock', [[('lists', 13)]], {})}, help_text='Main content for the split-view page.')),
|
204
82
|
],
|
205
83
|
options={
|
206
|
-
|
207
|
-
|
84
|
+
'verbose_name': 'split-view page',
|
85
|
+
'verbose_name_plural': 'split-view pages',
|
208
86
|
},
|
209
|
-
bases=(
|
87
|
+
bases=('wagtailcore.page',),
|
210
88
|
),
|
211
89
|
migrations.CreateModel(
|
212
|
-
name=
|
90
|
+
name='Article',
|
213
91
|
fields=[
|
214
|
-
(
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
),
|
220
|
-
(
|
221
|
-
"content_object",
|
222
|
-
modelcluster.fields.ParentalKey(
|
223
|
-
on_delete=django.db.models.deletion.CASCADE,
|
224
|
-
related_name="tagged_items",
|
225
|
-
to="hpk.article",
|
226
|
-
),
|
227
|
-
),
|
228
|
-
(
|
229
|
-
"tag",
|
230
|
-
models.ForeignKey(
|
231
|
-
on_delete=django.db.models.deletion.CASCADE,
|
232
|
-
related_name="%(app_label)s_%(class)s_items",
|
233
|
-
to="taggit.tag",
|
234
|
-
),
|
235
|
-
),
|
92
|
+
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
93
|
+
('tagline', models.CharField(blank=True, help_text='A short tagline for the article.')),
|
94
|
+
('summary', wagtail.fields.RichTextField(blank=True, help_text='A summary to be displayed in previews.')),
|
95
|
+
('content', wagtail.fields.StreamField([('rich_text', 0), ('code', 3), ('image', 4)], blank=True, block_lookup={0: ('wagtail.blocks.RichTextBlock', (), {}), 1: ('wagtail.blocks.TextBlock', (), {'help_text': None, 'required': True}), 2: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('python', 'Python'), ('javascript', 'JavaScript'), ('html', 'HTML'), ('css', 'CSS'), ('bash', 'Bash'), ('plaintext', 'Plain Text')], 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('code', 1), ('language', 2)]], {}), 4: ('wagtail.images.blocks.ImageChooserBlock', (), {})}, help_text='Main content for the article.')),
|
96
|
+
('page_type', models.ForeignKey(blank=True, help_text='Select the type of article.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='picata.articletype')),
|
236
97
|
],
|
237
98
|
options={
|
238
|
-
|
99
|
+
'abstract': False,
|
239
100
|
},
|
101
|
+
bases=('wagtailcore.page',),
|
240
102
|
),
|
241
|
-
migrations.
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
103
|
+
migrations.CreateModel(
|
104
|
+
name='PageTagRelation',
|
105
|
+
fields=[
|
106
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
107
|
+
('content_object', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='picata.article')),
|
108
|
+
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='picata.pagetag')),
|
109
|
+
],
|
110
|
+
options={
|
111
|
+
'abstract': False,
|
112
|
+
},
|
251
113
|
),
|
252
114
|
migrations.AddField(
|
253
|
-
model_name=
|
254
|
-
name=
|
255
|
-
field=
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
to=
|
262
|
-
|
115
|
+
model_name='article',
|
116
|
+
name='tags',
|
117
|
+
field=modelcluster.contrib.taggit.ClusterTaggableManager(blank=True, help_text='Tags for the article.', through='picata.PageTagRelation', to='picata.PageTag', verbose_name='Tags'),
|
118
|
+
),
|
119
|
+
migrations.CreateModel(
|
120
|
+
name='SocialSettings',
|
121
|
+
fields=[
|
122
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
123
|
+
('default_social_image', models.ForeignKey(blank=True, help_text='Default image for social media previews.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
|
124
|
+
('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')),
|
125
|
+
],
|
126
|
+
options={
|
127
|
+
'abstract': False,
|
128
|
+
},
|
263
129
|
),
|
264
130
|
]
|
picata/models.py
CHANGED
@@ -20,8 +20,6 @@ from django.db.models import (
|
|
20
20
|
from django.db.models.functions import Coalesce, ExtractYear
|
21
21
|
from django.http import HttpRequest
|
22
22
|
from django.urls import reverse
|
23
|
-
from hpk.typing import Args, Kwargs
|
24
|
-
from hpk.typing.wagtail import PageContext
|
25
23
|
from modelcluster.contrib.taggit import ClusterTaggableManager
|
26
24
|
from modelcluster.fields import ParentalKey
|
27
25
|
from taggit.models import TagBase, TaggedItemBase
|
@@ -37,6 +35,9 @@ from wagtail.query import PageQuerySet
|
|
37
35
|
from wagtail.search import index
|
38
36
|
from wagtail_modeladmin.options import ModelAdmin
|
39
37
|
|
38
|
+
from picata.typing import Args, Kwargs
|
39
|
+
from picata.typing.wagtail import PageContext
|
40
|
+
|
40
41
|
from .blocks import (
|
41
42
|
CodeBlock,
|
42
43
|
StaticIconLinkListsBlock,
|
@@ -103,7 +104,7 @@ class BasePage(Page):
|
|
103
104
|
|
104
105
|
def get_context(self, request: HttpRequest, *args: Args, **kwargs: Kwargs) -> BasePageContext:
|
105
106
|
"""Gather any publication and preview data available for the page into the context."""
|
106
|
-
from
|
107
|
+
from picata.helpers.wagtail import page_preview_data
|
107
108
|
|
108
109
|
context = super().get_context(request, *args, **kwargs)
|
109
110
|
context.update(page_preview_data(request, self))
|
@@ -356,7 +357,7 @@ class PostGroupPage(RoutablePageMixin, Page):
|
|
356
357
|
"""A top-level page for grouping various types of posts or articles."""
|
357
358
|
|
358
359
|
template = "picata/post_listing.html"
|
359
|
-
subpage_types: ClassVar[list[str]] = ["
|
360
|
+
subpage_types: ClassVar[list[str]] = ["picata.Article"]
|
360
361
|
|
361
362
|
intro = RichTextField(blank=True, help_text="An optional introduction to this group.")
|
362
363
|
|
@@ -465,7 +466,7 @@ class HomePage(BasePage):
|
|
465
466
|
|
466
467
|
def get_context(self, request: HttpRequest, *args: Args, **kwargs: Kwargs) -> HomePageContext:
|
467
468
|
"""Add content streams and a recent posts list to the context."""
|
468
|
-
from
|
469
|
+
from picata.helpers.wagtail import page_preview_data
|
469
470
|
|
470
471
|
recent_posts = Article.objects.live_for_user(request.user).by_date()
|
471
472
|
recent_posts = [page_preview_data(request, post) for post in recent_posts]
|
picata/settings/base.py
CHANGED
@@ -13,8 +13,8 @@ import contextlib
|
|
13
13
|
from os import getenv
|
14
14
|
from pathlib import Path
|
15
15
|
|
16
|
-
from
|
17
|
-
from
|
16
|
+
from picata.helpers import get_public_ip
|
17
|
+
from picata.log_utils import FormatterWithEverything
|
18
18
|
|
19
19
|
SRC_DIR = Path(__file__).resolve().parent.parent.parent
|
20
20
|
BASE_DIR = Path(SRC_DIR).parent
|
@@ -35,7 +35,7 @@ SECRET_KEY = getenv("SECRET_KEY")
|
|
35
35
|
|
36
36
|
INSTALLED_APPS = [
|
37
37
|
# Local apps
|
38
|
-
"
|
38
|
+
"picata.apps.Config",
|
39
39
|
# Wagtail
|
40
40
|
"wagtail.contrib.forms",
|
41
41
|
"wagtail.contrib.redirects",
|
@@ -72,10 +72,10 @@ MIDDLEWARE = [
|
|
72
72
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
73
73
|
"django.middleware.security.SecurityMiddleware",
|
74
74
|
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
75
|
-
"
|
75
|
+
"picata.middleware.HTMLProcessingMiddleware",
|
76
76
|
]
|
77
77
|
|
78
|
-
ROOT_URLCONF = "
|
78
|
+
ROOT_URLCONF = "picata.urls"
|
79
79
|
|
80
80
|
TEMPLATES = [
|
81
81
|
{
|
@@ -94,7 +94,7 @@ TEMPLATES = [
|
|
94
94
|
},
|
95
95
|
]
|
96
96
|
|
97
|
-
WSGI_APPLICATION = "
|
97
|
+
WSGI_APPLICATION = "picata.wsgi.application"
|
98
98
|
|
99
99
|
|
100
100
|
# Database
|
@@ -103,7 +103,7 @@ WSGI_APPLICATION = "hpk.wsgi.application"
|
|
103
103
|
DATABASES = {
|
104
104
|
"default": {
|
105
105
|
"ENGINE": "django.db.backends.postgresql",
|
106
|
-
"NAME": getenv("
|
106
|
+
"NAME": getenv("PICATA_DB"),
|
107
107
|
"USER": getenv("DB_USER"),
|
108
108
|
"PASSWORD": getenv("DB_PASSWORD"),
|
109
109
|
"HOST": getenv("DB_HOST", "localhost"),
|
@@ -158,14 +158,14 @@ LOGGING = {
|
|
158
158
|
},
|
159
159
|
"django_log": {
|
160
160
|
"level": "INFO",
|
161
|
-
"class": "
|
161
|
+
"class": "picata.log_utils.RotatingDailyFileHandler",
|
162
162
|
"filename": LOG_DIR / "django.log",
|
163
163
|
"formatter": "verbose",
|
164
164
|
},
|
165
|
-
"
|
165
|
+
"picata_log": {
|
166
166
|
"level": "INFO",
|
167
|
-
"class": "
|
168
|
-
"filename": LOG_DIR / "
|
167
|
+
"class": "picata.log_utils.RotatingDailyFileHandler",
|
168
|
+
"filename": LOG_DIR / "picata.log",
|
169
169
|
"formatter": "verbose",
|
170
170
|
},
|
171
171
|
"warnings_log": {
|
@@ -178,8 +178,8 @@ LOGGING = {
|
|
178
178
|
},
|
179
179
|
},
|
180
180
|
"loggers": {
|
181
|
-
"
|
182
|
-
"handlers": ["
|
181
|
+
"picata": {
|
182
|
+
"handlers": ["picata_log"],
|
183
183
|
"level": "INFO",
|
184
184
|
"propagate": True,
|
185
185
|
},
|
@@ -279,7 +279,7 @@ DEFAULT_FROM_EMAIL = getenv("ADMIN_EMAIL")
|
|
279
279
|
# Wagtail
|
280
280
|
# See https://docs.wagtail.org/en/stable/
|
281
281
|
|
282
|
-
WAGTAIL_SITE_NAME = "
|
282
|
+
WAGTAIL_SITE_NAME = "[Example Site Name]"
|
283
283
|
|
284
284
|
# Image serving
|
285
285
|
WAGTAILIMAGES_IMAGE_MODEL = "wagtailimages.Image"
|
@@ -294,7 +294,7 @@ WAGTAILSEARCH_BACKENDS = {
|
|
294
294
|
|
295
295
|
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
296
296
|
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
297
|
-
WAGTAILADMIN_BASE_URL = "https://" + getenv("FQDN", "
|
297
|
+
WAGTAILADMIN_BASE_URL = "https://" + getenv("FQDN", "example.com")
|
298
298
|
|
299
299
|
# https://docs.wagtail.org/en/stable/reference/settings.html#general-editing
|
300
300
|
WAGTAILADMIN_RICH_TEXT_EDITORS = {
|