django與django REST framework(DRF)開發筆記

前言

由於規畫上是做前後端分離的開發,django上採RESTful API,對於django本身著墨較少。以RESTful API為主,django本身只會用上admin、model。算是完全放棄templates了

不過django REST framework(後稱DRF)有些開發上仍會使用到部份django原生的功能

其實官方文件寫得相當清楚了!

這裡整理自己開發上遇到的一些坑及要注意的事項和一些使用筆記

可以使用左方的錨點快速定位

django

models.py

在model設定時間欄位,datetime.now不必加刮號。不然會變成model編譯時的時間

from django.db import models
from datetime import datetime
class myUserModel(models.model):
    created_time = models.DateTimeFiled(default=datetime.now)

若忘記給default,執行migrate時也會跳出提示問你要直接加還是回來程式改

settings.py

有自定義的user model,讓django以自定義的user model為主

建立好自己要的model樣式後,在settings.py裡全局指定到該model

AUTH_USER_MODEL = 'users.UserProfile'
                  # app名.model名

settings.py引入路徑,引用時就不必多打根目錄名字

如果有自己建立一個apps讓放所有app的話,可以在settings裡面引入路徑,這樣在使用時就可以少打一些字

import sys
sys.path.insert(0, BASE_DIR)
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

app

在app裡面,取得內建的user model

from django.contrib.auth import get_user_model

User=get_user_model()

若有自定義user model,django會優先去查找settings.pyAUTH_USER_MODEL,找不到再取得原生的


django REST framework

建議先照著DRF官方的Tutorial做一次,熟悉一下他的基本操作

會一路從最基本的功能帶到最終的功能:使用viewSet + 內建路由自動產生api路徑,只需使用少少的程式碼,就可以開發出一套RESTful API
而本身又可以自動產生說明文件,不必耗費心力維護文件

最好先熟悉django的CBV後在學習會比較容易上手

genericViewSet vs modelViewSet

modelViewSet預設就先做好所有crud,設好serializer和router後可直接使用
genericViewSet則沒有crud,要自行加入mixin才會有各crud功能

結論:crud全都要的話則使用modelViewSet;要自定義使用哪些crud的話,則使用genericViewSet

使用Router自動產生api路徑

django REST framework本身提供路由自動產生的功能
不必每新增一個功能就要自己寫路徑
在api的APP裡面新增一個urls.py

需配合viewset使用

from django.urls import include,path
from rest_framework.routers import DefaultRouter
from yourAPI.views import userViewSet,todoViewSet

router = DefaultRouter()
router.register('users',userViewSet)
router.register('todolist',todoViewSet)

urlpatterns = [
    path('',include(router.urls)),
]

在到django的url.py新增

path('api/',include('yourAPI.urls')),

Nested Router

預設DRF無此功能,官方推薦使用drf-nested-routers套件

套件官方文件寫得很清楚了!只是一開始沒理解花了不少時間測試QQ

假設以發票中獎期別分別再對應底下所擁有的發票號碼,而後方再輸入primary key時可以得到該發票的明細

可以產生/invoicesLottery/{sn}/myInvoicesList/{pk}/這種Router

當網址輸入

  1. /invoicesLottery/→得到所有期別的中獎號碼
  2. /invoicesLottery/{sn}→得到指定期別的中獎號碼明細(頭獎、二獎、三獎號碼…)
  3. /invoicesLottery/{sn}/myInvoicesList/→得到自己當期已儲存的發票清單
  4. /invoicesLottery/{sn}/myInvoicesList/{id}/→得到該發票的明細資料(比如pk、建立時間、消費記錄等)

而對應層級中若各別需指定權限,依各自指定的ViewSet而定

比如在取得第一層的/invoicesLottery/時是不限定權限,而在取得第二層的/myInvoicesList/時需驗證權限

那在invoicesLotteryViewSet中就不必寫permission_classes = (IsAuthenticated,);在第二層的myInvoicesListViewSet寫上permission_classes = (IsAuthenticated,)

作法如下

url.py

from rest_framework_nested import routers
from myAPI import myInvoicesListViewSet, # 取得user儲存自己發票的資料,即第二層路由
				  invoicesLotteryViewSet # 取得儲存在資料庫的中獎號碼的資料,即第一層路由

router = routers.SimpleRouter()
router.register('invoicesLottery',invoicesLotteryViewSet) # 註冊第一層路由 (跟drf原生用法一樣)

myInvoices_router = routers.NestedSimpleRouter(router,r'invoicesLottery',lookup='pk') 
# 第二個參數是指定要掛在哪個路由之後。lookup指定要使用viewset中哪個欄位做查詢對象

myInvoices_router.register('myInvoicesList',myInvoicesListViewSet) # 註冊第二層路由

urlpatterns = [
    path('',include(router.urls)),
    path('',include(myInvoices_router.urls)),  # 建立路徑時,仍需要將第二層的寫進去!
]

views.py

第一層invoicesLotteryViewSet的寫法可以跟一般ViewSet一樣,不必特意改

而放在第二層的invoicesViewSet,則需要修改get_queryset,補上傳入的參數

class invoicesLotteryViewSet(ModelViewSet):
  	queryset = invoicesLottery.objects.all()
    serializer_class = invoicesLotterySerializer
    # 不指定權限,不必寫permission_classes
    # 屬於第一層路由,也不必修改get_queryset
    
