mcpField
A
ac-django

ac-django

github.com/soulianeDevelopment1688 installsUpdated 4d ago
Repository

About ac-django

ac-django is a Django-focused skill for developers working with Django projects, with optional DRF guidance. It combines version-aware recommendations with detailed references for models, migrations, views, testing, and infrastructure, and layers its rules on ac-python.

Key features

  1. 01Django 6.1 guidance and Django 5.2 LTS notes
  2. 02Optional DRF practices
  3. 03Reference guides by development topic
  4. 04Model, migration, and query design guidance
  5. 05Django-specific testing and tooling conventions

Use cases

  1. 01Designing models and database migrations
  2. 02Reviewing DRF serializers and query performance
  3. 03Implementing views, forms, or background tasks
  4. 04Writing Django tests and configuring tooling

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/souliane/skills --skill ac-django

After installing, type/ac-djangoin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
admin-and-drf.md
antipatterns.md
background-and-infra.md
models-and-schema.md
squashing-migrations.md
testing-and-tooling.md
transactions-and-migrations.md
troubleshooting.md
views-and-templates.md
rules/
astgrep_scan.py
no-complexity-suppressions.yml
no-pytest-django-db.yml
pyproject_complexity.py
testcase-no-pytest-parametrize.yml

Details

From the project README

Django Bible (Django 6 · optional DRF)

Targets: Django 6.1 on Python 3.14 · Previous line: Django 5.2 LTS (trailing section) · API: DRF when you choose it

Version Policy

This skill documents one Django line: the current one. Keep it that way when you update it.

  • Main path = the current feature release. Today that is Django 6.1. When a newer feature release lands, the main path moves to it in the same edit.
  • Unmarked prose means the first release of the current line — Django 6.0. Anything the line gained after that carries a (6.1+) marker placed right after the API or feature it qualifies: `QuerySet.fetch_mode()` (6.1+). One form only. Do not write "Django X.Y+ adds", "new in X.Y", "as of X.Y", or a ### Django X.Y note heading — the marker is the whole convention, and it is only useful if it is the only one.
  • Older lines get one trailing section each, at the end of SKILL.md, and nothing anywhere else — the reference files carry the current line only and get no trailing section of their own. Today that is ## Previous line: Django 5.2 LTS. It carries the diff a reader on that version needs: what is missing, what to use instead, and what to change on upgrade. A version note interrupting the main path is a defect — move it down.
  • Research order when refreshing. Release notes and official docs first (docs.djangoproject.com/en/<version>/releases/<version>/ and the topic pages they link). Then the authors already cited under Canonical Sources. Blogs and newsletters last, briefly, for what the docs do not cover.
  • Rotation. When Django releases the next line, the current trailing section is replaced by the line being retired, unmarked prose is re-based on the new line's first release, and markers below that release are dropped. Django 6.2 is the last release under the A.B scheme; feature releases after it use a calendar YYYY format, one every January. "Current line" then means the newest YYYY release, and the same rules apply unchanged.

Canonical Sources

Reference Files (load as needed)

FileCoversWhen to load
references/models-and-schema.mdModels, fields, constraints, QuerySets, managers, ORM performanceModel changes, query optimization, schema design
references/transactions-and-migrations.mdTransactions, locking, idempotency, migration safety, FK index opsMulti-step writes, migrations, schema changes
references/squashing-migrations.mdSquashing an app's migrations: regenerate and keep replaces (default), squashmigrations, or a django_migrations rewrite without replaces; refusing part-way databases; rollbackShortening a long migration history that deployed databases already ran
references/views-and-templates.mdViews, forms, templates, partials, HTMX, file uploads, i18n, middleware, management commands, connection pooling, reference snippetsView/template work, form handling, HTMX, uploads
references/background-and-infra.mdBackground tasks, security, settings, observability, caching, asyncTasks, deployment, logging, caching, async views
references/admin-and-drf.mdDjango Admin, DRF serializers, viewsets, permissions, pagination, versioningAdmin customization, API endpoints
references/testing-and-tooling.mdTesting bible, Factory Boy, tooling, DX enforcementWriting tests, CI setup, linting
references/antipatterns.mddjango-antipatterns.com tips, patternsCode review, avoiding common mistakes
references/troubleshooting.mdCommon Django errors and fixesDiagnosing migration, N+1, on_commit issues

Dependencies

