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.
Files changed (72) hide show
  1. sum/__init__.py +1 -0
  2. sum/boilerplate/.env.example +124 -0
  3. sum/boilerplate/.gitea/workflows/ci.yml +33 -0
  4. sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
  5. sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
  6. sum/boilerplate/.github/workflows/ci.yml +36 -0
  7. sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
  8. sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
  9. sum/boilerplate/.gitignore +45 -0
  10. sum/boilerplate/README.md +259 -0
  11. sum/boilerplate/manage.py +34 -0
  12. sum/boilerplate/project_name/__init__.py +5 -0
  13. sum/boilerplate/project_name/home/__init__.py +5 -0
  14. sum/boilerplate/project_name/home/apps.py +20 -0
  15. sum/boilerplate/project_name/home/management/__init__.py +0 -0
  16. sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
  17. sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
  18. sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
  19. sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
  20. sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
  21. sum/boilerplate/project_name/home/models.py +13 -0
  22. sum/boilerplate/project_name/settings/__init__.py +5 -0
  23. sum/boilerplate/project_name/settings/base.py +348 -0
  24. sum/boilerplate/project_name/settings/local.py +78 -0
  25. sum/boilerplate/project_name/settings/production.py +106 -0
  26. sum/boilerplate/project_name/urls.py +33 -0
  27. sum/boilerplate/project_name/wsgi.py +16 -0
  28. sum/boilerplate/pytest.ini +5 -0
  29. sum/boilerplate/requirements.txt +25 -0
  30. sum/boilerplate/static/client/.gitkeep +3 -0
  31. sum/boilerplate/templates/overrides/.gitkeep +3 -0
  32. sum/boilerplate/tests/__init__.py +3 -0
  33. sum/boilerplate/tests/test_health.py +51 -0
  34. sum/cli.py +42 -0
  35. sum/commands/__init__.py +10 -0
  36. sum/commands/backup.py +308 -0
  37. sum/commands/check.py +128 -0
  38. sum/commands/init.py +265 -0
  39. sum/commands/promote.py +758 -0
  40. sum/commands/run.py +96 -0
  41. sum/commands/themes.py +56 -0
  42. sum/commands/update.py +301 -0
  43. sum/config.py +61 -0
  44. sum/docs/USER_GUIDE.md +663 -0
  45. sum/exceptions.py +45 -0
  46. sum/setup/__init__.py +17 -0
  47. sum/setup/auth.py +184 -0
  48. sum/setup/database.py +58 -0
  49. sum/setup/deps.py +73 -0
  50. sum/setup/git_ops.py +463 -0
  51. sum/setup/infrastructure.py +576 -0
  52. sum/setup/orchestrator.py +354 -0
  53. sum/setup/remote_themes.py +371 -0
  54. sum/setup/scaffold.py +500 -0
  55. sum/setup/seed.py +110 -0
  56. sum/setup/site_orchestrator.py +441 -0
  57. sum/setup/venv.py +89 -0
  58. sum/system_config.py +330 -0
  59. sum/themes_registry.py +180 -0
  60. sum/utils/__init__.py +25 -0
  61. sum/utils/django.py +97 -0
  62. sum/utils/environment.py +76 -0
  63. sum/utils/output.py +78 -0
  64. sum/utils/project.py +110 -0
  65. sum/utils/prompts.py +36 -0
  66. sum/utils/validation.py +313 -0
  67. sum_cli-3.0.0.dist-info/METADATA +127 -0
  68. sum_cli-3.0.0.dist-info/RECORD +72 -0
  69. sum_cli-3.0.0.dist-info/WHEEL +5 -0
  70. sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
  71. sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
  72. sum_cli-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """
2
+ Migrations package for the home app.
3
+ """
@@ -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,5 @@
1
+ """
2
+ Settings package for multi-environment configuration.
3
+
4
+ This package allows different settings modules for local and production environments.
5
+ """
@@ -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,5 @@
1
+ [pytest]
2
+ DJANGO_SETTINGS_MODULE = project_name.settings.local
3
+ python_files = test_*.py
4
+ python_functions = test_*
5
+ addopts = -v --tb=short
@@ -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
@@ -0,0 +1,3 @@
1
+ # Placeholder to preserve client static files directory in Git
2
+ # Place client-specific CSS, JS, and images here.
3
+ # Example: client/css/custom.css, client/images/logo.png
@@ -0,0 +1,3 @@
1
+ # Placeholder to preserve template overrides directory in Git
2
+ # Place template overrides here mirroring sum_core template paths.
3
+ # Example: overrides/sum_core/home_page.html
@@ -0,0 +1,3 @@
1
+ """
2
+ Test package for this client project.
3
+ """