mcpField
A
ac-django

ac-django

github.com/souliane开发1688 次安装4 天前更新
代码仓库

关于 ac-django

ac-django 是面向 Django 项目开发者的技能,也提供可选的 DRF 指南。它按版本整理建议,并以专题参考资料覆盖模型、迁移、视图、测试和基础设施;规则建立在 ac-python 之上。

主要功能

  1. 01Django 6.1 指南与 Django 5.2 LTS 说明
  2. 02可选的 DRF 实践指南
  3. 03按开发主题组织的参考资料
  4. 04模型、迁移与查询设计建议
  5. 05Django 专用测试和工具约定

适用场景

  1. 01设计模型和数据库迁移
  2. 02审查 DRF 序列化器与查询性能
  3. 03实现视图、表单或后台任务
  4. 04编写 Django 测试并配置工具

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/ac-django确认已加载。

包含的文件

除 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

详细介绍

Django 指南(Django 6 · DRF 可选)

目标版本: Python 3.14 上的 Django 6.1 · 上一版本线: Django 5.2 LTS(末尾章节)· API: 按需选择 DRF

版本策略

本技能只记录一个 Django 版本线:当前版本线。更新时请保持这一原则。

  • 主线 = 当前功能版本。 目前是 Django 6.1。有更新的功能版本发布时,主线应在同一次编辑中切换到新版本。
  • 未标注的正文表示当前版本线的首个版本——Django 6.0。该版本线在此之后新增的内容,都应在其对应的 API 或功能之后紧跟 (6.1+) 标记:`QuerySet.fetch_mode()` (6.1+)。只采用这一种形式。不要写“Django X.Y+ 新增”“X.Y 新增”或“自 X.Y 起”,也不要使用 ### Django X.Y 注释 标题——标记就是全部约定,而且只有保持唯一时才有用。
  • 旧版本线各自在 SKILL.md 末尾保留一个章节,其他任何地方都不应提及——参考文件仅涵盖当前版本线,末尾也不设自己的章节。目前该章节为 ## Previous line: Django 5.2 LTS。其中记录该版本的读者需要了解的差异:缺少什么、可用什么替代,以及升级时需要做什么。若版本说明打断了主线内容,就是缺陷——将其移到末尾。
  • 更新时的调研顺序。 首先查阅发行说明和官方文档(docs.djangoproject.com/en/<version>/releases/<version>/ 及其链接的主题页面)。然后查阅“权威来源”中已经引用的作者。最后简要查阅博客和时事通讯,了解文档未涵盖的内容。
  • 轮换规则。 Django 发布下一条版本线时,当前的末尾章节替换为即将退役的版本线;未标注的正文改以新版本线的首个版本为基准,并移除低于该版本的标记。Django 6.2 是 A.B 版本方案下的最后一个版本;之后的功能版本将采用日历年份 YYYY 格式,每年 1 月发布一次。届时,“当前版本线”指最新的 YYYY 版本,其他规则保持不变。

权威来源

参考文件(按需加载)

文件涵盖内容加载时机
references/models-and-schema.md模型、字段、约束、QuerySet、管理器、ORM 性能模型变更、查询优化、架构设计
references/transactions-and-migrations.md事务、锁定、幂等性、迁移安全、外键索引操作多步写入、迁移、架构变更
references/squashing-migrations.md压缩应用的迁移:重新生成并保留 replaces(默认方式)、使用 squashmigrations,或在不使用 replaces 的情况下重写 django_migrations;拒绝处于迁移中途的数据库;回滚缩短已部署数据库运行过的冗长迁移历史
references/views-and-templates.md视图、表单、模板、局部模板、HTMX、文件上传、国际化、中间件、管理命令、连接池、参考代码片段视图/模板开发、表单处理、HTMX、上传
references/background-and-infra.md后台任务、安全、设置、可观测性、缓存、异步任务、部署、日志记录、缓存、异步视图
references/admin-and-drf.mdDjango Admin、DRF 序列化器、视图集、权限、分页、版本控制Admin 自定义、API 端点
references/testing-and-tooling.md测试指南、Factory Boy、工具、开发体验规范编写测试、CI 设置、代码检查
references/antipatterns.mddjango-antipatterns.com 提示与模式代码审查、避免常见错误
references/troubleshooting.md常见 Django 错误及修复方法诊断迁移、N+1、on_commit 问题

