Skip to content

Latest commit

 

History

History
649 lines (494 loc) · 25.2 KB

File metadata and controls

649 lines (494 loc) · 25.2 KB

七、AJAX 和 RESTful API

在本章中,我们将使用 Flask Untivent 为博客应用创建一个 RESTful API。RESTful API 是通过提供表示博客的高度结构化数据,以编程方式访问博客的一种方式。Flask Untivent 可以很好地处理我们的 SQLAlchemy 模型,还可以处理复杂的任务,例如序列化和结果过滤。我们将使用 RESTAPI 为我们的博客条目构建一个支持 AJAX 的评论功能。到本章结束时,您将能够为 SQLAlchemy 模型创建易于配置的 API,并在 Flask 应用中生成和响应 AJAX 请求。

在本章中,我们将:

  • 创建一个模型来存储博客条目上的评论
  • 安装保温瓶
  • 为注释模型创建 RESTful API
  • 构建一个前端,使用 Ajax 与我们的 API 进行通信

创建评论模型

在开始创建 API 之前,我们需要为希望共享的资源创建一个数据库模型。我们正在构建的 API 将用于使用 AJAX 创建和检索评论,因此我们的模型将包含所有与存储未经验证的用户评论相关的字段。

出于我们的目的,以下字段应足够:

  • name,发表评论的人的姓名
  • email,发表评论的人的电子邮件地址,我们将仅使用它来显示Gravatar中他们的图像
  • URL,评论人博客的 URL
  • ip_address,评论人的 IP 地址
  • body,实际意见
  • statusPublicSpam,Deleted之一
  • created_timestamp,创建注释的时间戳
  • entry_id,与评论相关的博客条目 ID

让我们通过在我们的应用的models.py模块中创建注释模型定义来开始编码:

class Comment(db.Model):
    STATUS_PENDING_MODERATION = 0
    STATUS_PUBLIC = 1
    STATUS_SPAM = 8
    STATUS_DELETED = 9

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))
    email = db.Column(db.String(64))
    url = db.Column(db.String(100))
    ip_address = db.Column(db.String(64))
    body = db.Column(db.Text)
    status = db.Column(db.SmallInteger, default=STATUS_PUBLIC)
    created_timestamp = db.Column(db.DateTime, default=datetime.datetime.now)
    entry_id = db.Column(db.Integer, db.ForeignKey('entry.id'))

    def __repr__(self):
        return '<Comment from %r>' % (self.name,)

在添加了Comment模型定义之后,我们需要在CommentEntry模型之间建立 SQLAlchemy 关系。您还记得,我们在通过条目关系设置UserEntry之间的关系时曾经这样做过一次。我们将通过在Entry模型中添加一个 comments 属性来为Comment实现这一点。

tags关系下方,将以下代码添加到Entry模型定义中:

class Entry(db.Model):
    # ...
    tags = db.relationship('Tag', secondary=entry_tags,
        backref=db.backref('entries', lazy='dynamic'))
    comments = db.relationship('Comment', backref='entry', lazy='dynamic')

我们已经将关系指定为lazy='dynamic',正如您在第 5 章中回忆的那样,验证用户,这意味着任何给定Entry实例上的comments属性都将是可过滤的查询。

创建架构迁移

为了开始使用我们的新模型,我们需要更新我们的数据库模式。使用manage.py助手,为Comment模型创建模式迁移:

(blog) $ python manage.py db migrate
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'comment'
 Generating /home/charles/projects/blog/app/migrations/versions/490b6bc5f73c_.py ... done

然后运行upgrade应用迁移:

(blog) $ python manage.py db upgrade
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.migration] Running upgrade 594ebac9ef0c -> 490b6bc5f73c, empty message

Comment型号现在可以使用了!此时,如果我们使用常规 Flask 视图实现注释,我们可能会创建一个注释蓝图并开始编写一个视图来处理注释创建。但是,我们将使用 RESTAPI 公开注释,并直接从前端使用 AJAX 创建注释。

安装烧瓶不动

