一. 资源的序列化和反序列化准备
把内部格式转换为JSON格式的过程称为序列化,我们将资源序列化的函数定义在各模型类中,提供给客户端的资源表示没有必要与数据库模型字段完全一致。返回的数据中除了自身描述,我们还添加了返回其它资源的URL:
class User(UserMixin, db.Model):
def to_json(self):
json_user = {
'self_url': url_for('api.get_user', id=self.id),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts_url': url_for('api.get_user_posts', id=self.id),
'followed_posts_url': url_for('api.get_followed_posts', id=self.id),
'post_count': self.posts.count()
}
return json_user
class Post(db.Model):
def to_json(self):
json_post = {
'self-url': url_for('api.get_post', id=self.id),
'body': self.body,
'body_html': self.body_html,
'author_url': url_for('api.get_user', id=self.author_id),
'comments_url': url_for('api.get_post_comments', id=self.id),
'comment_count': self.comments.count()
}
return json_post
class Comment(db.Model):
def to_json(self):
comment_json = {
'self_url': url_for('api.get_comment', id=self.id),
'post_url': url_for('api.get_post', id=self.post_id),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author_url': url_for('api.get_user', id=self.author_id)
}
return comment_json
序列化的逆向操作称为反序列化。把JSON反序列化成模型时,客户端提供的数据可能无效,错误或者多余。因此在进行反序列化时进行参数验证是必不可少的。
class Post(db.Model):
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body.strip() == '':
raise ValidationError('The post body is Invalid.')
return Post(body=body)
class Comment(db.Model):
@staticmethod
def from_json(json_comment):
body = json_comment.get('body')
if body is None or body.strip() == '':
raise ValidationError('Invalid Comment')
return Comment(body=body)
二. 实现资源的各个端点,并对大型资源集合进行分页
下面我们将分别实现如下API资源
用户资源端点的实现:
from flask import jsonify, request, current_app, url_for
from . import api
from ..models import User, Post
@api.route('/users/<int:id>')
def get_user(id):
user = User.query.get_or_404(id)
return jsonify(user.to_json())
@api.route('/users/<int:id>/posts/')
def get_user_posts(id):
user = User.query.get_or_404(id)
page = request.args.get('id', 1, type=int)
pagination = user.posts.order_by(Post.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_user_posts', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_user_posts', id=id, page=page+1)
first = url_for('api.get_user_posts', id=id, page=1)
last = url_for('api.get_user_posts', id=id, page=pagination.pages)
return jsonify({
'posts': [post.to_json() for post in posts],
'first': first,
'next': next,
'prev': prev,
'last': last,
'count': pagination.total,
'pages': pagination.pages
})
@api.route('/users/<int:id>/followed/posts/')
def get_followed_posts(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate(
page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_followed_posts', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_followed_posts', id=id, page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev': prev,
'next': next,
'count': pagination.total,
'pages': pagination.pages,
'first': url_for('api.get_followed_posts', id=id, page=1),
'last': url_for('api.get_followed_posts', id=id, page=pagination.pages)
})
文章资源端点的实现:
from flask import jsonify, request, g, url_for,current_app
from ..models import Post, Permission
from .. import db
from . import api
from .decorators import permission_required
from .errors import forbidden
@api.route('/posts/')
def get_posts():
page = request.args.get('page', 1, type=int)
pagination = Post.query.order_by(Post.timestamp.desc()).paginate(
page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_posts', page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_posts', page=page+1)
return jsonify({
'posts': [ post.to_json() for post in posts ],
'prev': prev,
'next': next,
'count': pagination.total,
'pages': pagination.pages,
'first': url_for('api.get_posts', page=1),
'last': url_for('api.get_posts', page=pagination.pages)
})
@api.route('/posts/<int:id>')
def get_post(id):
post = Post.query.get_or_404(id)
return jsonify(post.to_json())
@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE)
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user
db.session.add(post)
db.session.commit()
return jsonify(post.to_json()), 201, {'Location': url_for('api.get_post', id=post.id)}
@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE)
def edit_post(id):
post = Post.query.get_or_404(id)
if g.current_user != post.author and not g.current_user.can(Permission.ADMIN):
return forbidden('Insufficient permissions')
post.body = request.json.get('body', post.body)
db.session.commit()
return jsonify(post.to_json())
第一个路由处理获取文章集合的请求,使用列表推导生成文章的JSON版本,并使用paginate分页返回。prev_url和next_url分别是前一页和后一页的URL。如果某个方向没有分页了,则相应的字段为None。count是集合中元素的总数,pages是总页数。
博客资源的POST请求处理程序把一篇博客文章插入数据库,返回201状态码,并把Location首部的值设为刚创建的资源的URL。注意,为了便于客户端操作,响应的主体中还包含了新建的资源。如此一来,客户端就无需在创建资源后再发起一次GET请求来获取资源。
在创建和修改文章资源时,我们还使用到了一个装饰器:permission_required装饰器,用来检查权限,拒绝未授权用户访问相关视图:
app/api/decorators.py:permission_required装饰器
from functools import wraps
from flask import g
from .errors import forbidden
def permission_required(permission):
def decorate(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.current_user.can(permission):
return forbidden('Insufficient permissions')
return f(*args, **kwargs)
return decorated_function
return decorate
此外,视图函数中还需要检查用户是文章的作者或者管理员,如果这种检查要应用到多个视图函数中,为避免代码重复,也可以定义相关的装饰器。
comment资源端点的实现与post基本一致,不再赘述,直接贴出代码:
from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Comment, Post, Permission
from . import api
from .decorators import permission_required
@api.route('/comments/')
def get_comments():
page = request.args.get('page', 1, type=int)
pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
comments = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_comments', page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_comments', page=page+1)
return jsonify({
'comments': [ comment.to_json() for comment in comments ],
'prev': prev,
'next': next,
'total': pagination.total,
'pages': pagination.pages,
'first': url_for('api.get_comments', page=1),
'last': url_for('api.get_comments', page=pagination.pages)
})
@api.route('/comments/<int:id>')
def get_comment(id):
comment = Comment.query.get_or_404(id)
return jsonify(comment.to_json())
@api.route('/posts/<int:id>/comments/')
def get_post_comments(id):
post = Post.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
comments = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_post_comments', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_post_comments', id=id, page=page+1)
return jsonify({
'comments': [comment.to_json() for comment in comments],
'prev': prev,
'next': next,
'total': pagination.total,
'pages': pagination.pages,
'first': url_for('api.get_post_comments', id=id, page=1),
'last': url_for('api.get_post_comments', id=id, page=pagination.pages)
})
@api.route('/posts/<int:id>/comments/', methods=['POST'])
@permission_required(Permission.COMMENT)
def new_post_comment(id):
post = Post.query.get_or_404(id)
comment = Comment.from_json(request.json)
comment.author = g.current_user
comment.post = post
db.session.add(comment)
db.session.commit()
return jsonify(comment.to_json()), 201, {'Location': url_for('api.get_comment', id=comment.id)}