依赖项

需要 ac-python(通过 requires: frontmatter 字段声明)。此技能在 ac-python 的通用 Python 指南之上叠加 Django 专属规则——请参阅下文“与 ac-python 一起加载时的覆盖规则”。

与生命周期技能搭配使用时,为采用 worktree 工作流的 Django 项目提供 Django 最佳实践背景。

与 ac-python 一起加载时的覆盖规则

同时加载 ac-django 和 ac-python 时,以下 Django 专属规则优先于通用 Python 指南:

主题ac-python(通用)ac-django(优先)
测试基类普通 pytest 类(class TestFoo:)django.test.TestCase(必要时使用 TransactionTestCase)
参数化pytest.mark.parametrizeunittest_parametrize
测试数据工厂普通 build_...() 函数使用 DjangoModelFactory、traits、build()/create() 的 Factory Boy
共享设置pytest fixturessetUpTestData()(类级别,对数据库测试更快)
时间模拟任意工具(freezegun、time_machine 等)time_machine(团队默认)

ac-python 的其他所有指南(风格、类型标注、OOP、导入、ruff 配置)在 Django 项目中均保持不变。

触发 QA(发布门槛)

发布技能变更前,使用示例提示词验证激活行为:

  • 应触发:
    • “添加一个 Django 模型字段和迁移。”
    • “检查这个 DRF 序列化器和 QuerySet 的性能问题。”
    • “修复这个 Django 事务/on_commit 错误。”
  • 不应触发:
    • “为一个工单设置 git worktree。”
    • “实施项目交付工作流并创建 MR。”
    • “创建一份 Notion 调研摘要。”

如果触发不足或过度触发,请在发布前收紧 description 中的提示线索。

示例:添加新的模型字段

用户说:“在 Address 模型中添加一个 postal_code 字段”

  1. 加载 references/models-and-schema.md,了解字段类型和约束
  2. 加载 references/transactions-and-migrations.md,了解迁移安全性
  3. 向模型添加字段,并设置适当的验证器、约束;如果会用于查询,则设置 db_index
  4. 创建迁移,并使用 makemigrations --check 验证
  5. 在测试中为新字段添加工厂特征

首要原则

始终优先查阅 Django 文档
  • 按照 Django 文档说明的方式使用 Django。
  • 优先使用内置功能,除非 Django 存在明确的文档说明的不足。
默认采用胖模型(但并非唯一答案)

对于中小型模型,胖模型是首选默认方案,而非教条。

  • 业务规则和不变量的默认归属位置:
    • 模型实例方法(单个对象的行为——Django 所说的“行级”归属位置)
    • QuerySet/Manager 方法(集合行为——Django 所说的“表级”归属位置)
  • 这符合 Django 文档以及 James Bennett 的“无服务层”主张:模型及其管理器和查询集_本身就是_其他代码所交互的 API。
  • 默认情况下,领域逻辑不放在 services.py 中。

当模型变成上帝对象时,胖模型就不再合适。胖模型是指围绕_自身_数据拥有丰富且内聚行为的模型;上帝对象则是指一个模型不断累积互不相关的职责和编排逻辑,最终变得无人能从头到尾读懂。有关相关信号和升级路径,请参阅下文“胖模型何时变成上帝对象”——应对方式_不是_条件反射般地引入服务层。

行为就近放置(避免章鱼式扩散)
  • 将行为与以下内容放在一起:
    • 数据(模型/查询集)
    • UI(放在使用它们的模板中的局部模板)
协调与业务逻辑

可在边界层(视图/表单/序列化器)执行:

  • 安排调用顺序
  • 选择聚合根对象
  • 跨多个领域调用时包裹事务

不可在边界层执行:

  • 不变量
  • 工作流规则
  • 定义正确性的状态转换

