20 KiB
Red Panda Approval™ — Django Addendum
Owner: Robert Helewka <r@helu.ca> Version: 1.01 Last reviewed: 2026-04-18 Parent document: Red_Panda_Standards_V1-00.md
This document extends the main Red Panda Standards with Django-specific conventions. Where the two documents overlap, the main standard governs — this addendum only adds Django-specific detail or explicitly-noted exceptions.
🐾 Red Panda Approval™
This project follows Red Panda Approval standards — our gold standard for Django application quality. Code must be elegant, reliable, and maintainable to earn the approval of our adorable red panda judges.
The 5 Sacred Django Criteria
- Fresh Migration Test — Clean migrations from empty database
- Elegant Simplicity — No unnecessary complexity
- Observable & Debuggable — Proper logging and error handling
- Consistent Patterns — Follow Django conventions
- Actually Works — Passes all checks and serves real user needs
Environment Standards
- Virtual environment: ~/env/PROJECT/bin/activate
- Use pyproject.toml for project configuration (no setup.py, no requirements.txt)
- Python version: specified in pyproject.toml
- Dependencies: floor-pinned with ceiling (e.g.
Django>=5.2,<6.0)
Dependency Pinning
# Correct — floor pin with ceiling
dependencies = [
"Django>=5.2,<6.0",
"djangorestframework>=3.14,<4.0",
"cryptography>=41.0,<45.0",
]
# Wrong — exact pins in library packages
dependencies = [
"Django==5.2.7", # too strict, breaks downstream
]
Exact pins (==) are only appropriate in application-level lock files, not in reusable library packages.
Directory Structure
myproject/ # Git repository root ├── .gitignore ├── README.md ├── pyproject.toml # Project configuration (moved to repo root) ├── docker-compose.yml ├── .env # Docker Compose environment │ # ANGELIA_DB_ENGINE=postgresql │ # ANGELIA_DB_NAME=angelia2 │ # ANGELIA_DB_USER=angelia │ # ANGELIA_DB_PASSWORD=changeme │ # ANGELIA_DB_HOST=db │ # ANGELIA_DB_PORT=5432 ├── .env.example │ ├── project/ # Django project root (manage.py lives here) │ ├── manage.py │ ├── Dockerfile │ ├── .env # Local development environment │ │ # ANGELIA_DB_ENGINE=sqlite ├── .env.example │ ├── config/ # Django configuration module │ │ ├── init.py │ │ ├── settings.py │ │ ├── urls.py │ │ ├── wsgi.py │ │ └── asgi.py │ │ │ ├── accounts/ # Django app │ │ ├── init.py │ │ ├── models.py │ │ ├── views.py │ │ └── urls.py │ │ │ ├── blog/ # Django app │ │ ├── init.py │ │ ├── models.py │ │ ├── views.py │ │ └── urls.py │ │ │ ├── static/ │ │ ├── css/ │ │ └── js/ │ │ │ └── templates/ │ └── base.html │ ├── web/ # Nginx configuration │ └── nginx.conf │ ├── db/ # PostgreSQL configuration │ └── postgresql.conf │ └── docs/ # Project documentation └── index.md
Settings Structure
- Use a single settings.py file
- Use django-environ or python-dotenv for environment variables
- Never commit .env files to version control
- Provide .env.example with all required variables documented
- Create .gitignore file
- Create a .dockerignore file
Environment Variables
All env vars in .env MUST use the SERVICENAME_ prefix (per main standard). The examples below use ANGELIA_ — substitute the actual service name for your app.
PostgreSQL settings (only if SERVICENAME_DB_ENGINE=postgresql)
ANGELIA_DB_NAME=angelia2
ANGELIA_DB_USER=angelia
ANGELIA_DB_PASSWORD=changeme
ANGELIA_DB_HOST=db
ANGELIA_DB_PORT=5432
Rules
- Never use
DATABASE_URLordj-database-url— always individual vars - Never use unprefixed
DB_HOST/APP_DB_NAME— always service-prefixed - The Django
Settingsclass declares each prefixed var explicitly so the full config is documented in one place .envis gitignored;.env.examplewith placeholder values is committed
Code Organization
- Imports: PEP 8 ordering (stdlib, third-party, local)
- Type hints on function parameters
- CSS: External .css files only (no inline styles, no embedded
<style>tags) - JS: External .js files only (no inline handlers, no embedded
<script>blocks) - Maximum file length: 1000 lines
- If a file exceeds 500 lines, consider splitting by domain concept
Database Conventions
- Migrations run cleanly from empty database
- Never edit deployed migrations
- Use meaningful migration names: --name add_email_to_profile
- One logical change per migration when possible
- Test migrations both forward and backward
Development vs Production
- Development: SQLite
- Production: PostgreSQL
Caching
- Expensive queries are cached
- Cache keys follow naming convention
- TTLs are appropriate (not infinite)
- Invalidation is documented
- Key Naming Pattern: {app}:{model}:{identifier}:{field}
Model Naming
- Model names: singular PascalCase (User, BlogPost, OrderItem)
- Correct English pluralization on related names
- All models have created_at and updated_at
- All models define str and get_absolute_url
- TextChoices used for status fields
- related_name defined on ForeignKey fields
- Related names: plural snake_case with proper English pluralization
Forms
- Use ModelForm with explicit fields list (never all)
Field Naming
- Foreign keys: singular without _id suffix (author, category, parent)
- Boolean fields: use prefixes (is_active, has_permission, can_edit)
- Date fields: use suffixes (created_at, updated_at, published_on)
- Avoid abbreviations (use description, not desc)
Required Model Fields
- All models should include:
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
- Consider adding:
- id = models.UUIDField(primary_key=True) for public-facing models
- is_active = models.BooleanField(default=True) for soft deletes
Indexing
- Add db_index=True to frequently queried fields
- Use Meta.indexes for composite indexes
- Document why each index exists
Queries
- Use select_related() for foreign keys
- Use prefetch_related() for reverse relations and M2M
- Avoid queries in loops (N+1 problem)
- Use .only() and .defer() for large models
- Add comments explaining complex querysets
Docstrings
- Use Sphinx style docstrings
- Document all public functions, classes, and modules
- Skip docstrings for obvious one-liners and standard Django overrides
Views
- Use Function-Based Views (FBVs) exclusively
- Explicit logic is preferred over implicit inheritance
- Extract shared logic into utility functions
URLs & Identifiers
- Public URLs use short UUIDs (12 characters) via
shortuuid - Never expose sequential IDs in URLs (security/enumeration risk)
- Internal references may use standard UUIDs or PKs
URL Patterns
- Resource-based URLs (RESTful style)
- Namespaced URL names per app
- Trailing slashes (Django default)
- Flat structure preferred over deep nesting
Background Tasks
- All tasks are run synchronously unless the design specifies background tasks are needed for long operations
- Long operations use Celery tasks
- Use Memcached, task progress pattern: {app}:task:{task_id}:progress
- Tasks are idempotent
- Tasks include retry logic
- Tasks live in app/tasks.py
- RabbitMQ is the Message Broker
- Flower Monitoring: Use for debugging failed tasks
Celery Observability (per main standard)
Celery workers are "long-running background workers" under the main standard and MUST comply with its Background Worker & Queue Monitoring section:
- Heartbeat: every 60 seconds at INFO level, e.g.
logger.info("celery worker alive, processed %d tasks in last 5m, queue depth: %d", n, depth). Implement as a Celery beat task or a dedicated heartbeat thread. - Startup / shutdown / crash-exit logged at INFO — hook
worker_ready,worker_shutdown,worker_process_initsignals. - Queue depth exposed as a Prometheus metric (via
celery-exporteror equivalent) so a growing-queue-with-no-consumers alert can fire at ERROR severity. - Grafana staleness alert:
absent_over_time({service_name="celery_worker_<app>"}[10m])→ ERROR → email via AlertManager. - Crash-on-start: rely on the systemd unit or Docker restart policy to log the exit — do not assume the crashing Celery worker will log its own death.
Logging (per main standard)
Django apps follow the main standard's Log Level Standards. Django-specific implementation notes:
- Default level:
WARNINGfor app loggers in production. Business logic only surfaces when degraded or broken. - Level casing: UPPERCASE (
INFO,WARNING,ERROR,DEBUG) — Python/Django convention. - Never use
print()— alwayslogger = logging.getLogger(__name__). - Client telemetry received at
POST /api/v1/telemetryMUST be logged atWARNINGlevel (browser-side errors are user-facing problems, not server failures). - Access log filtering: Gunicorn AND the upstream reverse proxy (nginx) must not emit 2xx/3xx entries for
/live,/ready,/metrics,/nginx_status,/health*,/ping, or service-specific probes like/mcp/health. Filter these in the access-log handler. Both trailing-slash and non-trailing-slash forms MUST be matched. Implementation recipes are in the Gunicorn and nginx subsections under Health Check Endpoints below. - Structured output: log to stdout in a format Alloy can parse (JSON preferred). Every log line MUST carry a
levellabel downstream. - Expected conditions are not ERROR: failed logins, form validation errors, 404s on user-supplied slugs → WARNING or INFO. Reserve ERROR for things that are actually broken.
Health Check Endpoints (per main standard)
Every Django service MUST expose:
| Endpoint | Purpose | Auth |
|---|---|---|
GET /live/ |
Liveness — process is running | None |
GET /ready/ |
Readiness — DB, cache, upstream deps all healthy | None |
GET /metrics |
Prometheus metrics | IP-restricted, no JWT |
- Trailing slash: standard is
/live/and/ready/. Django'sAPPEND_SLASHredirects un-slashed requests to the canonical slashed form — document as an exception only if you disable that behavior. - Readiness logic MUST actually probe dependencies:
connection.ensure_connection()for the DB, a Memcachedping, a minimal RabbitMQ connection check. A barereturn HttpResponse(status=200)fails the main standard. - Do NOT require authentication on health endpoints — HAProxy and Prometheus scrapers cannot authenticate.
/metricsis exposed viadjango-prometheus(preferred) and IP-restricted to internal networks per the main standard.
Internal-network allowlist (nginx)
Any endpoint restricted to "internal networks only" (/metrics, /nginx_status, nginx-prometheus-exporter scrape targets, etc.) MUST use the full RFC1918 + loopback allowlist — all four ranges, in this order:
allow 127.0.0.0/8; # loopback
allow 10.0.0.0/8; # RFC1918 — primary internal range
allow 172.16.0.0/12; # RFC1918 — Docker default bridge range
allow 192.168.0.0/16; # RFC1918
deny all;
Omitting 10.0.0.0/8 is the most common mistake and will silently break Prometheus scrapes from hosts on that network. Do not copy a shorter allowlist from older configs.
Gunicorn configuration
Gunicorn MUST:
- Log access AND error output to stdout/stderr — never a file inside the container. The Docker logging driver (syslog → Alloy in our stack) is the single collection point.
- Use a
gunicorn.conf.pyreferenced via--configso configuration lives in version control rather than a growing CMD string. - Filter probe paths out of the access log via a
logging.Filterattached to thegunicorn.accesslogger in BOTHon_starting(master) ANDpost_worker_init(workers — Gunicorn re-applies logger config per worker, so a master-only filter is silently stripped).
Canonical launch command:
CMD ["gunicorn", \
"--config", "/srv/<app>/gunicorn.conf.py", \
"--bind", ":8080", \
"--workers", "3", \
"--timeout", "120", \
"--keep-alive", "5", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"<app>.wsgi:application"]
Canonical gunicorn.conf.py probe filter:
import logging
import re
_PROBE_PATH = re.compile(
r"^(?:/live|/ready|/metrics|/nginx_status|/health[^ ]*|/ping|/mcp/health)/?(?:\?|$)"
)
class _ProbePathFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
request = getattr(record, "args", None)
if isinstance(request, dict):
# Gunicorn access log atoms: 'U' = URL path, 'r' = full request line
path = request.get("U") or request.get("r", "")
else:
path = record.getMessage()
return not _PROBE_PATH.search(path)
_filter = _ProbePathFilter()
def on_starting(server):
logging.getLogger("gunicorn.access").addFilter(_filter)
def post_worker_init(worker):
logging.getLogger("gunicorn.access").addFilter(_filter)
Update the probe-path regex if the service exposes additional health endpoints (e.g. sidecar servers). Do NOT special-case by status code — a 500 on /ready/ is noise in Gunicorn's access log but is already surfaced via the readiness probe failing and the error log.
Nginx access-log filtering
The reverse proxy sees the same probe traffic and will log it unless filtered. Use a map + conditional access_log:
http {
map $request_uri $loggable {
default 1;
~^/live(/|\?|$) 0;
~^/ready(/|\?|$) 0;
~^/metrics(/|\?|$) 0;
~^/nginx_status(/|\?|$) 0;
~^/health 0;
~^/ping(/|\?|$) 0;
~^/mcp/health(/|\?|$) 0;
}
access_log /var/log/nginx/access.log combined if=$loggable;
# ...
}
This is an nginx-wide switch — do not duplicate per location block. Error logging is unaffected; genuine 4xx/5xx on probe paths still surface via the error log and the probe itself failing.
See Red_Panda_Standards_V1-00.md §Health Check Endpoints for the full definition.
Testing
- Framework: Django TestCase (not pytest)
- Separate test files per module: test_models.py, test_views.py, test_forms.py
Frontend Standards
New Projects (DaisyUI + Tailwind)
- DaisyUI 4 via CDN for component classes
- Tailwind CSS via CDN for utility classes
- Theme management via Themis (DaisyUI
data-themeattribute) - All apps extend
themis/base.htmlfor consistent navigation - No inline styles or scripts
Existing Projects (Bootstrap 5)
- Bootstrap 5 via CDN
- Bootstrap Icons via CDN
- Bootswatch for theme variants (if applicable)
- django-bootstrap5 and crispy-bootstrap5 for form rendering
Preferred Packages
Core Django
- django>=5.2,<6.0
- django-environ — Environment variables
Authentication & Security
- django-allauth — User management
- django-allauth-2fa — Two-factor authentication
API Development
- djangorestframework>=3.14,<4.0 — REST APIs
- drf-spectacular — OpenAPI/Swagger documentation
Encryption
- cryptography — Fernet encryption for secrets/API keys
Background Tasks
- celery — Async task queue
- django-celery-progress — Progress bars
- flower — Celery monitoring
Caching
- pymemcache — Memcached backend
Observability
- django-prometheus —
/metricsendpoint in Prometheus exposition format - celery-exporter (or equivalent) — queue depth metrics for Celery workers
Database
- psycopg[binary] — PostgreSQL adapter
- shortuuid — Short UUIDs for public URLs
Production
- gunicorn — WSGI server
Shared Apps
- django-heluca-themis — User preferences, themes, key management, navigation
Deprecated / Removed
pytz— Use stdlibzoneinfo(Python 3.9+, Django 4+)Pillow— Only add if your app needs ImageFielddjango-heluca-core— Replaced by Themisdj-database-url— Use individual Django DB env vars instead
Anti-Patterns to Avoid
Models
- Don't use
Model.objects.get()without handlingDoesNotExist - Don't use
null=TrueonCharFieldorTextField(useblank=True, default="") - Don't use
related_name='+'unless you have a specific reason - Don't override
save()for business logic (use signals or service functions) - Don't use
auto_now=Trueon fields you might need to manually set - Don't use
ForeignKeywithout specifyingon_deleteexplicitly - Don't use
Meta.orderingon large tables (specify ordering in queries)
Queries
- Don't query inside loops (N+1 problem)
- Don't use
.all()when you need a subset - Don't use raw SQL unless absolutely necessary
- Don't forget
select_related()andprefetch_related()
Views
- Don't put business logic in views
- Don't use
request.POST.get()without validation (use forms) - Don't return sensitive data in error messages
- Don't forget
login_requireddecorator on protected views
Forms
- Don't use
fields = '__all__'in ModelForm - Don't trust client-side validation alone
- Don't use
excludein ModelForm (use explicitfields)
Templates
- Don't use
{{ variable }}for URLs (use{% url %}tag) - Don't put logic in templates
- Don't use inline CSS or JavaScript (external files only)
- Don't forget
{% csrf_token %}in forms
Security
- Don't store secrets in
settings.py(use environment variables) - Don't commit
.envfiles to version control - Don't use
DEBUG=Truein production - Don't expose sequential IDs in public URLs
- Don't use
mark_safe()on user-supplied content - Don't disable CSRF protection
Imports & Code Style
- Don't use
from module import * - Don't use mutable default arguments
- Don't use bare
except:clauses - Don't ignore linter warnings without documented reason
Migrations
- Don't edit migrations that have been deployed
- Don't use
RunPythonwithout a reverse function - Don't add non-nullable fields without a default value
Celery Tasks
- Don't pass model instances to tasks (pass IDs and re-fetch)
- Don't assume tasks run immediately
- Don't forget retry logic for external service calls
- Don't run a Celery worker without a heartbeat (see Celery Observability)
Logging
- Don't use
print()— always uselogging.getLogger(__name__) - Don't log at ERROR for expected conditions (failed logins, 404s, validation errors)
- Don't log at INFO for successful probes of
/live,/ready,/metrics - Don't log passwords, tokens, API keys, session cookies, or PII at any level
- Don't use lowercase level names in Python code (UPPERCASE for Django/Python)
Exceptions
Per the main standard, deviations from Red Panda requirements MUST be recorded rather than hidden. Third-party Django packages, framework defaults, or deliberate trade-offs all go here.
| Service | Standard waived | Reason | Reviewed |
|---|---|---|---|
| (add as discovered) |
Exceptions MUST be re-reviewed on the doc's Last reviewed date. Remove entries whose underlying reason has gone away.