class myInvoicesListViewSet(ModelViewSet):
    queryset = Invoices.objects.all()
    serializer_class = InvoicesSerializer
    permission_classes = (IsAuthenticated,) # 有權限要求的第二層路由
    pagination_class = InvoicesPagination
    def get_queryset(self):
        return Invoices.objects.filter(Q(owner=self.request.user) & Q(id=self.kwargs['pk']))
      	# 後者的self.kwargs裡面的pk,是以url中傳入的lookup指定名稱為主。比如lookup="abc",這裡就要寫"abc"

關於serializer 基礎

修改DRF 頁面底下的顯示名稱

在資料欄上加入label="文字",可以修改drf的頁面底下的顯示名稱

等同於django model的verbose

serializer 的datetime可以自定義時間格式

# 若不指定,預設為2018-07-02T21:42:53
create_time = DateTimeField(read_only=True,format='%Y-%m-%d  %H:%M:%S')
# 指定後,則會變成:2018-07-02 21:42:53

serializers.SerializerMethodField

可以自定義檢查條件
function名稱固定為get_欄位名稱

abc = serializers.SerializerMethodField()

def get_abc(self,obj):
    # obj 是serializer對象(自己)
    # 一般的charFiled是直接從db取值後轉型態後回傳。
    # SerializerMethodField則是運算邏輯自己寫,再回傳計算後的結果
    pass

需要配合django原生的filter功能
在viewset中配置

from django_filters.restframework import DjangoFilterBackend # 要記得import成.restframework的
from rest_framework import filters

class GoodsListViewSet():
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer
    filter_backends = (DjangoFilterBackend,filters.searchFilter,filtersOrderingFielter)
    search_fields= ('name','birth',help_text="搜尋")
    ording_fields=('id','created_date')

分頁 Pagination

from rest_framework.pagination import PageNumberPagination
class productsPagination(PageNumberPagination):
    page_size = 10 # 每頁最大筆數
    page_size_query_param = 'page_size'
    page_query_param="p" # 修改使用的參數名稱,預設是page
    max_page_size = 100  # 最多回傳筆數

接著再要使用此calss做分頁條件的viewset中
新增如下

pagination_class = productsPagination

Pagination 增加總頁數(或自定義回傳的資料)

from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response

class InvoicesPagination(PageNumberPagination):
    page_size = 10 # 每頁最大筆數
    max_page_size = 100  # 最多回傳筆數
    def get_paginated_response(self, data):
        return Response({
            'links': {
               'next': self.get_next_link(),
               'previous': self.get_previous_link()
            },
            'count': self.page.paginator.count,
            'total_pages': self.page.paginator.num_pages,
            'results': data
        })

自動產生說明文件

urls.py

from rest_framework.documentation import include_docs_urls
#在url配置
    path(r'docs/', include_docs_urls(title="只是個打字的")),

在model或serializer上,寫help_text,文檔裡會自動新增至description裡

※文件的資訊是從serializer來,若跳過此步自定義api的話,無法自動產生

對於需要權限的api,左下方的Authentication可以測試登入

限制API訪問速率(避免爬蟲過量爬取)

settings.py裡新增

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

AnonRateThrottle:暱名用戶,用ip來判
UserRateThrottle:登入用戶,用session來判斷

超過限制後,會回傳http 429錯誤

修改API傳入參數

預設DRF是以Primary Key為主,若想要以其他欄位當參數的話
在viewset裡輸入

lookup_field='mobile'

傳入參數就會由/api/users/{id}變成/api/users/{mobile}嘍!

POST資料,獲得當下USER

寫在serializer裡

#user即model的user變數
user = serializers.HiddenField(
    default=serializers.CurrentUserDefault()
)

serializer呼叫serializer時,將圖片網址補上域名

serializer預設是不會自動加上域名
平常使用時,都會自動傳入。所以不必另外寫
要在補上參數context={'request': self.context['request']}

原理:原始碼在context有看到request時,就會自動將域名補上

cache

使用套件drf-extensions,有強化許多功能!具體細節參考官方文件

from rest_framework_extensions.cache.mixins import CacheResponseMixin

在viewset繼承CacheResponseMixin
繼承cache,盡量放在第一個

signals

apps.py

from django.apps import AppConfig

class UsersConfig(AppConfig):
    name = 'users'
    verbose_name = "用戶管理"
# 加ready後,import signals
    def ready(self):
        import users.signals

新建signals.py,裡面寫所有signals

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model

User = get_user_model()

@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
    if created:
        password = instance.password
        instance.set_password(password) # password儲存加密後的密文至userModel
        instance.save()

動態調整permission_classes、serializer_class

假設有一個使用情況:
UserViewSet在get與post時有權限的分別
create(post)建立帳號時權限是AllowAny
retrieve(get)取得個人資料時是IsAuthenticated

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication )

    def get_permissions(self):
        if self.action == "retrieve":
            return [permissions.IsAuthenticated()]
        elif self.action == "create":
            return []
        return []
    def get_serializer_class(self):
        if self.action == "retrieve":
            return UserDetailSerializer
        elif self.action == "create":
            return UserRegSerializer

        return UserDetailSerializer