Registration for this Django 4.0.3 project works well except for failure to present error messages and previously completed form field contents. In other words, registration is working with both user_form and profile_form completed and the database is updated as expected.
What is not working
- If the user mismatches passwords no error is returned and page returns a blank form (I was under the impression by passing the forms back as a context dictionary that field data would be preserved - not sure what I'm missing in this regard)
- The error check in
forms.pyto ensure a unique email is not working either (I do use forms error-checking extensively throughout the project). However, given the failure to present the password mismatch error I am inclined to believe both these symptoms share the same cause. - When registration is not valid (password mismatch or non-original email address) blank forms are returned. The fact that forms validation is not working properly suggests that there is something inherently wrong in the register view. However, after looking at this for a couple of days I'm hoping that the bright minds of Stack Overflow will be able to point me in the right direction...
I am including all the relevant code snippets in anticipation of their being requested to support diagnosis. Given that the only element of user registration that is not working is the failure to return error messages and preserve form field contents what I've included may well be over-kill. And, I won't be surprised if I've missed something. If so, please comment with any such requests and I'll update the question.
Code Snippets
settings.py
This snippet is included to show the default password validators.
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
models.py
I have added a one-to-one relationship between the built-in User model and my own Profile model. I am aware that the User model can be extended, but opted for a Profile model (for which the Django Documentation acknowledges as acceptable). I have omitted the COUNTRY_CHOICES constant as, by my estimation, these 200+ lines of code will not support diagnosis.
from django.db import models
from django.contrib.auth.models import User
from phone_field import PhoneField
# ...
# ####################### CHOICE CONSTANTS #######################
PRONOUN_CHOICES = [
('---', '---'),
('She/her', 'She/her'),
('He/him', 'He/him'),
('They/them', 'They/them'),
('Rather not say', 'Rather not say'),
]
NOTIFICATION_PREFERENCE = [
('None', 'None'),
('Email', 'EMail'),
('Text/SMS', 'Text/SMS'),
]
# Omitted COUNTRY_CHOICES to save space
# ...
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
pronoun = models.CharField(max_length=20, choices=PRONOUN_CHOICES, default='---')
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
full_name = models.CharField(max_length=200, blank=True, null=True)
address_1 = models.CharField(max_length=100)
address_2 = models.CharField(max_length=100, blank=True, null=True)
city = models.CharField(max_length=50)
state_province_county = models.CharField(max_length=100, verbose_name='State/Province/County')
postal_zip_code = models.CharField(max_length=10, verbose_name='postal/zip code')
country = models.CharField(max_length=100, choices=COUNTRY_CHOICES, default='---')
phone = PhoneField(blank=True, help_text='Contact phone number')
notification_preference = models.CharField(
max_length=10,
choices=NOTIFICATION_PREFERENCE,
default='Email',
help_text='''If you would like to receive booking confirmation notifications from us please select your preference.'''
)
def __str__(self):
return f'{self.first_name} {self.last_name}'
forms.py
I'm using forms.py to validate for a unique email. I added this because the original approach (inspired by Corey Schafer) didn't appear to prevent duplicate email addresses.
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from .models import Profile
# ############ Profile form validation ############
def profile_form_validation(form, form_type):
cleaned_data = super(form_type, form).clean()
if not cleaned_data.get('phone') and cleaned_data.get('notification_preference') == 'Text/SMS':
form.add_error(
'phone',
'A phone number must be entered in order to be able to receive text messages on your phone.'
)
return
# ############ Forms ############
class UserRegisterForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = [
'username',
'email',
'password1',
'password2',
]
def clean(self):
cleaned_data = super(UserRegisterForm, self).clean()
if User.objects.filter(email=cleaned_data.get('email')).exists():
self.add_error(
'email',
'That email address is already associated with an account. Please use another email address.',
)
return self.cleaned_data
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = [
'pronoun',
'first_name',
'last_name',
'address_1',
'address_2',
'city',
'state_province_county',
'postal_zip_code',
'country',
'phone',
'notification_preference',
]
def clean(self):
profile_form_validation(self, ProfileUpdateForm)
return self.cleaned_data
views.py
My instinct is that the cause of the problem is with the view to process registration. Please note that:
- I am using login() to automatically log the user in once registration is complete.
- The auto-login works if the both passwords match and the email address entered is unique.
- The redirect to 'events' works.
- The only functionality not working is the failure to return error messages or preserve previously completed fields (excepting password1 and password2 obviously).
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth import login
from .models import Profile
from .forms import (
UserRegisterForm,
ProfileUpdateForm,
)
def register(request):
if request.method == 'POST':
user_form = UserRegisterForm(request.POST)
profile_form = ProfileUpdateForm(request.POST)
if user_form.is_valid() and profile_form.is_valid():
# Process user form
user = user_form.save(commit=False)
user.save()
# Process profile form
profile_form.instance.user = user
profile_form.instance.full_name = f'{profile_form.instance.first_name} {profile_form.instance.last_name}'
profile_form.save()
# Log in user after registration
login(request, user)
messages.success(request, f'Your account (username:{username}) has been created. '
f'You are now logged in!')
return redirect('events')
else:
user_form = UserRegisterForm()
profile_form = ProfileUpdateForm()
return render(request, 'users/register.html', {'user_form': user_form, 'profile_form': profile_form})
else:
user_form = UserRegisterForm()
profile_form = ProfileUpdateForm()
return render(request, 'users/register.html', {'user_form': user_form, 'profile_form': profile_form})
urls.py
Including urls.py is probably over-kill.
from django.contrib.auth import views as auth_views
from django.urls import path
# ...
urlpatterns = [
# ...
path('register/', user_views.register, name='register'),
# ...
]
register.html
Template inclusion is also likely over-kill. However, this demonstrates the explicit use of form.errors. Again, there is no problem with template rendering (so I'm omitting base.html and the css file).
{% extends 'base.html' %}
{% load crispy_forms_filters %}
{% load crispy_forms_tags %}
{% block title %}Register{% endblock %}
{% block content %}
<section class="content-section">
<h3 class="mb-2"><strong>Registration</strong></h3>
</section>
{% if form.errors %}
<section class="content-section-danger alert-section">
<div>
<p class="alert-danger">Please review the following:</p>
<ul>
{% for key, value in form.errors.items %}
{% for error in value %}
{% if error != '' %}
<li class="alert-danger">{{ error }}</li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</div>
</section>
{% endif %}
<section class="content-section">
<div class="row">
<div class="col-lg-12 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-BlueGray">
<strong>Your Account Details</strong>
</li>
</ul>
</div>
</div>
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<div class="row">
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ user_form.username|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ user_form.email|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ user_form.password1|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ user_form.password2|as_crispy_field }}
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-12 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-BlueGray">
<strong>Your Profile</strong>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.pronoun|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.first_name|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.last_name|as_crispy_field }}
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-9 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.address_1|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.address_2|as_crispy_field }}
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.city|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.state_province_county|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.postal_zip_code|as_crispy_field }}
</li>
</ul>
</div>
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.country|as_crispy_field }}
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-3 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.phone|as_crispy_field }}
</li>
</ul>
</div>
{% if SMS_MESSAGING_ACTIVE == 'Yes' %}
<div class="col-lg-9 pt-2 pb-2">
<ul class="list-group">
<li class="list-group-item list-group-item-white">
{{ profile_form.notification_preference|as_crispy_field }}
</li>
</ul>
</div>
{% else %}
{{ profile_form.notification_preference.as_hidden }}
{% endif %}
</div>
<div class="row">
<div class="col-lg-3">
</div>
</div>
</fieldset>
<div class="row pt-4 pb-2">
<div class="col-lg-3">
<button class="btn btn-success col-12" type="submit">Register</button>
</div>
</div>
</form>
<div class="pt-2">
<small class="text-muted">
Already have an account? <a href="{% url 'login' %}" class="ml-2 text-BlueGray">Log In</a>
</small>
</div>
</section>
{% endblock content %}