Django Bootstrap 表单如何显示 Confirm Password 校验错误?
可以在 ModelForm 中声明 password2,在 clean() 中比较两次密码,再用 self.add_error("password2", ...) 将错误绑定到确认密码字段。模板渲染 {{ form.password2 }} 及其错误信息即可,无须在 model 中增加字段。
用于用户注册时,建议使用 Django 的 UserCreationForm。它已经处理了密码确认、密码规则校验和密码哈希保存,能减少重复代码。
为什么现有代码没有产生确认密码错误
Meta.widgets 只配置表单字段使用的控件,不会创建字段。因此,在其中添加 'password2' 并不会让 form.password2 自动存在。
需要先在表单类中声明 password2,并在字段定义中设置 widget 和 error_messages。Meta 中的这些配置只作用于自动生成的字段。参见 Django ModelForm 字段配置说明。
原模板还有一处判断:
{% if form.errors %} is-invalid {% endif %}
这会让确认密码框在表单任何位置出现错误时都标红。判断字段状态时,应使用该字段自己的 errors。
在现有 ModelForm 中添加确认密码校验
# forms.py
from django import forms
from .models import UserMaster
class UserMasterForm(forms.ModelForm):
password2 = forms.CharField(
label="Confirm Password",
strip=False,
widget=forms.PasswordInput(
attrs={
"placeholder": "Confirm Password",
"autocomplete": "new-password",
}
),
error_messages={
"required": "请再次输入密码。",
},
)
class Meta:
model = UserMaster
fields = ["username", "password"]
widgets = {
"username": forms.TextInput(
attrs={"placeholder": "Enter User Name"}
),
"password": forms.PasswordInput(
attrs={
"placeholder": "Enter Password",
"autocomplete": "new-password",
}
),
}
error_messages = {
"username": {
"required": "请输入用户名。",
"unique": "该用户名已被使用。",
},
"password": {
"required": "请输入密码。",
},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 密码中的首尾空白应参与比较。
self.fields["password"].strip = False
for field in self.fields.values():
field.widget.attrs["class"] = "form-control"
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
password2 = cleaned_data.get("password2")
if (
password is not None
and password2 is not None
and password != password2
):
self.add_error(
"password2",
forms.ValidationError(
"两次输入的密码不一致。",
code="password_mismatch",
),
)
return cleaned_data
password2 是显式声明的表单字段,即使没有列入这里的 Meta.fields,也会参与表单校验。它没有对应的模型字段,因此不会自动保存到数据库。
校验时有几处需要注意:
- 字段默认必填,确认密码为空时会自动产生
required错误。 - 字段校验失败后,可能不在
cleaned_data中,因此使用cleaned_data.get()取值。 - 两项都通过字段校验后才比较,避免在“必填”错误之外又出现“不一致”错误。
- 保留
super().clean(),以维持ModelForm原有的校验流程,包括唯一性校验。
在 clean() 中直接抛出 ValidationError,通常会产生表单级错误。要把错误明确放到确认密码字段下,应使用 add_error("password2", ...)。参见 Django 表单校验文档。
这段代码只处理字段校验。原来的 UserMaster 是普通模型,直接调用该表单的 save() 会保存明文密码,不能直接用于真实账户注册。
注册用户时优先使用 UserCreationForm
如果项目使用 Django 默认用户模型,可以改用:
# forms.py
from django.contrib.auth.forms import UserCreationForm
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs["class"] = "form-control"
UserCreationForm 已经包含以下字段:
usernamepassword1password2
两次密码不一致时,错误会绑定到 password2。表单还会执行配置的密码校验器,并在保存时对密码进行哈希处理。参见 Django 用户创建表单说明。
这里的密码字段名是 password1。如果模板单独渲染密码字段,需要相应修改;后面的循环模板可以直接复用。
这个方案使用 Django 认证系统的用户模型,不能直接将其替换为题目中的普通 UserMaster 模型。项目若已有自定义用户模型,需要根据模型结构调整注册表单。
在校验失败后统一添加 Bootstrap 错误样式
密码比较继续使用服务器端校验即可,无须再用 JavaScript 实现一遍。Bootstrap 5 可以通过 .is-invalid 和 .invalid-feedback 展示服务器返回的错误,无须额外添加 .was-validated。参见 Bootstrap 5 校验样式说明。
使用 RegisterForm 的视图可以这样写:
# views.py
from django.shortcuts import redirect, render
from .forms import RegisterForm
def change_class_for_error(form):
for field in form.visible_fields():
if not field.errors:
continue
attrs = field.field.widget.attrs
classes = attrs.get("class", "").split()
if "is-invalid" not in classes:
classes.append("is-invalid")
attrs["class"] = " ".join(classes)
attrs["aria-invalid"] = "true"
describedby = attrs.get("aria-describedby", "").split()
if field.help_text:
describedby.append(f"{field.auto_id}_helptext")
describedby.append(f"{field.auto_id}_error")
attrs["aria-describedby"] = " ".join(
dict.fromkeys(describedby)
)
def register_view(request):
if request.method == "POST":
form = RegisterForm(request.POST)
if form.is_valid():
form.save()
return redirect("login")
change_class_for_error(form)
else:
form = RegisterForm()
return render(
request,
"mainapp/register.html",
{"form": form},
)
change_class_for_error 会保留控件原有的 CSS 类,并将错误说明与对应输入框关联。它也适用于前面的 UserMasterForm,但密码保存逻辑仍需另行处理。
校验失败后,要将同一个绑定了 POST 数据的 form 返回模板;重新创建空表单会丢失错误信息。
用循环统一渲染字段和错误
用户名、密码和确认密码的展示结构相同,可以用循环渲染,新增字段时也不用复制整段 HTML。
使用以下模板前,页面需要加载 Bootstrap 5 CSS,项目也需要配置名为 login 的路由:
{# mainapp/register.html #}
<form method="post" class="mx-2" novalidate>
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% for error in hidden.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endfor %}
<div class="row g-3">
{% for field in form.visible_fields %}
<div class="col-md-6">
<label
for="{{ field.id_for_label }}"
class="form-label"
>
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div
id="{{ field.auto_id }}_error"
class="invalid-feedback"
>
{% for error in field.errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<div
id="{{ field.auto_id }}_helptext"
class="form-text"
>
{{ field.help_text }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<div class="d-flex gap-3 mt-3 mb-2">
<button type="submit" class="btn btn-primary">
Save
</button>
<a href="{% url 'login' %}" class="btn btn-warning">
Cancel
</a>
</div>
</form>
错误消息的标签需要在循环内正确闭合,原模板中的这一问题已在上面修正。取消操作使用独立的 <a>,避免在 <button> 中嵌套 <a>。
还需要注意什么
novalidate 只关闭浏览器提交时的原生表单校验,Django 的服务器端校验仍会执行。保留它,可以让必填等错误统一由服务器返回。
PasswordInput 默认不会在校验失败后回填密码。因此,即使只是用户名重复,用户也可能需要重新输入两次密码。
题目中的 password = models.CharField(max_length=15) 也需要调整。密码哈希字符串通常超过 15 个字符,单纯将明文密码换成哈希值,仍会遇到存储长度问题。使用 Django 认证用户模型及其密码 API,可以避免自行处理这些细节;普通 UserMaster 并不自带 set_password()。参见 Django 密码管理文档。
备注:内容仅供参考。