Requires ac-python (declared via the requires: frontmatter field). This skill layers Django-specific rules on top of ac-python's generic Python guidelines — see "Overrides When Loaded Alongside ac-python" below.

When used alongside lifecycle skills, provides Django best practices context for Django projects using the worktree workflow.

Overrides When Loaded Alongside ac-python

When both ac-django and ac-python are loaded, the following Django-specific rules take precedence over the generic Python guidelines:

Topicac-python (generic)ac-django (wins)
Test base classPlain pytest classes (class TestFoo:)django.test.TestCase (or TransactionTestCase when needed)
Parametrizationpytest.mark.parametrizeunittest_parametrize
Test data factoriesbuild_...() plain functionsFactory Boy with DjangoModelFactory, traits, build() / create()
Shared setuppytest fixturessetUpTestData() (class-level, faster for DB-backed tests)
Time mockingany (freezegun, time_machine, etc.)time_machine (house default)

All other ac-python guidelines (style, typing, OOP, imports, ruff config) apply unchanged in Django projects.

Trigger QA (Release Gate)

Before shipping skill changes, validate activation behavior with sample prompts:

  • Should trigger:
    • "Add a Django model field and migration."
    • "Review this DRF serializer and queryset for performance issues."
    • "Fix this Django transaction/on_commit bug."
  • Should NOT trigger:
    • "Set up git worktrees for a ticket."
    • "Implement project delivery workflow and create an MR."
    • "Create a Notion research summary."

If behavior under-triggers or over-triggers, tighten description cues before release.

Example: Adding a new model field

User says: "Add a postal_code field to the Address model"

  1. Load references/models-and-schema.md for field types and constraints
  2. Load references/transactions-and-migrations.md for migration safety
  3. Add field to model with appropriate validators, constraints, and db_index if queried
  4. Create migration, verify with makemigrations --check
  5. Add factory trait for the new field in tests

Prime Directives

Django docs first (always)
  • Use Django the way Django documents it.
  • Prefer built-ins over dependencies unless Django has a clear documented gap.
Fat Model is the default (not the only answer)

