Docs: Pallas Agents
This commit is contained in:
@@ -1,499 +0,0 @@
|
|||||||
# 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](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
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# 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](Red_Panda_Standards_V1-00.md#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:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
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:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
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:
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
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](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.
|
|
||||||
Reference in New Issue
Block a user