django 自帶 user 字段擴展及頭像上傳

django 及 rest_framework 筆記鏈接如下:
django 入門筆記:環(huán)境及項目搭建
django 入門筆記:數(shù)據(jù)模型
django 入門筆記:視圖及模版
django 入門筆記:Admin 管理系統(tǒng)及表單
django 入門筆記:通用視圖類重構(gòu)視圖
django_rest_framework 入門筆記:Serializer
django_rest_framework 入門筆記:視圖函數(shù)重構(gòu)
django_rest_framework 入門筆記:分頁,多條件篩選及權(quán)限認(rèn)證設(shè)置
django 自帶 user 字段擴展及頭像上傳

一. 擴展 django 自帶的 User Model 字段

在實際項目中,django 自帶的 user model 的字段很多時候不能夠滿足我們的需求锌钮,可能我們需要在 user 中加入商城積分等字段奇钞,這就需要通過擴展的 user model 字段來實現(xiàn)。目前擴展 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)
    
# 當(dāng)生成 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 做些設(shè)置

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)

然后通過遷移數(shù)據(jù)庫,django 會同時生成 user 表和 user_profile 表

這個方法雖然是官方推薦的饼煞,但是個人還是覺得很麻煩,獲取 profile 的字段值還需要通過 user 表的外鍵獲取 profile 實例诗越,然后再獲取字段值砖瞧,再考慮到 django 的通過外鍵生成反向字段也需要做些處理,如果說我們只需要額外添加一個字段嚷狞,需要做那么多工作块促,我是拒絕的。

2. 通過繼承 AbstractUser 來實現(xiàn)

首先需要在 models.py 文件中創(chuàng)建自己的 user model

from django.contrib.auth.models import AbstractUser
from django.db import models

# 有個坑床未,擴展字段后密碼會明文竭翠,當(dāng)我們提交的時候需要進行設(shè)置,在后面的代碼中會提到
class User(AbstractUser):
    phone_num = models.CharField(max_length=20, unique=True, blank=True)

然后我們需要在 settings.py 文件中設(shè)置 AUTH_USER_MODEL 的值

# settings.py
AUTH_USER_MODEL = '[your_app_name].[your_user_model_name]' # 例如我這邊設(shè)置的值為 'expert.User'

在 admin 中薇搁,我們按照之前的方法照常設(shè)置就行

這種方法斋扰,需要在我們做數(shù)據(jù)庫遷移之前,先定義好我們自己的 User Model 然后將 settings.py 文件中的 AUTH_USER_MODEL 替換后再進行數(shù)據(jù)庫遷移啃洋,需要告訴 django 系統(tǒng)传货,這個項目我要用自己的 User 了,不用你的裂允。這種方法適合只需要擴展 User Model 中的字段损离,會繼承之前 AbstractUser 的全部字段,系統(tǒng)自帶的 User 也是通過繼承 AbstractUser 來實現(xiàn)的

# 系統(tǒng)的 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'

如果說系統(tǒng)自帶的 user 很多字段都不符合項目的需要貌踏,基本上需要完全替換,那目前提到的兩種方法都不適合窟勃,需要通過第三種方法來替換

3. 通過 AbstractBaseUser 替換 User

這種方法會比較麻煩祖乳,說實話前兩種方法已經(jīng)可以解決我們的問題,不到迫不得已個人覺得沒必要用這種方法

# AbstractUser 就是通過繼承 AbstractBaseUser 來實現(xiàn)的秉氧,
# 我們寫這部分時候可以通過修改 AbstractUser 的源碼實現(xiàn)

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(_('創(chuàng)建時間'), auto_now_add=True)
    is_active = models.BooleanField(_('激活狀態(tài)'), 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):
        # 具體發(fā)送郵件的邏輯可以自行查找實現(xiàn),郵箱驗證的時候需要用到
        pass
    
    def message_to_user(self):
        # 具體發(fā)送短信邏輯可以自行查找實現(xiàn)
        pass

在 UserManager 中汁咏,需要通過 create_usercreate_superuser 的方法來創(chuàng)建用戶和超級用戶亚斋,所以必須重寫這些方法才可以,這里根據(jù)系統(tǒng)的 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('必須設(shè)置郵箱')
        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 文件中設(shè)置 AUTH_USER_MODEL 為自定義的 User Model

