7.一次性搞定「新增 + 编辑」班级

当年我第一次做 CRUD 的时候,「新增一个页面、编辑一个页面」,我是当成 两套完全不同的功能 来写的。
结果是什么?
- 模板写了两份
- 表单逻辑写了两遍
- 改一个字段,要改四个地方
写完那一刻,我自己都不想维护。
所以这一章,我想解决的不是「功能怎么实现」, 而是一个新手一定会焦虑的问题:
“新增和编辑,到底该不该分开写?”
答案是:不该。
1.为什么新增和编辑要放在一起?
先别急着写代码,我带你「观察」一下真实项目。
我先打开最原始的成品系统,点一下「新增班级」。
你会看到:
- 一个表单
- 班级名称
- 班级编号
然后我再点「编辑某个班级」。


你会发现一件非常关键的事情:
页面长得一模一样。
唯一的区别是:
- 新增:表单是空的
- 编辑:表单自动填了旧数据
这一刻我才意识到一件事:
新增和编辑,本质上是“同一个表单的两种状态”。
所以这一章,我们的核心策略只有一句话:
一套模板 + 两个视图类 = 新增 & 编辑
2.先别写视图,先把 URL 想清楚
很多新手一上来就写 View,写着写着就迷路了。
我建议你先问自己一个很具体的问题:
用户点击哪里?跳到哪个地址?
我们需要两个地址:
- 新增班级
- 编辑某一个具体班级
所以在 grades/urls.py 中,我是这样规划的:
# 代码位置:grades/urls.py
from django.urls import path,include
from .views import GradeListView,GradeCreateView,GradeUpdateView # 引入2个视图类
urlpatterns = [
path('',GradeListView.as_view(),name='grades_list'),
# 新增下面2个path
path('create/', GradeCreateView.as_view(), name='grade_create'),
path('<int:pk>/update/', GradeUpdateView.as_view(), name='grade_update'),
]
这里有两个关键点你一定要注意:
- 编辑一定要带 pk(主键) 因为你得告诉 Django:你到底在改哪一条数据
- URL 名字一定要有语义
grade_create、grade_update,别给未来的自己挖坑
👉 到这一步,你先不用急着跑代码, 确认一件事就够了:路径设计合理。
3.视图类怎么写?
说实话,我第一次用 CBV(类视图) 的时候也很慌。因为我什么都没写,只是简单配置一下,它就运行成功了。而有时它报错,我又不知道错在哪里 😅
在grades/urls.py文件中,我引入了GradeCreateView和GradeUpdateView 两个视图类,接下来,先把两个类「搭出来」,代码如下:
# 代码位置:grades/views.py
from django.views.generic import ListView, CreateView, UpdateView
from .forms import GradeForm
class GradeCreateView(CreateView):
"""
创建年级视图
使用 Django 的通用 CreateView,实现新增年级记录的功能
"""
# 指定要操作的模型
model = Grade
# 指定渲染的模板文件
template_name = 'grades/grade_form.html'
# 指定使用的表单类(包含字段验证、错误提示等)
form_class = GradeForm
class GradeUpdateView(UpdateView):
"""
修改年级视图
使用 Django 的通用 UpdateView,实现编辑已有年级记录的功能
"""
# 指定要操作的模型
model = Grade
# 指定渲染的模板文件(与创建页面共用同一模板,很常见)
template_name = 'grades/grade_form.html'
# 指定使用的表单类(通常与创建视图使用同一个表单)
form_class = GradeForm
使用Django框架最爽的地方在于:不用写,只是简单配置即可。
4.为什么 Django 强迫你用 Form?
在GradeCreateView和GradeUpdateView 两个视图类中,我设置了form_class属性,它主要用于创建表单内容,并对表单进行验证。例如,无论新增还是编辑,输入“班级名”时,至少不能为空。以及还有一些个性化的验证,那么我们就可以通过表单类一次设置。
Form表单类 中只做一件事
在grades/目录下,新建一个forms.py文件。目录结构如下图。