有了我们的模型,我们现在就可以安装 Flask Untivent 了,这是一个第三方 Flask 扩展,使得为您的 SQLAlchemy 模型构建 RESTful API 变得简单。确保您已激活博客应用的虚拟环境后,使用pip安装 Flask Untivent:

(blog) $ pip install Flask-Restless

您可以通过打开交互式解释器并获取安装的版本来验证是否安装了扩展。别忘了,您的确切版本号可能会有所不同。

(blog) $ ./manage.py shell

In [1]: import flask_restless

In [2]: flask_restless.__version__
Out[2]: '0.13.0'

现在我们已经安装了 Flask Untivent,让我们将其配置为与我们的应用一起使用。

设置烧瓶不停

与其他 Flask 扩展一样,我们将在app.py模块中首先配置一个将管理新 API 的对象。在 Flask Untivent 中,这个对象称为APIManager,它将允许我们为 SQLAlchemy 模型创建 RESTful 端点。将以下行添加到app.py

# Place this import at the top of the module alongside the other extensions.
from flask.ext.restless import APIManager

# Place this line below the initialization of the app and db objects.
api = APIManager(app, flask_sqlalchemy_db=db)

因为 API 将同时依赖于 Flask API 对象和Comment模型,所以我们需要确保不创建任何循环模块依赖项。我们可以通过在 app 目录的根目录下创建一个新模块api.py,来避免引入循环导入。

让我们从最基本的开始,看看 Flask Untivent 在开箱即用时提供了什么。将以下代码添加到api.py

from app import api
from models import Comment

api.create_api(Comment, methods=['GET', 'POST'])

api.py中的代码在APIManager对象上调用create_api()方法。此方法将使用额外的 URL 路由和视图代码填充我们的应用,这些路由和代码共同构成 RESTful API。methods 参数表示我们只允许GETPOST请求(意味着可以读取或创建评论,但不能编辑或删除评论)。

最后一个操作是将main.py中的新 API 模块导入到我们的应用中。我们导入模块纯粹是为了它的副作用,注册 URL 路由。将以下代码添加到main.py

from app import app, db
import admin
import api
import models
import views

...

提出 API 请求

在一个终端中,启动开发服务器。在另一个终端中,让我们看看当我们向我们的 API 端点发出GET请求时会发生什么(注意,没有尾随的正斜杠):

$ curl 127.0.0.1:5000/api/comment
{
 "num_results": 0,
 "objects": [],
 "page": 1,
 "total_pages": 0
}

数据库中没有注释,因此没有对象被序列化并返回给我们。然而,有一些有趣的元数据告诉我们数据库中有多少对象,我们在哪个页面上,以及总共有多少个注释页面。

让我们通过将一些 JSON 数据发布到我们的 API 来创建一个新的注释(我假设数据库中的第一个条目的 id 为1。我们将使用curl提交一个POST请求,其中包含一个新评论的 JSON 编码表示:

$ curl -X POST -H "Content-Type: application/json" -d '{
 "name": "Charlie",
 "email": "charlie@email.com",
 "url": "http://charlesleifer.com",
 "ip_address": "127.0.0.1",
 "body": "Test comment!",
 "entry_id": 1}' http://127.0.0.1:5000/api/comment

假设没有输入错误,API 将响应以下数据,确认新的Comment的创建:

{
  "body": "Test comment!",
  "created_timestamp": "2014-04-22T19:48:33.724118",
  "email": "charlie@email.com",
  "entry": {
    "author_id": 1,
    "body": "This is an entry about Python, my favorite programming language.",
    "created_timestamp": "2014-03-06T19:50:09",
    "id": 1,
    "modified_timestamp": "2014-03-06T19:50:09",
    "slug": "python-entry",
    "status": 0,
    "title": "Python Entry"
  },
  "entry_id": 1,
  "id": 1,
  "ip_address": "127.0.0.1",
  "name": "Charlie",
  "status": 0,
  "url": "http://charlesleifer.com"
}

正如您所看到的,我们发布的所有数据都包含在响应中,此外还有其他字段数据,如新注释的 id 和时间戳。令人惊讶的是,甚至对应的Entry对象也被序列化并包含在响应中。