通過 2 , 3 方法擴展 User Model 后帅刊,當(dāng)別的表和 User 表有鏈表結(jié)構(gòu)的時候,最好通過 settings.AUTH_USER_MODEL 進行導(dǎo)入漂问,而不是通過 User 直接導(dǎo)入赖瞒,還有需要注意的是,一定要先把擴展的 User Model 先進行數(shù)據(jù)遷移蚤假,否則會產(chǎn)生不必要的麻煩栏饮。而官方推薦的則可以不用考慮這個問題

4. 通過元類進行擴展

在網(wǎng)上還找到一種通過元類來擴展字段的方法,這邊附上鏈接 通過元類擴展 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()
            # 將密碼轉(zhuǎn)為密文袍嬉,前面留下的坑,這一步必須要有灶平,否則登錄的時候會出問題冬竟,
            # 不管是哪種方法進行擴展都需要
            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)

擴展后的返回字段如下圖
register_result
login_result
二. 通過 ImageField 上傳頭像

上傳頭像之前,需要先安裝 Pillow 模塊 pip install pillow民逼,然后在我們的 settings.py 文件中加入如下代碼

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace("\\", "/")

接著在 model 類中加入 ImageField 屬性的字段,例如 avatar涮帘,再做數(shù)據(jù)庫遷移拼苍,如果沒有安裝 pillow 模塊,數(shù)據(jù)庫遷移是會被打斷的调缨,然后提示你需要安裝 pillow

class CustomUser(models.Model):
    # ...
    avatar = models.ImageField(upload_to="/avatar/", blank=True, null=True)

數(shù)據(jù)庫遷移后修改 serializers 文件疮鲫,添加 UploadAvatarSerializer,我們需要客戶端回傳的數(shù)據(jù)包括圖片弦叶,以及圖片的后綴名俊犯,如果自己去獲取后綴名,通過 pillow 轉(zhuǎn)換再取后綴可能會比較麻煩伤哺,所以直接讓前端上傳燕侠,所以我們定義的 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'])
            # 判斷文件的父文件是否存在,不存在創(chuàng)建
            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)
            # 將保存的路徑更新到數(shù)據(jù)庫
            CustomUser.objects.filter(id=request.user.id).update(avatar=avatar_file_path)
            # 返回結(jié)果
            return Response({'code': '200', "avatar": avatar_file_path}, 
                            status=status.HTTP_200_OK)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

注釋能看懂的小伙伴就可以跳過這部分啦绢彤,沒完全懂的小伙伴繼續(xù)跟上節(jié)奏~首先我們需要添加權(quán)限七问,需要用戶登錄后才可以進行修改或者上傳頭像,所以指定 permission_classes 為 permissions.IsAuthenticated茫舶,可以看下 IsAuthenticated 的源碼

class IsAuthenticated(BasePermission):
    """
    Allows access only to authenticated users.
    """
    # 當(dāng)用戶登錄了而且獲取認(rèn)證才返回 True
    def has_permission(self, request, view):
        return request.user and request.user.is_authenticated

認(rèn)證由于用的是 Android 端械巡,所以直接使用系統(tǒng)的 TokenAuthentication,parser_classes 和 renderer_classes 基本每個 view 都類似饶氏,但是涉及到圖片上傳讥耗,一定要添加 FileUploadParser

用戶端上傳圖片后,我們直接保留到項目下疹启。首先需要通過 is_valid() 判斷上傳的數(shù)據(jù)是否有效古程,有效則通過 pillow 的 Image 模塊的 open 打開上傳圖片,否則返回 HTTP_400_BAD_REQUEST

打開上傳的圖片后會產(chǎn)生一個副本皮仁,這個副本用來保存到項目下籍琳,也就是用戶的頭像文件。那么就需要設(shè)計頭像的文件名贷祈,用來保存頭像的副本文件趋急。至于如何設(shè)計每個人都不同,我的思路是每個用戶的頭像都保存到各自以用戶名為名的文件夾下势誊,為了防止前一張頭像被覆蓋呜达,所以用時間戳作為圖像名,然后加上客戶端回傳的后綴名就可以生成我們的圖片文件了粟耻。

