Django作为一个大而全的框架,本身已经有一个非常不错的鉴权系统,对大多数的应用可以做到开箱即用,在很大程度上提高了系统的开发效率。它本身比较安全,可以覆盖多数的应用场景。不过有时候我们需要针对一些特定的应用做一些特殊的调整。
一般而言,我们都需要存储一些和用户相关的信息,比如用户的生日,位置等等的信息。
本篇文章就主要来看一下如何对Django自带的用户模型实现一些简单的扩展,我们会尽量使用django自带的一些特性,而不是所有都自己实现。
扩展用户模型的方法
一般来说有4种方式实现对用户模型的扩展:
- 用代理模型的方式
代理模型:所谓的代理模型实际是实现一个模型而不在数据库中建立相应的表,一般用来改变已经存在的模型的一些行为(比如修改默认排序,增加新的方法等等)而不改变已经存在的数据库结构。
通过代理模型的方式扩展比较简单,也有一定的局限性,一个具体的实现如下:
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
的方法。
使用代理模型的前提是你不需要引入新的用户信息,只是去增加或者修改一些模型的方法.
- 增加一对一的用户模型
这种方式实际是建立一个和已经有的用户表相互关联的用户资料表,在新的资料表中只存储和用户相关的信息,而不会涉及权限相关的信息。
这是一种比较常用的用户模型的扩展方法,一般我个人也是常用这种方法。需要说明的是这种方式会降低一些用户信息的检索,插入等操作的效率。基本上说你每次获取相关信息的时候都会增加一个额外的查询,不过这些查询也可以通过一定的技巧避免,这个我们后面再讨论。
我个人一般把扩展的模型命名为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')
- 通过扩展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)
其实我做的主要就是去掉了username
和is_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)
- 通过扩展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的移植。
总结
好了,以上就是用户扩展的几种方式,在你的应用中可以根据自己的需要采用相应的扩展方法。