现在我们在数据库中有了一条注释,让我们尝试对我们的 API 发出另一个GET请求,如下所示:

$ curl 127.0.0.1:5000/api/comment
{
 "num_results": 1,
 "objects": [
 {
 "body": "Test comment!",
 "created_timestamp": "2014-04-22T19:48:33.724118",
 "email": "charlie@email.com",
 "entry": {
 "author_id": 1,
 "body": "This is an entry about Python, my favorite programming language.",
 "created_timestamp": "2014-03-06T19:50:09",
 "id": 1,
 "modified_timestamp": "2014-03-06T19:50:09",
 "slug": "python-entry",
 "status": 0,
 "title": "Python Entry"
 },
 "entry_id": 1,
 "id": 1,
 "ip_address": "127.0.0.1",
 "name": "Charlie",
 "status": 0,
 "url": "http://charlesleifer.com"
 }
 ],
 "page": 1,
 "total_pages": 1
}

第一个对象包含与我们发出POST请求时返回给我们的数据完全相同的数据。此外,周围的元数据已经改变,以反映数据库中现在有一条注释的事实。

使用 AJAX 创建注释

为了允许用户发表评论,我们首先需要一种方法来捕获他们的输入,我们将通过创建一个带有wtformsForm类来实现。此表单应允许用户输入其姓名、电子邮件地址、可选 URL 和评论。

在条目蓝图的表单模块中,添加以下表单定义:

class CommentForm(wtforms.Form):
    name = wtforms.StringField('Name', validators=[validators.DataRequired()])
    email = wtforms.StringField('Email', validators=[
        validators.DataRequired(),
        validators.Email()])
    url = wtforms.StringField('URL', validators=[
        validators.Optional(),
        validators.URL()])
    body = wtforms.TextAreaField('Comment', validators=[
        validators.DataRequired(),
        validators.Length(min=10, max=3000)])
    entry_id = wtforms.HiddenField(validators=[
        validators.DataRequired()])

    def validate(self):
        if not super(CommentForm, self).validate():
            return False

        # Ensure that entry_id maps to a public Entry.
        entry = Entry.query.filter(
            (Entry.status == Entry.STATUS_PUBLIC) &
            (Entry.id == self.entry_id.data)).first()
        if not entry:
            return False

        return True

您可能想知道为什么我们要指定验证器,因为 API 将处理发布的数据。我们这样做是因为 Flask Untivent 不提供验证,但它确实提供了一个钩子,我们可以在其中执行验证。通过这种方式,我们可以在 RESTAPI 中利用 WTForms 验证。

为了在条目详细信息页面中使用表单,我们需要在呈现详细信息模板时将表单传递到上下文中。打开条目蓝图并导入新的CommentForm

from entries.forms import EntryForm, ImageForm, CommentForm

然后修改的detail视图,将表单实例传递到上下文中。我们将使用所请求条目的值预先填充entry_id隐藏字段:

@entries.route('/<slug>/')
def detail(slug):
    entry = get_entry_or_404(slug)
    form = CommentForm(data={'entry_id': entry.id})
    return render_template('entries/detail.html', entry=entry, form=form)

现在表单处于详细模板上下文中,剩下的就是呈现表单。在名为comment_form.htmlentries/templates/entries/includes/中创建一个空模板,并添加以下代码:

{% from "macros/form_field.html" import form_field %}
<form action="/api/comment" class="form form-horizontal" id="comment-form" method="post">
  {{ form_field(form.name) }}
  {{ form_field(form.email) }}
  {{ form_field(form.url) }}
  {{ form_field(form.body) }}
  {{ form.entry_id() }}
  <div class="form-group">
    <div class="col-sm-offset-3 col-sm-9">
      <button type="submit" class="btn btn-default">Submit</button>
    </div>
  </div>
</form>

有趣的是,我们没有将form_field宏用于entry_id字段。这是因为我们不希望注释表单为用户不可见的字段显示标签。相反,我们将用这个值初始化表单。

最后,我们需要在detail.html模板中包含注释表单。在条目正文下方,添加以下标记:

