博文接口實現–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__ = __repr__
- 注意: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()方法就是返回一個內建的
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映射配置中
- 本質上,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 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
-
事務的使用方法
-
裝飾器用法
@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/
需要先登錄
- 帶jwt訪問
文章接口實現
- 根據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)