@django-stack/cli 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +33 -2
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +1305 -0
- package/dist/index.d.ts +251 -0
- package/dist/index.js +1 -0
- package/dist/stack-OTrWHXrO.js +712 -0
- package/package.json +52 -3
- package/templates/backend/.dockerignore +11 -0
- package/templates/backend/Dockerfile +45 -0
- package/templates/backend/config/__init__.py +0 -0
- package/templates/backend/config/asgi.py +7 -0
- package/templates/backend/config/settings.py +87 -0
- package/templates/backend/config/studio.py +20 -0
- package/templates/backend/config/urls.py +9 -0
- package/templates/backend/config/wsgi.py +7 -0
- package/templates/backend/conftest.py +7 -0
- package/templates/backend/content/__init__.py +0 -0
- package/templates/backend/content/admin.py +107 -0
- package/templates/backend/content/apps.py +6 -0
- package/templates/backend/content/blocks.py +51 -0
- package/templates/backend/content/migrations/0001_initial.py +418 -0
- package/templates/backend/content/migrations/__init__.py +0 -0
- package/templates/backend/content/models.py +234 -0
- package/templates/backend/content/seed.py +153 -0
- package/templates/backend/content/tests/__init__.py +0 -0
- package/templates/backend/content/tests/test_api.py +73 -0
- package/templates/backend/entrypoint.sh +27 -0
- package/templates/backend/manage.py +16 -0
- package/templates/backend/pyproject.toml +33 -0
- package/templates/backend/vendor/.gitkeep +0 -0
- package/templates/base/.github/workflows/deploy.yml +70 -0
- package/templates/base/AGENTS.md +78 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/Caddyfile +51 -0
- package/templates/base/README.md +87 -0
- package/templates/base/SECURITY.md +12 -0
- package/templates/base/compose.yaml +303 -0
- package/templates/base/env.example +80 -0
- package/templates/base/gitignore +15 -0
- package/templates/base/package.json +21 -0
- package/templates/frontend-astro/.dockerignore +4 -0
- package/templates/frontend-astro/Dockerfile +32 -0
- package/templates/frontend-astro/astro.config.mjs +38 -0
- package/templates/frontend-astro/package.json +31 -0
- package/templates/frontend-astro/public/favicon.svg +1 -0
- package/templates/frontend-astro/src/components/Blocks.astro +32 -0
- package/templates/frontend-astro/src/components/ContactForm.astro +68 -0
- package/templates/frontend-astro/src/components/Image.astro +34 -0
- package/templates/frontend-astro/src/components/PostCard.astro +41 -0
- package/templates/frontend-astro/src/components/PostList.astro +39 -0
- package/templates/frontend-astro/src/components/RichText.astro +14 -0
- package/templates/frontend-astro/src/components/Seo.astro +20 -0
- package/templates/frontend-astro/src/components/blocks/CallToAction.astro +23 -0
- package/templates/frontend-astro/src/components/blocks/Image.astro +24 -0
- package/templates/frontend-astro/src/components/blocks/Quote.astro +10 -0
- package/templates/frontend-astro/src/components/blocks/Text.astro +8 -0
- package/templates/frontend-astro/src/layouts/Base.astro +122 -0
- package/templates/frontend-astro/src/lib/api.ts +24 -0
- package/templates/frontend-astro/src/lib/cache.ts +58 -0
- package/templates/frontend-astro/src/lib/headless.ts +344 -0
- package/templates/frontend-astro/src/middleware.ts +69 -0
- package/templates/frontend-astro/src/pages/404.astro +13 -0
- package/templates/frontend-astro/src/pages/[...slug].astro +21 -0
- package/templates/frontend-astro/src/pages/blog/[slug].astro +50 -0
- package/templates/frontend-astro/src/pages/blog/index.astro +15 -0
- package/templates/frontend-astro/src/pages/category/[slug].astro +25 -0
- package/templates/frontend-astro/src/pages/index.astro +28 -0
- package/templates/frontend-astro/src/pages/preview/exit.ts +7 -0
- package/templates/frontend-astro/src/pages/preview/index.ts +26 -0
- package/templates/frontend-astro/src/pages/revalidate.ts +19 -0
- package/templates/frontend-astro/src/pages/robots.txt.ts +7 -0
- package/templates/frontend-astro/src/pages/rss.xml.ts +19 -0
- package/templates/frontend-astro/src/pages/sitemap.xml.ts +29 -0
- package/templates/frontend-astro/src/pages/tag/[slug].astro +24 -0
- package/templates/frontend-astro/src/styles/global.css +101 -0
- package/templates/frontend-astro/test/blocks.test.ts +42 -0
- package/templates/frontend-astro/test/cache.test.ts +22 -0
- package/templates/frontend-astro/tsconfig.json +5 -0
- package/templates/frontend-astro/vendor/.gitkeep +0 -0
- package/templates/frontend-astro/vitest.config.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@django-stack/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Leon van der Grient",
|
|
7
|
+
"homepage": "https://github.com/dwarsbit/django-stack/tree/main/packages/cli#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dwarsbit/django-stack.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/dwarsbit/django-stack/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"django",
|
|
16
|
+
"django-stack",
|
|
17
|
+
"headless-cms",
|
|
18
|
+
"jamstack"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"templates",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"dstack": "./dist/bin.js"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@bomb.sh/tab": "^0.0.22",
|
|
46
|
+
"@clack/prompts": "^1.8.1",
|
|
47
|
+
"commander": "^15.0.0",
|
|
48
|
+
"picocolors": "^1.1.1"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node scripts/copy-templates.mjs && tsdown",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "node scripts/copy-templates.mjs && vitest run"
|
|
54
|
+
}
|
|
6
55
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# The backend image: Django, the API and the admin. The `dev` target runs
|
|
4
|
+
# the development server with the source mounted; `prod` runs Gunicorn.
|
|
5
|
+
FROM python:3.13-slim AS base
|
|
6
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
7
|
+
PYTHONUNBUFFERED=1 \
|
|
8
|
+
UV_COMPILE_BYTECODE=1 \
|
|
9
|
+
UV_LINK_MODE=copy \
|
|
10
|
+
UV_PROJECT_ENVIRONMENT=/venv \
|
|
11
|
+
PATH="/venv/bin:$PATH"
|
|
12
|
+
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /bin/uv
|
|
13
|
+
# The Postgres client, for the backups (pg_dump and pg_restore).
|
|
14
|
+
RUN apt-get update \
|
|
15
|
+
&& apt-get install --yes --no-install-recommends postgresql-client \
|
|
16
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
17
|
+
RUN groupadd --system app && useradd --system --gid app --home-dir /app app
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
|
|
20
|
+
FROM base AS deps
|
|
21
|
+
COPY pyproject.toml uv.lock* ./
|
|
22
|
+
COPY vendor/ vendor/
|
|
23
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
24
|
+
uv sync --no-dev --no-install-project
|
|
25
|
+
|
|
26
|
+
# Development runs as root, so files the container writes into the mounted
|
|
27
|
+
# source (such as new migrations) work on every host. Production does not.
|
|
28
|
+
FROM deps AS dev
|
|
29
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
30
|
+
uv sync --no-install-project
|
|
31
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
32
|
+
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
|
33
|
+
|
|
34
|
+
FROM base AS prod
|
|
35
|
+
COPY --from=deps /venv /venv
|
|
36
|
+
# The code is owned by root: the app user can read it, not change it.
|
|
37
|
+
COPY . .
|
|
38
|
+
RUN DJANGO_SECRET_KEY=collectstatic-only python manage.py collectstatic --noinput \
|
|
39
|
+
&& mkdir -p /app/media /app/backups && chown -R app:app /app/media /app/backups
|
|
40
|
+
USER app
|
|
41
|
+
EXPOSE 8000
|
|
42
|
+
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
|
|
43
|
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/readyz', timeout=4)"
|
|
44
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
45
|
+
CMD ["gunicorn", "config.asgi:application", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-", "--no-control-socket"]
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings of the {{project_name}} backend.
|
|
3
|
+
|
|
4
|
+
The environment-driven settings (secrets, database, cache, mail, hosts,
|
|
5
|
+
storage) and the defaults for Django Headless and Django Content Studio
|
|
6
|
+
come from ``django_stack.settings.configure``; see the variables in
|
|
7
|
+
``.env.example`` at the project root. Settings below the call override it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import django_stack.settings
|
|
13
|
+
|
|
14
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
15
|
+
|
|
16
|
+
DJANGO_STACK = {
|
|
17
|
+
# Models anyone may create through the API, such as the contact form.
|
|
18
|
+
"PUBLIC_CREATE": ["content.formsubmission"],
|
|
19
|
+
# The apps export_content and import_content move between environments.
|
|
20
|
+
"CONTENT_APPS": ["content"],
|
|
21
|
+
# The function `manage.py seed_content` runs.
|
|
22
|
+
"SEED_CONTENT": "content.seed.seed",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
django_stack.settings.configure(globals(), base_dir=BASE_DIR)
|
|
26
|
+
|
|
27
|
+
INSTALLED_APPS = [
|
|
28
|
+
"django.contrib.admin", # Content Studio records its activity log with it
|
|
29
|
+
"django.contrib.auth",
|
|
30
|
+
"django.contrib.contenttypes",
|
|
31
|
+
"django.contrib.sessions",
|
|
32
|
+
"django.contrib.messages",
|
|
33
|
+
"django.contrib.staticfiles",
|
|
34
|
+
"rest_framework",
|
|
35
|
+
"huey.contrib.djhuey",
|
|
36
|
+
"huey.contrib.djhuey.stats",
|
|
37
|
+
"headless",
|
|
38
|
+
"content_studio",
|
|
39
|
+
"content_studio.media_library",
|
|
40
|
+
"content_studio.contrib.password_reset",
|
|
41
|
+
"django_stack",
|
|
42
|
+
"content",
|
|
43
|
+
# Last: builds the API routes once every model is exposed.
|
|
44
|
+
"headless.rest",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
MIDDLEWARE = [
|
|
48
|
+
"django.middleware.security.SecurityMiddleware",
|
|
49
|
+
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
50
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
51
|
+
"django.middleware.common.CommonMiddleware",
|
|
52
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
53
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
54
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
55
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
ROOT_URLCONF = "config.urls"
|
|
59
|
+
WSGI_APPLICATION = "config.wsgi.application"
|
|
60
|
+
ASGI_APPLICATION = "config.asgi.application"
|
|
61
|
+
|
|
62
|
+
TEMPLATES = [
|
|
63
|
+
{
|
|
64
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
65
|
+
"DIRS": [],
|
|
66
|
+
"APP_DIRS": True,
|
|
67
|
+
"OPTIONS": {
|
|
68
|
+
"context_processors": [
|
|
69
|
+
"django.template.context_processors.request",
|
|
70
|
+
"django.contrib.auth.context_processors.auth",
|
|
71
|
+
"django.contrib.messages.context_processors.messages",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
78
|
+
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
|
79
|
+
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
|
80
|
+
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
81
|
+
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
LANGUAGE_CODE = "en-us"
|
|
85
|
+
TIME_ZONE = "UTC"
|
|
86
|
+
USE_I18N = True
|
|
87
|
+
USE_TZ = True
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Content Studio site of {{project_name}}: the admin at /admin/.
|
|
3
|
+
CONTENT_STUDIO["ADMIN_SITE"] points here. See
|
|
4
|
+
https://djangocontentstudio.org/docs/extensions for the hooks and extensions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from content_studio.admin import ContentStudioSite
|
|
8
|
+
from content_studio.contrib.huey import HueyAdapter
|
|
9
|
+
from content_studio.dashboard.tasks import TaskDashboard
|
|
10
|
+
from content_studio.extensions import UserMenuLink
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SiteStudio(ContentStudioSite):
|
|
14
|
+
extensions = [
|
|
15
|
+
TaskDashboard(adapter=HueyAdapter()),
|
|
16
|
+
UserMenuLink(url="/tasks/", label="Tasks", icon="ph-bold ph-queue"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
admin_site = SiteStudio()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from django.urls import include, path
|
|
2
|
+
from django_stack.urls import media_urlpatterns
|
|
3
|
+
|
|
4
|
+
urlpatterns = [
|
|
5
|
+
path("admin/", include("content_studio.urls")),
|
|
6
|
+
path("api/", include("headless.rest.urls")),
|
|
7
|
+
path("", include("django_stack.urls")), # /healthz and /readyz
|
|
8
|
+
*media_urlpatterns(), # uploaded files, in development only
|
|
9
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The studios of the content models: how they look in Content Studio at
|
|
3
|
+
/admin/. See https://djangocontentstudio.org/docs/registering-models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from content_studio import register
|
|
7
|
+
from content_studio.admin import ModelStudio
|
|
8
|
+
from content_studio.form import ActivityLog, Field, FormSet
|
|
9
|
+
from django_stack.studio import PreviewButton
|
|
10
|
+
|
|
11
|
+
from .models import Author, Category, FormSubmission, Menu, Page, Post, Redirect, SiteSettings, Tag
|
|
12
|
+
|
|
13
|
+
# Components on more than one studio need their own component_id.
|
|
14
|
+
PAGE_PREVIEW = PreviewButton(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a01")
|
|
15
|
+
POST_PREVIEW = PreviewButton(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a02")
|
|
16
|
+
PAGE_ACTIVITY = ActivityLog(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a03")
|
|
17
|
+
POST_ACTIVITY = ActivityLog(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a04")
|
|
18
|
+
SEO = FormSet(
|
|
19
|
+
title="Search and sharing",
|
|
20
|
+
description="Leave empty to use the title and the site settings.",
|
|
21
|
+
fields=["seo_title", "seo_description", "share_image"],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@register(Page)
|
|
26
|
+
class PageStudio(ModelStudio):
|
|
27
|
+
icon = "ph-bold ph-file-text"
|
|
28
|
+
table_display = ["title", "path", "status", "published_at"]
|
|
29
|
+
list_filter = ["status"]
|
|
30
|
+
search_fields = ["title", "path"]
|
|
31
|
+
ordering = ["path"]
|
|
32
|
+
prepopulated_fields = {"slug": ["title"]}
|
|
33
|
+
allow_duplication = True
|
|
34
|
+
edit_width = "wide"
|
|
35
|
+
edit_main = [
|
|
36
|
+
FormSet(title="Page", fields=["title", Field("slug", col_span=1), Field("parent", col_span=1)]),
|
|
37
|
+
FormSet(title="Body", fields=["body"]),
|
|
38
|
+
SEO,
|
|
39
|
+
]
|
|
40
|
+
edit_sidebar = ["status", "published_at", PAGE_PREVIEW, "order", PAGE_ACTIVITY]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@register(Post)
|
|
44
|
+
class PostStudio(ModelStudio):
|
|
45
|
+
icon = "ph-bold ph-article"
|
|
46
|
+
table_display = ["title", "author", "status", "published_at"]
|
|
47
|
+
list_filter = ["status", "categories", "tags", "author"]
|
|
48
|
+
search_fields = ["title", "excerpt"]
|
|
49
|
+
ordering = ["-published_at"]
|
|
50
|
+
prepopulated_fields = {"slug": ["title"]}
|
|
51
|
+
allow_duplication = True
|
|
52
|
+
edit_width = "wide"
|
|
53
|
+
edit_main = [
|
|
54
|
+
FormSet(title="Post", fields=["title", "slug", "cover", "excerpt"]),
|
|
55
|
+
FormSet(title="Body", fields=["body"]),
|
|
56
|
+
SEO,
|
|
57
|
+
]
|
|
58
|
+
edit_sidebar = ["status", "published_at", POST_PREVIEW, "author", "categories", "tags", POST_ACTIVITY]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@register(Category, Tag)
|
|
62
|
+
class TaxonomyStudio(ModelStudio):
|
|
63
|
+
icon = "ph-bold ph-tag"
|
|
64
|
+
table_display = ["name", "slug"]
|
|
65
|
+
search_fields = ["name"]
|
|
66
|
+
prepopulated_fields = {"slug": ["name"]}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@register(Author)
|
|
70
|
+
class AuthorStudio(ModelStudio):
|
|
71
|
+
icon = "ph-bold ph-user-circle"
|
|
72
|
+
table_display = ["name", "slug"]
|
|
73
|
+
search_fields = ["name"]
|
|
74
|
+
prepopulated_fields = {"slug": ["name"]}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@register(Menu)
|
|
78
|
+
class MenuStudio(ModelStudio):
|
|
79
|
+
icon = "ph-bold ph-list"
|
|
80
|
+
table_display = ["name", "slug"]
|
|
81
|
+
prepopulated_fields = {"slug": ["name"]}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@register(SiteSettings)
|
|
85
|
+
class SiteSettingsStudio(ModelStudio):
|
|
86
|
+
icon = "ph-bold ph-gear"
|
|
87
|
+
is_singleton = True
|
|
88
|
+
edit_main = [
|
|
89
|
+
FormSet(title="Site", fields=["site_name", "tagline", "logo", "home_page"]),
|
|
90
|
+
FormSet(title="Search and sharing", fields=["seo_description", "share_image"]),
|
|
91
|
+
FormSet(title="Footer", fields=["footer_text", "social_links"]),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@register(Redirect)
|
|
96
|
+
class RedirectStudio(ModelStudio):
|
|
97
|
+
icon = "ph-bold ph-arrow-bend-up-right"
|
|
98
|
+
table_display = ["old_path", "new_path", "permanent"]
|
|
99
|
+
search_fields = ["old_path", "new_path"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@register(FormSubmission)
|
|
103
|
+
class FormSubmissionStudio(ModelStudio):
|
|
104
|
+
icon = "ph-bold ph-envelope"
|
|
105
|
+
table_display = ["name", "email", "created_at"]
|
|
106
|
+
search_fields = ["name", "email", "message"]
|
|
107
|
+
ordering = ["-created_at"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The blocks editors build page and post bodies from. Add a block here, add
|
|
3
|
+
it to a BlockField's ``blocks`` and render it in the frontend's block
|
|
4
|
+
registry (``frontend/src/components/blocks``). Blocks need no migration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from content_studio.blocks import Block
|
|
8
|
+
from content_studio.fields import HTMLField
|
|
9
|
+
from content_studio.media_library.fields import MediaField
|
|
10
|
+
from django.db import models
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextBlock(Block):
|
|
14
|
+
body = HTMLField()
|
|
15
|
+
|
|
16
|
+
class Meta:
|
|
17
|
+
description = "Rich text with headings, lists and links"
|
|
18
|
+
icon = "ph-bold ph-text-t"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ImageBlock(Block):
|
|
22
|
+
image = MediaField(file_type="image", blank=False)
|
|
23
|
+
caption = models.CharField(max_length=300, blank=True)
|
|
24
|
+
|
|
25
|
+
class Meta:
|
|
26
|
+
description = "An image from the media library"
|
|
27
|
+
icon = "ph-bold ph-image"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class QuoteBlock(Block):
|
|
31
|
+
text = models.TextField()
|
|
32
|
+
attribution = models.CharField(max_length=200, blank=True)
|
|
33
|
+
|
|
34
|
+
class Meta:
|
|
35
|
+
description = "A quotation"
|
|
36
|
+
icon = "ph-bold ph-quotes"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CallToActionBlock(Block):
|
|
40
|
+
title = models.CharField(max_length=200)
|
|
41
|
+
text = models.TextField(blank=True)
|
|
42
|
+
button_label = models.CharField(max_length=60)
|
|
43
|
+
button_url = models.CharField(max_length=500, help_text="A path on the site, such as /contact, or a full URL.")
|
|
44
|
+
|
|
45
|
+
class Meta:
|
|
46
|
+
name = "call_to_action"
|
|
47
|
+
description = "A highlighted message with a button"
|
|
48
|
+
icon = "ph-bold ph-megaphone"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
BODY_BLOCKS = [TextBlock, ImageBlock, QuoteBlock, CallToActionBlock]
|