{% block content %}
  {{ entry.body }}

  <h4 id="comment-form">Submit a comment</h4>
 {% include "entries/includes/comment_form.html" %}
{% endblock %}

使用开发服务器,尝试导航到详细信息页面以获取任何条目。您应该看到一个评论表单:

Creating comments using AJAX

AJAX 表单提交

为了简化 AJAX 请求,我们将使用 jQuery 库。如果您愿意的话,可以随意替换另一个 JavaScript 库,但是因为 jQuery 是如此普遍(并且与 Bootstrap 配合得很好),我们将在本节中使用它。如果到目前为止您一直在关注代码,那么 jQuery 应该已经包含在所有页面中。现在我们需要创建一个 JavaScript 文件来处理评论提交。

statics/js/中创建一个名为comments.js的新文件,并添加以下 JavaScript 代码:

Comments = window.Comments || {};

(function(exports, $) { /* Template string for rendering success or error messages. */
  var alertMarkup = (
    '<div class="alert alert-{class} alert-dismissable">' +
    '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' +
    '<strong>{title}</strong> {body}</div>');

  /* Create an alert element. */
  function makeAlert(alertClass, title, body) {
    var alertCopy = (alertMarkup
                     .replace('{class}', alertClass)
                     .replace('{title}', title)
                     .replace('{body}', body));
    return $(alertCopy);
  }

  /* Retrieve the values from the form fields and return as an object. */
  function getFormData(form) {
    return {
      'name': form.find('input#name').val(),
      'email': form.find('input#email').val(),
      'url': form.find('input#url').val(),
      'body': form.find('textarea#body').val(),
      'entry_id': form.find('input[name=entry_id]').val()
    }
  }

  function bindHandler() {
    /* When the comment form is submitted, serialize the form data as JSON
             and POST it to the API. */
    $('form#comment-form').on('submit', function() {
      var form = $(this);
      var formData = getFormData(form);
      var request = $.ajax({
        url: form.attr('action'),
        type: 'POST',
        data: JSON.stringify(formData),
        contentType: 'application/json; charset=utf-8',
        dataType: 'json'
      });
      request.success(function(data) {
        alertDiv = makeAlert('success', 'Success', 'your comment was posted.');
        form.before(alertDiv);
        form[0].reset();
      });
      request.fail(function() {
        alertDiv = makeAlert('danger', 'Error', 'your comment was not posted.');
        form.before(alertDiv);
      });
      return false;
    });
  }

  exports.bindHandler = bindHandler;
})(Comments, jQuery);

comments.js代码处理将表单数据(序列化为 JSON)发布到REST API。它还处理获取API 响应并显示成功或错误消息。

detail.html模板中,我们只需要包含脚本并绑定提交处理程序。将以下块替代添加到详图样板:

{% block extra_scripts %}
  <script type="text/javascript" src="{{ url_for('static', filename='js/comments.js') }}"></script>
  <script type="text/javascript">
    $(function() {
      Comments.bindHandler();
    });
  </script>
{% endblock %}

继续并尝试提交一两条评论。

验证 API 中的数据

不幸的是对我们来说,我们的 API 没有对传入数据执行任何类型的验证。为了验证POST数据,我们需要使用 Flask Untivent 提供的挂钩。Flask Untivent 调用这些钩子请求预处理器和后处理器。

让我们看看如何使用 POST 预处理器对我们的注释数据执行一些验证。首先打开api.py并进行以下更改:

from flask.ext.restless import ProcessingException

from app import api
from entries.forms import CommentForm
from models import Comment

def post_preprocessor(data, **kwargs):
    form = CommentForm(data=data)
    if form.validate():
        return form.data
    else:
        raise ProcessingException(
            description='Invalid form submission.',
            code=400)

api.create_api(
    Comment,
    methods=['GET', 'POST'],
    preprocessors={
        'POST': [post_preprocessor],
    })

