Skip to main content

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

image-20260105103452557

当年我第一次做 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'),
]

这里有两个关键点你一定要注意:

  1. 编辑一定要带 pk(主键) 因为你得告诉 Django:你到底在改哪一条数据
  2. URL 名字一定要有语义 grade_creategrade_update,别给未来的自己挖坑

👉 到这一步,你先不用急着跑代码, 确认一件事就够了:路径设计合理。

3.视图类怎么写?

说实话,我第一次用 CBV(类视图) 的时候也很慌。因为我什么都没写,只是简单配置一下,它就运行成功了。而有时它报错,我又不知道错在哪里 😅

在grades/urls.py文件中,我引入了GradeCreateViewGradeUpdateView 两个视图类,接下来,先把两个类「搭出来」,代码如下:

# 代码位置: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?

GradeCreateViewGradeUpdateView 两个视图类中,我设置了form_class属性,它主要用于创建表单内容,并对表单进行验证。例如,无论新增还是编辑,输入“班级名”时,至少不能为空。以及还有一些个性化的验证,那么我们就可以通过表单类一次设置。

Form表单类 中只做一件事

在grades/目录下,新建一个forms.py文件。目录结构如下图。

image-20260104122946740

在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.一套模板,怎么同时支持新增和编辑?

接下来,该考虑模版了。CreateViewUpdateView两视图类都是这样设置的:

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。这样当点击对应的班级时,进入该班级的编辑页面。效果如下:

image-20260104133422644

image-20260104133445066

8.CSRF 报错不是 bug,是保护你

我们测试一下新增功能,点击新增按钮,填写信息如下所示。

image-20260104133620874

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

image-20260104133947584

我当年也懵过,后来我才明白:

这是 Django 在保护你的网站不被伪造请求攻击。

解决方式只有一句, 在<form>表单中添加csrf_token标签:

<form>
{% csrf_token %}
</form>

它不会影响页面展示,但能让你的表单真正能上线用

再次尝试新增班级信息,添加成功后,效果如下所示。

image-20260104134241874

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 成就感

image-20260104134439188

image-20260104134506645

image-20260104134722476

11.表单验证:非法提交如何处理

如果用户在填写表单时,不符合预期怎么办?例如,

  • 添加班级时,填写的班级名称班级编号都已经存在,怎么办?
  • 编辑班级时,填写的班级名称班级编号都已经存在,怎么办?

不用急,可以先测试一下效果。

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

image-20260104154158921

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

image-20260104154730815

什么?我们没有新增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_lengthunique字段属性,所以form表单就会依此进行验证。

例如,我们设置班级编号超过10个字符,效果如下:

image-20260104155628094

本章小结:你现在已经超过 80% 新手了

到这里,你已经掌握了:

  • 新增 & 编辑为什么要合并
  • CreateView / UpdateView 的真实用法
  • ModelForm 在项目里的正确位置
  • 表单校验和错误展示的标准写法

更重要的是,你应该已经开始感受到 Django 的一句潜台词:

“你只需要配置,剩下的我来。”

下一步,你现在就可以做的事

别跳过,真的。

请你现在立刻做三件事:

  1. 自己完整敲一遍新增 & 编辑
  2. 故意制造一个重复班级名,看错误提示是否正确
  3. 删掉一个字段,看看模板是否还能正常工作

下一章,我们会做一件非常「像真项目」的事:

把“新增 / 编辑 / 删除/ 查询”串成一个完整的操作闭环。

你已经在真正写 Web 应用 了,不是在练 Demo。