Populate the client website template with catalog feature flags.

Extract always-on public/portal/UTM plus optional email_sms, directmail, blog, payments, social, and social_ai so new client sites can be bootstrapped from this seed.

Refs #1
Refs #2

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 07:55:26 -05:00
co-authored by Cursor
parent 45d0888d33
commit 787f0e48fb
297 changed files with 32534 additions and 3 deletions
View File
+10
View File
@@ -0,0 +1,10 @@
from django.contrib import admin
from core.models import StoredFile
@admin.register(StoredFile)
class StoredFileAdmin(admin.ModelAdmin):
list_display = ("filename", "kind", "content_type", "size", "created_at")
list_filter = ("kind", "content_type")
readonly_fields = ("size", "content_type", "created_at", "updated_at")
+11
View File
@@ -0,0 +1,11 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"
def ready(self):
from core.registry import register_feature
register_feature("core")
+9
View File
@@ -0,0 +1,9 @@
from core.registry import enabled_features, portal_nav, public_nav
def feature_registry(request):
return {
"portal_nav": portal_nav(),
"public_nav_extra": public_nav(),
"enabled_features": enabled_features(),
}
View File
@@ -0,0 +1,46 @@
import time
from django.core.management.base import BaseCommand
from core.registry import run_dispatchers
class Command(BaseCommand):
help = (
"Enqueue due scheduled work registered by installed apps. "
"Pass --loop to poll until stopped (worker entrypoint)."
)
def add_arguments(self, parser):
parser.add_argument(
"--loop",
action="store_true",
help="Poll forever until SIGINT/SIGTERM.",
)
parser.add_argument(
"--interval",
type=int,
default=15,
help="Seconds between polls when --loop is set (default 15).",
)
def handle(self, *args, **options):
self.verbosity = int(options.get("verbosity", 1))
if options["loop"]:
interval = max(1, int(options["interval"] or 15))
self.stdout.write(f"Polling due scheduled work every {interval}s.")
try:
while True:
self._dispatch_once()
time.sleep(interval)
except KeyboardInterrupt:
self.stdout.write("dispatch_due loop stopped.")
return
self._dispatch_once()
def _dispatch_once(self):
enqueued = run_dispatchers()
if enqueued:
self.stdout.write(self.style.SUCCESS(f"Enqueued {enqueued} due item(s)."))
elif self.verbosity >= 2:
self.stdout.write("No due items.")
+35
View File
@@ -0,0 +1,35 @@
# Generated by Django 6.1 on 2026-08-26 11:38
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='StoredFile',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('kind', models.CharField(choices=[('campaign_image', 'Campaign image'), ('social_image', 'Social image'), ('social_video', 'Social video'), ('invoice_pdf', 'Invoice PDF')], default='campaign_image', max_length=32)),
('filename', models.CharField(blank=True, max_length=255)),
('content_type', models.CharField(max_length=128)),
('size', models.PositiveIntegerField(default=0)),
('data', models.BinaryField()),
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_files', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
]
View File
+52
View File
@@ -0,0 +1,52 @@
import uuid
from django.conf import settings
from django.db import models
class TimeStampedModel(models.Model):
"""Abstract base with created/updated timestamps."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class UUIDPrimaryKeyModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class StoredFile(UUIDPrimaryKeyModel, TimeStampedModel):
"""Binary file blob in the database (no filesystem media storage)."""
class Kind(models.TextChoices):
CAMPAIGN_IMAGE = "campaign_image", "Campaign image"
SOCIAL_IMAGE = "social_image", "Social image"
SOCIAL_VIDEO = "social_video", "Social video"
INVOICE_PDF = "invoice_pdf", "Invoice PDF"
kind = models.CharField(
max_length=32, choices=Kind.choices, default=Kind.CAMPAIGN_IMAGE
)
filename = models.CharField(max_length=255, blank=True)
content_type = models.CharField(max_length=128)
size = models.PositiveIntegerField(default=0)
data = models.BinaryField()
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="uploaded_files",
)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.filename or str(self.pk)
+105
View File
@@ -0,0 +1,105 @@
"""Optional-app registry. Apps register from AppConfig.ready() — never from always-on imports."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable
from django.urls import reverse
@dataclass(frozen=True)
class NavItem:
section: str
label: str
url_name: str
group: str = "Overview"
order: int = 100
GROUP_ORDER = {
"Overview": 10,
"Outreach": 20,
"Content": 30,
"Social": 40,
"Billing": 50,
}
_portal_nav: list[NavItem] = []
_public_nav: list[NavItem] = []
_dashboard_collectors: list[Callable] = []
_dispatchers: list[Callable] = []
_features: set[str] = set()
def register_feature(name: str) -> None:
_features.add(name)
def enabled_features() -> frozenset[str]:
return frozenset(_features)
def register_portal_nav(
*,
section: str,
label: str,
url_name: str,
group: str = "Overview",
order: int = 100,
) -> None:
item = NavItem(
section=section, label=label, url_name=url_name, group=group, order=order
)
_portal_nav.append(item)
def register_public_nav(
*,
section: str,
label: str,
url_name: str,
order: int = 100,
) -> None:
_public_nav.append(
NavItem(section=section, label=label, url_name=url_name, order=order)
)
def register_dashboard_collector(fn: Callable) -> None:
"""fn(request) -> dict merged into dashboard home context."""
_dashboard_collectors.append(fn)
def register_dispatcher(fn: Callable) -> None:
"""fn() -> int count of items enqueued. Called by dispatch_due."""
_dispatchers.append(fn)
def portal_nav() -> list[NavItem]:
return sorted(
_portal_nav,
key=lambda i: (GROUP_ORDER.get(i.group, 99), i.order, i.label),
)
def public_nav() -> list[NavItem]:
return sorted(_public_nav, key=lambda i: (i.order, i.label))
def collect_dashboard_context(request) -> dict:
extra: dict = {}
for fn in _dashboard_collectors:
extra.update(fn(request) or {})
return extra
def run_dispatchers() -> int:
total = 0
for fn in _dispatchers:
total += int(fn() or 0)
return total
def reverse_nav(item: NavItem) -> str:
return reverse(item.url_name)
+19
View File
@@ -0,0 +1,19 @@
"""Shared schedule parsing for campaigns and social posts."""
from datetime import datetime
from django.utils import timezone
def parse_scheduled_for(raw: str | None):
"""Parse optional ``datetime-local`` value into an aware datetime."""
value = (raw or "").strip()
if not value:
return None
try:
parsed = datetime.fromisoformat(value)
except ValueError as exc:
raise ValueError("Invalid schedule datetime.") from exc
if timezone.is_naive(parsed):
return timezone.make_aware(parsed, timezone.get_current_timezone())
return parsed
+39
View File
@@ -0,0 +1,39 @@
from io import StringIO
from django.core.management import call_command
from django.test import Client, TestCase, override_settings
from django.urls import reverse
class HealthzTests(TestCase):
def test_healthz_ok(self):
response = Client().get("/healthz/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["status"], "ok")
class UnderConstructionTests(TestCase):
@override_settings(SITE_UNDER_CONSTRUCTION=True)
def test_home_redirects_when_gated(self):
response = Client().get("/")
self.assertEqual(response.status_code, 302)
self.assertIn("/under-construction", response["Location"])
@override_settings(SITE_UNDER_CONSTRUCTION=False)
def test_home_ok_when_open(self):
response = Client().get("/")
self.assertEqual(response.status_code, 200)
class PublicSmokeTests(TestCase):
def test_about_and_contact_get(self):
client = Client()
self.assertEqual(client.get(reverse("public:about")).status_code, 200)
self.assertEqual(client.get(reverse("public:contact")).status_code, 200)
class DispatchDueTests(TestCase):
def test_quiet_when_nothing_due(self):
out = StringIO()
call_command("dispatch_due", stdout=out)
self.assertEqual(out.getvalue(), "")
+92
View File
@@ -0,0 +1,92 @@
"""Feature-flag isolation: flags control INSTALLED_APPS membership and deps."""
import ast
from pathlib import Path
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
def _read_settings_source() -> str:
return (Path(__file__).resolve().parent.parent / "client_site/settings/base.py").read_text()
class FeatureFlagSettingsTests(SimpleTestCase):
def test_optional_apps_are_gated_in_settings(self):
src = _read_settings_source()
self.assertIn("FEATURE_EMAIL_SMS", src)
self.assertIn("FEATURE_DIRECT_MAIL", src)
self.assertIn("FEATURE_BLOG", src)
self.assertIn("FEATURE_PAYMENTS", src)
self.assertIn("FEATURE_SOCIAL", src)
self.assertIn("FEATURE_SOCIAL_AI", src)
self.assertIn("email_sms.apps.EmailSmsConfig", src)
self.assertIn("directmail.apps.DirectmailConfig", src)
self.assertIn("blog.apps.BlogConfig", src)
self.assertIn("payments.apps.PaymentsConfig", src)
self.assertIn("social_ai.apps.SocialAiConfig", src)
def test_payments_requires_email_sms_in_source(self):
src = _read_settings_source()
self.assertIn("FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS", src)
self.assertIn("FEATURE_SOCIAL_AI requires FEATURE_SOCIAL", src)
class AlwaysOnImportIsolationTests(SimpleTestCase):
"""Always-on modules must not import optional apps at module level."""
OPTIONAL = {
"email_sms",
"directmail",
"blog",
"payments",
"social",
"social_ai",
}
ALWAYS_ON = [
"dashboard/views.py",
"public/views.py",
"core/management/commands/dispatch_due.py",
"client_site/urls.py",
]
def test_no_module_level_optional_imports(self):
root = Path(__file__).resolve().parent.parent
for rel in self.ALWAYS_ON:
path = root / rel
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
top = node.module.split(".")[0]
# Allow inside functions — ast.ImportFrom at module body only
if isinstance(node, (ast.Import, ast.ImportFrom)):
pass
# Check only top-level
for node in tree.body:
if isinstance(node, ast.Import):
for alias in node.names:
top = alias.name.split(".")[0]
self.assertNotIn(top, self.OPTIONAL, f"{rel} imports {alias.name}")
if isinstance(node, ast.ImportFrom) and node.module:
top = node.module.split(".")[0]
self.assertNotIn(top, self.OPTIONAL, f"{rel} imports {node.module}")
class InstalledOptionalAppsTests(TestCase):
"""Dev defaults install optional apps so the template is a working seed."""
def test_dev_installs_catalog_apps(self):
labels = {c.label for c in apps.get_app_configs()}
for label in ("email_sms", "directmail", "blog", "payments", "social", "social_ai"):
self.assertIn(label, labels)
def test_optional_urls_resolve_when_installed(self):
self.assertTrue(reverse("email_sms:campaign_list").startswith("/portal/messaging/"))
self.assertTrue(reverse("directmail:postcard_designer").startswith("/portal/direct-mail/"))
self.assertTrue(reverse("blog:list").startswith("/blog/"))
self.assertTrue(reverse("payments:invoice_list").startswith("/portal/payments/"))
self.assertTrue(reverse("social:composer").startswith("/portal/social/"))
self.assertTrue(reverse("social_ai:generate").startswith("/portal/social/api/generate/"))
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from core import views
app_name = "core"
urlpatterns = [
path("files/<uuid:pk>/", views.stored_file, name="stored_file"),
]
+26
View File
@@ -0,0 +1,26 @@
import io
from django.http import FileResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_GET
from core.models import StoredFile
def healthz(_request):
"""Liveness probe for deploy / NPM health checks."""
return JsonResponse({"status": "ok"})
@require_GET
def stored_file(request, pk):
"""Public fetch (UUID acts as capability token)."""
stored = get_object_or_404(StoredFile, pk=pk)
response = FileResponse(
io.BytesIO(bytes(stored.data)),
content_type=stored.content_type or "application/octet-stream",
)
if stored.filename:
response["Content-Disposition"] = f'inline; filename="{stored.filename}"'
response["Cache-Control"] = "public, max-age=86400"
return response