Template
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:
@@ -0,0 +1,12 @@
|
||||
"""Load environment-specific Django settings based on DJANGO_ENV."""
|
||||
|
||||
import os
|
||||
|
||||
_environment = os.environ.get("DJANGO_ENV", "dev").lower()
|
||||
|
||||
if _environment == "prod":
|
||||
from .prod import * # noqa: F403
|
||||
elif _environment == "beta":
|
||||
from .beta import * # noqa: F403
|
||||
else:
|
||||
from .dev import * # noqa: F403
|
||||
@@ -0,0 +1,381 @@
|
||||
"""Shared Django settings for all environments."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def env(key: str, default: str | None = None) -> str | None:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
|
||||
def env_bool(key: str, default: bool = False) -> bool:
|
||||
value = os.environ.get(key)
|
||||
if value is None:
|
||||
return default
|
||||
return value.lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def env_list(key: str, default: str = "") -> list[str]:
|
||||
value = os.environ.get(key, default)
|
||||
if not value:
|
||||
return []
|
||||
value = value.strip()
|
||||
if value.startswith("["):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
if isinstance(parsed, list):
|
||||
return [str(item).strip() for item in parsed if str(item).strip()]
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
|
||||
|
||||
def database_config() -> dict:
|
||||
database_url = env("DATABASE_URL")
|
||||
if database_url:
|
||||
parsed = urlparse(database_url)
|
||||
return {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": parsed.path.lstrip("/"),
|
||||
"USER": parsed.username or "",
|
||||
"PASSWORD": parsed.password or "",
|
||||
"HOST": parsed.hostname or "",
|
||||
"PORT": str(parsed.port or 5432),
|
||||
}
|
||||
}
|
||||
|
||||
if env("DB_HOST"):
|
||||
return {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": env("DB_NAME", "client_site"),
|
||||
"USER": env("DB_USER", "client_site"),
|
||||
"PASSWORD": env("DB_PASSWORD", ""),
|
||||
"HOST": env("DB_HOST"),
|
||||
"PORT": env("DB_PORT", "5432"),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def build_csrf_trusted_origins(
|
||||
allowed_hosts: list[str], explicit: list[str] | None = None
|
||||
) -> list[str]:
|
||||
"""Build CSRF_TRUSTED_ORIGINS for Django 4+ Origin checks on HTTPS POSTs."""
|
||||
if explicit:
|
||||
return explicit
|
||||
|
||||
local_hosts = {"localhost", "127.0.0.1", "0.0.0.0"}
|
||||
origins: list[str] = []
|
||||
for host in allowed_hosts:
|
||||
if not host or host == "*" or host.startswith("."):
|
||||
continue
|
||||
hostname = host.split(":")[0]
|
||||
scheme = "http" if hostname in local_hosts else "https"
|
||||
origins.append(f"{scheme}://{host}")
|
||||
return origins
|
||||
|
||||
|
||||
SECRET_KEY = env(
|
||||
"DJANGO_SECRET_KEY",
|
||||
"django-insecure-dev-only-change-me-before-production",
|
||||
)
|
||||
|
||||
DEBUG = env_bool("DJANGO_DEBUG", False)
|
||||
|
||||
allowed_hosts = env_list("DJANGO_ALLOWED_HOSTS", "*")
|
||||
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = build_csrf_trusted_origins(
|
||||
ALLOWED_HOSTS,
|
||||
env_list("DJANGO_CSRF_TRUSTED_ORIGINS"),
|
||||
)
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
# Catalog feature flags. Secret env is the source of truth.
|
||||
# Dev/test default ON so the template is a working seed; prod/beta secrets
|
||||
# must set each flag explicitly (see scripts/validate-env.sh).
|
||||
_feature_default = os.environ.get("DJANGO_ENV", "dev").lower() not in {"prod", "beta"}
|
||||
FEATURE_EMAIL_SMS = env_bool("FEATURE_EMAIL_SMS", _feature_default)
|
||||
FEATURE_DIRECT_MAIL = env_bool("FEATURE_DIRECT_MAIL", _feature_default)
|
||||
FEATURE_BLOG = env_bool("FEATURE_BLOG", _feature_default)
|
||||
FEATURE_PAYMENTS = env_bool("FEATURE_PAYMENTS", _feature_default)
|
||||
FEATURE_SOCIAL = env_bool("FEATURE_SOCIAL", _feature_default)
|
||||
FEATURE_SOCIAL_AI = env_bool("FEATURE_SOCIAL_AI", _feature_default)
|
||||
|
||||
if FEATURE_PAYMENTS and not FEATURE_EMAIL_SMS:
|
||||
raise ImproperlyConfigured("FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS")
|
||||
if FEATURE_SOCIAL_AI and not FEATURE_SOCIAL:
|
||||
raise ImproperlyConfigured("FEATURE_SOCIAL_AI requires FEATURE_SOCIAL")
|
||||
|
||||
CORE_APPS = [
|
||||
"core.apps.CoreConfig",
|
||||
"public.apps.PublicConfig",
|
||||
"accounts.apps.AccountsConfig",
|
||||
"dashboard.apps.DashboardConfig",
|
||||
"leads.apps.LeadsConfig",
|
||||
"contacts.apps.ContactsConfig",
|
||||
"analytics.apps.AnalyticsConfig",
|
||||
]
|
||||
OPTIONAL_APPS = [
|
||||
(FEATURE_EMAIL_SMS, "email_sms.apps.EmailSmsConfig"),
|
||||
(FEATURE_DIRECT_MAIL, "directmail.apps.DirectmailConfig"),
|
||||
(FEATURE_BLOG, "blog.apps.BlogConfig"),
|
||||
(FEATURE_PAYMENTS, "payments.apps.PaymentsConfig"),
|
||||
(FEATURE_SOCIAL, "social.apps.SocialConfig"),
|
||||
(FEATURE_SOCIAL_AI, "social_ai.apps.SocialAiConfig"),
|
||||
]
|
||||
DJANGO_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"whitenoise.runserver_nostatic",
|
||||
"django.contrib.staticfiles",
|
||||
"phonenumber_field",
|
||||
"django_recaptcha",
|
||||
"dj_queue",
|
||||
]
|
||||
INSTALLED_APPS = (
|
||||
CORE_APPS
|
||||
+ [app for enabled, app in OPTIONAL_APPS if enabled]
|
||||
+ DJANGO_APPS
|
||||
)
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"analytics.middleware.UTMTrackingMiddleware",
|
||||
"analytics.middleware.PublicPageViewMiddleware",
|
||||
"public.middleware.UnderConstructionMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "client_site.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "client_site" / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"public.context_processors.site_branding",
|
||||
"public.context_processors.tianji_tracking",
|
||||
"core.context_processors.feature_registry",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "client_site.wsgi.application"
|
||||
|
||||
DATABASES = database_config()
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "America/Chicago"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "client_site" / "static",
|
||||
]
|
||||
# Uploaded blobs live in the DB (core.StoredFile). Default storage is
|
||||
# in-memory only so nothing is written to disk accidentally.
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.memory.InMemoryStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
LOGIN_URL = "accounts:login"
|
||||
LOGIN_REDIRECT_URL = "dashboard:home"
|
||||
LOGOUT_REDIRECT_URL = "accounts:login"
|
||||
|
||||
# --- Feature gates ---
|
||||
# When true, public visitors only see the under-construction page.
|
||||
# Prod launch holding page: SITE_UNDER_CONSTRUCTION=true
|
||||
# Beta (full app): SITE_UNDER_CONSTRUCTION=false
|
||||
SITE_UNDER_CONSTRUCTION = env_bool("SITE_UNDER_CONSTRUCTION", False)
|
||||
|
||||
# --- Branding / public site ---
|
||||
SITE_NAME = env("SITE_NAME", "Your Company")
|
||||
SITE_TAGLINE = env("SITE_TAGLINE", "A tagline for the public site")
|
||||
PUBLIC_SITE_URL = env("PUBLIC_SITE_URL", "")
|
||||
CONTACT_PHONE = env("CONTACT_PHONE", "")
|
||||
CONTACT_EMAIL = env("CONTACT_EMAIL", "hello@example.com")
|
||||
CONTACT_ADDRESS = env("CONTACT_ADDRESS", "")
|
||||
CONTACT_SERVICE_AREA = env("CONTACT_SERVICE_AREA", "")
|
||||
CREDIT_NAME = env("CREDIT_NAME", "AI ML Operations, LLC")
|
||||
CREDIT_URL = env("CREDIT_URL", "https://aimloperations.com")
|
||||
|
||||
|
||||
def format_from_email(address: str | None, display_name: str | None = None) -> str:
|
||||
"""
|
||||
Ensure From header includes a display name for inbox UIs.
|
||||
|
||||
Bare ``noreply@…`` shows as "noreply". Prefer
|
||||
``"Your Company" <noreply@…>`` (RFC 5322).
|
||||
|
||||
Docker Compose ``.env`` cannot parse split quotes like
|
||||
``"Name" <addr>`` — use a bare address (this helper adds SITE_NAME)
|
||||
or one pair of quotes around the whole value:
|
||||
``DEFAULT_FROM_EMAIL="Your Company <addr@domain>"``.
|
||||
"""
|
||||
raw = (address or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
# Strip a single layer of wrapping quotes from env/compose.
|
||||
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in {'"', "'"}:
|
||||
raw = raw[1:-1].strip()
|
||||
if "<" in raw and ">" in raw:
|
||||
return raw
|
||||
name = (display_name or SITE_NAME or "").strip()
|
||||
if not name:
|
||||
return raw
|
||||
safe = name.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{safe}" <{raw}>'
|
||||
|
||||
|
||||
# Inbox "From" — not the same as EMAIL_HOST_USER (SMTP login).
|
||||
DEFAULT_FROM_EMAIL = format_from_email(
|
||||
env("DEFAULT_FROM_EMAIL", "noreply@example.com")
|
||||
)
|
||||
|
||||
# --- reCAPTCHA v3 ---
|
||||
RECAPTCHA_PUBLIC_KEY = env("RECAPTCHA_PUBLIC_KEY", "")
|
||||
RECAPTCHA_PRIVATE_KEY = env("RECAPTCHA_PRIVATE_KEY", "")
|
||||
|
||||
# --- Email (SMTP2GO) ---
|
||||
EMAIL_HOST = env("EMAIL_HOST", "mail.smtp2go.com")
|
||||
EMAIL_HOST_USER = env("EMAIL_HOST_USER", "")
|
||||
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", "")
|
||||
EMAIL_PORT = int(env("EMAIL_PORT", "2525") or "2525")
|
||||
EMAIL_USE_TLS = env_bool("EMAIL_USE_TLS", True)
|
||||
EMAIL_BACKEND = env(
|
||||
"EMAIL_BACKEND",
|
||||
"django.core.mail.backends.smtp.EmailBackend",
|
||||
)
|
||||
|
||||
# --- SMTP2GO SMS ---
|
||||
SMTP2GO_SMS_API_KEY = env("SMTP2GO_SMS_API_KEY", "")
|
||||
SMTP2GO_SMS_API_URL = env(
|
||||
"SMTP2GO_SMS_API_URL",
|
||||
"https://api.smtp2go.com/v3/sms/send",
|
||||
)
|
||||
# Optional shared secret: ?token=… or Authorization: Bearer …
|
||||
SMTP2GO_WEBHOOK_SECRET = env("SMTP2GO_WEBHOOK_SECRET", "")
|
||||
|
||||
# --- Postcards (PCM Integrations default) ---
|
||||
POSTCARD_PROVIDER = env("POSTCARD_PROVIDER", "pcm")
|
||||
# Login credentials for POST /auth/login → short-lived Bearer token.
|
||||
PCM_API_KEY = env("PCM_API_KEY", "")
|
||||
PCM_API_SECRET = env("PCM_API_SECRET", "")
|
||||
# Optional child-app reference for PCM multi-account (childRefNbr).
|
||||
PCM_CHILD_REF_NBR = env("PCM_CHILD_REF_NBR", "")
|
||||
# PCM webhook auth: each subscription has its own signature secret (copy from PCM UI).
|
||||
# Prefer PCM_WEBHOOK_SECRETS=sec1,sec2,… ; PCM_WEBHOOK_SECRET still accepted (single).
|
||||
PCM_WEBHOOK_SECRETS = env("PCM_WEBHOOK_SECRETS", "")
|
||||
PCM_WEBHOOK_SECRET = env("PCM_WEBHOOK_SECRET", "")
|
||||
# JSON object: company, firstName, lastName, address, address2, city, state, zipCode
|
||||
PCM_RETURN_ADDRESS = env("PCM_RETURN_ADDRESS", "")
|
||||
PCM_RETURN_LINE1 = env("PCM_RETURN_LINE1", "")
|
||||
PCM_RETURN_LINE2 = env("PCM_RETURN_LINE2", "")
|
||||
PCM_RETURN_CITY = env("PCM_RETURN_CITY", "")
|
||||
PCM_RETURN_STATE = env("PCM_RETURN_STATE", "")
|
||||
PCM_RETURN_ZIP = env("PCM_RETURN_ZIP", "")
|
||||
# Legacy / alternate providers (POSTCARD_PROVIDER=lob|click2mail|postgrid)
|
||||
LOB_API_KEY = env("LOB_API_KEY", "")
|
||||
CLICK2MAIL_API_KEY = env("CLICK2MAIL_API_KEY", "")
|
||||
POSTGRID_API_KEY = env("POSTGRID_API_KEY", "")
|
||||
|
||||
# --- Social (native APIs) ---
|
||||
META_APP_ID = env("META_APP_ID", "")
|
||||
META_APP_SECRET = env("META_APP_SECRET", "")
|
||||
# LinkedIn Client ID/Secret live in SocialAppCredentials (portal UI), not env.
|
||||
SOCIAL_TOKEN_ENCRYPTION_KEY = env("SOCIAL_TOKEN_ENCRYPTION_KEY", "")
|
||||
|
||||
# --- Ollama (social post drafting) ---
|
||||
# Host-network reachable from app hosts; default matches internal LAN Ollama.
|
||||
OLLAMA_BASE_URL = env("OLLAMA_BASE_URL", "http://10.0.0.128:11434")
|
||||
OLLAMA_MODEL = env("OLLAMA_MODEL", "llama3.2")
|
||||
OLLAMA_TIMEOUT_SECONDS = int(env("OLLAMA_TIMEOUT_SECONDS", "120") or "120")
|
||||
|
||||
# --- Stripe (FEATURE_PAYMENTS) ---
|
||||
STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY", "")
|
||||
STRIPE_PUBLISHABLE_KEY = env("STRIPE_PUBLISHABLE_KEY", "")
|
||||
STRIPE_WEBHOOK_SECRET = env("STRIPE_WEBHOOK_SECRET", "")
|
||||
STRIPE_CURRENCY = env("STRIPE_CURRENCY", "usd")
|
||||
|
||||
# --- Nominatim (address suggest; server-side proxy only) ---
|
||||
# Self-hosted on ai-server-4080. Nominatim has no native API keys — gate with
|
||||
# LAN UFW + this Django proxy. Optional NOMINATIM_API_KEY is forwarded as
|
||||
# X-API-Key if you later put auth in front of Nominatim.
|
||||
NOMINATIM_BASE_URL = env("NOMINATIM_BASE_URL", "http://10.0.0.128:8089")
|
||||
NOMINATIM_TIMEOUT_SECONDS = int(env("NOMINATIM_TIMEOUT_SECONDS", "8") or "8")
|
||||
NOMINATIM_COUNTRY_CODES = env("NOMINATIM_COUNTRY_CODES", "us")
|
||||
NOMINATIM_USER_AGENT = env(
|
||||
"NOMINATIM_USER_AGENT",
|
||||
"client_site/1.0 (address-suggest; contact admin)",
|
||||
)
|
||||
NOMINATIM_API_KEY = env("NOMINATIM_API_KEY", "")
|
||||
|
||||
# --- Tianji analytics (pageviews + events; notice banner, not opt-in) ---
|
||||
TIANJI_ENABLED = env_bool("TIANJI_ENABLED", not DEBUG)
|
||||
TIANJI_TRACKER_URL = env(
|
||||
"TIANJI_TRACKER_URL",
|
||||
"https://tianji.aimloperations.com/tracker.js",
|
||||
)
|
||||
TIANJI_WEBSITE_ID = env("TIANJI_WEBSITE_ID", "cmshzhxdf6gee10qzkzrfn9iw")
|
||||
|
||||
# --- Django Tasks ---
|
||||
# Dev: ImmediateBackend (settings/dev.py). Beta/prod: dj-queue (settings/beta.py|prod.py).
|
||||
TASKS = {
|
||||
"default": {
|
||||
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Beta/staging settings — full functionality for preview."""
|
||||
|
||||
from .base import * # noqa: F403
|
||||
from .logging import build_logging_config, logging_level_for_env
|
||||
|
||||
DEBUG = env_bool("DJANGO_DEBUG", False) # noqa: F405
|
||||
TIANJI_ENABLED = env_bool("TIANJI_ENABLED", True) # noqa: F405
|
||||
|
||||
if DEBUG:
|
||||
import warnings
|
||||
|
||||
warnings.warn("DEBUG is enabled in beta environment.", stacklevel=1)
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SESSION_COOKIE_SECURE = not DEBUG
|
||||
CSRF_COOKIE_SECURE = not DEBUG
|
||||
|
||||
# Beta always runs the full app unless explicitly overridden.
|
||||
SITE_UNDER_CONSTRUCTION = env_bool("SITE_UNDER_CONSTRUCTION", False) # noqa: F405
|
||||
|
||||
DATABASE_ROUTERS = ["dj_queue.routers.DjQueueRouter"]
|
||||
TASKS = {
|
||||
"default": {
|
||||
"BACKEND": "dj_queue.backend.DjQueueBackend",
|
||||
"QUEUES": [],
|
||||
"OPTIONS": {},
|
||||
},
|
||||
}
|
||||
|
||||
LOGGING = build_logging_config(logging_level_for_env("beta"), "beta")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Development settings."""
|
||||
|
||||
from .base import * # noqa: F403
|
||||
from .logging import build_logging_config, logging_level_for_env
|
||||
|
||||
DEBUG = True
|
||||
|
||||
SITE_UNDER_CONSTRUCTION = env_bool("SITE_UNDER_CONSTRUCTION", False) # noqa: F405
|
||||
TIANJI_ENABLED = env_bool("TIANJI_ENABLED", False) # noqa: F405
|
||||
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.memory.InMemoryStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
EMAIL_BACKEND = env( # noqa: F405
|
||||
"EMAIL_BACKEND",
|
||||
"django.core.mail.backends.console.EmailBackend",
|
||||
)
|
||||
|
||||
# ALLOWED_HOSTS default "*" → empty CSRF_TRUSTED_ORIGINS, which breaks browser
|
||||
# Origin checks on POST. Always trust local runserver origins in dev.
|
||||
_local_csrf = [
|
||||
"http://127.0.0.1:8000",
|
||||
"http://localhost:8000",
|
||||
"http://0.0.0.0:8000",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://localhost:8001",
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = list(
|
||||
dict.fromkeys([*_local_csrf, *CSRF_TRUSTED_ORIGINS]) # noqa: F405
|
||||
)
|
||||
|
||||
LOGGING = build_logging_config(logging_level_for_env("dev"), "dev")
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Environment-specific logging configuration."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def build_logging_config(level: str, environment: str) -> dict:
|
||||
"""Return a Django LOGGING dict for the given level and environment name."""
|
||||
return {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": (
|
||||
f"{{levelname}} {{asctime}} {{name}} {{filename}}:{{lineno}} "
|
||||
f"{{process:d}} {{thread:d}} [env={environment}] {{message}}"
|
||||
),
|
||||
"style": "{",
|
||||
},
|
||||
"simple": {
|
||||
"format": (
|
||||
f"{{levelname}} [env={environment}] "
|
||||
f"{{filename}}:{{lineno}} {{message}}"
|
||||
),
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose" if environment == "dev" else "simple",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": level,
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["console"],
|
||||
"level": level,
|
||||
"propagate": False,
|
||||
},
|
||||
"django.request": {
|
||||
"handlers": ["console"],
|
||||
"level": "ERROR" if environment == "prod" else level,
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def logging_level_for_env(environment: str) -> str:
|
||||
override = os.environ.get("DJANGO_LOG_LEVEL")
|
||||
if override:
|
||||
return override.upper()
|
||||
|
||||
if environment == "dev":
|
||||
return "DEBUG"
|
||||
if environment == "beta":
|
||||
return "INFO"
|
||||
return "WARNING"
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Production settings."""
|
||||
|
||||
from .base import * # noqa: F403
|
||||
from .logging import build_logging_config, logging_level_for_env
|
||||
|
||||
DEBUG = False
|
||||
|
||||
TIANJI_ENABLED = env_bool("TIANJI_ENABLED", True) # noqa: F405
|
||||
|
||||
if not env("DJANGO_SECRET_KEY"): # noqa: F405
|
||||
raise ValueError("DJANGO_SECRET_KEY must be set in production.")
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Prod holds visitors on under-construction until launch (set false when ready).
|
||||
if env("SITE_UNDER_CONSTRUCTION") is None: # noqa: F405
|
||||
SITE_UNDER_CONSTRUCTION = True # noqa: F405
|
||||
|
||||
DATABASE_ROUTERS = ["dj_queue.routers.DjQueueRouter"]
|
||||
TASKS = {
|
||||
"default": {
|
||||
"BACKEND": "dj_queue.backend.DjQueueBackend",
|
||||
"QUEUES": [],
|
||||
"OPTIONS": {},
|
||||
},
|
||||
}
|
||||
|
||||
LOGGING = build_logging_config(logging_level_for_env("prod"), "prod")
|
||||
Reference in New Issue
Block a user