Django User Model Extension Strategies

Django has an excellent user authentication system built in. In most cases, we can use it out of the box, which saves a lot of time for developers and testers. But sometimes we need to expand it to meet the needs of our site.

Typically, there is a need to store additional data about users, for example, a brief biography (about), date of birth, location, and other similar data.

This article will talk about strategies that you can use to extend your custom Django model, rather than write it from scratch.



Expansion strategies


We briefly describe the strategies for expanding the Django user model and the need for their application. And then we will reveal the configuration details for each strategy.

  1. Simple model extension (proxy)

    This strategy is without creating new tables in the database. Used to change the behavior of an existing model (for example, default ordering, adding new methods, etc.) without affecting the existing database schema.

    You can use this strategy when you do not need to store additional information in the database, but simply add additional methods or change the query manager of the model.

  2. Using one-to-one communication with a user model (user profiles)

    This is a strategy using an additional ordinary Django model with its own table in the database, which is linked by the user of the standard model through communication OneToOneField.

    You can use this strategy to store additional information that is not related to the authentication process (for example, date of birth). This is usually called a user profile.

  3. Расширение AbstractBaseUser

    This is a strategy for using a completely new user model that is inherited from AbstractBaseUser. Requires extreme caution and changing settings in settings.py. Ideally, it should be done at the beginning of the project, as it will significantly affect the database schema.

    You can use this strategy when your site has specific requirements regarding the authentication process. For example, in some cases it makes sense to use an email address as a token identification instead of a username.

  4. Расширение AbstractUser

    This is a strategy for using a new user model that is inherited from AbstractUser. Requires extreme caution and changing settings in settings.py. Ideally, it should be done at the beginning of the project, as it will significantly affect the database schema.

    You can use this strategy when the Django authentication process itself is completely satisfying and you do not want to change it. However, you want to add some additional information directly to the user model, without the need to create an additional class (as in option 2).



Simple model extension (proxy)


This is the least time consuming way to extend your user model. It is completely limited in shortcomings, but also does not have any wide possibilities.

models.py
from django.contrib.auth.models import User
from .managers import PersonManager
classPerson(User):
    objects = PersonManager()
    classMeta:
        proxy = True
        ordering = ('first_name', )
    defdo_something(self):
        ...

In the above example, we defined the extension of the model by Usermodel Person. We say Django is a proxy model by adding the following property inside class Meta:

Proxy = True

Also in the example, a custom model manager is assigned, the default order is changed, and a new method is defined do_something().

It is worth noting that User.objects.all()it Person.objects.all()will query the same database table. The only difference is the behavior that we define for the proxy model.



Using one-to-one communication with a user model (user profiles)


Most likely, this is what you need. Personally, I use this method in most cases. We will be creating a new Django model to store additional information that is related to the user model.

Keep in mind that using this strategy generates additional queries or connections within the query. Basically, all the time when you request data, an additional request will be triggered. But this can be avoided in most cases. I will say a few words about how to do this, below.

models.py
from django.db import models
from django.contrib.auth.models import User
classProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

Now let's add a little magic: let's define signals so that our model is Profileautomatically updated when creating / changing model data User.

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
classProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)
@receiver(post_save, sender=User)defcreate_user_profile(sender, instance, created, **kwargs):if created:
        Profile.objects.create(user=instance)
@receiver(post_save, sender=User)defsave_user_profile(sender, instance, **kwargs):
    instance.profile.save()

We are "hooked" create_user_profile()and save_user_profile()an event model storage User. Such a signal is called post_save.

And now an example Django template using data Profile:

<h2>{{ user.get_full_name }}</h2><ul><li>Username: {{ user.username }}</li><li>Location: {{ user.profile.location }}</li><li>Birth Date: {{ user.profile.birth_date }}</li></ul>

And you can like this:

defupdate_profile(request, user_id):
    user = User.objects.get(pk=user_id)
    user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...'
    user.save()

Generally speaking, you should never call persistence methods Profile. All this is done using the model User.

If you need to work with forms, then below are sample code for this. Remember that you can process data from more than one model (class) at a time (from one form).

forms.py
classUserForm(forms.ModelForm):classMeta:
        model = User
        fields = ('first_name', 'last_name', 'email')
classProfileForm(forms.ModelForm):classMeta:
        model = Profile
        fields = ('url', 'location', 'company')