在forms.py文件中,创建表单类GradeForm(这个名字就是视图中form_class 属性的值)。代码如下:
# 代码目录:grades/forms.py
from django import forms
from .models import Grade
class GradeForm(forms.ModelForm):
class Meta:
model = Grade
fields = ['grade_name', 'grade_number']
这一刻你要意识到一件非常爽的事情:
- 表单字段 = Model 字段
- 校验规则 = Model 校验
- 错误信息 = Django 自动生成
你不是偷懒,是在用框架的设计。
5.一套模板,怎么同时支持新增和编辑?
接下来,该考虑模版了。CreateView和UpdateView两视图类都是这样设置的:
template_name = 'grades/grade_form.html'
那么,一套模板,怎么同时支持新增和编辑?
这是本章的灵魂问题。
核心判断只有一句:
{% if object %}
编辑班级
{% else %}
新增班级
{% endif %}
你不需要自己传变量。
Django 已经帮你做好了:
- CreateView:
object不存在,因为还没有创建新对象 - UpdateView:
object自动带着当前数据对象
这一刻你会惊叹:妙妙妙。
6.遍历表单字段,而不是一个个手写
如果你还在模板里这样写:
<input type="text" name="grade_name">
<input type="text" name="grade_number">
那我真心建议你停一下。
因为你正在做一件很危险的事:
让模板和 Model 强耦合。
也就是这里的name属性值其实就是模型中定义的。不信去对比一下。
那么,是不是可以直接使用Model里的字段呢?
其实Django已经为我们考虑到了。
再去看一下我们的GradeForm表单类是如何定义的。
class GradeForm(forms.ModelForm)
它继承了父类forms.ModelForm模型类。所以我们直接使用它。
代码如下:
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}"> {{ field.label }}:</label>
{{ field }}
<ul class="errors">
{% for error in field.errors %}
<li> {{ error }} </li>
`{% endfor %}`
</ul>
</div>
`{% endfor %}`
【代码解析】
- 遍历 form 对象里的每一个字段(form 通常是 forms.Form 或 forms.ModelForm 的实例) field 每次循环时会是一个 BoundField 对象,包含了字段的所有信息。
- field.id_for_label:自动生成的 input 的 id 值(例如 id="id_title")
- field.label:字段的显示名称(可以是模型的 verbose_name,也可以是 Form 中定义的 label)
- field.errors 是一个错误列表
{{ field }}直接输出字段的 HTML 控件(<input>、<select>、<textarea>等)
根据grades/urls.py中路由的配置,访问“http://127.0.0.1:8000/grades/create/”,新增班级页面效果如下:

访问“http://127.0.0.1:8000/grades/1/”,显示班级id为1的信息,编辑班级效果如下:

好处只有一句话:
字段怎么变,模板不用动。
7.设置新增和编辑按钮跳转
在grades_list.htlm列表页面,找到新增按钮, 设置href属性,代码如下:
<a href="{% url 'grade_create' %}">
<button type="button" class="add">新增</button>
</a>
在该页面,找到编辑按钮, 设置href属性,代码如下:
<a href="{% url 'grade_update' grade.id %}">
<button class="add">编辑</button>
</a>
注意上面2段代码的区别。编辑时,需要明确知道要被编辑的班级id, 所以后面添加一个参数 grade.id。这样当点击对应的班级时,进入该班级的编辑页面。效果如下:


8.CSRF 报错不是 bug,是保护你
我们测试一下新增功能,点击新增按钮,填写信息如下所示。

第一次点「保存」直接报 CSRF 错误时,如下图。

我当年也懵过,后来我才明白:
这是 Django 在保护你的网站不被伪造请求攻击。
解决方式只有一句, 在<form>表单中添加csrf_token标签:
<form>
{% csrf_token %}
</form>
它不会影响页面展示,但能让你的表单真正能上线用。
再次尝试新增班级信息,添加成功后,效果如下所示。

