天天看點

博文接口實作--Django播客系統(八)

博文接口實作–Django播客系統(八)

文章目錄

  • ​​博文接口實作--Django播客系統(八)​​
  • ​​建立博文應用​​
  • ​​釋出接口實作​​
  • ​​顯示事務處理​​
  • ​​文章接口實作​​
  • ​​清單頁接口實作​​
  • ​​改寫校驗函數​​
  • 功能分析
POST /posts/ 文章釋出,試圖類PostView

請求體 application/json
{
    "title":"string",
    "content":"string"
}

響應
201 釋出成功
400 請求資料錯誤      
GET /posts/(\d+) 檢視指定文章,試圖函數getpost

響應
200 成功傳回文章内容
404 文章不存在      
GET /posts/ 文章清單頁,視圖類PostView

響應
200 成功傳回文章清單      

建立博文應用

  • ​python manage.py startapp post​

  • 注意:一定要把應用post加入到settings.py中,否則不能遷移
# djweb/settings.py中修改
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user',
    "post"
]      
  • 添加對應路由
  1. 修改djweb/urls.py檔案
# 修改djweb/urls.py檔案
urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^$',index),
    re_path(r'^index$',index),#不同路徑可以指向同一個函數執行
    re_path(r'^users/',include("user.urls")),
    re_path(r'^posts/',include("post.urls"))
]      
  1. 建立post/urls.py檔案
# 建立post/urls.py檔案 配置路由資訊
from django.conf.urls import re_path
from .views import PostView,getpost

urlpatterns = [
    re_path(r'^$',PostView.as_view()), #/posts/  視圖函數PostView
    re_path(r'^(\d+)$',getpost),
]      
  1. 修改post/views.py檔案
from django.http import HttpResponse,HttpRequest,JsonResponse
from django.views.decorators.http import require_GET
from django.views import View

class PostView(View):
    pass

def getpost(request:HttpRequest,id):
    print(id)
    return JsonResponse({},status=201)      
  • 建構資料庫模型
  1. 修改post/models.py檔案
from django.db import models
from user.models import User

# post表
class Post(models.Model):
    class Meta:
        db_table = "post"
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=256,null=False)
    postdate = models.DateTimeField(null=False)
    # 從post檢視作者,從post檢視内容
    author = models.ForeignKey(User,on_delete=models.PROTECT) #指定外鍵,migrate會生成author_id字段
    # self.content可以通路Content執行個體,其内容是self.content.content

    def __repr__(self):
        return '<Post {} {} {} {} >'.format(
            self.id,self.title,self.author_id,self.content
        )

    __str__ = __repr__

# content表
class Content(models.Model):
    class Meta:
        db_table = "content"
    # 沒有主鍵,會自動建立一個自增主鍵
    post = models.OneToOneField(Post,primary_key=True,on_delete=models.PROTECT) #一對一,這邊會有一個外鍵post_id引用post.id
    content = models.TextField(null=False)

    def __repr__(self):
        return "<Content {} {} >".format(self.post.pk,self.content[:20])

    __str__ =      
  1. 注意:on_delete在Django2.0開始,on_delete必須提供,參考​​https://docs.djangoproject.com/en/1.11/ref/models/fields/#django.db.models.ForeignKey.on_delete​​
  1. models.CASCADE 級聯删除。Django在DELETE級聯上模拟SOL限制的行為,并删除包含外鍵的對象。
  2. models.PROTECT 通過引發ProtectedError (django.db.IntegrityError的子類)防止删除引用的對象。
  3. models.SET_NULL 設定外鍵為空;這隻有在null為真時才有可能。
  4. models.SET_DEFAULT 将外鍵設定為其預設值;必須為外鍵設定預設值。
  5. models.DO_NOTHING 不采取任何行動。如果資料庫後端強制執行引用完整性,這将導緻IntegrityError,除非手動向資料庫字段添加一個SOL ON DELETE限制。
  • 視圖分類
  1. function-based view 視圖函數:視圖功能有函數實作
  2. class-based view 視圖類:視圖功能有基于django.views.View類的子類實作
  1. django.views.View類原理
  • django.views.View類本質就是一個對請求方法分發到與請求方法同名函數的排程器。
  • django.views.View類,定義了http的方法的小寫名稱清單,這些小寫名稱其實就是處理請求的方法名的小寫。as_views()方法就是傳回一個内建的​

    ​view(request,*args,**kwargs)​

    ​​函數,本質上其實還是url映射到了函數上,隻不過view函數内部會調用​

    ​dispatch(request,*args,**kwargs)​

    ​分發函數。
  • dispatch函數中使用request對象的請求方法小寫和http_method_names中允許的HTTP方法比對,比對說明是正确的HTTP請求方法,然後嘗試再View子類中找該方法,調用後傳回結果。找不到該名稱方法,就執行http_method_not_allowed方法傳回405狀态碼
  • 看到了getattr等反射函數,說明基于反射實作的。
  • as_view()方法,用在url映射配置中
  1. 本質上,as_view()方法還是把一個類僞裝成了一個視圖函數。
  2. 這個視圖函數,内部使用了一個分發函數,使用請求方法名稱吧請求分發給存在的同名函數處理。