我们的 API 将现在使用CommentForm中的验证逻辑验证提交的评论。我们通过为POST方法指定一个预处理器来实现这一点。我们实现为post_preprocessorPOST预处理器接受反序列化的POST数据作为参数。然后我们可以将这些数据输入我们的CommentForm并调用它的validate()方法。如果验证失败,我们将发出一个ProcessingException,向 Flask Antientive 发出该数据无法处理的信号,并返回一个400错误的请求响应。

在下面的屏幕截图中,我没有提供需要的注释字段。我尝试提交评论时收到错误消息:

Validating data in the API

预处理器和后处理器

我们刚刚看了一个使用烧瓶不宁的POST方法预处理器的例子。在下面的表中,您可以看到其他可用的挂钩:

|

方法名

|

描述

|

预处理器参数

|

后处理器参数

| | --- | --- | --- | --- | | GET_SINGLE | 通过主键检索单个对象 | instance_id,对象的主键 | result,对象的字典表示 | | GET_MANY | 检索多个对象 | search_params,用于过滤结果集的搜索参数字典 | result,表示对象的search_params | | PUT_SINGLE | 按主键更新单个对象 | instance_id数据,用于更新对象的数据字典 | result,更新对象的字典表示 | | PUT_MANY | 更新多个对象 | search_params,用于确定要更新哪些对象的搜索参数字典。数据,用于更新对象的数据字典。 | query,表示要更新的对象的 SQLAlchemy 查询。data``search_params | | POST | 创建一个新实例 | data,数据字典,用于填充新对象 | result,新对象的字典表示 | | DELETE | 按主键删除实例 | instance_id,要删除对象的主键 | was_deleted,表示对象是否被删除的布尔值 |

使用 AJAX 加载注释

现在我们可以使用 AJAX 创建经过验证的评论,让我们使用 API 检索评论列表,并将它们显示在博客条目下面。为此,我们将从 API 中读取值,并动态创建 DOM 元素以显示注释。正如您可能还记得的,在我们之前检查的 API 响应中,有相当多的私有信息被返回,包括每个注释的相关Entry的完整序列化表示。就我们的目的而言,这些信息是多余的,而且还会浪费带宽。

让我们首先对 comments 端点进行一些额外的配置,以限制返回的Comment字段。在api.py中,对api.create_api()的呼叫进行以下补充:

api.create_api(
    Comment,
    include_columns=['id', 'name', 'url', 'body', 'created_timestamp'],
    methods=['GET', 'POST'],
    preprocessors={
        'POST': [post_preprocessor],
    })

请求评论列表现在为我们提供了更易于管理的响应,不会泄露实施细节或私人数据:

$ curl http://127.0.0.1:5000/api/comment
{
 "num_results": 1,
 "objects": [
 {
 "body": "Test comment!",
 "created_timestamp": "2014-04-22T19:48:33.724118",
 "name": "Charlie",
 "url": "http://charlesleifer.com"
 }
 ],
 "page": 1,
 "total_pages": 1
}

一个很好的功能是在用户评论旁边显示一个化身。Gravatar 是一项免费的化身服务,允许用户将自己的电子邮件地址与图像关联。我们将使用评论者的电子邮件地址来显示他们相关的化身(如果存在)。如果用户尚未创建化身,则将显示抽象模式。

让我们在Comment模型上添加一个方法,为用户的 Gravatar 图像生成 URL。打开models.py并将以下方法添加到Comment

def gravatar(self, size=75):
    return 'http://www.gravatar.com/avatar.php?%s' % urllib.urlencode({
        'gravatar_id': hashlib.md5(self.email).hexdigest(),
        'size': str(size)})

您还需要确保在模型模块的顶部导入hashliburllib

如果我们试图将 Gravatar 包含在列列表中,Flask untivent 将引发一个异常,因为gravatar实际上是一个方法。幸运的是,Flask Untivent 提供了一种在序列化对象时包含方法调用结果的方法。在api.py中,对create_api()呼叫进行以下补充:

api.create_api(
    Comment,
    include_columns=['id', 'name', 'url', 'body', 'created_timestamp'],
    include_methods=['gravatar'],
    methods=['GET', 'POST'],#, 'DELETE'],
    preprocessors={
        'POST': [post_preprocessor],
    })

