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的移植。

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 版权: https://github.com/haiiiiiyun/awesome-django-cn Aweso...
    若与阅读 23,002评论 3 241
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 我记得自己18岁第一次谈恋爱时,心里想的是,“一阵子”. 即使再无知,仿佛“初恋大多不会走得很远”的事实也是一个常...
    Skullman阅读 431评论 0 0
  • 岁月是能看到的东西, 荏苒间云淡风起, 看尽繁花似锦, 品味如诗一样的暖阳, 踏过那白雪又变祥云。 岁月是无法预测...
    冯保中阅读 530评论 0 6
  • Gzip是什么 复制大神们的解释吧: GZIP最早由Jean-loup Gailly和Mark Adler创建,用...
    岛民小强阅读 15,643评论 11 25