弃用提示(在 6.1 中标记,将在下一个主版本中移除)

避免在新代码中使用以下内容:

  • 扁平的 EMAIL_* 设置(EMAIL_BACKEND、EMAIL_HOST、EMAIL_PORT 等)已弃用,建议改用 MAILERS 字典设置(与 DATABASES/CACHES 类似);新代码不应再添加扁平设置。
  • mail.get_connection(),以及邮件辅助函数的 connection / fail_silently / auth_user / auth_password 参数——应改为传入 using= 和一个 MAILERS 别名。
  • 不带参数调用的 QuerySet.select_related(),以及 ModelAdmin.list_select_related = True——始终传入明确的字段名称。
  • 未指定明确字段名的 QuerySet.values_list(flat=True)。
  • 未接受 fetch_mode 关键字参数的 Model.from_db() 实现。
  • 在 JSONField 的精确查找右侧使用 None 来表示 JSON 标量 null——请改用 JSONNull()。仅影响查询:存储 None 仍会写入 SQL NULL,不受影响,键查找和索引查找也不受影响。
  • 双点模板查找({{ book..title }}),它会解析为对空字符串的查找。
  • django.db.transaction.savepoint()——请改用 savepoint_create()。
  • salted_hmac()/base64_hmac() 的默认 HMAC 算法将从 sha1 切换为 sha256——不要将对 sha1 的预期硬编码。

Django 6.1 的说明将 Django 7.0 列为移除版本。Django 发布页面指出,6.2 是采用 A.B 版本方案的最后一个版本,此后的功能版本将采用 YYYY 格式。上游没有明确说明“7.0”如何对应这一版本方案。

项目布局与边界

领域优先的应用
  • 按领域能力拆分应用,而不是按技术层拆分。
  • 每个应用都应能从头到尾读懂。
公共、内部与管理界面
  • 将公共 API/HTML 界面与内部/管理界面分开。
  • 优先使用多个独立模块,而不是一个庞大的 views.py。
“core/” 准则
  • 允许: 请求 ID 中间件、日志辅助工具、设置检查、小型共享基类混入
  • 禁止: 仅仅因为某些行为是共享的,就隐藏领域行为
命名
  • 使用业务语言。
  • 优先使用动词方法:approve()、reject()、settle()、cancel()。

团队风格约定(询问一次并记住)

有些风格选择同样合理——哪种“正确”取决于团队。在项目中首次遇到以下任一情况时,询问用户团队的偏好,并将其保存到项目的 MEMORY.md(自动记忆)中,以便在后续会话中沿用。

主题选项 A选项 B询问内容
属性/关联检查LBYL: hasattr()、isinstance()、getattr(obj, attr, default)EAFP: try/except AttributeError“团队在检查可选属性和反向关联时,更倾向于 LBYL(hasattr/getattr),还是 EAFP(try/except)?”
QuerySet 是否存在的检查if qs.exists(): 然后 qs.first()obj = qs.first(); if obj:“使用单次查询(first() + None 检查),还是显式调用 exists() + first()?”
序列化器中的空值处理对每个可空字段显式设置 allow_null=True依赖模型字段的 null=True 推断“在序列化器字段上显式设置 allow_null,还是从模型推断?”

规则:

  • 每个项目只在首次出现时询问——如果已保存,就不要再次询问。
  • 如果项目覆盖技能已记录该偏好,将其视为答案——不要再次询问。
  • 将答案保存为 project 类型的记忆,并使用描述性标题(例如:“Django 团队约定:属性检查采用 LBYL”)。

胖模型原则(逻辑所在之处)

放置表(严格)
关注点所属位置备注
不变量、状态转换、领域计算模型方法“告知,不询问”
集合逻辑QuerySet 方法可链式调用
图加载QuerySet 方法for_api()、for_list()、with_*()
请求验证表单/DRF 序列化器边界验证
授权视图/DRF 权限尽早检查
渲染模板/序列化器不访问数据库
封装变更

公开领域方法,按以下顺序执行:验证状态 → 执行变更 → 持久化更改 → 安全地安排副作用(在提交后)。

