1. 功能分析
POST /posts/ 文章发布,视图类PostView
请求体 application/json
{
"title":"string",
"content":"string"
}
响应
201 发布成功
400 请求数据错误
GET /posts/(\d+) 查看指定文章,视图函数getpost
响应
200 成功返回文章内容
404 文章不存在
GET /posts/ 文章列表页,视图类PostView
响应
200 成功返回文章列表
2. 创建博文应用
$ python manage.py startapp post
注意:一定要把应用post加入到settings.py中,否则不能迁移
路由:
# blog/urls.py
urlpatterns = [
# 下面三个有一个即可
url(r'^admin/', admin.site.urls), # url在 2.x版本 re_path
url(r'^$', index),
url(r'^index$', index),
url(r'^users/', include('user.urls')), # 多级路由,查看include的原码
url(r'^posts/', include('post.urls'))
]
# post/urls.py
from django.conf.urls import url
from .views import PostView, get_post, put, delete
# from user.views import authenticate
urlpatterns = [
# 路径/posts/
# View类调用as_view之后类等价一个视图函数,可以被装饰
# 装饰器函数新的函数
# url(r'^$', authenticate(PostView.as_view())), # 如果所有视图函数都需要认证,可以使用此种方法
url(r'^$', PostView.as_view()),
url(r'^(\d+)$', get_post),
url(r'^put$', put),
url(r'^delete$', delete)
]
模型:
from django.db import models
from user.models import User
class Post(models.Model):
class Meta:
db_table = 'post'
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=128, null=False) # 文章标题不可以为空
postdate = models.DateTimeField(null=False)
author = models.ForeignKey(User, on_delete=models.PROTECT) # 指定外键,migrate会生成author_id字段
delete = models.BooleanField(null=False)
# self.content是Content的实例,其内容是self.content.content
def __repr__(self):
return "<Post {} {} {} {}>".format(self.id, self.title, self.postdate, self.content)
__str__ = __repr__
class Content(models.Model):
class Meta:
db_table = "content"
# 没有主键,会自动创建一个自增主键
post = models.OneToOneField(Post, primary_key=True, on_delete=models.PROTECT)
# 一对一,这边会有一个外键post_id引用post.id;models.PROTECT是不允许删除
content = models.TextField(null=False)
delete = models.BooleanField(null=False)
def __repr__(self):
return "<Content {} {}>".format(self.post.id, self.content[:20])
__str__ = __repr__
on_delete:
- models.CASCADE
- models.PROTECT
- models.SET_NULL
- models.DO_NOTHING
- 在Django 2.0开始,on_delete必须提供,
2.1 视图分类
- function-based view 视图函数:视图功能有函数实现
- class-based view 视图类:视图功能有基于django.views.View类的子类实现
django.views.View类原理:django.views.View类本质就是一个对请求方法分发到与请求方法同名函数的调度器。 django.views.View类,定义了http的方法的小写名称列表,这些小写名称其实就是处理请求的方法名的小写。as_view()方法就是返回一个内建的 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()方法还是把一个类伪装成了一个视图函数。
这个视图函数,内部使用了一个分发函数,使用请求方法名称把请求分发给存在的同名函数处理。
但是这种方式适合把PostView类所有方法都认证,但是实际上就post方法要认证。所以,authenticate还是需要加载到post方法上去。因此,要修改authenticate函数。
2.2 发布接口实现
用户从浏览器端提交Json数据,包含title,content。提交博文需要认证用户,从请求的header中验证jwt。 新建一个工具包,将jsonify函数挪进去。
# utils/__init__.py
def json_ify(instance, allow=None, exclude=()): # exclude是排除在外的字段
"""此函数的作用是过滤输出字段"""
# allow优先,如果有,就使用allow指定的字段,这时候exclude无效
# allow如果为空,就全体,但要将exclude中的排除
cls = type(instance)
if allow:
fn = (lambda x: x.name in allow)
else:
fn = (lambda x: x.name not in exclude)
return {k.name: getattr(instance, k.name) for k in filter(fn, cls._meta.fields)}
d = {
'400': {'error': "用户或密码错误"},
'401': {'error': "用户输入错误"},
'404': {'error': 'Not found'},
'204': {'Good!': '删除成功'}
}
@authenticate
def post(self, request: HttpRequest): # 提交文章数据走这里
print('post +++++++++++')
post = Post()
content = Content()
try:
payload = simplejson.loads(request.body)
print(payload, '~~~~~~~~~~~++++++++++++')
post.title = payload['title']
text = payload['content']
# post.author = User(id=request.user.id) # 这是一个对象,包含user的id、name、email、password
post.author = request.user # 一个对象,推荐使用这种写法
post.postdate = datetime.datetime.now(
datetime.timezone(datetime.timedelta(hours=8))) # 数据库存放的是UTC(世界统一时间),与北京时间差8小时
# post.save() # post已经save后(注意save会自动提交),如果出现异常,那么异常后的代码是执行不了的,(content是没法save的)
# raise Exception() # 抛出异常
# 该怎么解决呢?利用事务的原子性;另外还要注意,post必须要小save了,content才能save,两者顺序不能换
with transaction.atomic(): # 原子操作
post.save() # save完后,post表的id就有了
content.post = post # 考虑此处为什么是post,而不是post.id ==》此处的写法是Content定义时,就是一对一引用Post
content.content = text
# print(content.post, '^^^^^^^^^^^^') # 注意这两句是不能打印的,直接报错'str' object is not callable
# print(content.content, '###########')
content.save()
return JsonResponse({
'post': json_ify(post, allow=['id', 'title'])
}, status=201) # 增加成功,状态码用201,不要随便修改
except Exception as e:
logging.error(e)
return JsonResponse({'error': '用户输入错误'}, status=400)
显示事务处理:Django中每一次save()调用就会自动提交,那么上例中第一次事务提交后如果第二次提交前出现异常,则 post.save()不会回滚。解决办法可以使用事务的原子方法,参考 https://docs.djangoproject.com/en/1.11/topics/db/transactions/#django.db.transaction.atomic
2.3 文章接口实现
根据post_id查询博文并返回。这里需要认证吗? 如果博文只能作者看,就需要认证。我们这里的公开给所有人看,所以不需要认证。同样,下面的list接口也是不 需要认证的。
@require_GET
def get_post(request: HttpRequest, _id): # 详情页,查看一篇文章,与文章相关的所有信息都应该返回
print(request)
# print(_id, type(_id)) # 类型是字符串
try:
_id = int(_id)
post = Post.objects.get(pk=_id) # only one
print(post, '101010100101010100')
return JsonResponse({
'post':
{
'id': post.id,
'title': post.title,
'postdate': int(post.postdate.timestamp()),
'author': post.author.name,
'author_id': post.author_id, # post.author.id
'content': post.content.content # 后面的content.content就是上面的text
}
})
except Exception as e:
logging.error(e)
return JsonResponse(d['404'], status=404)
2.4 列表页接口实现
发起GET请求,通过查询字符串 http://url/posts/?page=2 查询第二页数据。
进一步完整分页功能:
GET /posts/?page=3&size=20 文章列表页,视图类PostView
响应
200 成功返回文章列表
完善分页:分页信息,一般有:当前页/总页数、每页条数、记录总数。 当前页:page ,每页条数:size,每页最多多少行 ,总页数:pages = math.ceil(count/size) ,记录总数:total,从select * from table来。
def validate(dic: dict, name: str, default, validate_func):
try:
r = int(dic.get(name, default))
ret = validate_func(r, default)
except Exception as e:
logging.error(e)
ret = default
return ret
def get(self, request: HttpRequest): # 获取全体文章走这里
print('get ~~~~~~~~~~~')
page = validate(request.GET, 'page', 1, lambda x, y: x if x > 0 else y)
print(page)
# try: # 页码
# page = int(request.GET.get('page', 2))
# page = page if page > 0 else 1
# except Exception as e:
# logging.error(e)
# page = 1
#
# try: # 每页条数(每页最多能写多少行)
# # 注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
# size = int(request.GET.get('size', 2))
# size = size if 0 < size < 101 else 20
# except Exception as e:
# logging.error(e)
# size = 20
size = validate(request.GET, 'size', 20, lambda x, y: x if 0 < x < 101 else y)
try: # 每页条目数
start = (page - 1) * size
posts = Post.objects.order_by('-pk') # 根据主键降序排列
total = posts.count()
posts = posts[start: start + size]
print(posts.query, '77777777777777777')
# SELECT `post`.`id`, `post`.`title`, `post`.`postdate`, `post`.`author_id`
# FROM `post` ORDER BY `post`.`id` DESC LIMIT 2 OFFSET 2 7777777777
return JsonResponse({
'posts': [
json_ify(post, allow=['id', 'title'])
for post in posts
], # 列表解析式
'pagination': {
'page': page,
'size': size,
'total': total,
'pages': math.ceil(total / size)
}
}, status=200)
except Exception as e:
logging.error(e)
return JsonResponse(d['400'], status=400)
博文的更新、修改、删除代码:
# 更新、修改
@authenticate
def put(request: HttpRequest):
print('put ~~~~~~~~~~~~~~~~~~~')
# post = Post()
# content = Content()
try:
payload = simplejson.loads(request.body)
_id = payload["id"]
post = Post.objects.get(pk=_id, author_id=request.user.id)
content = Content.objects.get(pk=_id)
print(payload, '6666666666')
title = payload['title']
post.title = title
content.content = payload['content']
post.postdate = datetime.datetime.now(
datetime.timezone(datetime.timedelta(hours=8)))
with transaction.atomic():
post.save()
content.save()
return JsonResponse({
'post': {
'author': post.author.name,
'author_id': post.author_id,
'title': post.title,
'postdate': post.postdate,
'content': post.content.content
}
}, status=201)
except Exception as e:
logging.error(e)
return JsonResponse(d['400'], status=400)
@authenticate
def delete(request: HttpRequest):
print('delete ~~~~~~~~~~~~~~')
print(request)
try:
payload = simplejson.loads(request.body)
_id = payload['id']
post = Post.objects.get(pk=_id)
content = Content.objects.get(pk=_id)
# content.content = payload['content']
post.postdate = datetime.datetime.now(
datetime.timezone(datetime.timedelta(hours=8)))
author_id = post.author_id
if author_id != request.user.id:
return JsonResponse(d['400'], status=400)
post.delete = True
content.delete = True
with transaction.atomic():
post.save()
content.save()
return HttpResponse(status=204)
# return JsonResponse({
# 'post': {
# 'id': post.id,
# 'author': post.author.name,
# 'author_id': post.author_id,
# 'postdate': post.postdate,
# 'content': post.content.content
# }
#
# }, status=204)
except Exception as e:
logging.error(e)
return JsonResponse(d['400'], status=400)
博文项目源码:https://github.com/sqsltr520/python