agentleFS
charlesmsiegelcharlesmsiegel/coding-skills/skills/django-code-doctor/SKILL.md

django-code-doctor

Skill2 starsChanged 4 months agoMITView on GitHub

Review, simplify, upgrade, and write idiomatic Django. Finds N+1 queries and per-row writes, model definition problems, missing object-level authorization, insecure settings, work hidden in templates, form and admin problems, DRF permission gaps, risky migrations, async and transaction-boundary bugs, Django-shaped test smells, and abstractions that never earned their keep. Version-aware across Django 4, 5, and 6 - it detects the project's version and reports what breaks on the way to a target, so it drives a 4-to-5, 4-to-6, or 5-to-6 upgrade. Use when the user asks to simplify, refactor, optimize, review, or upgrade Django code, or mentions models, QuerySets, the ORM, select_related, views, serializers, DRF, forms, admin, signals, middleware, migrations, Celery, or Django settings - and when writing new Django, for which references/django-idioms.md gives one canonical answer per task. For general Python analysis use python-code-doctor.

  • Reads credentials
  • Installs packages

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every detector takes --format text|json and --ignore type1,type2, emits 🔴/🟡/🟢 severities, and produces the same flat findings shape as …

    line 72
  2. Very specific:

    Every detector is gated on the project actually being Django (a manage.py, or django imports).

    line 118

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Django Code Doctor

Django's defaults make several expensive mistakes easy and invisible: a loop that issues a query per row reads exactly like a loop that doesn't, a template can hit the database with nothing in the Python to show for it, and an endpoint that checks you are logged in looks identical to one that checks you own the row. This skill finds those mechanically, then gives you the judgment to decide what to do.

It does three jobs:

  • Review — fifteen detectors over an existing project.
  • Upgrade — what breaks between the Django this project is on and the one you want, with the severity derived from the target.
  • Author — references/django-idioms.md is prescriptive: one canonical answer per task, version-tagged, for writing Django that does not need this skill's other half later.

Its scope is Django. Complexity, duplication, dead code, exception handling, and resource leaks are not Django questions and this skill does not look for them — if the python-code-doctor skill is installed, run it too for that half; if it is not, say so in the report rather than implying the code was reviewed for it.

Deterministic detectors

Let SKILL=/path/to/this/skill — the directory holding this SKILL.md. Commands run from the Django project being reviewed, not from the skill directory. Needs Python 3.11+ and nothing else: the detectors parse source with the stdlib ast module and never import the project or start Django, so no virtualenv, settings module, or database is required.

python "$SKILL/scripts/analyze_django.py" /path/to/project      # everything, one report
python "$SKILL/scripts/analyze_django.py" . --format json > report.json
python "$SKILL/scripts/analyze_django.py" . --skip templates,overengineering
python "$SKILL/scripts/analyze_django.py" . --ignore no_default_ordering
python "$SKILL/scripts/analyze_django.py" . --target-version 5.2

# The database
python "$SKILL/scripts/find_query_issues.py" .        # N+1, per-row writes/reads, lost updates, raw SQL
python "$SKILL/scripts/find_model_issues.py" .        # __str__, null on text, on_delete, related_name, save(update_fields)
python "$SKILL/scripts/find_migration_issues.py" .    # historical models, reversibility, NOT NULL adds, leaf conflicts
python "$SKILL/scripts/find_transaction_issues.py" .  # external calls in atomic(), IntegrityError, select_for_update

# The request path
python "$SKILL/scripts/find_view_issues.py" .         # ownership filters, csrf_exempt, open redirects, fat views
python "$SKILL/scripts/find_form_issues.py" .         # fields='__all__', clean() returns, save_m2m, class-scope querysets
python "$SKILL/scripts/find_drf_issues.py" .          # default permissions, unscoped querysets, serializer N+1
python "$SKILL/scripts/find_admin_issues.py" .        # changelist N+1, action permissions, mark_safe in the admin
python "$SKILL/scripts/find_template_issues.py" .     # queries and relation walks, csrf_token, vars in <script>
python "$SKILL/scripts/find_async_issues.py" .        # sync ORM in async, unawaited a*, tasks and on_commit

# The project
python "$SKILL/scripts/find_django_security.py" .     # DEBUG, SECRET_KEY, headers, mark_safe, |safe
python "$SKILL/scripts/find_settings_issues.py" .     # AUTH_USER_MODEL, middleware order, prod backends
python "$SKILL/scripts/find_django_overengineering.py" .  # abstract models, managers, mixins, signals, services
python "$SKILL/scripts/find_test_issues.py" .         # assertNumQueries, on_commit under TestCase, setUpTestData
python "$SKILL/scripts/find_version_issues.py" .      # what breaks on the way to the target — see below

For a health review, add --target-version <declared major.minor> to analyze_django.py, for example --target-version 5.2. Its default target is the newest release in the upgrade table, so an untargeted run also asks whether the project is ready for that newer Django. Use that default or a newer explicit target for upgrade work, and label the target. Missing version evidence is a coverage gap, not permission to treat upgrade warnings as current breakage.

Every detector takes --format text|json and --ignore type1,type2, emits 🔴/🟡/🟢 severities, and produces the same flat findings shape as python-code-doctor — so any of them pipes straight into the bundled reporter:

python "$SKILL/scripts/analyze_django.py" . --format json \
  | python "$SKILL/scripts/format_findings.py" --format cards

format_findings.py is this skill's own copy, so the pipe works whether or not python-code-doctor is installed. analyze_django.py builds the project graph and parses each file once, sharing both with all fifteen detectors — which is most of the runtime on a large project.

Evidence limits

Lazy ModelChoiceField querysets are standard Django configuration, not import-time snapshots. Inherited model display methods and ordering count as implementations. Reads during validation hooks and guarded reads are not evidence that validation was skipped. A get_or_create() call alone cannot prove a race: database uniqueness may make it safe, so unresolved cases are candidates. Likewise, commit=False without a visible save_m2m() is a candidate when the form's many-to-many fields and caller lifecycle cannot be established.

Form-save checks follow the constructed form receiver, rather than confusing unrelated model saves with form saves. A missing explicit is_valid() call is a candidate, not proof that validation was bypassed: Django ModelForm saves trigger validation through self.errors, and custom saves can validate or avoid writes.

The three findings to read first

  • missing_ownership_filter / unscoped_viewset_queryset — a view or a ViewSet looks up records without scoping them to request.user. This is the most common real vulnerability in Django application code: the endpoint checks that you are logged in and then hands you somebody else's row. Verify each by hand; the detectors cannot see authorization enforced through a custom mixin, a permission class, or a base class.
  • django_end_of_life — the project is on a Django that receives no security fixes. As of August 2026 that is every Django 4 release, plus 5.0 and 5.1. This subsumes every other finding in the report.
  • n_plus_one_query — the finding that turns into latency. Reported only when the loop iterates a queryset, the body reaches through a real relation on the loop variable, and nothing in the chain prefetches it.

They stay quiet outside Django

Every detector is gated on the project actually being Django (a manage.py, or django imports). On anything else they report nothing and say so on stderr — so silence is never mistaken for a clean bill of health. find_drf_issues.py is gated twice, on DRF imports as well.

Upgrading Django 4 → 5 → 6

python "$SKILL/scripts/find_version_issues.py" .                 # vs. the current release
python "$SKILL/scripts/find_version_issues.py" . --target 5.2    # the LTS
python "$SKILL/scripts/find_version_issues.py" . --from 4.2 --target 6.1
python "$SKILL/scripts/find_version_issues.py" . --list-known    # what the table covers

The version is detected from pyproject.toml (PEP 621 and Poetry), requirements*.txt, setup.cfg, or Pipfile, and the report names the source — a manifest pin and whatever happens to be installed are different claims. When no version can be determined, nothing version-conditional is reported and the report says so, because a deprecation warning built on a guessed version is worse than silence.

Severity is derived, not hand-assigned:

ConditionMeaning
Removed at or before the target🔴The project will not run on the target
Deprecated on the version in use🟡Works today, on a clock
Removal scheduled after the target🟢Fix it while you are here

The change table is a snapshot as of Django 6.1 (August 2026). When the target is newer than that, the detector says so on stderr — web-search the release notes for the gap before reporting the check as complete: docs.djangoproject.com/en/N/releases/ and /internals/deprecation/.

Then follow references/django-upgrade-runbook.md. The two rules that matter most: one feature release at a time (the deprecation shims exist so this works), and the version bump and the idiom rewrites are separate commits — one diff that does both is unreviewable and unbisectable.

Writing new Django

Load references/django-idioms.md before writing models, views, forms, settings, or tasks. It gives one canonical answer per task rather than surveying the options, and tags anything that needs 5.0+, 5.2+, or 6.0+.

Two decisions it front-loads because they are expensive later: set AUTH_USER_MODEL on day one, and put the ownership filter in the same query as the fetch.

Use the project's own tools when they exist

The detectors are stdlib-only on purpose, but three questions they structurally cannot answer need a live Django:

python "$SKILL/scripts/run_external_tools.py" .                        # run what is installed
python "$SKILL/scripts/run_external_tools.py" . --run-migrations-check # imports the project
python "$SKILL/scripts/run_external_tools.py" . --tools django-upgrade --fix   # MUTATES
ToolAnswers what parsing cannot
makemigrations --check --dry-runIs any model change unmigrated? Only Django can compare against the migration graph — and a missing migration passes every test, then fails on deploy
check --deployWhat Django itself thinks of the real settings module, including environment overrides
pip-auditWhich advisories apply to the pinned Django. This skill can say a release is past end-of-life; only a database can say which CVEs that means
django-upgradeThe actual rewrites. This skill reports; that tool applies

