Propagating Django model clean() methods ValidationErrors into the form for user signup

I am using django-allauth's SignupForm to create new custom user objects. To ensure that any data put into the database is valid I have most of my validation within my model's clean() method. This is called by calling self.full_clean() within the save() method of my custom user model. By putting validation here (rather than in any signup form I use) I can ensure I hopefully have better data integrity as objects cannot be saved (even when using the API) without first being cleaned & valid.

The issue I am having occurs when I am raising ValidationErrors within my models's clean() method. Instead of correctly propagating the ValidationErrors from the model to the signup form (which would then nicely display them in my template as an error list) the errors are raised as normal causing a server crash/HTTP 500 error (see below)traceback of ValidationError

Here is my custom user model (I have overridden many of the default fields provided by AbstractUser in order to customise certain aspects of their functionality):

class User(AbstractUser):
    username = models.CharField(
        "Username",
        max_length=30,
        unique=True,
        validators=[
            RegexValidator(
                r"^[\w.-]+\Z",
                "Enter a valid username. This value may contain only letters, digits and ./_ characters."
            ),
            ReservedNameValidator,
            validate_confusables
        ],
        error_messages={
            "unique": "A user with that username already exists."
        }
    )
    email = models.EmailField(
        "Email Address",
        unique=True,
        validators=[
            HTML5EmailValidator,
            validate_free_email,
            validate_confusables_email,
            validate_tld_email,
            validate_example_email
        ],
        error_messages={
            "unique": f"That Email Address is already in use by another user."
        }
    )
    bio = models.TextField(
        "Bio",
        max_length=200,
        blank=True
    )
    verified = models.BooleanField("Is verified?", default=False)  # TODO: Add verification process
    following = models.ManyToManyField(
        "self",
        symmetrical=False,
        related_name="followers",
        blank=True
    )
    is_staff = models.BooleanField(
        "Is a staff member?",
        default=False,
        help_text="Designates whether the user can log into this admin site."
    )
    is_superuser = models.BooleanField(
        "Is a superuser?",
        default=False,
        help_text="Designates that this user has all permissions without explicitly assigning them."
    )
    is_active = models.BooleanField(
        "Is visible?",
        default=True,
        help_text="Designates whether this user is visible. Unselect this instead of deleting accounts.",
    )
    date_joined = models.DateTimeField(
        "Date Joined",
        default=timezone.now,
        editable=False
    )
    last_login = models.DateTimeField(
        "Last Login",
        blank=True,
        null=True,
        editable=False
    )

    class Meta:
        verbose_name = "User"

    def clean(self):
        if self.is_superuser:
            self.is_staff = self.is_superuser

        if (get_user_model().objects.filter(username__icontains="pulsifi").count() > settings.PULSIFI_ADMIN_COUNT or not self.is_staff) and "pulsifi" in self.username.lower():
            raise ValidationError({"username": "That username is not allowed."}, code="invalid")

        if get_user_model().objects.filter(id=self.id).exists():
            username_check_list: list[str] = get_user_model().objects.exclude(id=self.id).values_list("username", flat=True)
        else:
            username_check_list: list[str] = get_user_model().objects.values_list("username", flat=True)
        for username in username_check_list:
            if get_string_similarity(self.username, username) >= settings.USERNAME_SIMILARITY_PERCENTAGE:
                raise ValidationError({"username": "That username is too similar to a username belonging to an existing user."}, code="unique")

        if self.email.count("@") == 1:
            local: str
            whole_domain: str
            local, whole_domain = self.email.split("@", maxsplit=1)

            extracted_domain = tldextract.extract(whole_domain)

            local = local.replace(".", "")

            if "+" in local:
                local = local.split("+", maxsplit=1)[0]

            if extracted_domain.domain == "googlemail":
                extracted_domain = ExtractResult(subdomain=extracted_domain.subdomain, domain="gmail", suffix=extracted_domain.suffix)

            elif (get_user_model().objects.filter(username__icontains="pulsifi").count() > settings.PULSIFI_ADMIN_COUNT or not self.is_staff) and extracted_domain.domain == "pulsifi":
                raise ValidationError({"email": f"That Email Address cannot be used."}, code="invalid")

            self.email = "@".join([local, extracted_domain.fqdn])

        if EmailAddress.objects.filter(email=self.email).exclude(user=self).exists():
            raise ValidationError({"email": f"The Email Address: {self.email} is already in use by another user."}, code="unique")

        if self.verified:
            NO_EMAIL_ERROR = ValidationError({"verified": "User cannot become verified without at least one verified email address."})
            if get_user_model().objects.filter(id=self.id).exists():
                if not self.emailaddress_set.filter(verified=True).exists():
                    raise NO_EMAIL_ERROR
            else:
                raise NO_EMAIL_ERROR

        super().clean()

    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

Here is my signup view:

from allauth.account.views import SignupView

class Signup_View(SignupView):
    template_name = "pulsifi/signup.html"

How can I correctly propagate any ValidationErrors raised in the user model's clean() method to the form so that they show in my rendered error list (in a similar way to the rendering in the admin forms).

Back to Top