通過 request.user.username 獲取用戶名查近,time.time() 來獲取時間戳,通過 ‘.’ 切割時間戳挤忙,然后把毫秒數(shù)省去霜威,通過 split('.')[0] 來獲取蔚携,那么我們保存到項目下的文件名可以通過如下方式拼接生成

"avatar/{}/{}.{}".format(request.user.username, name, suffix)

在保存副本之前贡这,需要通過 os.path.exists 判斷該文件的父文件夾是否存在,如果不存在的話就需要通過手動創(chuàng)建眼滤,創(chuàng)建完父文件夾后赏僧,就可以通過 os.path.join()來拼接文件的完整路徑了大猛。

定義好文件路徑后,接著通過 save() 函數(shù)保存圖片副本淀零,還有很重要的一步就是需要把文件路徑更新到數(shù)據(jù)庫挽绩。如果是在本地測試,數(shù)據(jù)庫顯示的文件地址會是你本地項目的地址驾中,但是當(dāng)你從客戶端發(fā)出請求后唉堪,收到的會是服務(wù)器地址模聋,沒毛病,不要以為自己保存的路徑錯了巨坊。例如我保存的地址為 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

請求信息

內(nèi)容不多撬槽,但是一定要細(xì)心注意文件夾的名稱一定要細(xì)心注意文件夾的名稱趾撵,一定要細(xì)心注意文件夾的名稱侄柔,重要的事說三遍,反正我在文件名上卡了好久占调,最后發(fā)現(xiàn)少加了一層暂题,還是不夠仔細(xì)啊。

三. 補充一下

之前在討論群中究珊,有個老鐵問如何通過存在的數(shù)據(jù)庫反向生成 model薪者,因為比較簡單,所以這邊記順帶錄下剿涮。首先言津,我從豆瓣爬取下來的數(shù)據(jù)庫如下圖所示

數(shù)據(jù)庫數(shù)據(jù)
然后打開命令行 python manage.py inspectdb 然后就會根據(jù)數(shù)據(jù)庫的數(shù)據(jù)生成 model,結(jié)果如下圖所示
反向生成model
將生成的代碼復(fù)制到我們的 models 文件中取试,特別要注意下圈出來的部分悬槽,大功告成!

最后瞬浓,附上另一個 DRF 練習(xí)項目的地址初婆,本部分涉及的內(nèi)容也在其中 簡易接單系統(tǒng)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市猿棉,隨后出現(xiàn)的幾起案子磅叛,更是在濱河造成了極大的恐慌,老刑警劉巖萨赁,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弊琴,死亡現(xiàn)場離奇詭異,居然都是意外死亡杖爽,警方通過查閱死者的電腦和手機访雪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掂林,“玉大人,你說我怎么就攤上這事坝橡⌒喊铮” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵计寇,是天一觀的道長锣杂。 經(jīng)常有香客問我脂倦,道長,這世上最難降的妖魔是什么元莫? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任赖阻,我火速辦了婚禮,結(jié)果婚禮上踱蠢,老公的妹妹穿的比我還像新娘火欧。我一直安慰自己,他們只是感情好茎截,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布苇侵。 她就那樣靜靜地躺著,像睡著了一般企锌。 火紅的嫁衣襯著肌膚如雪榆浓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天撕攒,我揣著相機與錄音陡鹃,去河邊找鬼。 笑死抖坪,一個胖子當(dāng)著我的面吹牛萍鲸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柳击,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼猿推,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捌肴?” 一聲冷哼從身側(cè)響起蹬叭,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎状知,沒想到半個月后秽五,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡饥悴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年坦喘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片西设。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓣铣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贷揽,到底是詐尸還是另有隱情棠笑,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布禽绪,位于F島的核電站蓖救,受9級特大地震影響洪规,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜循捺,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一斩例、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧从橘,春花似錦念赶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至牺勾,卻和暖如春正罢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驻民。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工翻具, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人回还。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓裆泳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親柠硕。 傳聞我的和親對象是個殘疾皇子工禾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容