The script never installs anything: absent tools are listed under missing_tools with a pip install hint. When that list is non-empty and the tools would help, ask the user whether to install them and only install on confirmation. --fix and --run-migrations-check are opt-in because they mutate or execute.

Reviewing a change (diff lens)

For an AI-written feature or a CR, review what changed, not the legacy around it:

python "$SKILL/scripts/analyze_diff.py"                  # vs. merge-base with the default branch
python "$SKILL/scripts/analyze_diff.py" origin/main --path .

The context is still built from the whole project — whether a loop is an N+1 depends on models in another file — and only the reporting is narrowed. The five whole-tree categories it cannot run (over-engineering, settings, migrations, the suite-wide test check, and the version sweep) are named in the output, so silence about them does not read as a clean bill of health. Run analyze_django.py separately for those.

What the detectors cannot decide

Deterministic checks find the mechanical problems. Everything below is a judgment call, and getting it wrong in either direction is expensive — load the guide.

Load this when…File
Writing new Django — one canonical answer per task, version-tagged for 4/5/6references/django-idioms.md
Running an upgrade — the campaign, what changed at each version, what to adopt afterreferences/django-upgrade-runbook.md
Deciding how to fix a query problem: select_related vs prefetch_related, what bulk operations skip, when raw SQL is rightreferences/django-orm.md
Deciding where logic belongs: fat model vs service layer, signals vs explicit calls, CBV vs FBV, app boundariesreferences/django-architecture.md
Judging whether an abstraction earns its keep — managers, mixins, middleware, service layersreferences/django-overengineering.md
Reviewing security beyond what settings show — object-level authorization, escaping, uploads, the adminreferences/django-security.md
Writing or reviewing a migration, especially a data migration or a zero-downtime deployreferences/django-migrations.md
Reviewing a DRF API — endpoint vs row permissions, serializer N+1, paginationreferences/django-drf.md
Judging form and admin design — both are security boundaries treated as plumbingreferences/django-forms-and-admin.md
Async views, the a* ORM surface, Celery or django.tasks, and transaction boundariesreferences/django-async-and-tasks.md
Building the safety net before refactoring — query counts, characterization tests, Django's test trapsreferences/django-safety-net.md

Workflow

  1. Run analyze_django.py. Triage the mechanical findings before reading anything by hand.
  2. Check the version first. django_end_of_life outranks everything else in the report; an unpatched framework is a likelier way in than any finding below it.
  3. Verify every 🔴 by hand. The authorization findings have real false-positive modes — enforcement through a mixin, a permission class, or an overridden get_queryset is invisible to a parser.
  4. Measure the query findings before and after. django-debug-toolbar, assertNumQueries, or CaptureQueriesContext. A prefetch that doesn't reduce the query count is a guess that happened to compile.
  5. Review the hot code with the guides open. Effort follows churn, not line count — git log --since="1 year ago" --name-only --pretty=format: | grep '\.py$' | sort | uniq -c | sort -rn | head -30.
  6. Pin behavior before refactoring. Django code is heavily coupled to the database; a test that exercises the real query is the only safety net that counts. See references/django-safety-net.md.
  7. Ratchet. assertNumQueries around the view that had the N+1, and makemigrations --check in CI, are what stop both coming back.

Output & ticketing

The deliverable is an artifact, never a side effect: a findings list, cards, JSON, or the full report — returned inline or saved as a file. scripts/format_findings.py renders any detector's JSON into those shapes.

This skill does not create tickets in any system on its own. When the user wants findings filed, ask which ticket software or MCP to use and create them through that tool — never assume or fabricate a tracker.

Rules

  • Behavior is sacred, and in Django that includes the number of queries and the number of round trips. A refactor that changes either has changed behavior under load even when every test passes.
  • Migrations are not refactorable. Changing a model field changes the schema, and once a migration has run somewhere you do not control, it is history. Never "simplify" a field definition without generating and reading the migration.
  • A missing related_name cannot be changed freely once other code uses the default accessor. Grep before renaming.
  • Don't remove a signal without finding every sender. Signals are invisible at the call site — that is precisely why they should usually be explicit methods, and also why removing one is riskier than it looks.
  • Don't remove a password hasher in the same deploy as an upgrade. Dropping one from PASSWORD_HASHERS locks out every user still stored under it.
  • Don't refactor cold code. An odd abstraction in a module nobody has touched in three years is costing nothing.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.