django 及 rest_framework 筆記鏈接如下:
django 入門筆記:環境及項目搭建
django 入門筆記:數據模型
django 入門筆記:視圖及模版
django 入門筆記:Admin 管理系統及表單
django 入門筆記:通用視圖類重構視圖
django_rest_framework 入門筆記:Serializer
django_rest_framework 入門筆記:視圖函數重構
django_rest_framework 入門筆記:分頁,多條件篩選及權限認證設置
django 自帶 user 字段擴展及頭像上傳
一. 擴展 django 自帶的 User Model 字段
在實際項目中,django 自帶的 user model 的字段很多時候不能夠滿足我們的需求,可能我們需要在 user 中加入商城積分等字段,這就需要通過擴展的 user model 字段來實現。目前擴展 user 字段我找到的有 4 種方式。
1. 通過新建 model 然后指定 OneToOne 的 user 字段(官方推薦)
class UserProfile(models.Model):
user = models.OneToOne(User, related_name="profile", on_delete=models.CASCADE)
phone_num = models.CharField(max_length=20, unique=True, blank=True)
# 當生成 user 的時候自動生成 UserProfile,參考之前的 token 生成
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile(sender, instance=None, created=False, **kwargs):
if created:
profile, created = UserProfile.objects.get_or_create(user=instance)
接著需要在 admin 做些設置
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class ProfileInline(admin.StackInlin):
model = UserProfile
can_delete = False
verbose_name_plural = "profile"
class UserAdmin(UserAdmin):
inlines = (ProfileInline,)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
然后通過遷移數據庫,django 會同時生成 user 表和 user_profile 表
這個方法雖然是官方推薦的,但是個人還是覺得很麻煩,獲取 profile 的字段值還需要通過 user 表的外鍵獲取 profile 實例,然后再獲取字段值,再考慮到 django 的通過外鍵生成反向字段也需要做些處理,如果說我們只需要額外添加一個字段,需要做那么多工作,我是拒絕的。
2. 通過繼承 AbstractUser 來實現
首先需要在 models.py 文件中創建自己的 user model
from django.contrib.auth.models import AbstractUser
from django.db import models
# 有個坑,擴展字段后密碼會明文,當我們提交的時候需要進行設置,在后面的代碼中會提到
class User(AbstractUser):
phone_num = models.CharField(max_length=20, unique=True, blank=True)
然后我們需要在 settings.py 文件中設置 AUTH_USER_MODEL 的值
# settings.py
AUTH_USER_MODEL = '[your_app_name].[your_user_model_name]' # 例如我這邊設置的值為 'expert.User'
在 admin 中,我們按照之前的方法照常設置就行
這種方法,需要在我們做數據庫遷移之前,先定義好我們自己的 User Model 然后將 settings.py 文件中的 AUTH_USER_MODEL 替換后再進行數據庫遷移,需要告訴 django 系統,這個項目我要用自己的 User 了,不用你的。這種方法適合只需要擴展 User Model 中的字段,會繼承之前 AbstractUser 的全部字段,系統自帶的 User 也是通過繼承 AbstractUser 來實現的
# 系統的 User 源碼,AbstractUser 的源碼也不是很多,可以自行查看
class User(AbstractUser):
"""
Users within the Django authentication system are represented by this
model.
Username, password and email are required. Other fields are optional.
"""
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
如果說系統自帶的 user 很多字段都不符合項目的需要,基本上需要完全替換,那目前提到的兩種方法都不適合,需要通過第三種方法來替換
3. 通過 AbstractBaseUser 替換 User
這種方法會比較麻煩,說實話前兩種方法已經可以解決我們的問題,不到迫不得已個人覺得沒必要用這種方法
# AbstractUser 就是通過繼承 AbstractBaseUser 來實現的,
# 我們寫這部分時候可以通過修改 AbstractUser 的源碼實現
from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
# 假如我們不需要原來 User 中的 username 和 is_staff 字段,直接通過 email 作為用戶名
class User(AbstractUser, PermissionsMixin):
email = models.EmailField(_('郵箱'), unique=True)
phone = models.CharField(_('手機號'), unique=True, blank=True)
first_name = models.CharField(_('名'), max_length=30, blank=True)
last_name = models.CharField(_('姓'), max_length=30, blank=True)
date_joined = models.DateTimeField(_('創建時間'), auto_now_add=True)
is_active = models.BooleanField(_('激活狀態'), default=True)
picture = models.ImageField(upload_to='pictures/', null=True, blank=True)
objects = UserManager()
# 定義登錄的字段
USERNAME_FIELD = 'email'
# 通過 createsuperuser 命令行時候必填字段
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_full_name(self):
full_name = '{} {}'.format(self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
return self.first_name
def email_to_user(self):
# 具體發送郵件的邏輯可以自行查找實現,郵箱驗證的時候需要用到
pass
def message_to_user(self):
# 具體發送短信邏輯可以自行查找實現
pass
在 UserManager 中,需要通過 create_user
和 create_superuser
的方法來創建用戶和超級用戶,所以必須重寫這些方法才可以,這里根據系統的 UserManager 源碼進行修改
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('必須設置郵箱')
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)
替換 User 中的 UserManager,然后還是需要在 settings.py 文件中設置 AUTH_USER_MODEL 為自定義的 User Model
通過 2 , 3 方法擴展 User Model 后,當別的表和 User 表有鏈表結構的時候,最好通過 settings.AUTH_USER_MODEL 進行導入,而不是通過 User 直接導入,還有需要注意的是,一定要先把擴展的 User Model 先進行數據遷移,否則會產生不必要的麻煩。而官方推薦的則可以不用考慮這個問題
4. 通過元類進行擴展
在網上還找到一種通過元類來擴展字段的方法,這邊附上鏈接 通過元類擴展 User 字段 不多講述。
5. User 字段擴展實踐
# models.py
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
class User(AbstractUser):
phone_num = models.CharField(max_length=20, blank=True, unique=True)
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_token(sender, instance, created, **kwargs):
if created:
token, created = Token.objects.get_or_create(user=instance)
# serializers.py
from rest_framework import serializers
from expert_user.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
# views.py
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from expert_user.serializers import UserSerializer
from expert_user.models import User
class RegisterView(APIView):
def post(self, request, format=None):
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
# 將密碼轉為密文,前面留下的坑,這一步必須要有,否則登錄的時候會出問題,
# 不管是哪種方法進行擴展都需要
user.set_password(user.password)
User.objects.filter(id=user.id).update(password=user.password)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# urls.py
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from expert_user import views
app_name = "user"
urlpatterns = [
url(r'^register/$', views.RegisterView.as_view(), name="register"),
]
urlpatterns = format_suffix_patterns(urlpatterns)
擴展后的返回字段如下圖二. 通過 ImageField 上傳頭像
上傳頭像之前,需要先安裝 Pillow 模塊 pip install pillow
,然后在我們的 settings.py 文件中加入如下代碼
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace("\\", "/")
接著在 model 類中加入 ImageField 屬性的字段,例如 avatar,再做數據庫遷移,如果沒有安裝 pillow 模塊,數據庫遷移是會被打斷的,然后提示你需要安裝 pillow
class CustomUser(models.Model):
# ...
avatar = models.ImageField(upload_to="/avatar/", blank=True, null=True)
數據庫遷移后修改 serializers 文件,添加 UploadAvatarSerializer,我們需要客戶端回傳的數據包括圖片,以及圖片的后綴名,如果自己去獲取后綴名,通過 pillow 轉換再取后綴可能會比較麻煩,所以直接讓前端上傳,所以我們定義的 serializer 如下:
class UploadAvatarSerializer(serializers.ModelSerializer):
avatar = serializers.ImageField(max_length=None, allow_empty_file=False, use_url=True)
suffix = serializers.CharField(allow_blank=False)
class Meta:
model = CustomUser
fields = ['avatar', 'suffix']
接著來完成我們的 view 視圖
class AvatarUploadView(APIView):
throttle_classes = ()
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
parser_classes = (parsers.FormParser, parsers.MultiPartParser,
parsers.JSONParser, parsers.FileUploadParser,)
renderer_classes = (renderers.JSONRenderer,)
def post(self, request, format='json'):
serializer = UploadAvatarSerializer(data=request.data)
if serializer.is_valid():
# 時間戳作為頭像的名稱
name = str(time.time()).split('.')[0].strip()
# 上傳頭像的后綴
suffix = request.data['suffix'].split(r'"')[1].strip()
# 圖像名稱
avatar_name = "avatar/{}/{}.{}".format(request.user.username, name, suffix)
# pillow 打開圖像,保存副本
avatar = Image.open(request.data['avatar'])
# 判斷文件的父文件是否存在,不存在創建
if not os.path.exists('media/avatar/' + request.user.username):
os.makedirs('media/avatar/' + request.user.username)
# 文件保存的具體路徑
avatar_file_path = os.path.join(settings.MEDIA_ROOT, avatar_name).replace("\\", "/")
avatar.save(avatar_file_path)
# 將保存的路徑更新到數據庫
CustomUser.objects.filter(id=request.user.id).update(avatar=avatar_file_path)
# 返回結果
return Response({'code': '200', "avatar": avatar_file_path},
status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
注釋能看懂的小伙伴就可以跳過這部分啦,沒完全懂的小伙伴繼續跟上節奏~首先我們需要添加權限,需要用戶登錄后才可以進行修改或者上傳頭像,所以指定 permission_classes 為 permissions.IsAuthenticated,可以看下 IsAuthenticated 的源碼
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
# 當用戶登錄了而且獲取認證才返回 True
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
認證由于用的是 Android 端,所以直接使用系統的 TokenAuthentication,parser_classes 和 renderer_classes 基本每個 view 都類似,但是涉及到圖片上傳,一定要添加 FileUploadParser
用戶端上傳圖片后,我們直接保留到項目下。首先需要通過 is_valid()
判斷上傳的數據是否有效,有效則通過 pillow 的 Image 模塊的 open 打開上傳圖片,否則返回 HTTP_400_BAD_REQUEST
打開上傳的圖片后會產生一個副本,這個副本用來保存到項目下,也就是用戶的頭像文件。那么就需要設計頭像的文件名,用來保存頭像的副本文件。至于如何設計每個人都不同,我的思路是每個用戶的頭像都保存到各自以用戶名為名的文件夾下,為了防止前一張頭像被覆蓋,所以用時間戳作為圖像名,然后加上客戶端回傳的后綴名就可以生成我們的圖片文件了。
通過 request.user.username
獲取用戶名,time.time()
來獲取時間戳,通過 ‘.’ 切割時間戳,然后把毫秒數省去,通過 split('.')[0]
來獲取,那么我們保存到項目下的文件名可以通過如下方式拼接生成
"avatar/{}/{}.{}".format(request.user.username, name, suffix)
在保存副本之前,需要通過 os.path.exists
判斷該文件的父文件夾是否存在,如果不存在的話就需要通過手動創建,創建完父文件夾后,就可以通過 os.path.join()
來拼接文件的完整路徑了。
定義好文件路徑后,接著通過 save()
函數保存圖片副本,還有很重要的一步就是需要把文件路徑更新到數據庫。如果是在本地測試,數據庫顯示的文件地址會是你本地項目的地址,但是當你從客戶端發出請求后,收到的會是服務器地址,沒毛病,不要以為自己保存的路徑錯了。例如我保存的地址為 C:/ProjectsFloder/PythonProjects/expert/expert_project/media/avatar/kuky_xs/1525787433.jpg
實際在客戶端返回的地址為
http://192.168.0.103:8080/media/C%3A/ProjectsFloder/PythonProjects/expert/expert_project/media/avatar/kuky_xs/1525787433.jpg
內容不多,但是一定要細心注意文件夾的名稱,一定要細心注意文件夾的名稱,一定要細心注意文件夾的名稱,重要的事說三遍,反正我在文件名上卡了好久,最后發現少加了一層,還是不夠仔細啊。
三. 補充一下
之前在討論群中,有個老鐵問如何通過存在的數據庫反向生成 model,因為比較簡單,所以這邊記順帶錄下。首先,我從豆瓣爬取下來的數據庫如下圖所示
python manage.py inspectdb
然后就會根據數據庫的數據生成 model,結果如下圖所示最后,附上另一個 DRF 練習項目的地址,本部分涉及的內容也在其中 簡易接單系統