# Pathrule Pattern: Django (1.0.0)
# ::pathrule:package:django

### [RULE] Fetch related data explicitly; never let a loop generate queries  (path: /apps)
<!-- scope: folder | priority: high | strict -->

Django's lazy relations are the single most common performance bug in the framework: the code reads fine, the tests pass on ten rows, and production fires one query per row.

- Declare traversal up front: `select_related` for forward `ForeignKey` and `OneToOneField`, `prefetch_related` for reverse and `ManyToManyField`. If a template or serializer walks a relation, the queryset that feeds it must already include it.
- Count and sum in the database with `Count`, `Sum`, and `annotate`, not by loading rows into Python. `len(qs)` loads every row; `qs.count()` does not.
- Narrow the columns you actually read with `only()` or `values()`, and stream large result sets with `.iterator()` so a single request does not hold the whole table in memory.
- Never issue queries inside a loop or a template tag. If you need per-row data, build a dict keyed by id in one query and look it up.
- When you must drop to SQL, use `raw()` with parameters or `django.db.connection` with placeholders. String-formatted SQL and `.extra()` are injection surfaces.

---

### [RULE] Split settings per environment and read every secret from the environment  (path: /config)
<!-- scope: folder | priority: high | strict -->

Django's default settings module is built for a first run on localhost, not for production. Shipping it unchanged is how a project ends up with `DEBUG = True` in front of real users, which prints stack traces and settings to anyone who triggers an error.

- Keep a base settings module plus per-environment overrides (or one module driven entirely by environment variables). Nothing environment-specific is committed.
- Read `SECRET_KEY`, database URL, and every third-party credential from the environment and fail loudly at startup when one is missing. A default value for a secret is a production incident waiting for a deploy.
- Set `DEBUG = False` and a real `ALLOWED_HOSTS` list in production, and turn on `SECURE_SSL_REDIRECT`, `SECURE_HSTS_SECONDS`, `SESSION_COOKIE_SECURE`, and `CSRF_COOKIE_SECURE`.
- Run `python manage.py check --deploy` in CI and treat its warnings as failures.

See the secrets-env-management pattern for how the values are stored and rotated.

---

### [RULE] Keep migrations reversible and non-blocking  (path: /apps)
<!-- scope: folder | priority: high | advisory -->

A migration that works on an empty dev database can hold an exclusive lock on a large production table for minutes. Treat migrations as deploys of their own.

- Split schema changes from data backfills. A schema migration should be fast and lock-light; a backfill runs in batches and can be re-run.
- Add columns as nullable (or with no default that requires a table rewrite), deploy the code that writes them, backfill, then add the constraint. Never do all four in one migration.
- Give every `RunPython` a `reverse_code` (or `migrations.RunPython.noop`) so the migration is reversible, and never import models directly; use `apps.get_model` inside the function.
- Do not edit a migration that has already run in any shared environment. Add a new one.
- Review generated migrations before committing them; `makemigrations` will happily drop and recreate a column when a field is renamed.

---

### [MEMORY] Async views and the ORM: what is safe to await  (path: /apps)

Django's async story is real but partial, and the boundary is where projects get hurt. The rule of thumb: an `async def` view may only touch the ORM through its async API.

- The ORM exposes async variants (`aget`, `acreate`, `asave`, `adelete`, `afirst`, `acount`, `aupdate_or_create`) and supports `async for` over a queryset. A plain sync call inside an async context raises `SynchronousOnlyOperation`.
- Evaluation is still where the query happens. `qs.filter(...)` builds; `async for` or an `a`-prefixed method executes.
- Wrap unavoidable sync work (a third-party library, a sync-only ORM path) in `sync_to_async(..., thread_sensitive=True)` rather than calling it directly.
- Async only pays off when the view actually waits on I/O it can overlap: several outbound HTTP calls, a slow external API, streaming. A single query per request is faster in a plain sync view.
- Async views need an ASGI server (uvicorn, daphne, hypercorn) in front. Under WSGI they are run in a thread and you get the complexity without the benefit.

See /apps for the query rule and /config for the settings rule.

---

### [MEMORY] Where logic lives: fat models, thin views, explicit services  (path: /apps)

Django gives you many places to put behaviour, and the difference between a maintainable project and a pile of views is choosing deliberately.

- Row-level behaviour goes on the model. Reusable query logic goes on a custom `Manager` or `QuerySet` so it composes (`Order.objects.paid().recent()`) instead of being copy-pasted into views.
- A workflow that spans several models, sends mail, or calls a third party goes in a plain service function under the app, called by the view. It is testable without a request and reusable from a management command or a task.
- Views translate a request into a call and a response. If a view is longer than a screen, the logic belongs elsewhere.
- Use signals sparingly and never for business rules. `post_save` side effects are invisible at the call site and fire in tests, fixtures, and bulk operations you did not think about. Call the service explicitly.
- Bulk paths (`bulk_create`, `bulk_update`, `queryset.update()`) skip `save()` and signals by design. If a rule must always hold, enforce it in the database or in the service, not in `save()`.

See /apps for the migration rule; the rest-api-design pattern covers the HTTP surface when you add DRF.

---

### [MEMORY] Test setup that stays fast as the suite grows  (path: /tests)

Django's test tooling is fast if you let it be, and a slow suite is the reason teams stop running it.

- Prefer factories (`factory_boy`) over JSON fixtures. Fixtures rot silently as the schema changes and force every test to carry rows it does not use.
- Reuse the test database between runs (`--reuse-db` with pytest-django, `--keepdb` with the Django runner) and only rebuild when migrations change.
- Use `pytest.mark.django_db` (or `TestCase`) so each test runs in a transaction that rolls back. Reach for `TransactionTestCase` only when you genuinely test commit behaviour, because it truncates tables and is much slower.
- Guard the N+1 rule with `assertNumQueries` (or `django_assert_num_queries`) on your highest-traffic endpoints. It is the only test that fails when someone removes a `select_related`.
- Test at the boundary you care about: services with plain function calls, views through the test client, and templates only where rendering logic is real.

See /apps for the query rule this suite protects.

---

### [SKILL] django-query-review  (path: /)

---
name: django-query-review
description: Django review checklist for query efficiency, settings safety, async correctness, and migration risk. Run before merging any change that adds a view, serializer, template, or migration.
---

# Django review

## Queries
- [ ] Every relation a view, serializer, or template touches is in `select_related` or `prefetch_related`.
- [ ] No query inside a loop, template tag, or property called per row.
- [ ] Counts and aggregates run in the database (`count()`, `annotate`, `Sum`), not in Python.
- [ ] Large reads use `only()`/`values()` and `.iterator()` where the result set is unbounded.
- [ ] Any raw SQL is parameterized; no f-strings, no `.extra()`.
- [ ] A high-traffic endpoint has an `assertNumQueries` guard.

## Settings and security
- [ ] No new setting hardcodes an environment value or a secret.
- [ ] `manage.py check --deploy` still passes.
- [ ] User input reaches the database through a form or serializer, not straight from `request`.

## Async
- [ ] `async def` views use only the ORM's async API (`aget`, `async for`, ...).
- [ ] Sync-only calls are wrapped in `sync_to_async(thread_sensitive=True)`.
- [ ] The view actually overlaps I/O; otherwise it is a sync view.

## Migrations
- [ ] Schema change and data backfill are separate migrations.
- [ ] New columns land nullable, are backfilled, then constrained.
- [ ] Every `RunPython` has a reverse and uses `apps.get_model`.
- [ ] No already-applied migration was edited.
