博文接口實現--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"))
      ]
      
    2. 新建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),
      ]
      
    3. 修改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__ = __repr__
    
    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 wrapper
    
    • 修改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()
      
    2. 上下文用法

      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/需要先登錄
      django7_001
      django7_002

文章接口實現

  • 根據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)

django7_003

列表頁接口實現

  • 發起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)

改寫校驗函數

  • 修改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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章