I'm upgrading a Django 3.0 app to 5.1 and have been moving slowly through each minor release. So far so good.
However, once I went from Django 5.0 to 5.1, I saw changed behavior with my "Create New User" page which uses a UserCreationForm
form that allows empty passwords. If no password is supplied, a random one is generated.
Now, if I submit the form with an empty password I get "required field" errors on the password fields, even though they are both explicitly set as required=False
.
I saw there were UserCreationForm
changes in Django 5.1.0 and 5.1.1. I tried using AdminUserCreationForm
and setting the usable_password field to None
, but it still won't allow empty passwords like before.
Any ideas?
Environment
Python 3.12.8
Django 5.1.5
Crispy Forms 2.3
Simplified Code
from django.contrib.auth.forms import AdminUserCreationForm
from crispy_forms.helper import FormHelper
class SignupForm(AdminUserCreationForm): # previously using UserCreationForm
usable_password = None # Newly added
# Form fields
sharedaccountflag = forms.ChoiceField(
label = 'Cuenta compartida',
required = True
)
# Constructor
def __init__(self, *args, **kwargs):
# Call base class constructor
super(SignupForm, self).__init__(*args, **kwargs)
# Set password fields as optional
self.fields['password1'].required = False
self.fields['password2'].required = False
# Set form helper properties
self.helper = FormHelper()
self.helper.form_tag = False
# Specify model and which fields to include in form
class Meta:
model = get_user_model()
fields = ('password1', 'password2', 'sharedaccountflag')
Update
I used Serhii's example and modified it so the normal validation is called when appropriate. I also changed back to use UserCreationForm
:
class SignupForm(UserCreationForm):
# Override default validation to allow empty password (change in Django 5.1)
def validate_passwords(
self,
password1_field_name = "password1",
password2_field_name = "password2"
):
# Store password values
password1 = self.cleaned_data.get(password1_field_name)
password2 = self.cleaned_data.get(password2_field_name)
# Do nothing if passwords are not required and no value is provided
if (
not self.fields[password1_field_name].required and
not self.fields[password2_field_name].required and
not password1.strip() and
not password2.strip()
):
pass
# Call default validation if password is required OR a value is provided
else:
super().validate_passwords(password1_field_name, password2_field_name)
Yes. In new versions of Django
, the source code has changed and the behaviour of the BaseUserCreationForm
class has changed accordingly. The password1
and password2
fields are now created using the static method SetPasswordMixin.create_password_fields()
, and they default to required=False
. This can be easily checked here. But even though the fields are optional, the validate_passwords
method is always called, in the clean
method and checks that the fields are not empty. For example, when you call something like this form.is_valid()
, clean
will be called.
If you need behaviour where empty passwords are allowed, given required=False
you can define a custom validate_passwords
method like the one in the code below, this will allow you to create users with empty passwords:
from django.contrib.auth import forms
class CustomUserCreationForm(forms.UserCreationForm):
def validate_passwords(
self,
password1_field_name: str = "password1",
password2_field_name: str = "password2"):
def is_password_field_required_and_not_valid(field_name: str) -> bool:
is_required = self.fields[field_name].required
cleaned_value = self.cleaned_data.get(field_name)
is_field_has_errors = field_name in self.errors
return (
is_required
and not cleaned_value
and not is_field_has_errors
)
if is_password_field_required_and_not_valid(password1_field_name):
error = ValidationError(
self.fields[password1_field_name].error_messages["required"],
code="required",
)
self.add_error(password1_field_name, error)
if is_password_field_required_and_not_valid(password2_field_name):
error = ValidationError(
self.fields[password2_field_name].error_messages["required"],
code="required",
)
self.add_error(password2_field_name, error)
password1 = self.cleaned_data.get(password1_field_name)
password2 = self.cleaned_data.get(password2_field_name)
if password1 != password2:
error = ValidationError(
self.error_messages["password_mismatch"],
code="password_mismatch",
)
self.add_error(password2_field_name, error)
p.s. I'm not sure if that's the way it was intended, maybe there's just a mistake made, in validate_passwords
, where the required
flag is simply not taken into account.