grade_form.html完整代码如下:
{% extends 'base.html' %}
`{% load static %}`
{% block content %}
<link rel="stylesheet" href="{% static 'css/form.css' %}">
<div class="bottom">
<div class="container">
<h1 style="text-align:center">
{% if object %}
编辑班级
{% else %}
新增班级
{% endif %}
</h1>
<form method="post">
{% csrf_token %} <!-- 防止csrf攻击 -->
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}"> {{ field.label }}:</label>
{{ field }}
<ul class="errors">
{% for error in field.errors %}
<li> {{ error }} </li>
`{% endfor %}`
</ul>
</div>
`{% endfor %}`
<div class="handleButton">
<button type="submit" id="saveButton">保存</button>
</div>
</form>
</div>
</div>
{% endblock %}
9.新增成功后跳哪?这是必须想清楚的
如果你不写 success_url,
Django 会很诚实地告诉你:
“我不知道你想去哪。”
所以我一般统一跳回列表页:
from django.urls import reverse_lazy
class GradeCreateView(RoleRequiredMixin, CreateView):
model = Grade
template_name = 'grades/grade_form.html'
form_class = GradeForm
success_url = reverse_lazy('grades_list') # 跳转路径
class GradeUpdateView(RoleRequiredMixin, UpdateView):
model = Grade
template_name = 'grades/grade_form.html'
form_class = GradeForm
success_url = reverse_lazy('grades_list') # 跳转路径
reverse_lazy 的好处只有一个:
在类加载阶段不急着解析 URL,避免时序问题。
你现在不用完全懂, 记住:CBV 里优先用它。
10.编辑功能:几乎不用再写一行代码
这是最爽的一刻。
你会发现:
- 模板不用改
- 表单不用改
- 校验逻辑不用改
只需要:
- URL 传 pk
- 用 UpdateView
当你点击「编辑」, 数据自动填好那一刻——我敢保证,你会有一次真正的 Django 成就感。



11.表单验证:非法提交如何处理
如果用户在填写表单时,不符合预期怎么办?例如,
- 添加班级时,填写的
班级名称和班级编号都已经存在,怎么办? - 编辑班级时,填写的
班级名称和班级编号都已经存在,怎么办?
不用急,可以先测试一下效果。
新增班级时,如果已经存在,提示如下信息:

编辑班级时,如果已经存在,提示如下信息:

什么?我们没有新增1行代码就实现了这个功能。
下面我们来分析一下, Django是如何实现的?
先来看下forms.py中表单类GradeForm, 它的的model模型是Grade, 并且flels引入了2个字段。代码如下:
# 代码位置:forms.py
class GradeForm(forms.ModelForm):
class Meta:
model = Grade
fields = ['grade_name', 'grade_number']
接下来查看Grade模型。代码如下:
class Grade(models.Model):
# 班级名称:字符字段,最大长度50个字符,必须唯一,不能重复
# verbose_name 用于在 Django Admin 后台显示的字段标签
grade_name = models.CharField(
max_length=50,
unique=True,
verbose_name='班级名称'
)
# 班级编号:字符字段,最大长度10个字符,必须唯一,不能重复
# 通常用于存放如 “2023级1班” 的编号或简码
grade_number = models.CharField(
max_length=10,
unique=True,
verbose_name='班级编号'
)
上面的2个字段都设置了max_length和unique字段属性,所以form表单就会依此进行验证。
例如,我们设置班级编号超过10个字符,效果如下:

本章小结:你现在已经超过 80% 新手了
到这里,你已经掌握了:
- 新增 & 编辑为什么要合并
- CreateView / UpdateView 的真实用法
- ModelForm 在项目里的正确位置
- 表单校验和错误展示的标准写法
更重要的是,你应该已经开始感受到 Django 的一句潜台词:
“你只需要配置,剩下的我来。”
下一步,你现在就可以做的事
别跳过,真的。
请你现在立刻做三件事:
- 自己完整敲一遍新增 & 编辑
- 故意制造一个重复班级名,看错误提示是否正确
- 删掉一个字段,看看模板是否还能正常工作
下一章,我们会做一件非常「像真项目」的事:
把“新增 / 编辑 / 删除/ 查询”串成一个完整的操作闭环。
你已经在真正写 Web 应用 了,不是在练 Demo。
【大熊课堂精品课程】
Python零基础入门动画课: https://www.bilibili.com/cheese/play/ss7988
Django+Vue:全栈开发: https://www.bilibili.com/cheese/play/ss8134
PyQT6开发桌面软件: https://www.bilibili.com/cheese/play/ss12314
Python办公自动化: https://www.bilibili.com/cheese/play/ss14990
Cursor AI编程+MCP:零基础实战项目课: https://www.bilibili.com/cheese/play/ss105194189
Pandas数据分析实战: https://www.bilibili.com/cheese/play/ss734522035
AI大模型+Python小白应用实战: https://www.bilibili.com/cheese/play/ss3844