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