Background
I have a custom User model that I have extended from BaseUserManager
and AbstractBaseUser
. Similarly, I have a custom sign-up form that I’ve extended from UserCreationForm
(all these custom items are extended from django.contrib.auth
).
The custom UserCreationForm I’ve made has a method called clean_username()
that checks for usernames that do not have any alpha-numerical characters (-@
, --_
, +@-_
, etc.) and throws a ValidationError if that is the case (aside: the method also checks for usernames that, when slugified, would result in non-unique slugs).
Problem
When I test the aforementioned ValidationError, it still is somehow returning the form as valid,
- which causes the rest of my view’s logic to run,
- which causes the User Model’s
clean()
method to run,- and in the User Model’s
clean()
method, I have the same checks I have in the form’sclean_username()
method (for User’s created without using this particular form). These checks throw aValueError
, which is not ideal because I need the form to be re-rendered with the properValidationError
message so that users can fix what’s wrong and still sign up.
- and in the User Model’s
- which causes the User Model’s
None of this makes sense. I’ve put print messages in my clean_username()
method so that I know for sure the branch of conditional logic with the ValidationError
I want it to throw is running, but it’s either not throwing the error or the form is being incorrectly evaluated as valid
Potential Problem
The clean_username()
method on the form is something that was already in the code from django.contrib.auth.forms
, so maybe I’m not extending it correctly?
Error Message
[18/May/2024 02:05:46] "POST /signup/ HTTP/1.1" 200 10330
-----------method called .clean_username() is running-----------
-----------user slug is gonna be empty-----------
Internal Server Error: /signup/
Traceback (most recent call last):
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/djangoTerp/home/views.py", line 85, in signup
if form.is_valid():
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/forms/forms.py", line 201, in is_valid
return self.is_bound and not self.errors
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/forms/forms.py", line 196, in errors
self.full_clean()
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/forms/forms.py", line 435, in full_clean
self._post_clean()
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/contrib/auth/forms.py", line 129, in _post_clean
super()._post_clean()
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/forms/models.py", line 486, in _post_clean
self.instance.full_clean(exclude=exclude, validate_unique=False)
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/venv/lib/python3.8/site-packages/django/db/models/base.py", line 1477, in full_clean
self.clean()
File "/Users/TRON/Documents/SCIENCE/CMPTRS/HTML/webProjectsForPublishing/terp/terpBackend/djangoTerp/home/models.py", line 177, in clean
raise ValueError("Username must have at least one alpha-numerical character")
ValueError: Username must have at least one alpha-numerical character
Code
models.py
from django.db import models
from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import ASCIIUsernameValidator
class TerpUserManager(BaseUserManager):
def create_user(self, username, email, category, password=None):
"""
Creates and saves a User with the given email, category, username and password.
"""
if not email:
raise ValueError("Users must have an email address")
user = self.model(
# added .casefold() to make it so users can really only sign up with case-insensitive usernames. DB will still allow case-sensitive usernames, but the create_user and clean method ain't gonna allow it.
username=str(username).casefold(),
# added .casefold() to make it so users can really only sign up with case-insensitive local-portion email addresses. i know it ain't RCF 5322 spec, but I don't want duplicate users. DB will still allow case-sensitive local-portion email addresses, but the create_user and clean method ain't gonna allow it.
email=str(self.normalize_email(email)).casefold(),
category=category,
)
user.set_password(password)
user.clean()
user.save(using=self._db)
return user
class TerpUser(AbstractBaseUser, PermissionsMixin):
username_validator = ASCIIUsernameValidator()
username = models.CharField(
_("username"),
max_length=150,
unique=True,
db_index=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
slug = models.SlugField(_("URL slug"), null=False, unique=True)
first_name = models.CharField(_("first name"), max_length=70, blank=True)
last_name = models.CharField(_("last name"), max_length=70, blank=True)
email = models.EmailField(
_("email address"),
unique=True,
db_index=True,
help_text=_(
"Required."
),
error_messages={
"unique": _("A user with that email address already exists."),
},
)
# bunch of other fields, such as is_staff, is_active, date_joined, last_login, has_edu_email
objects = TerpUserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email", "category"]
def get_absolute_url(self):
return reverse("member-detail", kwargs={"slug": self.slug})
def clean(self):
super().clean()
# added .casefold() to make it so users can really only sign up with case-insensitive local-portion email addresses. i know it ain't RCF 5322 spec, but I don't want duplicate users. DB will still allow case-sensitive local-portion email addresses, but the create_user and clean method ain't gonna allow it.
self.email = str(self.__class__.objects.normalize_email(self.email)).casefold()
self.username = str(self.username).casefold()
# adding validation for 1) making sure username doesn't result in empty-string slug
potential_user_slug = slugify(self.username)
if potential_user_slug == "":
raise ValueError("Username must have at least one alpha-numerical character")
# adding validation for 2) making sure username doesn't result in identical slug to another user
if TerpUser.objects.filter(slug=potential_user_slug).exists():
raise ValueError("Username is not unique enough")
# set user's slug
self.slug = slugify(self.username)
forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
# error handling
from django.core.exceptions import ValidationError
class SignUpForm(UserCreationForm):
# fields such as email, first_name, last_name, that I defined manually
class Meta:
model = User
fields = [
'email', 'username', 'first_name', 'last_name', 'password1', 'password2',
]
help_texts = {
'username': 'Letters, digits, and @ . + - _ only',
}
def clean_username(self):
"""Reject usernames that differ only in case or whose resulting slugs differ only in case
This is modified from django.contrib.auth.forms-----------------------------."""
print("-----------method called .clean_username() is running-----------")
username = self.cleaned_data.get("username")
potential_user_slug = slugify(username)
# adding validation for 1) no empty slugs allowed
if potential_user_slug == "":
print("-----------user slug is gonna be empty-----------")
raise ValidationError("Username must have at least one alpha-numerical character")
# adding validation for 2) making sure username doesn't result in identical slug to another user
elif (
potential_user_slug
and self._meta.model.objects.filter(slug__iexact=potential_user_slug).exists()
):
self._update_errors(
ValidationError(
{
"username": self.instance.unique_error_message(
self._meta.model, ["username"]
)
}
)
)
elif (
username
and self._meta.model.objects.filter(username__iexact=username).exists()
):
self._update_errors(
ValidationError(
{
"username": self.instance.unique_error_message(
self._meta.model, ["username"]
)
}
)
)
else:
return username
Notice the print() statements in the clean_username()
method above. These print statements are executing in the Error readout above so I know that the branch of code with the ValidationError should be executing
views.py
from django.conf import settings
import requests
from home.forms import SignUpForm
def signup(request):
"""View for registering a new user in the system"""
recaptcha_site_key=settings.GOOGLE_RECAPTCHA_SITE_KEY
if request.user.is_authenticated:
return HttpResponseRedirect(('nope'))
else:
# If this is a POST request, then process the form data
if request.method == 'POST':
# Create a form instance and populate it with data from the request (binding):
form = SignUpForm(request.POST)
# Check if the form is valid:
if form.is_valid():
"""logic for reCAPTCHA validation"""
if result['success']:
# do stuff cuz gonna email confirm first
user = form.save(commit=False)
# check if user's email is academic or not
if bool(re.match(r".*.edu$", str(user.email))):
user.has_edu_email = True
else:
user.has_edu_email = False
user.save()
"""
----- Buncha stuff needed to send email through SendGrid ------------
"""
"""
Done sending email
"""
return HttpResponseRedirect(('account-activation-email-sent'))
else:
messages.error(request, 'Invalid reCAPTCHA. Please try again.')
else:
form = SignUpForm()
return render(request, 'registration/signup.html', {
'form': form, 'recaptcha_site_key': recaptcha_site_key,
})
Notice the if form.is_valid()
statement above. We can see in my error traceback that this statement is evaluating to true. Why is it evaluating to true when my custom UserCreationForm’s clean_username()
method should be throwing a ValidationError
?
Dug Myself into a Hole?
I realize that I likely have dug myself into a hole by creating URL slugs for each user from their username. This was probably a bad decision. It seemed terribly clever at the time.