Files
ouranos/docs/Red Panda Standards_Django_V1-01.md

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

  1. Fresh Migration Test — Clean migrations from empty database
  2. Elegant Simplicity — No unnecessary complexity
  3. Observable & Debuggable — Proper logging and error handling
  4. Consistent Patterns — Follow Django conventions
  5. 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_URL or dj-database-url — always individual vars
  • Never use unprefixed DB_HOST / APP_DB_NAME — always service-prefixed
  • The Django Settings class declares each prefixed var explicitly so the full config is documented in one place
  • .env is gitignored; .env.example with 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_init signals.
  • Queue depth exposed as a Prometheus metric (via celery-exporter or 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: WARNING for 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() — always logger = logging.getLogger(__name__).
  • Client telemetry received at POST /api/v1/telemetry MUST be logged at WARNING level (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 level label 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's APPEND_SLASH redirects 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 Memcached ping, a minimal RabbitMQ connection check. A bare return HttpResponse(status=200) fails the main standard.
  • Do NOT require authentication on health endpoints — HAProxy and Prometheus scrapers cannot authenticate.
  • /metrics is exposed via django-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.py referenced via --config so configuration lives in version control rather than a growing CMD string.
  • Filter probe paths out of the access log via a logging.Filter attached to the gunicorn.access logger in BOTH on_starting (master) AND post_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-theme attribute)
  • All apps extend themis/base.html for 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 — /metrics endpoint 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 stdlib zoneinfo (Python 3.9+, Django 4+)
  • Pillow — Only add if your app needs ImageField
  • django-heluca-core — Replaced by Themis
  • dj-database-url — Use individual Django DB env vars instead

Anti-Patterns to Avoid

Models

  • Don't use Model.objects.get() without handling DoesNotExist
  • Don't use null=True on CharField or TextField (use blank=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=True on fields you might need to manually set
  • Don't use ForeignKey without specifying on_delete explicitly
  • Don't use Meta.ordering on 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() and prefetch_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_required decorator on protected views

Forms

  • Don't use fields = '__all__' in ModelForm
  • Don't trust client-side validation alone
  • Don't use exclude in ModelForm (use explicit fields)

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 .env files to version control
  • Don't use DEBUG=True in 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 RunPython without 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 use logging.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.