让胖模型保持名副其实的封装规则(DabApps):绝不在模型外直接写入模型字段,也不调用 save()/create()/批量操作——应通过模型方法或管理器方法执行。视图/模板代码可以_读取_任意字段,但状态变更始终必须通过上述方法之一完成。这正是“胖模型”的含义:模型负责维护自身的不变量,而不是“模型中有很多代码”。

当臃肿模型变成上帝对象

臃肿模型适用于小型和中型模型。但对于一个已经吞并了无关职责的模型,这条规则就不适用了。将以下情况视为上帝对象的信号——当同一个模型出现两种或更多信号时,就不要再往里添加职责,而应按照下面的升级路径进行拆分:

  • **规模:**模型类主体超过约 200 行代码,或公开方法超过约 15–20 个。
  • **领域混杂:**同一个模型中的方法涉及不属于该模型自身数据的职责(例如,Order 既调用计费服务提供商,又生成配送标签,还计算忠诚度积分)。
  • 在 save() 中编排流程:save()(或某个单一方法)驱动跨多个其他模型的多步骤工作流、发送通知或调用外部 API——例如“取消计费协议时,一个方法会更新其他六七项内容”的情况。
  • **按职责划分的方法数量:**明显属于不同职责的方法群(例如,一个 User 同时负责身份验证、个人资料、计费和功能标记)。
  • **测试发出警报:**对某个方法进行单元测试时,必须设置半个数据库架构;或者某个模型对应的单个测试模块变得难以管理。

如果一个较长的模型仍然只涉及自身数据,且结构清晰,那它就不是上帝对象——不要仅仅因为代码行数多就重构。触发重构的是职责混杂或流程编排,而不是单纯的规模。

升级阶梯(只在信号迫使时向上攀升)

每一级都有一个 适用时机。选择能解决信号的最低一级——不要跳到服务层。

  • (a) QuerySet / Manager 方法。 适用时机: 逻辑属于集合级或表级(筛选、聚合、批量状态更改、Account.objects.create_trial() 这样的替代构造函数)。这是表级逻辑的首选位置——这是 Django 自己的建议。大多数“模型变得臃肿”的压力,实际上都是应放在这里而非实例上的表级逻辑。

  • (b) 内聚的模型 mixin,或有界上下文模型拆分。 适用时机: 单个模型上的信号是 混合领域 / 按职责统计方法数量——模型承担了多项内聚的工作。

    • 首选做法:将每个内聚的功能簇提取为一个抽象模型 mixin(BillingMixin、AuditMixin),让每项关注点都成为独立、易读的单元,同时保持表仍然只有一行。Mixin 必须真正内聚——只装“剩余方法”的 mixin,不过是多拆了几个文件的上帝对象。
    • 更进一步:如果这些关注点实际上属于_独立的聚合_,就拆分模型(通常也拆分应用),沿有界上下文划分——例如,将 Subscription / Invoice 从臃肿的 Account 中拆出。按领域能力拆分应用才是真正的扩展方式;参见上文“以领域为先的应用”。
  • (c) 无状态处理器函数(模块级),或专用处理器类。 适用时机: 工作需要协调多个模型,且没有适合承担该工作的单一聚合根——也就是跨聚合 / save() 负责协调的信号。这是“拆解过度复杂方法”的做法(Bennett 的后续文章),而不是把一团乱麻挪个地方。

    • 默认形式:应用中的无状态模块级函数(例如 app/operations.py / app/processors.py),将参与的对象作为参数,在一个 transaction.atomic() 中完成流程编排,并通过 on_commit 安排副作用。没有状态需要保存时,优先使用函数而非类(Luke Plant)。
    • 只有确实需要在协调过程中跨步骤保留状态(例如构建器、多阶段导入)时,才使用专用处理器 / 处理程序类。按操作命名(OrderCheckoutProcessor),默认保持无状态,并将不变量留在它所驱动的模型上——处理器负责安排流程,模型仍负责正确性。
    • 这一级负责_协调_,而非领域服务层:它调用模型 / 管理器方法,不会重新实现属于这些方法的业务规则。
  • (d) 服务层——最后手段,仅用于协调外部 API。 适用时机: 协调工作主要是与外部系统交互(Stripe、AWS、第三方 API),而这些调用的流程编排 / 错误处理 / 重试才是真正的复杂之处。此时,可以使用限定于该外部集成的 services.py。

    • 约束:services.py 不包含数据库业务逻辑 / 不变量——这些仍属于模型。服务负责协调外部调用,并将结果交给模型 / 管理器方法。
    • 这一级特意放在阶梯顶端:通用的“所有业务逻辑都放进 services.py”层(HackSoft / 企业级模式)是一种确实有依据的风格,但不是本指南的默认做法——它用一个并行层替代了模型即 API 的方式,而对于大多数应用,上面的几级已经足够。只有在团队明确作出并记录决策时,才采用项目范围的服务层;不要只因某个模型变得臃肿,就条件反射般地使用它。

