Django进阶教程:如何扩展Django用户模型

Django的内置身份验证系统非常棒。在大多数情况下,我们可以开箱即用,节省大量开发和测试工作。它适合大多数用例,并且非常安全。但是有时我们需要做一些很好的调整来适应我们的Web应用程序。

通常,我们希望存储更多与用户相关的数据。如果您的Web应用程序具有社会吸引力,您可能希望存储一个简短的个人信息、用户的地址,以及其他类似的用户信息。

在本文中,我将介绍多种不同的策略来扩展默认的Django用户模型。


扩展现有用户模型的方法

一般来说,扩展现有用户模型有四种不同的方法。

1:使用代理模型

什么是代理模型?

它是一种模型的继承,无需在数据库中创建新表。它用于在不影响现有数据库模式的情况下更改现有模型的行为(例如,默认排序、添加新方法等)。

什么时候应该使用代理模型?

当您不需要在数据库中存储额外的信息时,您应该使用代理模型来扩展现有的用户模型,但是我们需要添加额外的方法或更改模型的查询管理器。

2.使用One-To-One与用户模型关联起来。

什么是一对一链接?

它是一个常规的Django模型,它将拥有自己的数据库表,并通过OneToOneField与现有的用户模型保持一对一的关系。

我什么时候应该使用一对一的链接?

当需要存储与身份验证过程无关的现有用户模型的额外信息时,应该使用一对一链接。我们通常称之为用户配置文件。

3.扩展AbstractBaseUser,创建自定义用户模型

扩展AbstractBaseUser的自定义用户模型是什么?

它是从AbstractBaseUser继承的一个全新的用户模型,并通过settings.py引用。需要注意的是,这样的操作,我们应该在项目的开始阶段设计好完,因为它操作将极大地影响数据库模式。执行时要格外小心。

什么时候应该使用AbstractBaseUser的自定义用户模型?

当您的应用程序对身份验证过程有特定的需求时,您应该使用自定义用户模型。例如,在某些情况下,使用电子邮件地址登录,或者是使用手机验证登录的时候。

4.扩展AbstractUser的自定义用户模型

什么是AbstractUser自定义用户模型?

它是一个从AbstractUser继承的新用户模型。并通过settings.py引用。需要注意的是,这样的操作,我们应该在项目的开始阶段设计好完,因为它操作将极大地影响数据库模式。执行时要格外小心。

什么时候应该使用扩展AbstractUser的自定义用户模型?

当您对Django如何处理身份验证过程非常满意时,您应该使用它,并且您不会对它做任何更改。但你希望直接在用户模型中添加一些额外的信息,而不需要创建一个额外的类时(如选项2)。


使用代理模型扩展用户模型

这是扩展现有用户模型的影响较小的方法。 这种策略不会有任何缺点。 但它在很多方面的限制。

代码:

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):
        ...

在上面的示例中,我们定义了一个名为Person的代理模型。我们通过在Meta类中添加以下属性Proxy = True,告诉Django这是一个代理模型。

在本例中,我重新定义了默认排序,为模型分配了一个自定义管理器,并定义了一个新的方法do_something。

值得注意的是,User.objects.all()和Person.objects.all()将查询同一个数据库表。唯一的区别是我们为代理模型定义的行为。

如果这就是你所需要的,那就使用这种方法,简单。


使用One-To-One链接扩展用户模型

很有可能这就是你想要的。就我个人而言,这是我大部分时候使用的方法。我们将创建一个新的Django模型来存储与用户模型相关的额外信息。

请记住,使用此策略会导致额外的查询或连接来检索相关数据。基本上所有访问相关数据的时候,Django都会触发一个附加的查询。但这在大多数情况下是可以避免的。稍后我会再讲这个。

我通常将Django模型命名为Profile:

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)

这就是神奇的地方:我们现在将定义信号,这样当我们创建/更新用户实例时,我们的Profile模型将自动创建/更新。

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()

