User registration and user profiles
Site users can now log in, log out, change their password, and reset their password. However, we need to build a view to allow visitors to create a user account. They should be able to register and create a profile on our site. Once registered, users will be able to log in to our site using their credentials.
User registration
Let’s create a simple view to allow user registration on your website. Initially, you have to create a form to let the user enter a username, their real name, and a password.
Edit the forms.py
file located inside the account
application directory and add the following lines highlighted in bold:
from django import forms
from django.contrib.auth import get_user_model
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField(widget=forms.PasswordInput)
class UserRegistrationForm(forms.ModelForm):
password = forms.CharField(
label='Password',
widget=forms.PasswordInput
)
password2 = forms.CharField(
label='Repeat password',
widget=forms.PasswordInput
)
class Meta:
model = get_user_model()
fields = ['username', 'first_name', 'email']
We have created a model form for the user model. This form includes the fields username
, first_name
, and email
of the user model. We retrieve the user model dynamically by using the get_user_model()
function provided by the auth
application. This retrieves the user model, which could be a custom model instead of the default auth
User
model, since Django allows you to define custom user models. These fields will be validated according to the validations of their corresponding model fields. For example, if the user chooses a username that already exists, they will get a validation error because username
is a field defined with unique=True
.
In order to keep your code generic, use the get_user_model()
method to retrieve the user model and the AUTH_USER_MODEL
setting to refer to it when defining a model’s relationship with in to it, instead of referring to the auth
user model directly. You can read more information about this at https://docs.djangoproject.com/en/5.0/topics/auth/customizing/#django.contrib.auth.get_user_model.
We have also added two additional fields—password
and password2
—for users to set a password and to repeat it. Let’s add the field validation to check that both passwords are the same.
Edit the forms.py
file in the account
application and add the following clean_password2()
method to the UserRegistrationForm
class. The new code is highlighted in bold:
class UserRegistrationForm(forms.ModelForm):
password = forms.CharField(
label='Password',
widget=forms.PasswordInput
)
password2 = forms.CharField(
label='Repeat password',
widget=forms.PasswordInput
)
class Meta:
model = get_user_model()
fields = ['username', 'first_name', 'email']
def clean_password2(self):
cd = self.cleaned_data
if cd['password'] != cd['password2']:
raise forms.ValidationError("Passwords don't match.")
return cd['password2']
We have defined a clean_password2()
method to compare the second password against the first one and raise a validation error if the passwords don’t match. This method is executed when the form is validated by calling its is_valid()
method. You can provide a clean_<fieldname>()
method to any of your form fields to clean the value or raise form validation errors for a specific field. Forms also include a general clean()
method to validate the entire form, which is useful to validate fields that depend on each other. In this case, we use the field-specific clean_password2()
validation instead of overriding the clean()
method of the form. This avoids overriding other field-specific checks that the ModelForm
gets from the restrictions set in the model (for example, validating that the username is unique).
Django also provides a UserCreationForm
form that resides in django.contrib.auth.forms
and is very similar to the one we have created.
Edit the views.py
file of the account
application and add the following code highlighted in bold:
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render
from .forms import LoginForm, UserRegistrationForm
# ...
def register(request):
if request.method == 'POST':
user_form = UserRegistrationForm(request.POST)
if user_form.is_valid():
# Create a new user object but avoid saving it yet
new_user = user_form.save(commit=False)
# Set the chosen password
new_user.set_password(
user_form.cleaned_data['password']
)
# Save the User object
new_user.save()
return render(
request,
'account/register_done.html',
{'new_user': new_user}
)
else:
user_form = UserRegistrationForm()
return render(
request,
'account/register.html',
{'user_form': user_form}
)
The view for creating user accounts is quite simple. For security reasons, instead of saving the raw password entered by the user, we use the set_password()
method of the user model. This method handles password hashing before storing the password in the database.
Django doesn’t store clear text passwords; it stores hashed passwords instead. Hashing is the process of transforming a given key into another value. A hash function is used to generate a fixed-length value according to a mathematical algorithm. By hashing passwords with secure algorithms, Django ensures that user passwords stored in the database require massive amounts of computing time to break.
By default, Django uses the PBKDF2
hashing algorithm with a SHA256 hash to store all passwords. However, Django not only supports checking existing passwords hashed with PBKDF2
but also supports checking stored passwords hashed with other algorithms, such as PBKDF2SHA1
, argon2
, bcrypt
, and scrypt
.
The PASSWORD_HASHERS
setting defines the password hashers that the Django project supports. The following is the default PASSWORD_HASHERS
list:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.ScryptPasswordHasher',
]
Django uses the first entry of the list (in this case, PBKDF2PasswordHasher
) to hash all passwords. The rest of the hashers can be used by Django to check existing passwords.
The scrypt
hasher was introduced in Django 4.0. It is more secure and recommended over PBKDF2
. However, PBKDF2
is still the default hasher, as scrypt
requires OpenSSL 1.1+ and more memory.
You can learn more about how Django stores passwords and about the password hashers included at https://docs.djangoproject.com/en/5.0/topics/auth/passwords/.
Now, edit the urls.py
file of the account
application and add the following URL pattern highlighted in bold:
urlpatterns = [
# ...
path('', include('django.contrib.auth.urls')),
path('', views.dashboard, name='dashboard'),
path('register/', views.register, name='register'),
]
Finally, create a new template in the templates/account/
template directory of the account
application, name it register.html
, and make it look as follows:
{% extends "base.html" %}
{% block title %}Create an account{% endblock %}
{% block content %}
<h1>Create an account</h1>
<p>Please, sign up using the following form:</p>
<form method="post">
{{ user_form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Create my account"></p>
</form>
{% endblock %}
Create an additional template file in the same directory and name it register_done.html
. Add the following code to it:
{% extends "base.html" %}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>Welcome {{ new_user.first_name }}!</h1>
<p>
Your account has been successfully created.
Now you can <a href="{% url "login" %}">log in</a>.
</p>
{% endblock %}
Open http://127.0.0.1:8000/account/register/
in your browser. You will see the registration page you have created:
Figure 4.17: The account creation form
Fill in the details for a new user and click on the CREATE MY ACCOUNT button.
If all fields are valid, the user will be created, and you will see the following success message:
Figure 4.18: The account is successfully created page
Click on the log in link and enter your username and password to verify that you can access your newly created account.
Let’s add a link to register on the login template. Edit the registration/login.html
template and find the following line:
<p>Please, use the following form to log-in:</p>
Replace it with the following lines:
<p>
Please, use the following form to log-in.
If you don't have an account <a href="{% url "register" %}">register here</a>.
</p>
Open http://127.0.0.1:8000/account/login/
in your browser. The page should now look as follows:
Figure 4.19: The Log-in page including a link to register
We have now made the registration page accessible from the Log-in page.
Extending the user model
While the user model provided by Django’s authentication framework is sufficient for most typical scenarios, it does have a limited set of predefined fields. If you want to capture additional details relevant to your application, you may want to extend the default user model. For example, the default user model comes with the first_name
and last_name
fields, a structure that may not align with naming conventions in various countries. Additionally, you may want to store further user details or construct a more comprehensive user profile.
A simple way to extend the user model is by creating a profile model that contains a one-to-one relationship with the Django user model, and any additional fields. A one-to-one relationship is similar to a ForeignKey
field with the parameter unique=True
. The reverse side of the relationship is an implicit one-to-one relationship with the related model instead of a manager for multiple elements. From each side of the relationship, you access a single related object.
Edit the models.py
file of your account
application and add the following code highlighted in bold:
from django.db import models
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
date_of_birth = models.DateField(blank=True, null=True)
photo = models.ImageField(
upload_to='users/%Y/%m/%d/',
blank=True
)
def __str__(self):
return f'Profile of {self.user.username}'
Our user profile will include the user’s date of birth and an image of the user.
The one-to-one field user
will be used to associate profiles with users. We use AUTH_USER_MODEL
to refer to the user model instead of pointing to the auth.User
model directly. This makes our code more generic, as it can operate with custom-defined user models. With on_delete=models.CASCADE
, we force the deletion of the related Profile
object when a User
object gets deleted.
The date_of_birth
field is a DateField
. We have made this field optional with blank=True
, and we allow null
values with null=True
.
The photo
field is an ImageField
. We have made this field optional with blank=True
. An ImageField
field manages the storage of image files. It validates that the file provided is a valid image, stores the image file in the directory indicated with the upload_to
parameter, and stores the relative path to the file in the related database field. An ImageField
field is translated to a VARHAR(100)
column in the database by default. A blank string will be stored if the value is left empty.
Installing Pillow and serving media files
We need to install the Pillow library to manage images. Pillow is the de facto standard library for image processing in Python. It supports multiple image formats and provides powerful image processing functions. Pillow is required by Django to handle images with ImageField
.
Install Pillow by running the following command from the shell prompt:
python -m pip install Pillow==10.3.0
Edit the settings.py
file of the project and add the following lines:
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
This will enable Django to manage file uploads and serve media files. MEDIA_URL
is the base URL used to serve the media files uploaded by users. MEDIA_ROOT
is the local path where they reside. Paths and URLs for files are built dynamically by prepending the project path or the media URL to them for portability.
Now, edit the main urls.py
file of the bookmarks
project and modify the code, as follows. The new lines are highlighted in bold:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('account/', include('account.urls')),
]
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT
)
We have added the static()
helper function to serve media files with the Django development server during development (that is, when the DEBUG
setting is set to True
).
The static()
helper function is suitable for development but not for production use. Django is very inefficient at serving static files. Never serve your static files with Django in a production environment. You will learn how to serve static files in a production environment in Chapter 17, Going Live.
Creating migrations for the profile model
Let’s create the database table for the new Profile
model. Open the shell and run the following command to create the database migration for the new model:
python manage.py makemigrations
You will get the following output:
Migrations for 'account':
account/migrations/0001_initial.py
- Create model Profile
Next, sync the database with the following command in the shell prompt:
python manage.py migrate
You will see output that includes the following line:
Applying account.0001_initial... OK
Edit the admin.py
file of the account
application and register the Profile
model in the administration site by adding the code in bold:
from django.contrib import admin
from .models import Profile
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ['user', 'date_of_birth', 'photo']
raw_id_fields = ['user']
Run the development server using the following command from the shell prompt:
python manage.py runserver
Open http://127.0.0.1:8000/admin/
in your browser. Now, you should be able to see the Profile
model on the administration site of your project, as follows:
Figure 4.20: The ACCOUNT block on the administration site index page
Click on the Add link of the Profiles row. You will see the following form to add a new profile:
Figure 4.21: The Add profile form
Create a Profile
object manually for each of the existing users in the database.
Next, we will let users edit their profiles on the website.
Edit the forms.py
file of the account
application and add the following lines highlighted in bold:
# ...
from .models import Profile
# ...
class UserEditForm(forms.ModelForm):
class Meta:
model = get_user_model()
fields = ['first_name', 'last_name', 'email']
class ProfileEditForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['date_of_birth', 'photo']
These forms are as follows:
UserEditForm
: This will allow users to edit their first name, last name, and email, which are attributes of the built-in Django user model.ProfileEditForm
: This will allow users to edit the profile data that is saved in the customProfile
model. Users will be able to edit their date of birth and upload an image for their profile picture.
Edit the views.py
file of the account
application and add the following lines highlighted in bold:
# ...
from .models import Profile
# ...
def register(request):
if request.method == 'POST':
user_form = UserRegistrationForm(request.POST)
if user_form.is_valid():
# Create a new user object but avoid saving it yet
new_user = user_form.save(commit=False)
# Set the chosen password
new_user.set_password(
user_form.cleaned_data['password']
)
# Save the User object
new_user.save()
# Create the user profile
Profile.objects.create(user=new_user)
return render(
request,
'account/register_done.html',
{'new_user': new_user}
)
else:
user_form = UserRegistrationForm()
return render(
request,
'account/register.html',
{'user_form': user_form}
)
When users register on the site, a corresponding Profile
object will be automatically created and associated with the User
object created. However, users created through the administration site won’t automatically get an associated Profile
object. Both users with and without a profile (for example, staff users) can co-exist.
If you want to force profile creation for all users, you can use Django signals to trigger the creation of a Profile
object whenever a user is created. You will learn about signals in Chapter 7, Tracking User Actions, where you will engage in an exercise to implement this feature in the section Expanding your project using AI.
Now, we will let users edit their profiles.
Edit the views.py
file of the account
application and add the following code highlighted in bold:
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render
from .forms import (
LoginForm,
UserRegistrationForm,
UserEditForm,
ProfileEditForm
)
from .models import Profile
# ...
@login_required
def edit(request):
if request.method == 'POST':
user_form = UserEditForm(
instance=request.user,
data=request.POST
)
profile_form = ProfileEditForm(
instance=request.user.profile,
data=request.POST,
files=request.FILES
)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
else:
user_form = UserEditForm(instance=request.user)
profile_form = ProfileEditForm(instance=request.user.profile)
return render(
request,
'account/edit.html',
{
'user_form': user_form,
'profile_form': profile_form
}
)
We have added the new edit
view to allow users to edit their personal information. We have added the login_required
decorator to the view because only authenticated users will be able to edit their profiles. For this view, we use two model forms: UserEditForm
to store the data of the built-in user model and ProfileEditForm
to store the additional personal data in the custom Profile
model. To validate the data submitted, we call the is_valid()
method of both forms. If both forms contain valid data, we save both forms by calling the save()
method to update the corresponding objects in the database.
Add the following URL pattern to the urls.py
file of the account
application:
urlpatterns = [
#...
path('', include('django.contrib.auth.urls')),
path('', views.dashboard, name='dashboard'),
path('register/', views.register, name='register'),
path('edit/', views.edit, name='edit'),
]
Finally, create a template for this view in the templates/account/
directory and name it edit.html
. Add the following code to it:
{% extends "base.html" %}
{% block title %}Edit your account{% endblock %}
{% block content %}
<h1>Edit your account</h1>
<p>You can edit your account using the following form:</p>
<form method="post" enctype="multipart/form-data">
{{ user_form.as_p }}
{{ profile_form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Save changes"></p>
</form>
{% endblock %}
In the preceding code, we have added enctype="multipart/form-data"
to the <form>
HTML element to enable file uploads. We use an HTML form to submit both the user_form
and profile_form
forms.
Open the URL http://127.0.0.1:8000/account/register/
and register a new user. Then, log in with the new user and open the URL http://127.0.0.1:8000/account/edit/
. You should see the following page:
Figure 4.22: The profile edit form
You can now add the profile information and save the changes.
We will edit the dashboard template to include links to the edit profile and change password pages.
Open the templates/account/dashboard.html
template and add the following lines highlighted in bold:
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<p>
Welcome to your dashboard. You can <a href="{% url "edit" %}">edit your profile</a> or <a href="{% url "password_change" %}">change your password</a>.
</p>
{% endblock %}
Users can now access the form to edit their profile from the dashboard. Open http://127.0.0.1:8000/account/
in your browser and test the new link to edit a user’s profile. The dashboard should now look like this:
Figure 4.23: Dashboard page content, including links to edit a profile and change a password
Using a custom user model
Django also offers a way to substitute the user model with a custom model. The User
class should inherit from Django’s AbstractUser
class, which provides the full implementation of the default user as an abstract model. You can read more about this method at https://docs.djangoproject.com/en/5.0/topics/auth/customizing/#substituting-a-custom-user-model.
Using a custom user model will give you more flexibility, but it might also result in more difficult integration with pluggable applications that interact directly with Django’s auth
user model.