sum-cli 3.0.0__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.
- sum/__init__.py +1 -0
- sum/boilerplate/.env.example +124 -0
- sum/boilerplate/.gitea/workflows/ci.yml +33 -0
- sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
- sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
- sum/boilerplate/.github/workflows/ci.yml +36 -0
- sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
- sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
- sum/boilerplate/.gitignore +45 -0
- sum/boilerplate/README.md +259 -0
- sum/boilerplate/manage.py +34 -0
- sum/boilerplate/project_name/__init__.py +5 -0
- sum/boilerplate/project_name/home/__init__.py +5 -0
- sum/boilerplate/project_name/home/apps.py +20 -0
- sum/boilerplate/project_name/home/management/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
- sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
- sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
- sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
- sum/boilerplate/project_name/home/models.py +13 -0
- sum/boilerplate/project_name/settings/__init__.py +5 -0
- sum/boilerplate/project_name/settings/base.py +348 -0
- sum/boilerplate/project_name/settings/local.py +78 -0
- sum/boilerplate/project_name/settings/production.py +106 -0
- sum/boilerplate/project_name/urls.py +33 -0
- sum/boilerplate/project_name/wsgi.py +16 -0
- sum/boilerplate/pytest.ini +5 -0
- sum/boilerplate/requirements.txt +25 -0
- sum/boilerplate/static/client/.gitkeep +3 -0
- sum/boilerplate/templates/overrides/.gitkeep +3 -0
- sum/boilerplate/tests/__init__.py +3 -0
- sum/boilerplate/tests/test_health.py +51 -0
- sum/cli.py +42 -0
- sum/commands/__init__.py +10 -0
- sum/commands/backup.py +308 -0
- sum/commands/check.py +128 -0
- sum/commands/init.py +265 -0
- sum/commands/promote.py +758 -0
- sum/commands/run.py +96 -0
- sum/commands/themes.py +56 -0
- sum/commands/update.py +301 -0
- sum/config.py +61 -0
- sum/docs/USER_GUIDE.md +663 -0
- sum/exceptions.py +45 -0
- sum/setup/__init__.py +17 -0
- sum/setup/auth.py +184 -0
- sum/setup/database.py +58 -0
- sum/setup/deps.py +73 -0
- sum/setup/git_ops.py +463 -0
- sum/setup/infrastructure.py +576 -0
- sum/setup/orchestrator.py +354 -0
- sum/setup/remote_themes.py +371 -0
- sum/setup/scaffold.py +500 -0
- sum/setup/seed.py +110 -0
- sum/setup/site_orchestrator.py +441 -0
- sum/setup/venv.py +89 -0
- sum/system_config.py +330 -0
- sum/themes_registry.py +180 -0
- sum/utils/__init__.py +25 -0
- sum/utils/django.py +97 -0
- sum/utils/environment.py +76 -0
- sum/utils/output.py +78 -0
- sum/utils/project.py +110 -0
- sum/utils/prompts.py +36 -0
- sum/utils/validation.py +313 -0
- sum_cli-3.0.0.dist-info/METADATA +127 -0
- sum_cli-3.0.0.dist-info/RECORD +72 -0
- sum_cli-3.0.0.dist-info/WHEEL +5 -0
- sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
- sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
- sum_cli-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HomePage model for this client project.
|
|
3
|
+
|
|
4
|
+
Re-exports the HomePage from sum_core.pages. The actual implementation is in
|
|
5
|
+
sum_core, which ships to clients via pip install sum-core.
|
|
6
|
+
|
|
7
|
+
This module exists so the 'home' app is properly registered in INSTALLED_APPS
|
|
8
|
+
and Django's model discovery finds the HomePage model.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from sum_core.pages.home import HomePage, HomePageHeroCTA
|
|
12
|
+
|
|
13
|
+
__all__ = ["HomePage", "HomePageHeroCTA"]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Django/Wagtail settings shared across all environments.
|
|
3
|
+
|
|
4
|
+
This file contains settings common to both local development and production.
|
|
5
|
+
Environment-specific settings should be placed in local.py or production.py.
|
|
6
|
+
|
|
7
|
+
Replace 'project_name' with your actual project name after copying.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Core Django Settings
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Theme Configuration
|
|
26
|
+
# =============================================================================
|
|
27
|
+
#
|
|
28
|
+
# Per THEME-ARCHITECTURE-SPECv1, themes are copied into the client project
|
|
29
|
+
# at init-time. Runtime template/static resolution uses theme/active/, NOT
|
|
30
|
+
# sum_core. The .sum/theme.json file is for provenance tracking only.
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_canonical_theme_root() -> Path | None:
|
|
35
|
+
"""
|
|
36
|
+
Optional dev override to load templates/static directly from canonical theme.
|
|
37
|
+
|
|
38
|
+
Controlled via SUM_CANONICAL_THEME_ROOT. If set, we validate the path and
|
|
39
|
+
prefer canonical assets ahead of theme/active.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
canonical_root_env = os.getenv("SUM_CANONICAL_THEME_ROOT")
|
|
43
|
+
if not canonical_root_env:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
canonical_root = Path(canonical_root_env)
|
|
47
|
+
templates_dir = canonical_root / "templates"
|
|
48
|
+
|
|
49
|
+
if not canonical_root.exists():
|
|
50
|
+
raise ImproperlyConfigured(
|
|
51
|
+
f"SUM_CANONICAL_THEME_ROOT='{canonical_root_env}' does not exist. "
|
|
52
|
+
"Expected <root>/templates and optional <root>/static. "
|
|
53
|
+
"Unset SUM_CANONICAL_THEME_ROOT to disable."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not templates_dir.exists():
|
|
57
|
+
raise ImproperlyConfigured(
|
|
58
|
+
f"SUM_CANONICAL_THEME_ROOT='{canonical_root_env}' is missing required "
|
|
59
|
+
"<root>/templates. Expected layout: <root>/templates and optional "
|
|
60
|
+
"<root>/static. Unset SUM_CANONICAL_THEME_ROOT to disable."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return canonical_root
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_project_theme_slug() -> str | None:
|
|
67
|
+
"""
|
|
68
|
+
Read the selected theme slug from .sum/theme.json for logging/debugging.
|
|
69
|
+
|
|
70
|
+
This is provenance only - not used for runtime loading.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Theme slug if configured, None otherwise
|
|
74
|
+
"""
|
|
75
|
+
theme_file = BASE_DIR / ".sum" / "theme.json"
|
|
76
|
+
if not theme_file.exists():
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with theme_file.open("r", encoding="utf-8") as f:
|
|
81
|
+
config = json.load(f)
|
|
82
|
+
theme_value = config.get("theme")
|
|
83
|
+
return theme_value if isinstance(theme_value, str) else None
|
|
84
|
+
except (json.JSONDecodeError, OSError):
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_theme_template_dirs() -> list[Path]:
|
|
89
|
+
"""
|
|
90
|
+
Get template directories for the active theme.
|
|
91
|
+
|
|
92
|
+
Per THEME-ARCHITECTURE-SPECv1, templates are resolved from:
|
|
93
|
+
0. SUM_CANONICAL_THEME_ROOT/templates (optional dev override, highest priority)
|
|
94
|
+
1. theme/active/templates/ (client-owned theme)
|
|
95
|
+
2. templates/overrides/ (client-specific overrides)
|
|
96
|
+
3. APP_DIRS (sum_core fallback)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of theme template directory paths (empty if no theme installed)
|
|
100
|
+
"""
|
|
101
|
+
canonical_theme_root = _get_canonical_theme_root()
|
|
102
|
+
|
|
103
|
+
theme_template_dirs: list[Path] = []
|
|
104
|
+
if canonical_theme_root:
|
|
105
|
+
theme_template_dirs.append(canonical_theme_root / "templates")
|
|
106
|
+
|
|
107
|
+
theme_templates_dir = BASE_DIR / "theme" / "active" / "templates"
|
|
108
|
+
if theme_templates_dir.exists():
|
|
109
|
+
theme_template_dirs.append(theme_templates_dir)
|
|
110
|
+
|
|
111
|
+
return theme_template_dirs
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Theme slug from provenance (for logging/debugging only)
|
|
115
|
+
THEME_SLUG = _get_project_theme_slug()
|
|
116
|
+
|
|
117
|
+
# SECURITY WARNING: keep the secret key used in production secret!
|
|
118
|
+
# Override this via environment variable in production
|
|
119
|
+
SECRET_KEY: str = os.getenv(
|
|
120
|
+
"DJANGO_SECRET_KEY", "insecure-dev-only-change-in-production"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# =============================================================================
|
|
124
|
+
# Application Definition
|
|
125
|
+
# =============================================================================
|
|
126
|
+
|
|
127
|
+
INSTALLED_APPS: list[str] = [
|
|
128
|
+
# Django core
|
|
129
|
+
"django.contrib.admin",
|
|
130
|
+
"django.contrib.auth",
|
|
131
|
+
"django.contrib.contenttypes",
|
|
132
|
+
"django.contrib.sessions",
|
|
133
|
+
"django.contrib.messages",
|
|
134
|
+
"django.contrib.staticfiles",
|
|
135
|
+
# Wagtail core
|
|
136
|
+
"wagtail",
|
|
137
|
+
"wagtail.admin",
|
|
138
|
+
"wagtail.users",
|
|
139
|
+
"wagtail.images",
|
|
140
|
+
"wagtail.documents",
|
|
141
|
+
"wagtail.snippets",
|
|
142
|
+
"wagtail.sites",
|
|
143
|
+
"wagtail.search",
|
|
144
|
+
"wagtail.contrib.forms",
|
|
145
|
+
"wagtail.contrib.settings",
|
|
146
|
+
"wagtail.contrib.redirects",
|
|
147
|
+
# Wagtail dependencies
|
|
148
|
+
"modelcluster",
|
|
149
|
+
"taggit",
|
|
150
|
+
# SUM Core apps (the reusable platform package)
|
|
151
|
+
"sum_core",
|
|
152
|
+
"sum_core.pages",
|
|
153
|
+
"sum_core.banners",
|
|
154
|
+
"sum_core.navigation",
|
|
155
|
+
"sum_core.leads",
|
|
156
|
+
"sum_core.forms",
|
|
157
|
+
"sum_core.analytics",
|
|
158
|
+
"sum_core.seo",
|
|
159
|
+
"sum_core.seo_engine",
|
|
160
|
+
# Client home app (update after renaming project_name)
|
|
161
|
+
"project_name.home",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
MIDDLEWARE: list[str] = [
|
|
165
|
+
"sum_core.ops.middleware.CorrelationIdMiddleware", # Request correlation
|
|
166
|
+
"django.middleware.security.SecurityMiddleware",
|
|
167
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
168
|
+
"django.middleware.common.CommonMiddleware",
|
|
169
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
170
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
171
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
172
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
173
|
+
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
ROOT_URLCONF: str = "project_name.urls"
|
|
177
|
+
|
|
178
|
+
TEMPLATES = [
|
|
179
|
+
{
|
|
180
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
181
|
+
"DIRS": [
|
|
182
|
+
# Per THEME-ARCHITECTURE-SPECv1, resolution order:
|
|
183
|
+
# 0. SUM_CANONICAL_THEME_ROOT/templates (optional dev override)
|
|
184
|
+
# 1. theme/active/templates/ (client-owned theme)
|
|
185
|
+
# 2. templates/overrides/ (client-specific overrides)
|
|
186
|
+
# 3. APP_DIRS (sum_core fallback)
|
|
187
|
+
*_get_theme_template_dirs(),
|
|
188
|
+
BASE_DIR / "templates" / "overrides",
|
|
189
|
+
],
|
|
190
|
+
"APP_DIRS": True,
|
|
191
|
+
"OPTIONS": {
|
|
192
|
+
"context_processors": [
|
|
193
|
+
"django.template.context_processors.debug",
|
|
194
|
+
"django.template.context_processors.request",
|
|
195
|
+
"django.contrib.auth.context_processors.auth",
|
|
196
|
+
"django.contrib.messages.context_processors.messages",
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
WSGI_APPLICATION: str = "project_name.wsgi.application"
|
|
203
|
+
|
|
204
|
+
# =============================================================================
|
|
205
|
+
# Password Validation
|
|
206
|
+
# =============================================================================
|
|
207
|
+
|
|
208
|
+
AUTH_PASSWORD_VALIDATORS: list[dict[str, str]] = [
|
|
209
|
+
{
|
|
210
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
# =============================================================================
|
|
224
|
+
# Internationalization
|
|
225
|
+
# =============================================================================
|
|
226
|
+
|
|
227
|
+
LANGUAGE_CODE: str = "en-gb"
|
|
228
|
+
TIME_ZONE: str = "Europe/London"
|
|
229
|
+
USE_I18N: bool = True
|
|
230
|
+
USE_TZ: bool = True
|
|
231
|
+
|
|
232
|
+
# =============================================================================
|
|
233
|
+
# Static & Media Files
|
|
234
|
+
# =============================================================================
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _get_theme_static_dirs() -> list[Path]:
|
|
238
|
+
"""
|
|
239
|
+
Get static directories for the active theme.
|
|
240
|
+
|
|
241
|
+
Per THEME-ARCHITECTURE-SPECv1, static files are resolved from:
|
|
242
|
+
0. SUM_CANONICAL_THEME_ROOT/static/ (optional dev override, highest priority)
|
|
243
|
+
1. theme/active/static/ (client-owned theme, highest priority)
|
|
244
|
+
2. static/ (client-specific statics)
|
|
245
|
+
3. APP_DIRS (sum_core fallback)
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of theme static directory paths (empty if no theme installed)
|
|
249
|
+
"""
|
|
250
|
+
canonical_theme_root = _get_canonical_theme_root()
|
|
251
|
+
|
|
252
|
+
static_dirs: list[Path] = []
|
|
253
|
+
if canonical_theme_root:
|
|
254
|
+
canonical_static_dir = canonical_theme_root / "static"
|
|
255
|
+
if canonical_static_dir.exists():
|
|
256
|
+
static_dirs.append(canonical_static_dir)
|
|
257
|
+
|
|
258
|
+
theme_static_dir = BASE_DIR / "theme" / "active" / "static"
|
|
259
|
+
if theme_static_dir.exists():
|
|
260
|
+
static_dirs.append(theme_static_dir)
|
|
261
|
+
|
|
262
|
+
return static_dirs
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
STATIC_URL: str = "/static/"
|
|
266
|
+
STATICFILES_DIRS: list[Path] = [
|
|
267
|
+
# Per THEME-ARCHITECTURE-SPECv1, resolution order:
|
|
268
|
+
# 0. SUM_CANONICAL_THEME_ROOT/static/ (optional dev override)
|
|
269
|
+
# 1. theme/active/static/ (client-owned theme, highest priority)
|
|
270
|
+
# 2. static/ (client-specific statics)
|
|
271
|
+
*_get_theme_static_dirs(),
|
|
272
|
+
BASE_DIR / "static",
|
|
273
|
+
]
|
|
274
|
+
STATIC_ROOT: Path = Path(os.getenv("DJANGO_STATIC_ROOT", str(BASE_DIR / "staticfiles")))
|
|
275
|
+
|
|
276
|
+
MEDIA_URL: str = "/media/"
|
|
277
|
+
MEDIA_ROOT: Path = Path(os.getenv("DJANGO_MEDIA_ROOT", str(BASE_DIR / "media")))
|
|
278
|
+
|
|
279
|
+
DEFAULT_AUTO_FIELD: str = "django.db.models.BigAutoField"
|
|
280
|
+
|
|
281
|
+
# =============================================================================
|
|
282
|
+
# Wagtail Settings
|
|
283
|
+
# =============================================================================
|
|
284
|
+
|
|
285
|
+
# TODO: Update this to your site name
|
|
286
|
+
WAGTAIL_SITE_NAME: str = "My Site"
|
|
287
|
+
WAGTAIL_ENABLE_UPDATE_CHECK: str = "lts"
|
|
288
|
+
WAGTAILADMIN_BASE_URL: str = os.getenv("WAGTAILADMIN_BASE_URL", "http://localhost:8001")
|
|
289
|
+
|
|
290
|
+
# =============================================================================
|
|
291
|
+
# Cache Configuration (used for rate limiting)
|
|
292
|
+
# =============================================================================
|
|
293
|
+
|
|
294
|
+
CACHES = {
|
|
295
|
+
"default": {
|
|
296
|
+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
297
|
+
"LOCATION": "sum-core-cache",
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# =============================================================================
|
|
302
|
+
# File Upload Limits
|
|
303
|
+
# =============================================================================
|
|
304
|
+
|
|
305
|
+
FILE_UPLOAD_MAX_MEMORY_SIZE: int = 52_428_800 # 50MB
|
|
306
|
+
DATA_UPLOAD_MAX_MEMORY_SIZE: int = 52_428_800 # 50MB
|
|
307
|
+
|
|
308
|
+
# =============================================================================
|
|
309
|
+
# Django 6.0 Compatibility
|
|
310
|
+
# =============================================================================
|
|
311
|
+
|
|
312
|
+
# Silence Django 6.0 deprecation warning about URL scheme
|
|
313
|
+
# Default scheme will change from 'http' to 'https' in Django 6.0
|
|
314
|
+
FORMS_URLFIELD_ASSUME_HTTPS: bool = True
|
|
315
|
+
|
|
316
|
+
# =============================================================================
|
|
317
|
+
# Celery Configuration (defaults for development)
|
|
318
|
+
# =============================================================================
|
|
319
|
+
|
|
320
|
+
CELERY_BROKER_URL: str = os.getenv("CELERY_BROKER_URL", "memory://")
|
|
321
|
+
CELERY_RESULT_BACKEND: str = os.getenv("CELERY_RESULT_BACKEND", "cache+memory://")
|
|
322
|
+
|
|
323
|
+
# =============================================================================
|
|
324
|
+
# Email Configuration (defaults for development)
|
|
325
|
+
# =============================================================================
|
|
326
|
+
|
|
327
|
+
EMAIL_BACKEND: str = os.getenv(
|
|
328
|
+
"EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend"
|
|
329
|
+
)
|
|
330
|
+
EMAIL_HOST: str = os.getenv("EMAIL_HOST", "localhost")
|
|
331
|
+
EMAIL_PORT: int = int(os.getenv("EMAIL_PORT", "25"))
|
|
332
|
+
EMAIL_HOST_USER: str = os.getenv("EMAIL_HOST_USER", "")
|
|
333
|
+
EMAIL_HOST_PASSWORD: str = os.getenv("EMAIL_HOST_PASSWORD", "")
|
|
334
|
+
EMAIL_USE_TLS: bool = os.getenv("EMAIL_USE_TLS", "False").lower() == "true"
|
|
335
|
+
EMAIL_USE_SSL: bool = os.getenv("EMAIL_USE_SSL", "False").lower() == "true"
|
|
336
|
+
DEFAULT_FROM_EMAIL: str = os.getenv("DEFAULT_FROM_EMAIL", "noreply@example.com")
|
|
337
|
+
|
|
338
|
+
# =============================================================================
|
|
339
|
+
# Lead Notification Settings
|
|
340
|
+
# =============================================================================
|
|
341
|
+
|
|
342
|
+
LEAD_NOTIFICATION_EMAIL: str = os.getenv("LEAD_NOTIFICATION_EMAIL", "")
|
|
343
|
+
|
|
344
|
+
# =============================================================================
|
|
345
|
+
# Webhook Configuration
|
|
346
|
+
# =============================================================================
|
|
347
|
+
|
|
348
|
+
ZAPIER_WEBHOOK_URL: str = os.getenv("ZAPIER_WEBHOOK_URL", "")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local development settings.
|
|
3
|
+
|
|
4
|
+
This file contains settings for local development and staging.
|
|
5
|
+
Requires PostgreSQL - configure via environment variables in .env file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from sum_core.ops.logging import get_logging_config
|
|
13
|
+
from sum_core.ops.sentry import init_sentry
|
|
14
|
+
|
|
15
|
+
from .base import * # noqa: F401, F403
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Debug Settings
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
DEBUG: bool = True
|
|
22
|
+
ALLOWED_HOSTS: list[str] = ["localhost", "127.0.0.1", "[::1]", "testserver"]
|
|
23
|
+
|
|
24
|
+
# Support additional hosts via environment variable (e.g., for ngrok, custom domains)
|
|
25
|
+
_allowed_hosts_extra = os.getenv("ALLOWED_HOSTS_EXTRA", "")
|
|
26
|
+
if _allowed_hosts_extra:
|
|
27
|
+
ALLOWED_HOSTS.extend(
|
|
28
|
+
host.strip() for host in _allowed_hosts_extra.split(",") if host.strip()
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# CSRF trusted origins for local dev with tunnels/custom domains
|
|
32
|
+
CSRF_TRUSTED_ORIGINS: list[str] = [
|
|
33
|
+
origin.strip()
|
|
34
|
+
for origin in os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",")
|
|
35
|
+
if origin.strip()
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# =============================================================================
|
|
39
|
+
# Database - PostgreSQL (configured via environment variables)
|
|
40
|
+
# =============================================================================
|
|
41
|
+
|
|
42
|
+
DATABASES = {
|
|
43
|
+
"default": {
|
|
44
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
45
|
+
"NAME": os.environ["DJANGO_DB_NAME"],
|
|
46
|
+
"USER": os.environ["DJANGO_DB_USER"],
|
|
47
|
+
"PASSWORD": os.environ["DJANGO_DB_PASSWORD"],
|
|
48
|
+
"HOST": os.environ.get("DJANGO_DB_HOST", "localhost"),
|
|
49
|
+
"PORT": os.environ.get("DJANGO_DB_PORT", "5432"),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# Cache - Local memory cache for development
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
CACHES = {
|
|
58
|
+
"default": {
|
|
59
|
+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
60
|
+
"LOCATION": "client-cache",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# Celery - Run tasks synchronously for local development
|
|
66
|
+
# =============================================================================
|
|
67
|
+
|
|
68
|
+
CELERY_TASK_ALWAYS_EAGER: bool = True
|
|
69
|
+
CELERY_TASK_EAGER_PROPAGATES: bool = True
|
|
70
|
+
|
|
71
|
+
# =============================================================================
|
|
72
|
+
# Observability (sum_core.ops)
|
|
73
|
+
# =============================================================================
|
|
74
|
+
|
|
75
|
+
LOGGING = get_logging_config(debug=DEBUG)
|
|
76
|
+
|
|
77
|
+
# Initialize Sentry if SENTRY_DSN is set (no-ops otherwise)
|
|
78
|
+
init_sentry()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Production settings.
|
|
3
|
+
|
|
4
|
+
This file contains settings for production deployments.
|
|
5
|
+
All sensitive values must be provided via environment variables.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from sum_core.ops.logging import get_logging_config
|
|
13
|
+
from sum_core.ops.sentry import init_sentry
|
|
14
|
+
|
|
15
|
+
from .base import * # noqa: F401, F403
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Security Settings
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
DEBUG: bool = False
|
|
22
|
+
|
|
23
|
+
# SECURITY: ALLOWED_HOSTS must be set in production
|
|
24
|
+
ALLOWED_HOSTS: list[str] = [
|
|
25
|
+
host.strip() for host in os.getenv("ALLOWED_HOSTS", "").split(",") if host.strip()
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# SECURITY: Enforce HTTPS
|
|
29
|
+
SECURE_SSL_REDIRECT: bool = os.getenv("SECURE_SSL_REDIRECT", "True").lower() == "true"
|
|
30
|
+
SESSION_COOKIE_SECURE: bool = True
|
|
31
|
+
CSRF_COOKIE_SECURE: bool = True
|
|
32
|
+
SECURE_BROWSER_XSS_FILTER: bool = True
|
|
33
|
+
SECURE_CONTENT_TYPE_NOSNIFF: bool = True
|
|
34
|
+
X_FRAME_OPTIONS: str = "DENY"
|
|
35
|
+
|
|
36
|
+
# HSTS settings
|
|
37
|
+
SECURE_HSTS_SECONDS: int = 31536000 # 1 year
|
|
38
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS: bool = True
|
|
39
|
+
SECURE_HSTS_PRELOAD: bool = True
|
|
40
|
+
|
|
41
|
+
# SECURITY: CSRF trusted origins for reverse proxy deployments
|
|
42
|
+
CSRF_TRUSTED_ORIGINS: list[str] = [
|
|
43
|
+
origin.strip()
|
|
44
|
+
for origin in os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",")
|
|
45
|
+
if origin.strip()
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# SECURITY: Trust X-Forwarded-Proto header from reverse proxy (Caddy, nginx, AWS ALB)
|
|
49
|
+
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# Database - PostgreSQL for production
|
|
53
|
+
# =============================================================================
|
|
54
|
+
|
|
55
|
+
DATABASES = {
|
|
56
|
+
"default": {
|
|
57
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
58
|
+
"NAME": os.environ["DJANGO_DB_NAME"],
|
|
59
|
+
"USER": os.environ["DJANGO_DB_USER"],
|
|
60
|
+
"PASSWORD": os.environ["DJANGO_DB_PASSWORD"],
|
|
61
|
+
"HOST": os.environ["DJANGO_DB_HOST"],
|
|
62
|
+
"PORT": os.getenv("DJANGO_DB_PORT", "5432"),
|
|
63
|
+
"CONN_MAX_AGE": 60,
|
|
64
|
+
"OPTIONS": {
|
|
65
|
+
"connect_timeout": 10,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# =============================================================================
|
|
71
|
+
# Cache - Redis for production
|
|
72
|
+
# =============================================================================
|
|
73
|
+
|
|
74
|
+
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
|
75
|
+
|
|
76
|
+
CACHES = {
|
|
77
|
+
"default": {
|
|
78
|
+
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
|
79
|
+
"LOCATION": REDIS_URL,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# =============================================================================
|
|
84
|
+
# Celery - Production broker configuration
|
|
85
|
+
# =============================================================================
|
|
86
|
+
|
|
87
|
+
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", REDIS_URL) # noqa: F405
|
|
88
|
+
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", REDIS_URL) # noqa: F405
|
|
89
|
+
CELERY_TASK_ALWAYS_EAGER: bool = False
|
|
90
|
+
CELERY_TASK_EAGER_PROPAGATES: bool = False
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Static Files - Production configuration
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
# In production, static files should be served by a CDN or whitenoise
|
|
97
|
+
# This is a basic setup; extend as needed for your infrastructure
|
|
98
|
+
|
|
99
|
+
# =============================================================================
|
|
100
|
+
# Observability (sum_core.ops)
|
|
101
|
+
# =============================================================================
|
|
102
|
+
|
|
103
|
+
LOGGING = get_logging_config(debug=DEBUG)
|
|
104
|
+
|
|
105
|
+
# Initialize Sentry (required in production if SENTRY_DSN is set)
|
|
106
|
+
init_sentry()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URL routing for this client project.
|
|
3
|
+
|
|
4
|
+
Includes all sum_core endpoints plus Wagtail page serving.
|
|
5
|
+
Replace 'project_name' with your actual project name after copying.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.conf.urls.static import static
|
|
12
|
+
from django.contrib import admin
|
|
13
|
+
from django.urls import include, path
|
|
14
|
+
from wagtail import urls as wagtail_urls
|
|
15
|
+
from wagtail.admin import urls as wagtailadmin_urls
|
|
16
|
+
from wagtail.documents import urls as wagtaildocs_urls
|
|
17
|
+
|
|
18
|
+
urlpatterns = [
|
|
19
|
+
# Django admin (for FormConfiguration etc.)
|
|
20
|
+
path("django-admin/", admin.site.urls),
|
|
21
|
+
# Wagtail admin
|
|
22
|
+
path("admin/", include(wagtailadmin_urls)),
|
|
23
|
+
path("documents/", include(wagtaildocs_urls)),
|
|
24
|
+
# SUM Core endpoints
|
|
25
|
+
path("forms/", include("sum_core.forms.urls")), # Form submissions
|
|
26
|
+
path("", include("sum_core.ops.urls")), # /health/
|
|
27
|
+
path("", include("sum_core.seo.urls")), # sitemap.xml, robots.txt
|
|
28
|
+
# Wagtail page serving (must be last)
|
|
29
|
+
path("", include(wagtail_urls)),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
if settings.DEBUG:
|
|
33
|
+
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WSGI application entry point.
|
|
3
|
+
|
|
4
|
+
Replace 'project_name' with your actual project name after copying.
|
|
5
|
+
For production, ensure DJANGO_SETTINGS_MODULE points to production settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from django.core.wsgi import get_wsgi_application
|
|
13
|
+
|
|
14
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings.local")
|
|
15
|
+
|
|
16
|
+
application = get_wsgi_application()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Client Project Requirements
|
|
2
|
+
#
|
|
3
|
+
# Default: Git tag pinning (recommended for client projects)
|
|
4
|
+
# Replace SUM_CORE_GIT_REF with the actual tag/version, e.g., v0.1.0
|
|
5
|
+
#
|
|
6
|
+
# NOTE: For monorepo development mode, comment out the git install below
|
|
7
|
+
# and uncomment the editable install:
|
|
8
|
+
# -e ../../core
|
|
9
|
+
|
|
10
|
+
sum-core @ git+https://github.com/markashton480/sum-core.git@v0.7.2#subdirectory=core
|
|
11
|
+
|
|
12
|
+
# PostgreSQL driver for production deployments
|
|
13
|
+
psycopg[binary]>=3.2,<4
|
|
14
|
+
|
|
15
|
+
# Required by Django's Redis cache backend
|
|
16
|
+
redis>=5,<6
|
|
17
|
+
|
|
18
|
+
# WSGI server (used by typical systemd + Caddy deploy)
|
|
19
|
+
gunicorn>=22,<24
|
|
20
|
+
|
|
21
|
+
# Required by seeders for YAML content profiles
|
|
22
|
+
pyyaml>=6,<7
|
|
23
|
+
|
|
24
|
+
# Load environment variables from .env file
|
|
25
|
+
python-dotenv>=1.0,<2
|