博文接口實作–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"
]
- 添加對應路由
- 修改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"))
]
- 建立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),
]
- 修改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)
- 建構資料庫模型
- 修改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__ =
- 注意:on_delete在Django2.0開始,on_delete必須提供,參考https://docs.djangoproject.com/en/1.11/ref/models/fields/#django.db.models.ForeignKey.on_delete
- models.CASCADE 級聯删除。Django在DELETE級聯上模拟SOL限制的行為,并删除包含外鍵的對象。
- models.PROTECT 通過引發ProtectedError (django.db.IntegrityError的子類)防止删除引用的對象。
- models.SET_NULL 設定外鍵為空;這隻有在null為真時才有可能。
- models.SET_DEFAULT 将外鍵設定為其預設值;必須為外鍵設定預設值。
- models.DO_NOTHING 不采取任何行動。如果資料庫後端強制執行引用完整性,這将導緻IntegrityError,除非手動向資料庫字段添加一個SOL ON DELETE限制。
- 視圖分類
- function-based view 視圖函數:視圖功能有函數實作
- class-based view 視圖類:視圖功能有基于django.views.View類的子類實作
- django.views.View類原理
- django.views.View類本質就是一個對請求方法分發到與請求方法同名函數的排程器。
- django.views.View類,定義了http的方法的小寫名稱清單,這些小寫名稱其實就是處理請求的方法名的小寫。as_views()方法就是傳回一個内建的
函數,本質上其實還是url映射到了函數上,隻不過view函數内部會調用view(request,*args,**kwargs)
分發函數。dispatch(request,*args,**kwargs)
- dispatch函數中使用request對象的請求方法小寫和http_method_names中允許的HTTP方法比對,比對說明是正确的HTTP請求方法,然後嘗試再View子類中找該方法,調用後傳回結果。找不到該名稱方法,就執行http_method_not_allowed方法傳回405狀态碼
- 看到了getattr等反射函數,說明基于反射實作的。
- as_view()方法,用在url映射配置中
- 本質上,as_view()方法還是把一個類僞裝成了一個視圖函數。
- 這個視圖函數,内部使用了一個分發函數,使用請求方法名稱吧請求分發給存在的同名函數處理。
# 修改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
- 事務的使用方法
- 裝飾器用法
@transaction.atomic #裝飾器用法
def viewfunc(request):
# This code executes inside a transaction
do_stuff()
- 上下文用法
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)
- 啟動後測試
- 帶jwt通路
需要先登入http://127.0.0.1:8000/posts/
文章接口實作
- 根據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 成功傳回文章清單
- 完善分頁
- 分頁資訊,一般有:目前頁/總頁數、每頁條數,記錄總數。
- 目前頁: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)