rennerocha.com/content/posts/20230501-extending-django-user-with-custom-model.md

237 lines
No EOL
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Extending built-in Django User with a custom Model"
date: 2023-05-01T19:28:40-03:00
tags: ["django", "user", "python"]
slug: extending-django-user-with-custom-model
---
The [user authentication](https://docs.djangoproject.com/en/4.2/topics/auth/#user-authentication-in-django) system provided by Django is extremely powerful
and handles most of the authentication (Am I who I say I am?) and authorization (Am I
authorized to do what I want to do?) needs of an web project. User accounts, groups
and permissions, methods for handling passwords securely are part of this system.
Generally, this is adequate for most projects; however, there are situations where it becomes necessary to modify the behavior of a **User** or alter how their data is stored in the database. It should be noted that modifying the authorization and/or authentication process of a **User** will not be covered in this post.
Creating a user profile model is one solution to extend and modify the behavior of a **User**, as [discussed before]({{< ref "20230424-extending-django-user-profile-model" >}}), but that approach has some caveats that are solved with the use of a
custom user model.
# Custom User model
We can tell Django which is the model that will replace the built-in
`django.contrib.auth.models.User` model in the system. This is done by
providing a value to `AUTH_USER_MODEL` in your project.
{{<highlight python>}}
# myproject/myproject/settings.py
AUTH_USER_MODEL = "users.CustomUser"
{{</highlight>}}
> Usually I create a `users` application to group all custom user related
> code to keep everything in the same context
It is highly recommended that you do this configuration at the beginning
of the project, as after you have created your database tables, it will
require you to fix your schema, moving data between tables and reapplying
some migrations manually (see [#25313](https://code.djangoproject.com/ticket/25313)
for an overview of the steps involved).
Now we need to create our custom model.
This new user model can handle different authentication and authorization
schemes, can use different fields (e.g. email) to identify the user or
any other requirement that is not satisfied using the default Django user
model.
## Adding new fields to default User model
If the default **[User](https://docs.djangoproject.com/en/4.2/ref/contrib/auth/#django.contrib.auth.models.User)** model has everything we need and we want
to add some extra profile fields keeping authentication and authorization as
it is, create a custom model inheriting from `AbstractUser` would be enough.
{{<highlight python>}}
# myproject/users/models.py
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
date_of_birth = models.DateField(null=True)
{{</highlight>}}
{{<highlight python>}}
# myproject/myproject/settings.py
AUTH_USER_MODEL = "users.CustomUser"
{{</highlight>}}
As we will not create a new table to store the user related information, there is
no need for additional database queries to retrieve related models. We also don't
need to worry if a related model is created or not reducing the complexity of
the system.
## AbstractBaseUser
If we want to use a different field as the identifier (other than `email`),
use a different authorization system and/or having more control on what
fields we have in our user model, a custom model inheriting from
`AbstractBaseUser` is the choice.
`AbstractBaseUser` is the core implementation of a **User**. It has a
`password` field and methods to store and validate it securely. A
`last_login` datetime field and everything else is up to us to provide.
If using the default authentication backend, our model must have a single unique
field that will be the identifier of our user such as email address, username or
any other unique attribute.
You configure that attribute using `USERNAME_FIELD` that defines what is
the field that will uniquely identify the user.
`EMAIL_FIELD`
`REQUIRED_FIELDS`
`is_active` is an attribute on `AbstractBaseUser` defaulting to `True`
As an example, we will define a custom user that will use the email address
as the identifier, has the date of birth as one required field can store
the phone number and has a flag field in the database to indicate if the user
is active or not.
{{<highlight python>}}
# myproject/users/models.py
from django.contrib.auth.models import AbstractBaseUser
class CustomUser(AbstractBaseUser):
USERNAME_FIELD = "email"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = ["date_of_birth", ]
date_of_birth = models.EmailField(unique=True)
email = models.EmailField(unique=True)
is_active = models.BooleanField(default=True)
phone_number = models.CharField(max_length=16, blank=True)
{{</highlight>}}
FROM DOCS Authentication backends provide an extensible system for when a username and password stored with the user model need to be authenticated against a different service than Djangos default.
TODO: list all fields that are created with this
Any other fields needs to be created.
is_active is a property set to True, but you can define if you want
USERNAME_FIELD is needed identifier unique = True
EMAIL_FIELD to specifi what is the email field
REQUIRED_FIELDS fields required when createsuperuser
If we want to use `email` as the identifier of an user,
we need to set `USERNAME_FIELD` property
{{<highlight python>}}
# myproject/users/models.py
class CustomUser(AbstractBaseUser):
USERNAME_FIELD = "email"
email = models.EmailField()
{{</highlight>}}
One drawback is you need to specify a Custom Manager implementing
two methods: `create_user` and `create_superuser`. This is required
so we willll be able to run `python manage.py createsuperuser`
and `python manage.py create-user`.
If some fields (TODO list what fields) from `ABstractUser` are there, you can
use the built-in UserManager
FROM DOCS username, email, is_staff, is_active, is_superuser, last_login, and date_joined fields the same as Djangos default user, you can install Djangos UserManager
{{<highlight python>}}
# myproject/users/models.py
class CustomUser(AbstractBaseUser):
USERNAME_FILED = "email"
email = models.EmailField()
objects = UserManager()
{{</highlight>}}
If you don't want to use Django Admin (very unlikely), you're done,
and you can create/manage your users and authentication backend will
be able.
However, if you want to use this custom user model with Django admin, there are
some extra steps.
We need some fields and methods:
`is_staff` so you have access to django admin
`is_active` inactive users can't access django admin
to manage permissions to edit registered models
`has_perm`
`has_module_perms()`
***we don't need to use the built-in permission system (see PermissionMixin)***
And `UserCreationForm`< `UserChangeForm` and `PasswordResetForm` (if we don't have email field)
needs to be customized so we can use that.
Also we need a custom UserAdmin to use our custom forms.
This is a lot of things, and sometimes difficult to remember. The docs
are ok to describe the process, but IMO this should be much simpler :-P
If by any chance you want to use django model permission, you can
use PermissionMixin, so you have access to groups, etc (to be honest, I
never saw anything using permission, but groups)
Sometimes the username may not be the way you want to identify an user
(an email or social number would be more suitable for your project)
or we have different requirements for authorization and permission. In
those cases creating a custom user model is a option.
This has advatanges over the user of a user profile model and solve some
of the caveats of that approach such as:
- We won't need an extra database table to store your custom fields, so
there is no performance problems to access them;
- As the table that we store the User data required for Django authentication
is the same, we don't need to bother if your profile fields will be available
or not
When creating a custom user model, we have two different options
If you are going to this path, you can create your custom user
inheriting from two different classes:
If using Django Admin, we also need to register this new model.
{{<highlight python>}}
# myproject/users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
admin.site.register(CustomUser, UserAdmin)
{{</highlight>}}