创建博客-REST的资源

资源和JSON的序列化转换

开发Web程序时,经常需要在资源的内部表示和JSON之间进行转换,JSON是HTTP请求和响应使用的传输格式,下例是新添加到Post类中的to_json()方法

# app/models.py
class Post(db.Model):

    #...

    def to_json(self):
        json_post = {
            "url": url_for('api.get_post', id=self.id, _external=True),
            "body": self.body,
            "doby_html": self.body_html,
            "timestamp": self.timestamp,
            "author": url_for('api.get_user', id=self.author_id,
                              _external=True),
            "comments": url_for('api.get_post_comments', id=self.id,
                              _external=True),
            "comment_count": self.comments.count()
            }
        return json_post    

url、author和comments字段分别返回各自资源的URL,因此它们使用url_for()生成,所调用的路由即将在API蓝本中定义,注意,所有的url_for()方法都指定了参数_external=True,这么做是为了生成完整的URL,而不是生成传统Web程序中经常使用的相对URL

这段代码还说明表示资源时可以使用虚构的属性,comment_count字段是博客文章的评论数量,并不是模型的真实属性,它之所以包含在这个资源中是为了便于客户端使用

User模型的to_json()方法可以按照Post模型的方式定义,如下

# app/models.py

class User(UserMixin, db.Model):
#...
    def to_json(self):
        json_user = {
            "url": url_for('api.get_post', id=self.id, _external=True),
            "username": self.username,
            "member_since": self.member_since,
            "last_seen":self.last_seen,
            "posts": url_for('api.get_user_posts', id=self.id, _external=True),
            "followed_posts": url_for('api.get_user_followed_posts'
                                      id=self.id, _external=True),
            "post_count":self.posts.count()

        }
        return json_user

为了保护隐私,这个方法中用户的某些属性没有加入响应,例如email和role,这段代码再次声明,提供给客户端的资源表示没必要和数据库模型的内部表示完全一致

把JSON转换成模型时面临的问题是,客户端提供的数据可能无效、错误或者多余,下例是从JSON格式数据创建Post模型实例的方法

# app/models.py
from app.exceptions import ValidationError

    class Post(db.Model):

    #...

    @staticmethod
    def from_json(json_post):
        body = json_post.get('body')
        if body is None or body == '':
            raise ValidationError('post does not have a body')
        return Post(body=body)

上述代码在实现过程中只选择使用JSON字典中的属性,而把body_html属性忽略了,因为只要body属性的值发生变化,就会触发一个SQLAlchemy事件,自动在服务器端渲染Markdown,除非允许客户端倒填日期(这个程序并不提供此功能),否则无需指定timestamp属性,由于客户端无权选择博客文章的作者,所以没有使用author字段,author字段唯一能使用的值是通过认证的用户,commentscomment_count属性使用数据库关系自动生成,因此其中没有创建模型所需的有用信息,最后,url字段也被忽略了,因为在这个实现中资源的URL由服务器指派,而不是客户端

如何检查错误,如果没有body字段或者其值为空,from_json()方法会抛出ValidationError异常,在这种情况下,抛出异常才是处理错误的正确处理方法,因为from_json()方法并没有掌握处理问题的足够信息,唯有把错误交给调用者,由上层代码处理这个错误,ValidationError类是Python中的ValueError类的简单子类,具体定义如下:

# app/exceptions.py

class ValidationError(ValueError):
    pass

现在程序需要向客户端提供适当的响应以处理这个异常,为了避免在视图函数中编写捕获异常的代码,我们可创建一个全局异常处理程序,对于ValidationError异常,其处理程序如下:

# app/api_1_0/errors.py

@api.errorhandler(ValidationError)
def validation_error(e):
    return bad_request(e.args[0])

这里使用的errorhandler装饰器和注册HTTP状态码处理程序时使用的是同一个,只不过此时接受的参数是Exception类,只要抛出了指定类的异常,就会调用被装饰的函数,注意这个装饰器从API蓝本中调用,所以只有当处理蓝本中的路由时抛出了异常才会调用这个处理程序

使用这个技术时,视图函数中的代码可以写的十分简洁明了,而且无需检查错误

@api.route('/posts/', methods=['POST'])
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())

实现资源端点