# 修改post/urls.py檔案
from django.conf.urls import re_path
from .views import PostView,getpost
from user.views import authenticate #登入驗證裝飾器

urlpatterns = [
    # 路徑/posts/
    # View類調用as_view()之後類等價一個視圖函數,可以被裝飾
    # 裝飾器函數傳回新函數
    re_path(r'^$',authenticate(PostView.as_view())), #/posts/  視圖函數PostView
    re_path(r'^(\d+)$',getpost),
]      
  • 但是這種方式适合吧PostView類所有方法都認證,但是實際上就post方法要認證。是以,authenticate還是需要加載到post方法上去。是以,要修改authenticate函數
# 修改user/views.py檔案中對應内容
# 登入驗證裝飾器
def authenticate(viewfunc):
    def wrapper(*args):
        *s,request = args #保證最後一個取到request對象
        print(s)
        print(request)

        # 認證越早越好
        jwtheader  = request.META.get(settings.AUTH_HEADER,"")
        # print(request.META.keys())
        # print(request.META["HTTP_COOKIE"].get(settings.AUTH_HEADER,""))
        # print(request.META["HTTP_COOKIE"])
        print("-   ------------")
        if not jwtheader:
            return HttpResponse(status=401)
        print(jwtheader)
        # 解碼
        try:
            payload = jwt.decode(jwtheader,settings.SECRET_KEY,algorithms=["HS256"])
            # payload = "aa"
            print(payload)
        except Exception as e: #解碼有任何異常,都不能通過認證
            print(e)
            return HttpResponse(status=401)

        # 是否過期ToDO
        print("- "*30)
        try:
            user_id = payload.get("user_id",0)
            if user_id == 0:
                return HttpResponse(status=401)
            user = User.objects.get(pk=user_id)
            request.user = user
        except Exception as e:
            print(e)
            return HttpResponse(status=401)

        response = viewfunc(*args) #參數解構
        return response
    return      
  • 修改post/views.py檔案
# post/views.py
from django.http import HttpResponse,HttpRequest,JsonResponse
from django.views.decorators.http import require_GET
from django.views import View
from user.views import authenticate

class PostView(View): #不需要裝飾器決定請求方法了
    def get(self,request:HttpRequest): #擷取全體文章走這裡
        print("get ~~~~~~~~~~~~~~~~~~")
        return JsonResponse({},status=200)

    # 注意:由于PostView類中并不是所有方法都需要登入認證,所有将urls路徑映射中的登入認證去掉了,在這裡加上。
    @authenticate
    def post(self,request:HttpRequest):#送出文章資料走這裡
        print("post ++++++++++++++++++")
        return JsonResponse({}, status=200)

@require_GET
def getpost(request:HttpRequest,id):
    print(id)
    return JsonResponse({},status=201)      

釋出接口實作

  • 使用者從浏覽器端送出json資料,包含title,content.
  • 送出博文需要認證使用者,從請求的header中驗證jwt。
  • ​request:POST 标題、内容-》@authenticate ->視圖 post -> json新文章對象​

  • 建立工具包,調整jsonify函數,放入工具包内
# utils/__init__.py
# 篩選所需要的字段
def jsonify(instance,allow=None,exclude=[]):
    # allow優先,如果有,就使用allow指定的字段,這時候exclude無效
    # allow如果為空,就全體,但要看看有exclude中的要排除
    modelcls = type(instance)
    if allow:
        fn = (lambda x:x.name in allow)
    else:
        fn = (lambda x:x.name not in exclude)
    # from django.db.models.options import Options
    # m:Options = modelcls._meta
    # print(m.fields,m.pk)
    # print("----------")
    return {k.name:getattr(instance,k.name) for k in filter(fn,modelcls._meta.fields)}      

顯示事務處理

  • Django中每一次save()調用就會自動送出,那麼在第一次事務送出後如果第二次送出前出現異常,則post.save()不會復原。為了解決,可以使用事務的原子方法:參考​​https://docs.djangoproject.com/en/1.11/topics/db/transactions/#django.db.transaction.atomic​​
  • 事務的使用方法
  1. 裝飾器用法
@transaction.atomic #裝飾器用法
def viewfunc(request):
    # This code executes inside a transaction
    do_stuff()      
  1. 上下文用法
def viewfunc(request):
     # This code executes in autocommit mode (Django's default).
    do_stuff()

    with transaction.atomic(): #上下文用法
        # This code executes inside a transaction.
        do_more_stuff()      
  • 修改​

    ​post/views.py​

    ​檔案
