Django用户表的扩展

翻译原文

Django作为一个大而全的框架,本身已经有一个非常不错的鉴权系统,对大多数的应用可以做到开箱即用,在很大程度上提高了系统的开发效率。它本身比较安全,可以覆盖多数的应用场景。不过有时候我们需要针对一些特定的应用做一些特殊的调整。

一般而言,我们都需要存储一些和用户相关的信息,比如用户的生日,位置等等的信息。

本篇文章就主要来看一下如何对Django自带的用户模型实现一些简单的扩展,我们会尽量使用django自带的一些特性,而不是所有都自己实现。

扩展用户模型的方法

一般来说有4种方式实现对用户模型的扩展:

  1. 用代理模型的方式
    代理模型:所谓的代理模型实际是实现一个模型而不在数据库中建立相应的表,一般用来改变已经存在的模型的一些行为(比如修改默认排序,增加新的方法等等)而不改变已经存在的数据库结构。

通过代理模型的方式扩展比较简单,也有一定的局限性,一个具体的实现如下:

from django.contrib.auth.models import Users
from .managers import PersonManager

class Person(User):
     objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def do_something(self):
        ...

在上面的代码加我们增加了一个Person的代理类,通过增加一个Meta class: proxy = True来指定这是一个代理类,这是增加一个Manager的模型,然后定义了一个do_something的方法。

使用代理模型的前提是你不需要引入新的用户信息,只是去增加或者修改一些模型的方法.

  1. 增加一对一的用户模型
    这种方式实际是建立一个和已经有的用户表相互关联的用户资料表,在新的资料表中只存储和用户相关的信息,而不会涉及权限相关的信息。

这是一种比较常用的用户模型的扩展方法,一般我个人也是常用这种方法。需要说明的是这种方式会降低一些用户信息的检索,插入等操作的效率。基本上说你每次获取相关信息的时候都会增加一个额外的查询,不过这些查询也可以通过一定的技巧避免,这个我们后面再讨论。

我个人一般把扩展的模型命名为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)

下面我们会用一些技巧实现用户信息的自动更新:

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

我们为User模型的创建和保存操作添加了两个钩子,这样一但User表有保存操作,就会调用我们定义的钩子函数。

比如,如果需要在模板中使用用户信息,我们可以通过如下的方式实现:

<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方法,比如在用户表单的使用上:

#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中查询也是懒加载的,也就是说只有相应的数据被读取的时候都会产生数据库的查询操作,而对于我们的这种扩展方式,如果需要一次查询就得到所有的数据,我们可以通过如下的查询方式实现数据获取:

users = User.objects.all().select_related('profile')
  1. 通过扩展AbstractBaseUser实现一个新的用户表
    这种方式比较繁琐,一般在项目开始的时候就要做好,还要在settings中做相应的修改。这种做法一般在用户认证有特殊需求的时候采用,比如需要用邮箱登陆而不是用默认的用户名登陆。

这是一种特别繁琐的方式,一般我都会尽量避免,不过有时候你是绕不开这个方式的,对于一些特殊的情况,我们似乎别无选择。

这种方式我用过一次,不过坦白来说我也不确定我的做法是否合理。在那个项目中我需要用email登陆,username对我来说没有意义,因为我不用django admin,所以is_staff对我来说也没有用处。我是通过如下的方式定义我的用户模型的:

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)

我尽量与原生的用户模型保持一致,不过有几个地方需要特别注意:
USERNAME_FIELD:它是一个用户的唯一标识,它必须具有唯一性(unique=True
REQUIRED_FIELDS:是通过createsuperuser命令创建用户时会询问的用户信息。
is_active:用于表示用户是否激活
get_full_name()/get_short_name():获取用户信息的两个方法。

定义了用户表之后,还要自己实现UserManager类,它定义了如何创建新的用户,比如我定义的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)

其实我做的主要就是去掉了usernameis_staff方法(译者:其实is_staff还是非常重要的,另外django用户还有group和一些权限信息的定义,其实这些权限对admin页面非常重要,不过作者说不用admin,直接丢掉也是无可厚非)
定义的模型之后还要对定义的模型进行注册,否则django还是会采用默认的模型。注册的方式是在settings.py中添加:

AUTH_USER_MODEL = 'core.User'

注册之后对模型的引用就和一般的用户模型类似了。

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)

不过如果需要实现一个可移植的app,建议的新模型的定义方法如下:

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)
  1. 通过扩展AbstractUser实现一个新的用户表
    这种方式不能修改django用户的鉴权过程,也是特别简单的一种方法,因为django.contrib.auth.models.AbstractUser实际实现了对标准User模型的一个抽象,我们通过继承就可以实现自己的user模型:
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 = 'core.User'

因为它会影响整个数据库的结构,所以一般也是在最开始的时候就要定义好,而对需要引用User Model的其它模型,也是建议通过settings.AUTH_USER_MODEL定义外键,这样有助于app的移植。

总结

好了,以上就是用户扩展的几种方式,在你的应用中可以根据自己的需要采用相应的扩展方法。

发表新评论