相关阅读(selectors.py)。 不受写入侧阶梯限制,复杂的跨模型读取操作(报表 / 仪表板)如果不适合放在任何一个 QuerySet 上,可以放在 selectors.py 中并返回带类型的 DTO。Selector 只读,绝不修改数据。

本原则的参考资料

审查清单

领域规则 / 胖模型
  • 业务规则放在模型 / 查询集上(默认归属)
  • 边界层只负责编排
  • 不从模型 + 管理器方法之外写入字段、调用 save() 或执行批量操作
  • 不出现上帝对象:如果一个胖模型涉及多个混杂领域,或某个 save() / 方法驱动了多模型编排,就应沿升级路径处理(查询集 → mixin/拆分 → 处理器函数/类 → 仅用于外部 API 的服务),而不是继续膨胀
ORM 性能
  • 没有 N+1 查询
  • 查询集按序列化器 / 模板的需求构造
  • 已审查约束 / 索引
事务与副作用
  • 多步骤写入流程由 atomic() 包裹
  • 副作用通过 on_commit 安排
迁移
  • 强制线性迁移(每个应用只有一个叶节点 — 使用 django-linear-migrations 或手动审查)
  • makemigrations --check 通过
  • 迁移安全且便于审查
  • 删除迁移文件的合并迁移会保留 replaces,直到每个数据库都已记录该迁移,并且会在执行 migrate 前拒绝处于中间状态的数据库(如果没有 replaces:说明备份、行数、架构差异、重写和回滚计划)— 参见 references/squashing-migrations.md
  • 注释即代码(遵循 ac-python):不写重复签名的文档字符串,也不写复述 RunPython 函数体的行内注释 — 数据迁移的意图应由其函数名表达,而不是靠 6 行文档字符串
任务
  • 幂等
  • 在提交后入队
安全
  • 尽早检查授权
  • 保留 CSRF
  • 正确配置 CSP
架构健康度(模块级)

审查差异所涉及的完整文件,而不只是改动的行:

  • 默认采用胖模型:业务逻辑放在模型 / 查询集上,而非视图 / 命令 / CLI 中
  • 视图和管理命令只负责编排 — 不包含不变量或工作流规则
  • 不出现上帝模块(单个文件混合互不相关的关注点),也不出现上帝对象(单个模型不断累积无关关注点 / 多模型编排)
  • 多模型编排放在无状态的处理器函数 / 类中(或聚合根方法上),而不是硬塞进某个模型的 save()
  • pyproject.toml 中除 python-boilerplate 基线以外,没有抑制复杂度规则的配置(C901、PLR09xx)
  • 如果存在 services.py,其范围仅限外部 API 编排(Stripe、AWS),不包含数据库业务逻辑 — 项目级服务层应是有记录的团队决策,而非默认方案
测试
  • 对可空 / 可选数据使用 traits
  • 测试明确说明前置条件

通过 prek 强制执行

