8.7 KiB
🐾 Red Panda Approval™
The standard every change is judged against. Don't satisfy a checklist — satisfy the red pandas. Ask of each change: does this earn approval?
- Fresh Migration Test — migrations apply cleanly from an empty database.
- Elegant Simplicity — no unnecessary complexity; the obvious solution, done well.
- Observable & Debuggable — proper logging; failures say what broke and why.
- Consistent Patterns — follows Django conventions and the patterns already in this repo.
- Actually Works — passes all checks and serves a real user need.
Criteria 1 and 5 are externally verifiable — migrations apply or they don't; checks pass or they don't. Verify them, don't assert them. Criteria 2–4 are judgement calls: when in doubt, match what the repo already does rather than grading your own elegance.
If a paw print isn't leading the response, the rest of this file probably isn't being honoured either. Lead with one. 🐾
Conventions (always-on)
These are the rubric made concrete for the common case — writing models, views, forms, templates, and queries.
Models
- Names: singular PascalCase (
User,BlogPost,OrderItem). - Every model defines
__str__andget_absolute_url. - Every model has
created_at = DateTimeField(auto_now_add=True)andupdated_at = DateTimeField(auto_now=True). TextChoicesfor status fields.related_nameon everyForeignKey; plural snake_case with correct English pluralisation.- Public-facing models: consider
UUIDFieldprimary key andis_activefor soft deletes.
Field naming
- Foreign keys: singular, no
_idsuffix (author,category,parent). - Booleans: prefixed (
is_active,has_permission,can_edit). - Dates: suffixed (
created_at,updated_at,published_on). - No abbreviations (
description, notdesc).
Views
- Function-based views exclusively. Explicit logic over implicit inheritance. Extract shared logic into utility functions.
- Business logic lives in service functions, not views and not
save().
Forms
ModelFormwith an explicitfieldslist — never__all__, neverexclude.- Validate at the boundary; never trust client-side validation alone.
Queries
select_related()for FKs;prefetch_related()for reverse and M2M.- No queries inside loops (N+1). No
.all()when you need a subset. .only()/.defer()for large models. Comment non-obvious querysets.
URLs & identifiers
- Public URLs use 12-char short UUIDs via
shortuuid. Never expose sequential IDs (enumeration risk). Internal refs may use PKs. - Resource-based, namespaced URL names per app, trailing slashes, flat structure preferred.
Docstrings
- Google style. Document public classes, functions, methods, modules.
- Imperative one-line summary.
Args:/Returns:/Raises:only when the signature doesn't already convey it. Don't restate type hints in prose. - Skip obvious one-liners and standard Django overrides.
Code organisation
- PEP 8 import ordering (stdlib, third-party, local). Type hints on params.
- CSS and JS in external files only — no inline styles,
<style>, inline handlers, or<script>blocks. - File length: split by domain concept past ~500 lines; hard ceiling 1000.
Testing
- Django
TestCase(not pytest). Separate files per module:test_models.py,test_views.py,test_forms.py.
An app isn't done until it's reachable django-admin startapp builds an island. A complete-from-its-own-boundary app — models, views, urls, templates, tests all present and passing — is
Add to always-on Django CLAUDE.md — Conventions section
Insert this block under "Conventions (always-on)", as its own subsection. It is the universal Django definition-of-done. It fires for every app, not just registered tools.
An app isn't done until it's reachable
django-admin startapp builds an island. A complete-from-its-own-boundary
app — models, views, urls, templates, tests all present and passing — is
still unfinished if nothing in the running site links to it. "It works in
isolation" is not done; "a user can reach it from the running site" is done.
Before reporting a new app complete, wire it into the site:
INSTALLED_APPS— add the app's config.- Root URLconf —
include()the app'surls.pyinconfig/urls.py. An app whose URLconf isn't included has unreachable views, full stop. - Navigation / discovery — register the app so it surfaces wherever
this project expects apps to appear. This project uses an app
registry (see Project Setup): the app registers itself in its own
apps.py.ready()and the navigation template tag picks it up. Do not hand-edit nav templates or central list views — they read from the registry. - Verify reachability — confirm the app's main page actually loads
from the running site (not just that its tests pass). Per Red Panda
criterion 5, this is externally verifiable: load the page, don't assert
it works.
Why this rule exists: an LLM reasons locally and closes the visible task at
the app's own boundary. The wiring that makes an app reachable lives in
other files (
config/urls.py,INSTALLED_APPS, the registry) with no signal inside the new app pointing to them. Without this rule, the near-certain result is a fully-built, completely inaccessible app. The registry exists precisely so that "surface it" happens inside the app's own boundary (aregister()call inready()) — collapsing the wiring into the one place local reasoning will actually look.
The same principle generalises beyond Django: a new route that isn't mounted, a CLI subcommand not added to the dispatcher, a handler not registered — all the same failure. Done means connected, not written.
Always-on anti-patterns
The cross-cutting tripwires worth carrying everywhere. File-specific landmines (nginx, compose, broker) are in path-scoped rules.
- Models: no
.get()without handlingDoesNotExist; nonull=TrueonCharField/TextField(useblank=True, default=""); always specifyon_delete; don't overridesave()for business logic; noMeta.orderingon large tables. - Security: secrets via env vars, never in
settings.py; never commit.env; neverDEBUG=Truein production; nevermark_safe()on user-supplied content; never disable CSRF. - Templates:
{% url %}not{{ variable }}for URLs; no logic in templates;{% csrf_token %}in every form. - Imports/style: no
import *; no mutable default args; no bareexcept:; don't silence linter warnings without a documented reason.
Environment
- Virtual environment:
~/env/PROJECT/bin/activate(replace PROJECT). pyproject.tomlfor config — nosetup.py, norequirements.txt.- Dependencies floor-pinned with ceiling (
Django>=5.2,<6.0). Exact==pins only in application lock files, never in reusable packages. - Dev DB: SQLite. Production DB: PostgreSQL.
Path-scoped rules to create (.claude/rules/)
These hold the landmines extracted from the standards doc. Each loads only
when its paths match, keeping this file lean. Frontmatter shown.
nginx.md—paths: ["nginx/**", "**/*.conf"]— reverse-proxy reference config: Docker DNS resolver + variableproxy_pass,$proxy_x_forwarded_protomap, access-log filtering, RFC1918 allowlists (all four ranges),alwayssecurity headers.docker-compose.md—paths: ["docker-compose*.y*ml", ".env*"]— per-serviceenvironment:scoping (no sharedenv_file:),${VAR}interpolation,.env.exampleannotation convention, therepr()parse diagnostic.celery-tasks.md—paths: ["**/tasks.py"]— idempotency, retry logic, pass IDs not instances, synchronous-by-default, broker URL percent-encoding, progress pattern{app}:task:{task_id}:progress.migrations.md—paths: ["**/migrations/**"]— never edit deployed migrations;RunPythonneeds a reverse; no non-nullable field without a default; meaningful--name; test forward and backward.memcached.md—paths: ["**/settings.py", ".env*"]— bind0.0.0.0not localhost; container can't reach127.0.0.1; LAN hostname inKVDB_LOCATION; key pattern{app}:{model}:{identifier}:{field}.frontend.md—paths: ["**/templates/**", "**/static/**"]— DaisyUI+ Tailwind for new projects / Bootstrap 5 for existing; extendthemis/base.html; no inline styles or scripts.
Reference docs (consult on demand, don't inline)
docs/gotcha writeups: broker-URL/Kombu parsing, env-file parsing differences, nginx IP-caching. State the rule in the rule file; link the why here.- Preferred-packages list and per-app architecture: keep in
docs/, not in this always-on file.