现在我们需要实现用于处理不同资源的路由,GET请求往往是最简单的,因为它们只返回信息,无需修改信息,下例是博客文章的两个GET请求处理程序

# app/api_1_0/posts.py

@api.route('/posts/')
@auth.login_required
def get_posts():
    posts = Post.query.all()
    return jsonify({ 'posts': [post.to_json() for post in posts] })

@api.route('/posts/<int:id>')
@auth.login_required
def get_post(id):
    post = Post.query.get_or_404(id)
    return jsonify(post.to_json())

第一个路由处理获取文章集合的请求,这个函数使用列表推导生成所有文章的JSON版本,第二个路由返回单篇博客文章,如果在数据库中没找到指定id对应的文章,则返回404错误

404错误的处理程序在程序层定义,如果客户端请求JSON格式,就要返回JSON格式响应,如果要根据Web服务定制响应内容,也可在API蓝本中重新定义404错误处理程序

博客文章资源的POST请求处理程序把一篇新博客文章插入数据库,路由的定义如下例:

# app/api_1_0/posts.py
@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES)
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, _external=True)}

这个视图函数包含在permission_required装饰器中,确保通过认证的用户有写博客文章的权限,得益于前面实现的错误处理程序,创建博客文章的过程变得很直观,博客文章从JSON数据中创建,其作者是通过认证的用户,这个模型写入数据库之后,会返回201状态码,并把Location首部的值设为刚创建的这个资源的URL

为便于客户端操作,响应的主体中包含了新建的资源,如此一来,客户端就无需再创建资源后再立即发起一个GET请求以获取资源

用来防止未授权用户创建新博客文章的permission_required装饰器和程序中使用的类似,但会针对API蓝本进行自定义,具体实现如下:

# app/api_1_0/decotators.py

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decoreted_function(*args, **kwargs):
            if not g.current_user.can(permission):
                return forbidden('Insufficient permissions')
            return f(*args, **kwargs)
    return decorator

博客文章PUT请求的处理程序用来更新现有资源,如下:

# app/api_1_0/posts.py

@api.route('/posts/<int:id>', methods =["PUT"])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id):
    post = Post.query.get_or_404(id)
    if g.current_user != post.author and\
            not g.current_app.can(Psermission.ADMINISTER):
        return forbidden("Insufficient permissions")
    post.body = request.json.get('body', post.body)
    db.session.add(post)
    return jsonify(post.to_json())

本例中要进行的权限检查更为复杂,装饰器用来检查用户是否有写博客文章的权限,但为了确保用户能编辑博客文章,这个函数还要保证用户是文章的作者或是管理员,这个检查直接添加到视图函数中,如果这种函数要应用于多个视图函数,为避免代码重复,最好的方法是为其创建装饰器

因为程序不允许删除文章,所以没必要实现DELETE请求方法的处理程序

用户资源和评论资源的处理程序实现方法类似,下表列出了这个程序要实现的部分资源:

资源URL方法说明
/users/<int:id>GET一个用户
/users/<int:id>/posts/GET一个用户发布的博客文章
/user/<int:id>/timeline/GET一个用户所关注用户发布的文章
/posts/GET、POST所有博客文章
/posts/<int:id>GET、PUT一篇博客文章
/posts/<int:id/>comments/GET、POST一篇博客文章中的评论
/comments/GET所有评论
/comments/<int:id>GET一篇评论

这些资源只允许客户端实现Web程序提供的部分功能,支持的资源可以按需扩展,比如说提供关注者资源,支持评论管理,以及实现客户端需要的其他功能

分页大型资源集合

对大型资源集合来说,获取集合的GET请求消耗很大,而且难以管理,和Web程序一样,Web服务也可以对集合进行分页,实现方式见下:

# app/api_1_0/posts.py

@api.route('/posts/')
def get_posts():
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.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, _external=True)
    next = None
    if pagination.has_next:
        next = url_for('api.get_posts', page=page+1, _external=True)
    return jsonify({
        'posts':[post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total
                })

JSON响应格式中的posts字段依旧包含各篇文章,但现在这只是完整集合的一部分,如果资源有上一页和下一页,prevnext字段分别表示上一页和下一页资源的URLcount是集合中博客文章的总数,这种技术可应用于所有返回集合的路由

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值