当发生保存事件时,我们将create_user_profile和save_user_profile方法连接到用户模型。这种信号称为post_save。

这是一个NB的东西。现在,告诉我如何使用它。

在Django模板中查看这个示例

<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>

在视图方法中:

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()

一般来说,您永远不需要调用概要文件的save方法。一切都是通过用户模型完成的。

如果我使用Django表单呢?

你可以同时处理多个表单,看看这个代码片段:

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>

上面提到的那些额外的数据库查询呢?

Django关系是懒惰的。这意味着Django只会在访问一个相关属性时查询数据库。有时它会产生一些不希望的效果,比如触发数百或数千个查询。可以使用select_related方法缓解这个问题。

事先知道您需要访问相关数据,您可以在单个数据库查询中预取数据:

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

扩展AbstractBaseUser自定义模型扩展用户模型。

老实说,我想尽一切办法避免使用这样的方式。但有时你无法逃避,只能使用这样的方式。如果当你必须使用这个方案的时候,请继续看下去。

当我需要使用电子邮件地址作为auth令牌的这种情况下,用户名对我来说毫无用处。而且不需要is_staff标志,因为我没有使用Django Admin。

以下是我如何定义自己的用户模型:

from __future__ import unicode_literals
from django.db import models
from django.core.mail import send_mail
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)

我想让它尽可能接近现有的用户模型。由于我们继承了AbstractBaseUser,我们必须遵循以下规则:

USERNAME_FIELD:描述用户模型上作为唯一标识符的字段名称的字符串。该字段必须是唯一的(即:unique=True);

REQUIRED_FIELDS:通过createsuperesuperuser管理命令创建用户时提示的字段名列表;

is_active:一个布尔属性,指示用户是否被视为“active”;

get_full_name():用户的较长的形式标识符。一种常见的解释是用户的全名,但是可以是标识用户的任何字符串。

get_short_name():用于用户的一个简短的非正式标识符。通常的解释是用户的名字。

我还必须定义我自己的UserManager。这是因为现有的管理器定义了create_user和create_superuser方法。

所以现在我的UserManager是这样的:

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)

基本上,我已经清理了现有的UserManager,删除了username和is_staff属性。

现在,我们必须更新我们的settings.py。更具体地说,是AUTH_USER_MODEL属性。

AUTH_USER_MODEL = 'core.User'

这样我们就告诉Django使用我们的自定义模型而不是默认模型。在上面的示例中,我在一个名为core的应用程序中创建了自定义模型。

我应该如何引用这个模型?

我们命名一个叫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)

如果您正在创建一个可重用的应用程序,并希望向公众开放,那么强烈建议您使用以下策略:

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)

使用自定义模型扩展用户模型

这很简单,因为类django.contrib.auth.models.AbstractUser将默认用户作为抽象模型提供完整的实现。

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)

更新settings.py,定义AUTH_USER_MODEL

AUTH_USER_MODEL = 'core.User'

与前面的方法类似,应该在项目的开始和额外的关注中完成。它将改变整个数据库模式。此外,我宁愿向用户模型创建外键,从from django.conf import settings导入设置并引用settings.AUTH_USER_MODEL,而不是直接引用自定义用户模型。


总结

我们经历了四种不同的方法来扩展现有的用户模型。我尽力给你提供尽可能多的细节。正如我之前所说的,没有最好的解决方案。这将取决于你需要实现什么。保持简单,明智地选择。

  1. Proxy Model:您对Django用户提供的一切都很满意,不需要存储额外的信息。

  2. Proxy ModelUser Profile:您对Django处理auth的方式感到满意,并需要向用户添加一些与auth无关的属性。

  3. 继承AbstractBaseUser的自定义用户模型:Django处理auth的方式不适合您的项目。

  4. 从AbstractUser定制用户模型:Django处理auth的方式非常适合您的项目,但是您仍然希望添加额外的属性,而不必创建单独的模型。

文章评论 3