You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django注册表单:如何同时显示字段错误与非字段错误?

问题描述

我有一个包含email、password和confirm password字段的UserRegistrationForm。提交表单时,当Email和密码字段均存在错误(如无效Email、密码不匹配),希望同时显示Form.non_field_errors()和Form.errors。但目前的问题是:当所有字段都不正确时,仅先显示字段错误(如“Enter a valid email address”),修复邮箱后再次提交才会显示非字段错误(如“passwords do not match”),我希望两类错误能同时展示。

相关截图:

  • 输入无效邮箱和不匹配密码后的截图:
    输入无效邮箱和不匹配密码后的截图
  • 邮箱修正后密码仍不匹配的截图:
    邮箱修正后密码仍不匹配的截图

相关代码

forms.py

class UserRegistrationForm(forms.ModelForm):
    password2 = forms.CharField(
        label="New password confirmation",
        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
    )

    class Meta:
        model = User
        fields = [
            "email",
            "password",
        ]

    def _validate_passwords(self, password1, password2):
        password_errors = []

        try:
            validate_password(password1, self.instance)
        except ValidationError as e:
            password_errors.append(
                ValidationError(_("Weak password provided"), code="weak-password"),
            )

        if password1 and password2 and password1 != password2:
            password_errors.append(
                ValidationError(_("Passwords mismatch"), code="password-mismatch"),
            )

        if len(password_errors) > 0:
            raise ValidationError(password_errors)

    def clean(self):
        cleaned_data = super().clean()

        email = cleaned_data.get("email")
        password1 = cleaned_data.get("password")
        password2 = cleaned_data.get("password2")

        if email and password1 and password2:
            # email is already validated at Field level
            self._validate_passwords(password1, password2)

        return super().clean()

registration.html

<div>{{ form.non_field_errors }}</div>
<form method="post">
    {% csrf_token %}
    {% for field in form %}
        <div class="fieldWrapper">
            {{ field.errors }}
            {{ field.label_tag }} 
            {{ field }}
        </div>
    {% endfor %}
    <input type="submit" />
</form>

models.py

class CustomEmailField(models.EmailField):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def run_validators(self, value):
        pass

    def validate(self, value, model_instance):

        if value is not None and not value.endswith("@example.com"):
            raise ValidationError("Email must end with @example.com")


class CustomUserManager(auth_models.BaseUserManager):
    def create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError("The Email field must be set")

        try:
            email = self.normalize_email(email)
            validate_email(email)

        except ValidationError as e:
            print("errors: ", e)

        else:
            # execute if no exception
            user = self.model(email=email, **extra_fields)
            user.set_password(password)
            user.save()
            return user

    def create_superuser(self, email, password, **extra_fields):
        # Almost indentical implementation to Django's UserManager class
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)

        if extra_fields.get("is_staff") is not True:
            raise ValueError("Superuser must have is_staff=True.")
        if extra_fields.get("is_superuser") is not True:
            raise ValueError("Superuser must have is_superuser=True.")

        return self.create_user(email, password, **extra_fields)


class User(auth_models.AbstractUser):
  
    username = None
    email = CustomEmailField(unique=True)
    email_is_verified = models.BooleanField(default=False)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["password"]  # required fields when creating a user obj


    objects = CustomUserManager()

    def __str__(self):
        return self.email

views.py

class RegistrationView(TemplateView):
    template_name = "user/registration.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["form"] = UserRegistrationForm
        return context

    def post(self, request, *args, **kwargs):
        form = UserRegistrationForm(request.POST)
        context = {"form": form}

        if form.is_valid():
            user = form.save()

            context["success_message"] = "We have sent you an email verification."
            return HttpResponseRedirect(reverse("user:email-verification"))

        # form should contain some errors at this point
        return render(request, self.template_name, context)

解决方案

问题根源在clean方法的判断逻辑:原代码仅当邮箱字段验证通过(即cleaned_data中存在email值)时,才会执行密码验证。如果邮箱验证失败,cleaned_data.get("email")返回None,导致密码验证逻辑跳过,非字段错误无法触发。

修改forms.py中的clean方法:

def clean(self):
    cleaned_data = super().clean()

    password1 = cleaned_data.get("password")
    password2 = cleaned_data.get("password2")

    # 只要密码字段有值就执行验证,不再依赖邮箱字段的验证结果
    if password1 and password2:
        self._validate_passwords(password1, password2)

    return cleaned_data

同时注意:原代码最后返回super().clean()会覆盖当前的cleaned_data,改为直接返回处理后的cleaned_data。

修改后,即使邮箱字段存在错误,只要密码字段填写了内容,密码匹配和强度验证就会执行,对应的非字段错误会和邮箱的字段错误同时显示在页面上。


内容的提问来源于stack exchange,提问作者Jun Wen Soh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 21:13:16