views.py
@login_required@transaction.atomicdefupdate_profile(request):if request.method == 'POST':
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, _('Your profile was successfully updated!'))
            return redirect('settings:profile')
        else:
            messages.error(request, _('Please correct the error below.'))
    else:
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
    return render(request, 'profiles/profile.html', {
        'user_form': user_form,
        'profile_form': profile_form
    })

profile.html
<formmethod="post">{% csrf_token %}{{ user_form.as_p }}{{ profile_form.as_p }}<buttontype="submit">Save changes</button></form>

And about the promised query optimization. In full, the issue is considered in another article of mine .

But, in short, the Django relationship is lazy. Django makes a request to the database table, if you need to read one of its fields. Regarding our example, using the method will be effective select_related().

Knowing in advance that you need to access the associated data, you can proactively do this with one request:

users = User.objects.all().select_related('Profile')



Expansion AbstractBaseUser


To be honest, I try to avoid this method at all costs. But sometimes this is not possible. And that is wonderful. There is hardly such a thing as a better or worse solution. For the most part, there is a more or less suitable solution. If this is the most suitable solution for you, then go ahead.

I had to do it once. Honestly, I don’t know if there is a cleaner way to do this, but I haven’t found anything else.

I needed to use the email address as auth token, but usernameabsolutely was not needed. In addition, there was no need for a flag is_staffsince I did not use Django Admin.

This is how I defined my own user model:

from __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _
from .managers import UserManager
classUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
    is_active = models.BooleanField(_('active'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
    objects = UserManager()
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []
    classMeta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
    defget_full_name(self):'''
        Returns the first_name plus the last_name, with a space in between.
        '''
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()
    defget_short_name(self):'''
        Returns the short name for the user.
        '''return self.first_name
    defemail_user(self, subject, message, from_email=None, **kwargs):'''
        Sends an email to this User.
        '''
        send_mail(subject, message, from_email, [self.email], **kwargs)

I wanted to keep it as close as possible to the “standard” user model. Having inherited from, AbstractBaseUserwe must follow some rules:

  • USERNAME_FIELD- a string with the name of the model field, which is used as a unique identifier ( unique=Truein the definition);

  • REQUIRED_FIELDS - a list of field names that will be requested when creating a user using the control command createsuperuser

  • is_active - a logical attribute that indicates whether the user is considered “active”;

  • get_full_name() - long description of the user: not necessarily the full name of the user, it can be any line that describes the user;

  • get_short_name() - A short description of the user, for example, his name or nickname.

I also had my own UserManager. Because the existing manager defines create_user()and create_superuser()methods.

My UserManager looked like this:

from django.contrib.auth.base_user import BaseUserManager
classUserManager(BaseUserManager):
    use_in_migrations = Truedef_create_user(self, email, password, **extra_fields):"""
        Creates and saves a User with the given email and password.
        """ifnot email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user
    defcreate_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)
    defcreate_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)
        if extra_fields.get('is_superuser') isnotTrue:
            raise ValueError('Superuser must have is_superuser=True.')
        return self._create_user(email, password, **extra_fields)

In fact, I cleared the existing UserManagerfields usernameand is_staff.

Finishing touch. Need to change settings.py:

AUTH_USER_MODEL = 'core.User'

Thus, we tell Django to use our custom model instead of the supplied “in box”. In the example above, I created a custom model inside the application with the name core.

How to reference this model?

There are two ways. Consider a model called Course:

from django.db import models
from testapp.core.models import User
classCourse(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(User, on_delete=models.CASCADE)

Generally normal. But, if you plan to use the application in other projects or distribute, it is recommended to use the following approach:

from django.db import models
from django.conf import settings
classCourse(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)



Expansion AbstractUser


This is quite simple, since the class django.contrib.auth.models.AbstractUserprovides a complete implementation of the user model by default as an abstract model.

from django.db import models
from django.contrib.auth.models import AbstractUser
classUser(AbstractUser):
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

After that, you need to change settings.py:

AUTH_USER_MODEL = 'core.User'

As in the previous strategy, ideally, this should be done at the beginning of the project and with special care, as it will change the entire database schema. It is also a good rule to create keys to the user model through importing settings from django.conf import settingsand using settings.AUTH_USER_MODELinstead of directly referring to the class User.



Summary


Fine! We looked at four different expansion strategies for the “standard” user model. I tried to do this in as much detail as possible. But, as I said, there is no better solution. Everything will depend on what you want to receive.


Original
How to Extend Django User Model

Не стесняйте задавать вопросы и высказывать мнения об этом посте!

Also popular now: