📜 ⬆️ ⬇️

Django User Model Extension Strategies

Django has an excellent user authentication system. 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.

As a rule, 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 discuss strategies that you can use to extend your custom Django model, rather than writing it from scratch.
')


Expansion strategies


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

  1. Simple extension of the model (proxy)

    This strategy without creating new tables in the database. Used to change the behavior of an existing model (for example, ordering by default, 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 need to add additional methods or change the model query manager. →

  2. Using a one-to-one relationship with a user model

    This is a strategy using the extra regular Django model with its table in the database, which is connected by the user of the standard model through the OneToOneField connection.

    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, which is inherited from AbstractBaseUser . Requires extra care and setting changes 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 tag identification instead of a user name. →

  4. AbstractUser

    This is a strategy for using a new user model, which is inherited from AbstractUser . Requires extra care and setting changes 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 don’t want to change it. However, you want to add some additional information directly to the user's model, without the need to create an additional class (as in option 2). →



Simple extension of the model (proxy)


This is the least time consuming way to expand the user model. Fully limited in deficiencies, but does not have any great features.

models.py
 from django.contrib.auth.models import User from .managers import PersonManager class Person(User): objects = PersonManager() class Meta: proxy = True ordering = ('first_name', ) def do_something(self): ... 

In the example above, we defined the extension of the User model to the Person model. 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 do_something() method is defined.

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



Using a one-to-one relationship with a user model


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

Keep in mind that using this strategy generates additional requests or connections within the request. Basically, all the time when you request data, an additional request will be triggered. But this can be avoided for 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 class Profile(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: define the signals so that our Profile model is automatically updated when creating / changing User model data.

 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 class Profile(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) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): instance.profile.save() 

We hooked create_user_profile() and save_user_profile() on the User model save event. This signal is called post_save .

And now an example of a Django template using Profile data:

 <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 also like this:

 def update_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 Profile save methods. All this is done using the User model.

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

forms.py
 class UserForm(forms.ModelForm): class Meta: model = User fields = ('first_name', 'last_name', 'email') class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = ('url', 'location', 'company') 

views.py
 @login_required @transaction.atomic def update_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
 <form method="post"> {% csrf_token %} {{ user_form.as_p }} {{ profile_form.as_p }} <button type="submit">Save changes</button> </form> 

And about the promised query optimization. In full, the question is considered in my other article .

But, in short, Django's relationship is lazy. Django queries the database table if it is necessary to read one of its fields. Regarding our example, using the select_related() method is select_related() .

Knowing in advance that you need to access related data, you can pre-do it with one request:

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



AbstractBaseUser extension


To be honest, I try to avoid this method at all costs. But sometimes it is not possible. And it is beautiful. 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 my email address as an auth token , and username absolutely not needed. In addition, there was no need for the is_staff flag since I did not use Django Admin.

Here 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 class User(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 = [] class Meta: verbose_name = _('user') verbose_name_plural = _('users') def get_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() def get_short_name(self): ''' Returns the short name for the user. ''' return self.first_name def email_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. Inheriting from AbstractBaseUser we must follow some rules:


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

My UserManager looked like this:

 from django.contrib.auth.base_user import BaseUserManager class UserManager(BaseUserManager): use_in_migrations = True def _create_user(self, email, password, **extra_fields): """ Creates and saves a User with the given email and password. """ if not 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 def create_user(self, email, password=None, **extra_fields): extra_fields.setdefault('is_superuser', False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): extra_fields.setdefault('is_superuser', True) if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') return self._create_user(email, password, **extra_fields) 

In essence, I cleared the existing UserManager from the username and is_staff .

Finishing touch. Need to change settings.py :

 AUTH_USER_MODEL = 'core.User' 

So 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 named 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 class Course(models.Model): slug = models.SlugField(max_length=100) name = models.CharField(max_length=100) tutor = models.ForeignKey(User, on_delete=models.CASCADE) 

Overall fine. 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 class Course(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) 



AbstractUser extension


This is pretty simple, since the django.contrib.auth.models.AbstractUser class provides the full default user model implementation as an abstract model.

 from django.db import models from django.contrib.auth.models import AbstractUser class User(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 the 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 extreme caution, as it will change the entire database schema. It is also a good rule to create keys to the user model by importing settings from django.conf import settings and using settings.AUTH_USER_MODEL instead of directly referring to the User class.



Summary


Fine! We looked at four different strategies for expanding 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

Do not hesitate to ask questions and express opinions about this post !

Source: https://habr.com/ru/post/313764/


All Articles