上述可确定性检查的测试约定已作为 prek 钩子集成到此仓库(.pre-commit-hooks.yaml + ac-django/rules/)中。AST 结构检查采用 ast-grep YAML 规则;pyproject.toml 表面(ast-grep 无法解析,因为它不支持 TOML 语言)则由一个精简的独立 Python 钩子检查。使用这些规则的仓库通过 URL 和 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
钩子 id引擎失败条件
ac-django-no-pytest-django-dbast-grep使用了任何 pytest.mark.django_db——包括 @pytest.mark.django_db 装饰器,以及模块级的 pytestmark = pytest.mark.django_db(或列表)。请使用 django.test.TestCase。
ac-django-testcase-no-pytest-parametrizeast-grepTestCase 子类中的方法使用了 @pytest.mark.parametrize(pytest 会静默忽略此处的装饰器)——请使用 unittest_parametrize。模块级 pytest 风格的函数不会被标记。
ac-django-no-complexity-suppressionsast-grep源码或测试中有 # noqa: C901/PLR09xx 注释
ac-django-no-pyproject-complexity独立钩子pyproject.toml 的 ruff lint.ignore / lint.extend-ignore / lint.per-file-ignores 列表中有 C901/PLR09xx 条目

ast-grep 固定使用 0.44.1 版本。 包装器(ac-django/rules/astgrep_scan.py)会在 PATH 中存在 uv 时,通过 uvx --from ast-grep-cli==0.44.1 ast-grep 以隔离方式解析该版本(无需系统安装);否则回退到系统的 ast-grep。

这些检查采用默认失败、内联豁免的方式。 如果没有任何豁免,所有违规都会导致失败(新接入的仓库会立即受到全面强制检查)。没有基线文件,也没有数量上限——现有违规会在原处获得豁免:

  • ast-grep 规则——在违规行上方添加 ast-grep 原生的退出检查注释:# ast-grep-ignore[<rule-id>](例如 # ast-grep-ignore[ac-django-no-pytest-django-db])。它只会针对该节点屏蔽该规则;每个新出现的(未豁免)违规仍会导致失败。
  • ac-django-no-pyproject-complexity——在使用方仓库的钩子 args 中,用 --grandfather <code>@<location> 的形式逐一列出每个现有条目(例如 --grandfather [email protected]/**/*.py)。任何未列出的新条目都会导致失败;随着代码改进,列表只会缩短。

这样无需数据文件,也能保持递进规则“只收紧”的特性:豁免记录存放在违规所在的位置(内联注释或使用方自己的 .pre-commit-config.yaml),因此永远不会与代码树脱节。

仅供审查的约定(有意不设为强制门槛)。 Factory Boy 的使用以及通过 setUpTestData() 共享测试设置是团队约定,但为它们添加钩子会产生太多误报——存在许多合理的非工厂 create() 调用,也有许多需要逐个测试设置的 setUp()。这些内容仍列在审查清单中,不作为阻断性钩子。

上一版:Django 5.2 LTS

Django 5.2 是 LTS 版本,将在延长支持期内获得支持,直到 2028 年 4 月,因此仍有很多生产代码在使用它。以上内容均以 Django 6 为目标。本节说明两者的差异。

Django 6 有而 5.2 没有的功能
功能Django 65.2 中的替代方案
模板局部片段原生 {% partialdef %} / {% partial %}django-template-partials
后台任务原生 django.tasks(@task、.enqueue())Celery / Huey / RQ
CSP原生 CSP 中间件 + SECURE_CSPdjango-csp
获取模式(6.1+)QuerySet.fetch_mode()显式使用 select_related() / prefetch_related()
数据库级 on_delete(6.1+)DB_CASCADE / DB_SET_NULL / DB_SET_DEFAULT仅支持 Python 级的 on_delete
多个邮件发送器(6.1+)MAILERS扁平的 EMAIL_* 设置
压缩已压缩的迁移squashmigrations 接受已压缩的迁移先将较早的压缩迁移转换为普通迁移(删除其被替代的文件,移除 replaces)
升级
  • 进行任何手动重构之前,先运行 django-upgrade。
  • 使用兼容层时,明确保留升级 TODO:
    • # 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 是支持 Python 3.10 和 3.11 的最后一个版本系列;Django 6.x 需要 Python 3.12+。

评论

支持 Markdown