继续并尝试获取评论列表。您现在应该可以看到序列化响应中包含的 Gravatar URL。

检索评论列表

现在我们需要返回 JavaScript 文件并添加代码来检索注释列表。我们将通过向 API 传递搜索过滤器来实现这一点,API 将只检索与请求的博客条目相关联的评论。搜索查询表示为筛选器列表,每个筛选器指定以下内容:

  • 列名
  • 操作(例如,equals)
  • 要搜索的值

打开comments.js并在开始的行后添加以下代码:

(function(exports, $) {:
function displayNoComments() {
  noComments = $('<h3>', {
    'text': 'No comments have been posted yet.'});
  $('h4#comment-form').before(noComments);
}

/* Template string for rendering a comment. */
var commentTemplate = (
  '<div class="media">' +
    '<a class="pull-left" href="{url}">' +
      '<img class="media-object" src="{gravatar}" />' +
    '</a>' +
    '<div class="media-body">' +
    '<h4 class="media-heading">{created_timestamp}</h4>{body}' +
  '</div></div>'
);

function renderComment(comment) {
  var createdDate = new Date(comment.created_timestamp).toDateString();
  return (commentTemplate
          .replace('{url}', comment.url)
          .replace('{gravatar}', comment.gravatar)
          .replace('{created_timestamp}', createdDate)
          .replace('{body}', comment.body));
}

function displayComments(comments) {
  $.each(comments, function(idx, comment) {
    var commentMarkup = renderComment(comment);
    $('h4#comment-form').before($(commentMarkup));
  });
}

function load(entryId) {
  var filters = [{
    'name': 'entry_id',
    'op': 'eq',
    'val': entryId}];
  var serializedQuery = JSON.stringify({'filters': filters});

  $.get('/api/comment', {'q': serializedQuery}, function(data) {
    if (data['num_results'] === 0) {
      displayNoComments();
    } else {
      displayComments(data['objects']);
    }
  });
}

然后,在文件的底部附近,将加载函数与bindHandler导出一起导出,如下所示:

exports.load = load;
exports.bindHandler = bindHandler;

我们添加的新 JavaScript 代码向 API 发出 AJAX 请求,请求与给定条目相关联的注释。如果不存在任何注释,将显示一条消息,指示尚未作出任何注释。否则,条目将呈现为Entry主体下方的列表。

剩下的最后一个任务是在呈现页面时调用细节模板中的Comments.load()。打开detail.html并添加以下突出显示的代码:

<script type="text/javascript">
  $(function() {
    Comments.load({{ entry.id }});
    Comments.bindHandler();
  });
</script>

在做了一对评论后,评论列表如下图所示:

Retrieving the list of comments

作为练习,请查看是否可以编写代码来呈现用户发布的任何新注释。您会记得,当成功创建注释时,新数据将作为 JSON 对象返回。

阅读更多

Flask Untivent 支持许多配置选项,为了节省空间,本章无法介绍这些选项。搜索过滤器是一个非常强大的工具,我们只触及了可能的表面。此外,预处理和后处理挂钩可用于实现许多有趣的特性,例如:

  • 身份验证,可在预处理器中实现
  • GET_MANY的默认过滤器,可用于将注释列表限制为公开的注释,例如
  • 向序列化响应添加自定义值或计算值
  • 修改传入的POST值以设置模型实例的默认值

如果 RESTAPI 是应用中的一个关键组件,我强烈建议您花时间阅读 Flask Antientive文档。该文件可在网上找到 https://flask-restless.readthedocs.org/en/latest/

总结

在本章中,我们使用 Flask Untivent 扩展向我们的应用添加了一个简单的 REST API。然后,我们使用 JavaScript 和 Ajax 将前端与 API 集成,允许用户查看和发布新评论,而无需编写一行查看代码。

在下一章中,我们将致力于创建可测试的应用,并找到为此目的改进代码的方法。这也将允许我们验证我们编写的代码是否正在执行我们希望它执行的操作;不多不少。自动化这将给您信心,并确保 RESTful API 按预期工作。