generated from westfarn/web_django_template
Initial commit
This commit is contained in:
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Seed a client repo from this template.
|
||||
# Usage: scripts/bootstrap-client.sh <slug> "<Site Name>" <domain>
|
||||
# Example: scripts/bootstrap-client.sh acme_site "Acme HVAC" acmehvac.com
|
||||
set -euo pipefail
|
||||
|
||||
SLUG="${1:?Usage: $0 <slug> \"<Site Name>\" <domain>}"
|
||||
SITE_NAME="${2:?Usage: $0 <slug> \"<Site Name>\" <domain>}"
|
||||
DOMAIN="${3:?Usage: $0 <slug> \"<Site Name>\" <domain>}"
|
||||
|
||||
if [[ ! "$SLUG" =~ ^[a-z][a-z0-9_]*$ ]]; then
|
||||
echo "slug must be snake_case starting with a letter (e.g. acme_site)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if [[ "$SLUG" == "client_site" ]]; then
|
||||
echo "slug is already client_site — branding defaults only."
|
||||
else
|
||||
echo "Renaming package client_site → $SLUG"
|
||||
if [[ -d site/client_site ]]; then
|
||||
mv site/client_site "site/$SLUG"
|
||||
fi
|
||||
grep -rl 'client_site' \
|
||||
--exclude-dir=.git --exclude-dir=.venv --exclude-dir=__pycache__ \
|
||||
--exclude='*.png' --exclude='*.jpg' --exclude='*.woff*' --exclude='*.eot' \
|
||||
--exclude='uv.lock' . \
|
||||
| while read -r f; do
|
||||
sed -i "s/client_site/${SLUG}/g" "$f"
|
||||
done
|
||||
fi
|
||||
|
||||
python3 -c "
|
||||
from pathlib import Path
|
||||
name, domain = '''${SITE_NAME}''', '''${DOMAIN}'''
|
||||
for rel in ('.env.example', '.env.prod.example'):
|
||||
p = Path(rel)
|
||||
if not p.exists():
|
||||
continue
|
||||
text = p.read_text()
|
||||
text = text.replace('SITE_NAME=Your Company', f'SITE_NAME={name}')
|
||||
text = text.replace('hello@example.com', f'hello@{domain}')
|
||||
text = text.replace('noreply@example.com', f'noreply@{domain}')
|
||||
text = text.replace('PUBLIC_SITE_URL=https://example.com', f'PUBLIC_SITE_URL=https://{domain}')
|
||||
text = text.replace('DJANGO_ALLOWED_HOSTS=example.com,www.example.com',
|
||||
f'DJANGO_ALLOWED_HOSTS={domain},www.{domain}')
|
||||
p.write_text(text)
|
||||
"
|
||||
|
||||
echo
|
||||
echo "Next:"
|
||||
echo " 1. Brand public templates + static/brand/ (logo, colors, copy)."
|
||||
echo " 2. Copy .env.prod.example → ~/Documents/secrets/${SLUG}/${SLUG}_prod.env"
|
||||
echo " Flip only the FEATURE_* flags they bought. Fill provider secrets."
|
||||
echo " 3. Validate: ./scripts/validate-env.sh ~/Documents/secrets/${SLUG}/${SLUG}_prod.env"
|
||||
echo " 4. Infra: Postgres on 10.0.0.230, server-infra app_catalog, NPM vhost, CI deploy."
|
||||
echo " 5. Deploy. Smoke public home/contact, portal login, leads, UTM."
|
||||
echo " Disabled feature URLs must 404 and must not appear in portal nav."
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd /app/site
|
||||
|
||||
wait_for_database() {
|
||||
if [[ -z "${DATABASE_URL:-}" && -z "${DB_HOST:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Waiting for database..."
|
||||
for _ in $(seq 1 30); do
|
||||
if uv run python - <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_site.settings")
|
||||
|
||||
import django
|
||||
from django.db import connections
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
connections["default"].ensure_connection()
|
||||
except OperationalError:
|
||||
sys.exit(1)
|
||||
PY
|
||||
then
|
||||
echo "Database is ready."
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Database did not become ready in time." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_database
|
||||
|
||||
uv run python manage.py migrate --noinput
|
||||
|
||||
# Local compose defaults to DJANGO_ENV=dev: runserver + source bind-mount → hot reload.
|
||||
# Prod/beta images keep gunicorn (no file watch).
|
||||
if [[ "${DJANGO_ENV:-}" == "dev" || "${DJANGO_USE_RUNSERVER:-}" == "true" ]]; then
|
||||
echo "Starting Django runserver (auto-reload on)."
|
||||
exec uv run python manage.py runserver "${GUNICORN_BIND:-0.0.0.0:8000}"
|
||||
fi
|
||||
|
||||
uv run python manage.py collectstatic --noinput
|
||||
|
||||
exec uv run gunicorn client_site.wsgi:application \
|
||||
--bind "${GUNICORN_BIND:-0.0.0.0:8000}" \
|
||||
--workers "${GUNICORN_WORKERS:-2}"
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="${1:?Usage: $0 <env-file>}"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "Environment file not found: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
DJANGO_ENV="${DJANGO_ENV:-prod}"
|
||||
_true() { [[ "${1:-}" =~ ^(1|true|yes|on)$ ]]; }
|
||||
|
||||
required_vars=(
|
||||
DJANGO_SECRET_KEY
|
||||
DJANGO_ALLOWED_HOSTS
|
||||
DATABASE_URL
|
||||
WEB_PORT
|
||||
)
|
||||
|
||||
if [[ "$DJANGO_ENV" == "prod" || "$DJANGO_ENV" == "beta" ]]; then
|
||||
required_vars+=(
|
||||
RECAPTCHA_PUBLIC_KEY
|
||||
RECAPTCHA_PRIVATE_KEY
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$DJANGO_ENV" == "prod" ]]; then
|
||||
required_vars+=(
|
||||
SITE_UNDER_CONSTRUCTION
|
||||
TIANJI_WEBSITE_ID
|
||||
)
|
||||
fi
|
||||
|
||||
if _true "${FEATURE_EMAIL_SMS:-}"; then
|
||||
required_vars+=(
|
||||
EMAIL_HOST_USER
|
||||
EMAIL_HOST_PASSWORD
|
||||
SMTP2GO_SMS_API_KEY
|
||||
SMTP2GO_WEBHOOK_SECRET
|
||||
)
|
||||
fi
|
||||
if _true "${FEATURE_DIRECT_MAIL:-}"; then
|
||||
required_vars+=(
|
||||
PCM_API_KEY
|
||||
PCM_API_SECRET
|
||||
PCM_WEBHOOK_SECRETS
|
||||
PCM_RETURN_ADDRESS
|
||||
)
|
||||
fi
|
||||
if _true "${FEATURE_PAYMENTS:-}"; then
|
||||
required_vars+=(
|
||||
STRIPE_SECRET_KEY
|
||||
STRIPE_PUBLISHABLE_KEY
|
||||
STRIPE_WEBHOOK_SECRET
|
||||
)
|
||||
if ! _true "${FEATURE_EMAIL_SMS:-}"; then
|
||||
echo "FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if _true "${FEATURE_SOCIAL:-}"; then
|
||||
required_vars+=(SOCIAL_TOKEN_ENCRYPTION_KEY)
|
||||
fi
|
||||
if _true "${FEATURE_SOCIAL_AI:-}"; then
|
||||
required_vars+=(OLLAMA_BASE_URL OLLAMA_MODEL)
|
||||
if ! _true "${FEATURE_SOCIAL:-}"; then
|
||||
echo "FEATURE_SOCIAL_AI requires FEATURE_SOCIAL" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
missing=()
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
missing+=("$var")
|
||||
fi
|
||||
done
|
||||
|
||||
if ((${#missing[@]} > 0)); then
|
||||
echo "Missing required environment variables in $ENV_FILE:" >&2
|
||||
printf ' - %s\n' "${missing[@]}" >&2
|
||||
echo "Copy .env.prod.example to ~/Documents/secrets/<slug>_site/<slug>_site_${DJANGO_ENV}.env and set values." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$DJANGO_ENV" == "prod" && "$DJANGO_SECRET_KEY" == change-me* ]]; then
|
||||
echo "DJANGO_SECRET_KEY must be changed from the example value for production." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Environment validation passed (DJANGO_ENV=$DJANGO_ENV)."
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd /app/site
|
||||
|
||||
wait_for_database() {
|
||||
if [[ -z "${DATABASE_URL:-}" && -z "${DB_HOST:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Waiting for database..."
|
||||
for _ in $(seq 1 30); do
|
||||
if uv run python - <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_site.settings")
|
||||
|
||||
import django
|
||||
from django.db import connections
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
connections["default"].ensure_connection()
|
||||
except OperationalError:
|
||||
sys.exit(1)
|
||||
PY
|
||||
then
|
||||
echo "Database is ready."
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Database did not become ready in time." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_database
|
||||
|
||||
uv run python manage.py migrate --noinput
|
||||
|
||||
# Email branding (and anything else that resolves static URLs) needs the
|
||||
# ManifestStaticFilesStorage map. Web entrypoint already collectstatics;
|
||||
# worker must too or hashed lookups raise ValueError.
|
||||
uv run python manage.py collectstatic --noinput
|
||||
|
||||
interval="${DISPATCH_DUE_INTERVAL_SECONDS:-15}"
|
||||
echo "Starting dispatch_due poller (every ${interval}s)."
|
||||
uv run python manage.py dispatch_due --loop --interval "${interval}" &
|
||||
dispatch_pid=$!
|
||||
|
||||
cleanup() {
|
||||
echo "Stopping dispatch_due poller..."
|
||||
kill "${dispatch_pid}" 2>/dev/null || true
|
||||
wait "${dispatch_pid}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Dev uses ImmediateBackend: enqueue runs inline in dispatch_due. No dj-queue.
|
||||
if [[ "${DJANGO_ENV:-}" == "dev" ]]; then
|
||||
echo "Dev worker: dispatch_due loop publishes scheduled work."
|
||||
wait "${dispatch_pid}"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# dj-queue supervisor (workers + dispatcher + scheduler). Run on ONE host only.
|
||||
uv run python manage.py dj_queue
|
||||
Reference in New Issue
Block a user