# 修改`post/views.py`檔案
class PostView(View): #不需要裝飾器決定請求方法了
    def get(self,request:HttpRequest): #擷取全體文章走這裡
        print("get ~~~~~~~~~~~~~~~~~~")
        return JsonResponse({},status=200)

    # 注意:由于PostView類中并不是所有方法都需要登入認證,所有将urls路徑映射中的登入認證去掉了,在這裡加上。
    @authenticate 
    def post(self,request:HttpRequest):#送出文章資料走這裡
        print("post ++++++++++++++")
        post = Post()
        content = Content()

        try:
            payload = simplejson.loads(request.body)
            post.title = payload["title"]
            post.author = User(id=request.user.id)
            # post.author = request.user
            post.postdate = datetime.datetime.now(
                datetime.timezone(datetime.timedelta(hours=8))
            )
            with transaction.atomic(): #原子操作
                post.save()
                content.post = post
                content.content = payload["content"]
                content.save()
            return JsonResponse({
                "post":jsonify(post,allow=["id","title"])
            },status=200)
        except Exception as e:
            print(e)
            return HttpResponse(status=400)      
  • 啟動後測試
  1. 帶jwt通路​

    ​http://127.0.0.1:8000/posts/​

    ​​需要先登入
  2. 博文接口實作--Django播客系統(八)

文章接口實作

  • 根據post_id查詢博文并傳回。
  • 如果博文隻能作者看到,就需要認證,本次是公開,即所有人都能看到,是以不需要認證。同樣,下面的list接口也是不需要認證的。
  • ​request: GET post's id-> getpost 視圖函數 -> Json post + content​

# 修改`post/views.py`檔案
@require_GET
def getpost(request:HttpRequest,id):
    try:
        id = int(id)
        post = Post.objects.get(pk=id) #only one
        return JsonResponse({
            "post":{
                "id":post.id,
                "title":post.title,
                "author":post.author.name,
                "author_id":post.author_id, #post.author.id
                "postdate":post.postdate.timestamp(),
                "content":post.content.content
            }
        })
    except Exception as e:
        print(e)
        return HttpResponse(status=404)      

清單頁接口實作

  • 發起GET請求,通過查詢字元串​

    ​http://url/posts/?page=2​

    ​查詢第二頁資料
  • ​request: GET ?pate=5&size=20 ->視圖 get -> json文章清單​

GET /posts/?page=3&size=20 文章清單,視圖類PostView

響應
200 成功傳回文章清單      
  • 完善分頁
  1. 分頁資訊,一般有:目前頁/總頁數、每頁條數,記錄總數。
  • 目前頁:page
  • 每頁條數:size ,每頁最多多少行
  • 總頁數:pages = math.ceil(count/size)
  • 記錄總數:total,從select * from table來
# 修改post/views.py檔案
from django.http import HttpResponse,HttpRequest,JsonResponse
from django.views.decorators.http import require_GET
from django.views import View
from user.views import authenticate
from post.models import Post,Content
from user.models import User
import simplejson,datetime,math
from django.db import transaction
from utils import jsonify

class PostView(View): #不需要裝飾器決定請求方法了
    def get(self,request:HttpRequest): #擷取全體文章走這裡
        print("get ~~~~~~~~~~~~~~~~~~")
        try: #頁碼
            page = int(request.GET.get("page",1))
            page = page if page >0 else 1
        except:
            page = 1

        try: #每頁條數
            #注意,這個資料不要輕易讓浏覽器端改變,如果允許改變,一定要控制範圍
            size = int(request.GET.get("size",20))
            size = size if size >0 and size <101 else 20
        except:
            size = 20

        try: #每頁條目數
            start = (page - 1) * size
            posts = Post.objects.order_by("-pk")
            print(posts.query)
            total = posts.count()
            posts = posts[start:start + size]
            print(posts.query)
            return JsonResponse({
                "posts":[jsonify(post,allow=["id","title"]) for post in posts],
                "pagination":{
                    "page":page,
                    "size":size,
                    "total":total,
                    "pages":math.ceil(total / size)
                }
            })
        except Exception as e:
            print(e)
            return HttpResponse(status=400)      
  • 也可以使用Django提供的Paginator類來完成。
  • Paginator文檔​​https://docs.djangoproject.com/en/1.11/topics/pagination/​​
  • 但是,還是自己更加簡單明了些。

改寫校驗函數

  • 修改post/views.py檔案
# 修改post/views.py檔案
def validate(d:dict,name:str,type_func,default,validate_func):
    try:
        ret = type_func(d.get(name,default))
        ret = validate_func(ret,default)
    except:
        ret = default
    return ret

class PostView(View): #不需要裝飾器決定請求方法了
    def get(self,request:HttpRequest): #擷取全體文章走這裡
        print("get ~~~~~~~~~~~~~~~~~~")
        # 頁碼
        page = validate(request.GET,"page",int,1,lambda x,y:x if x>0 else y)
        # 每頁條數
        #注意,這個資料不要輕易讓浏覽器端改變,如果允許改變,一定要控制範圍
        size = validate(request.GET,"page",int,20,lambda x,y:x if x>0 and x<101 else y)

        try: #每頁條目數
            start = (page - 1) * size
            posts = Post.objects.order_by("-pk")
            print(posts.query)
            total = posts.count()

            posts = posts[start:start + size]
            print(posts.query)
            return JsonResponse({
                "posts":[jsonify(post,allow=["id","title"]) for post in posts],
                "pagination":{
                    "page":page,
                    "size":size,
                    "total":total,
                    "pages":math.ceil(total / size)
                }
            })
        except Exception as e:
            print(e)
            return HttpResponse(status=400)