Fat models win for small and medium models — the default, not a dogma.

  • Default home for business rules and invariants:
    • model instance methods (single-object behavior — Django's "row-level" home)
    • QuerySet/Manager methods (collection behavior — Django's "table-level" home)
  • This matches the Django docs and James Bennett's "no service layer" position: the models, with their managers/querysets, are the API other code talks to.
  • No services.py for domain logic by default.

Fat models stop being good once a model turns into a god object. A fat model is a model with rich, cohesive behavior over its own data; a god object is one model that has accreted unrelated concerns and orchestration until no one can read it end to end. See "When a fat model becomes a god object" below for the signals and the escalation ladder — the escapes are not a reflex service layer.

Locality of behavior (anti-octopus)
  • Co-locate behavior with:
    • the data (models/querysets)
    • the UI (partials inside the template that uses them)
Coordination vs business logic

Allowed at boundaries (views/forms/serializers):

  • sequencing calls
  • selecting aggregate root objects
  • transaction bracketing when spanning multiple domain calls

Not allowed at boundaries:

  • invariants
  • workflow rules
  • state transitions that define correctness

Deprecation Watch (flagged in 6.1, removal in the next major)

Avoid writing new code against these:

  • Flat EMAIL_* settings (EMAIL_BACKEND, EMAIL_HOST, EMAIL_PORT, etc.) are deprecated in favor of a MAILERS dict setting (mirrors DATABASES/CACHES); new code should not add to the flat settings.
  • mail.get_connection(), and the connection / fail_silently / auth_user / auth_password arguments to the mail helpers — pass using= with a MAILERS alias instead.
  • QuerySet.select_related() called with no arguments, and ModelAdmin.list_select_related = True — always pass explicit field names.
  • QuerySet.values_list(flat=True) without an explicit field name.
  • Model.from_db() implementations that do not accept the fetch_mode keyword argument.
  • None as the right-hand side of an exact lookup on a JSONField, meaning the JSON scalar null — query with JSONNull() instead. Querying only: storing None still writes SQL NULL and is unaffected, as are key and index lookups.
  • Double-dot template lookups ({{ book..title }}), which resolve to a lookup of the empty string.
  • django.db.transaction.savepoint() — use savepoint_create().
  • The default HMAC algorithm for salted_hmac()/base64_hmac() flips from sha1 to sha256 — don't hardcode sha1 expectations.

Django's 6.1 notes name Django 7.0 as the removal release. Django's release page states that 6.2 is the last release under the A.B scheme and that feature releases after it use a YYYY format. Upstream does not spell out how "7.0" maps onto that scheme.

Project Layout & Boundaries

Domain-first apps
  • Split apps by domain capability, not by technical layer.
  • Each app should be readable end-to-end.
Public vs internal vs admin
  • Separate public API/HTML surfaces from internal/admin surfaces.
  • Prefer separate modules over one giant views.py.
"core/" discipline
  • Allowed: request-id middleware, logging helpers, settings checks, small shared base mixins
  • Forbidden: hiding domain behavior "because it's shared"
Naming
  • Use business language.
  • Prefer verb methods: approve(), reject(), settle(), cancel().

Team Style Conventions (ask once, remember)

Some style choices are equally valid — the "right" answer depends on the team. When you encounter one of these for the first time in a project, ask the user for their team's preference and save it to the project's MEMORY.md (auto-memory) so it persists across sessions.

TopicOption AOption BWhat to ask
Attribute/relation checksLBYL: hasattr(), isinstance(), getattr(obj, attr, default)EAFP: try/except AttributeError"Does your team prefer LBYL (hasattr/getattr) or EAFP (try/except) for optional attribute and reverse-relation checks?"
Queryset existence checksif qs.exists(): then qs.first()obj = qs.first(); if obj:"Single query (first() + None check) or explicit exists() + first()?"
Null handling in serializersExplicit allow_null=True on every nullable fieldRely on model field null=True inference"Explicit allow_null on serializer fields, or infer from model?"

Rules:

  • Only ask on the first occurrence in a project — never re-ask if already saved.
  • If a project overlay skill already documents the preference, treat that as the answer — don't ask again.
  • Save the answer as a project type memory with a descriptive title (e.g., "Django team convention: LBYL for attribute checks").

Fat Model Doctrine (where logic lives)

Placement table (strict)
ConcernHomeNotes
invariants, transitions, domain calculationsmodel methods"tell, don't ask"
collection logicQuerySet methodschainable
graph loadingQuerySet methodsfor_api(), for_list(), with_*()
request validationforms / DRF serializersboundary validation
authorizationviews / DRF permissionscheck early
renderingtemplates / serializersno DB access
Encapsulate mutation

Expose domain methods that: validate state → perform mutation → persist changes → schedule side effects safely (after commit).

Encapsulation rule that keeps fat models honest (DabApps): never write to a model field or call save()/create()/bulk ops directly from outside the model — go through a model method or manager method. View/template code may read any field, but state changes always go through one of those methods. This is what makes "fat model" mean "the model owns its invariants", not "the model has lots of code".

When a fat model becomes a god object

Fat models are good for small/medium models. The rule does not scale to a model that has swallowed unrelated concerns. Treat the following as god-object signals — when two or more fire on one model, stop adding to it and climb the escalation ladder below:

  • Size: model class body over ~200 LOC, or more than ~15-20 public methods.
  • Mixed domains: methods on one model touch concerns that aren't that model's own data (e.g. Order with billing-provider calls and shipping-label generation and loyalty-points math).
  • Orchestration in save(): save() (or a single method) drives a multi-step workflow across several other models / sends notifications / calls external APIs — the "cancelling a billing agreement updates a half-dozen other things in one method" smell.
  • Method count by responsibility: clusters of methods that obviously belong to different responsibilities (a User that is also auth, also profile, also billing, also feature-flagging).
  • Tests scream: unit-testing one method forces you to set up half the schema, or a single test module for one model grows unmanageable.

A long model that is still entirely about its own data and reads cleanly is not a god object — don't refactor for LOC alone. The trigger is mixed concerns / orchestration, not size by itself.

Escalation ladder (climb only as far as a signal forces you)

Each rung has a WHEN. Take the lowest rung that fixes the signal — do not skip to a service layer.

  • (a) QuerySet / Manager methods. WHEN: the logic is collection- or table-level (filtering, aggregation, bulk state changes, alternate constructors like Account.objects.create_trial()). This is the first place table-level logic goes — Django's own guidance. Most "the model is getting fat" pressure is really table-level logic that belongs here, not on the instance.

  • (b) Cohesive model mixins or a bounded-context model split. WHEN: the signals are mixed domains / method-count-by-responsibility on a single model — the model is doing several cohesive jobs.

    • First reach: extract each cohesive cluster into an abstract model mixin (BillingMixin, AuditMixin) so each concern is its own readable unit while the table stays one row. Mixins must be genuinely cohesive — a mixin that is just "the rest of the methods" is the god object with extra files.
    • Bigger reach: if the concerns are really separate aggregates, split the model (and often the app) along the bounded context — e.g. pull Subscription/Invoice out of a god Account. Splitting apps by domain capability is the real scaling move; see "Domain-first apps" above.
  • (c) Stateless processor functions (module-level), or a dedicated processor class. WHEN: the work orchestrates several models and has no natural single aggregate root to own it — the cross-aggregate / save()-doing-orchestration signals. This is the "broke the over-complex method up" answer (Bennett's followup), not a relocation of the mess.

    • Default form: stateless module-level functions in the app (e.g. app/operations.py / app/processors.py) that take the participating objects as arguments, do the sequencing inside one transaction.atomic(), and schedule side effects via on_commit. Functions over classes when there is no state to hold (Luke Plant).
    • Reach for a dedicated processor/handler class only when there genuinely is multi-step state to carry across the orchestration (a builder, a multi-phase import). Name it for the operation (OrderCheckoutProcessor), keep it stateless-by-default, and keep the invariants on the models it drives — the processor sequences, the models still own correctness.
    • This rung is coordination, not a domain service layer: it calls model/manager methods, it does not re-implement the business rules that live on them.
  • (d) Service layer — last resort, external-API orchestration only. WHEN: the orchestration is dominated by talking to the outside world (Stripe, AWS, a third-party API) and the sequencing/error-handling/retry of those calls is the actual complexity. Then a services.py scoped to that external integration is fine.

    • Constraint: services.py holds no DB business logic / no invariants — those still live on the models. The service orchestrates external calls and hands results to model/manager methods.
    • This is deliberately the top of the ladder: a general "all business logic goes in services.py" layer (the HackSoft / enterprise pattern) is a real, defensible style, but it is not this skill's default — it trades the model-as-API for a parallel layer, and for most apps the rungs above cover the need. Adopt a project-wide service layer only as an explicit, documented team decision, not as a default reflex when one model got fat.

Companion reads (selectors.py). Independently of the write-side ladder, complex cross-model read operations (reporting/dashboards) that don't belong on any one QuerySet can live in selectors.py returning typed DTOs. Selectors are read-only; they never mutate.

Sources for this doctrine

Review Checklists

Domain rules / Fat Model
  • business rules live on models/querysets (the default home)
  • boundaries only orchestrate
  • no field writes / save() / bulk ops from outside model + manager methods
  • no god-object: a fat model touching mixed domains, or a save()/method driving multi-model orchestration, has climbed the escalation ladder (queryset → mixin/split → processor function/class → service for external APIs only) rather than just growing
ORM performance
  • no N+1
  • queryset shaped for serializer/template needs
  • constraints/indexes reviewed
Transactions and side effects
  • multi-step write flows wrapped in atomic()
  • side effects scheduled via on_commit
Migrations
  • linear migrations enforced (one leaf per app — django-linear-migrations or manual review)
  • makemigrations --check passes
  • migration safe/reviewable
  • a squash that deletes migration files keeps replaces until every database has recorded it and refuses part-way databases before migrate (without replaces: names its backup, row-count, schema-diff, rewrite and rollback plan) — see references/squashing-migrations.md
  • comments-as-code (per ac-python): no signature-echo docstring, no inline comment restating the RunPython body — a data migration's intent is its function name, not a 6-line docstring
Tasks
  • idempotent
  • enqueued after commit
Security
  • authz checked early
  • CSRF preserved
  • CSP configured correctly
Architectural Health (Module-Level)

Apply to full files touched by the diff, not just changed lines:

  • Fat Model is the default: business logic on models/querysets, not in views/commands/CLI
  • views and management commands only orchestrate — no invariants, no workflow rules
  • no god-module (single file mixing unrelated concerns) and no god-object (single model that has accreted unrelated concerns / multi-model orchestration)
  • multi-model orchestration sits in a stateless processor function / class (or on an aggregate-root method), not crammed onto one model's save()
  • no complexity rule suppressions (C901, PLR09xx) in pyproject.toml beyond the python-boilerplate baseline
  • services.py (if present) is scoped to external API orchestration (Stripe, AWS) and holds no DB business logic — a project-wide service layer is a documented team decision, not the default
Testing
  • traits used for nullable/optional data
  • tests explicit about preconditions

Enforcement via prek

The testing conventions above that are deterministically checkable ship as prek hooks in this repo (.pre-commit-hooks.yaml + ac-django/rules/). The AST-shaped checks are ast-grep YAML rules; the pyproject.toml surface (which ast-grep cannot parse — no TOML language) is a tiny standalone Python hook. A consuming repo references them by URL and rev:

- repo: https://github.com/souliane/skills
  rev: <commit-sha>
  hooks:
    - id: ac-django-no-pytest-django-db
    - id: ac-django-testcase-no-pytest-parametrize
    - id: ac-django-no-complexity-suppressions
    - id: ac-django-no-pyproject-complexity
Hook idEngineFails on
ac-django-no-pytest-django-dbast-grepany pytest.mark.django_db use — the @pytest.mark.django_db decorator and a module-level pytestmark = pytest.mark.django_db (or list). Use django.test.TestCase.
ac-django-testcase-no-pytest-parametrizeast-grep@pytest.mark.parametrize on a method inside a TestCase subclass (pytest silently ignores it there) — use unittest_parametrize. Module-level pytest-style functions are not flagged.
ac-django-no-complexity-suppressionsast-grepa # noqa: C901/PLR09xx comment in source/tests
ac-django-no-pyproject-complexitystandalonea C901/PLR09xx entry in a pyproject.toml ruff lint.ignore / lint.extend-ignore / lint.per-file-ignores list

ast-grep is pinned to 0.44.1. The wrapper (ac-django/rules/astgrep_scan.py) resolves it hermetically via uvx --from ast-grep-cli==0.44.1 ast-grep when uv is on PATH (no system install needed), falling back to a system ast-grep.

They fail-closed, grandfathered INLINE. With nothing grandfathered every violation fails (a fresh consumer gets full enforcement immediately). There is no baseline file and no count cap — existing violations are grandfathered in place:

  • ast-grep rules — add ast-grep's native opt-out comment on the line above the violation: # ast-grep-ignore[<rule-id>] (e.g. # ast-grep-ignore[ac-django-no-pytest-django-db]). It silences only that one rule on that one node; every new (un-ignored) occurrence still fails.
  • ac-django-no-pyproject-complexity — list each existing entry inline in the consuming repo's hook args as --grandfather <code>@<location> (e.g. --grandfather [email protected]/**/*.py). Any new entry not on the list fails; the list only shrinks as the code improves.

This keeps the ratchet's "tighten-only" property without a data file: the grandfather record lives where the violation lives (an inline comment, or the consumer's own .pre-commit-config.yaml), so it can never drift out of sync with the tree.

Review-only conventions (deliberately not hard-gated). Factory Boy usage and setUpTestData() for shared setup are house conventions, but a hook on them is too noisy — there are many legitimate non-factory create() calls and per-test setUp() needs. These stay review checklist items, not blocking hooks.

Previous line: Django 5.2 LTS

Django 5.2 is the LTS, in extended support until April 2028, so it is still what a lot of production code runs. Everything above targets Django 6. This section is the diff.

What Django 6 has that 5.2 does not
CapabilityDjango 6On 5.2 instead
Template partialsNative {% partialdef %} / {% partial %}django-template-partials
Background tasksNative django.tasks (@task, .enqueue())Celery / Huey / RQ
CSPNative CSP middleware + SECURE_CSPdjango-csp
Fetch modes (6.1+)QuerySet.fetch_mode()Explicit select_related() / prefetch_related()
DB-level on_delete (6.1+)DB_CASCADE / DB_SET_NULL / DB_SET_DEFAULTPython-level on_delete only
Multiple mailers (6.1+)MAILERSFlat EMAIL_* settings
Squashing a squashsquashmigrations accepts a squashed migrationTurn the earlier squash into a normal migration first (delete its replaced files, remove replaces)
Upgrading
  • Run django-upgrade before any manual refactor.
  • Leave explicit upgrade TODOs where a shim is in use:
    • # TODO(Django6): switch @shared_task -> @task and .delay() -> .enqueue()
    • # TODO(Django6): remove {% load partials %} (partials become native)
    • # TODO(Django6): replace django-csp with built-in CSP middleware
  • Django 5.2 is the last line supporting Python 3.10 and 3.11; Django 6.x needs Python 3.12+.

Comments

Markdown supported