4.3 Burt's Books
在 第三章 中,我们提出了 Burt's Book 作为使用 Tornado 模板工具构建复杂 Web 应用的例子。在本节中,我们将展示使用 MongoDB 作为数据存储的 Burt's Books 示例版本呢。
4.3.1 读取书籍(从数据库)
让我们从一些简单的版本开始:一个从数据库中读取书籍列表的 Burt's Books。首先,我们需要在我们的 MongoDB 服务器上创建一个数据库和一个集合,然后用书籍文档填充它,就像下面这样:
>>> import pymongo
>>> conn = pymongo.Connection()
>>> db = conn["bookstore"]
>>> db.books.insert({
... "title":"Programming Collective Intelligence",
... "subtitle": "Building Smart Web 2.0 Applications",
... "image":"/static/images/collective_intelligence.gif",
... "author": "Toby Segaran",
... "date_added":1310248056,
... "date_released": "August 2007",
... "isbn":"978-0-596-52932-1",
... "description":"<p>[...]</p>"
... })
ObjectId('4eb6f1a6136fc42171000000')
>>> db.books.insert({
... "title":"RESTful Web Services",
... "subtitle": "Web services for the real world",
... "image":"/static/images/restful_web_services.gif",
... "author": "Leonard Richardson, Sam Ruby",
... "date_added":1311148056,
... "date_released": "May 2007",
... "isbn":"978-0-596-52926-0",
... "description":"<p>[...]>/p>"
... })
ObjectId('4eb6f1cb136fc42171000001')
(我们为了节省空间已经忽略了这些书籍的详细描述。)一旦我们在数据库中有了这些文档,我们就准备好了。代码清单 4-3 展示了 Burt's Books Web 应用修改版本的源代码 burts_books_db.py。
代码清单 4-3 读取数据库:burts_books_db.py
import os.path
import tornado.locale
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options
import pymongo
define("port", default=8000, help="run on the given port", type=int)
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/", MainHandler),
(r"/recommended/", RecommendedHandler),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
ui_modules={"Book": BookModule},
debug=True,
)
conn = pymongo.Connection("localhost", 27017)
self.db = conn["bookstore"]
tornado.web.Application.__init__(self, handlers, **settings)
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render(
"index.html",
page_title = "Burt's Books | Home",
header_text = "Welcome to Burt's Books!",
)
class RecommendedHandler(tornado.web.RequestHandler):
def get(self):
coll = self.application.db.books
books = coll.find()
self.render(
"recommended.html",
page_title = "Burt's Books | Recommended Reading",
header_text = "Recommended Reading",
books = books
)
class BookModule(tornado.web.UIModule):
def render(self, book):
return self.render_string(
"modules/book.html",
book=book,
)
def css_files(self):
return "/static/css/recommended.css"
def javascript_files(self):
return "/static/js/recommended.js"
if __name__ == "__main__":
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
正如你看到的,这个程序和 第三章 中 Burt's Books Web 应用的原始版本几乎完全相同。它们之间只有两个不同点。其一,我们在我们的 Application 中添加了一个 db 属性来连接 MongoDB 服务器:
conn = pymongo.Connection("localhost", 27017)
self.db = conn["bookstore"]
其二,我们使用连接的 find 方法来从数据库中取得书籍文档的列表,然后在渲染 recommended.html 时将这个列表传递给 RecommendedHandler 的 get 方法。下面是相关的代码:
def get(self):
coll = self.application.db.books
books = coll.find()
self.render(
"recommended.html",
page_title = "Burt's Books | Recommended Reading",
header_text = "Recommended Reading",
books = books
)
此前,书籍列表是被硬编码在 get 方法中的。但是,因为我们在 MongoDB 中添加的文档和原始的硬编码字典拥有相同的域,所以我们之前写的模板代码并不需要修改。
像下面这样运行应用:
$ python burts_books_db.py
然后让你的浏览器指向 http://localhost:8000/recommended/ 。这次,页面和硬编码版本的 Burt's Books 看起来几乎一样(参见图 3-6)。
4.3.2 编辑和添加书籍
我们的下一步是添加一个接口用来编辑已经存在于数据库的书籍以及添加新书籍到数据库中。为此,我们需要一个让用户填写书籍信息的表单,一个服务表单的处理程序,以及一个处理表单结果并将其存入数据库的处理函数。
这个版本的 Burt's Books 和之前给出的代码几乎是一样的,只是增加了下面我们要讨论的一些内容。你可以跟随本书附带的完整代码阅读下面部分,相关的程序名为 burts_books_rwdb.py。
4.3.2.1 渲染编辑表单
下面是 BookEditHandler 的源代码,它完成了两件事情:
- GET 请求渲染一个显示已存在书籍数据的 HTML 表单(在模板 book_edit.html 中)。
- POST 请求从表单中取得数据,更新数据库中已存在的书籍记录或依赖提供的数据添加一个新的书籍。
下面是处理程序的源代码:
class BookEditHandler(tornado.web.RequestHandler):
def get(self, isbn=None):
book = dict()
if isbn:
coll = self.application.db.books
book = coll.find_one({"isbn": isbn})
self.render("book_edit.html",
page_title="Burt's Books",
header_text="Edit book",
book=book)
def post(self, isbn=None):
import time
book_fields = ['isbn', 'title', 'subtitle', 'image', 'author',
'date_released', 'description']
coll = self.application.db.books
book = dict()
if isbn:
book = coll.find_one({"isbn": isbn})
for key in book_fields:
book[key] = self.get_argument(key, None)
if isbn:
coll.save(book)
else:
book['date_added'] = int(time.time())
coll.insert(book)
self.redirect("/recommended/")
我们将在稍后对其进行详细讲解,不过现在先让我们看看如何在 Application 类中建立请求到处理程序的路由。下面是 Application 的 init 方法的相关代码部分:
handlers = [
(r"/", MainHandler),
(r"/recommended/", RecommendedHandler),
(r"/edit/([0-9Xx\-]+)", BookEditHandler),
(r"/add", BookEditHandler)
]
正如你所看到的,BookEditHandler 处理了两个不同路径模式的请求。其中一个是/add,提供不存在信息的编辑表单,因此你可以向数据库中添加一本新的书籍;另一个/edit/([0-9Xx-]+),根据书籍的 ISBN 渲染一个已存在书籍的表单。
4.3.2.2 从数据库中取出书籍信息
让我们看看 BookEditHandler 的 get 方法是如何工作的:
def get(self, isbn=None):
book = dict()
if isbn:
coll = self.application.db.books
book = coll.find_one({"isbn": isbn})
self.render("book_edit.html",
page_title="Burt's Books",
header_text="Edit book",
book=book)
如果该方法作为到/add 请求的结果被调用,Tornado 将调用一个没有第二个参数的 get 方法(因为路径中没有正则表达式的匹配组)。在这种情况下,默认将一个空的 book 字典传递给 book_edit.html 模板。
如果该方法作为到类似于/edit/0-123-456 请求的结果被调用,那么 isdb 参数被设置为 0-123-456。在这种情况下,我们从 Application 实例中取得 books 集合,并用它查询 ISBN 匹配的书籍。然后我们传递结果 book 字典给模板。
下面是模板(book_edit.html)的代码:
{% extends "main.html" %}
{% autoescape None %}
{% block body %}
<form method="POST">
ISBN <input type="text" name="isbn"
value="{{ book.get('isbn', '') }}"><br>
Title <input type="text" name="title"
value="{{ book.get('title', '') }}"><br>
Subtitle <input type="text" name="subtitle"
value="{{ book.get('subtitle', '') }}"><br>
Image <input type="text" name="image"
value="{{ book.get('image', '') }}"><br>
Author <input type="text" name="author"
value="{{ book.get('author', '') }}"><br>
Date released <input type="text" name="date_released"
value="{{ book.get('date_released', '') }}"><br>
Description<br>
<textarea name="description" rows="5"
cols="40">{% raw book.get('description', '')%}</textarea><br>
<input type="submit" value="Save">
</form>
{% end %}
这是一个相当常规的 HTML 表单。如果请求处理函数传进来了 book 字典,那么我们用它预填充带有已存在书籍数据的表单;如果键不在字典中,我们使用 Python 字典对象的 get 方法为其提供默认值。记住 input 标签的 name 属性被设置为 book 字典的对应键;这使得与来自带有我们期望放入数据库数据的表单关联变得简单。
同样还需要记住的是,因为 form 标签没有 action 属性,因此表单的 POST 将会定向到当前 URL,这正是我们想要的(即,如果页面以/edit/0-123-456 加载,POST 请求将转向/edit/0-123-456;如果页面以/add 加载,则 POST 将转向/add)。图 4-1 所示为该页面渲染后的样子。
图 4-1 Burt's Books:添加新书的表单
4.3.2.3 保存到数据库中
让我们看看 BookEditHandler 的 post 方法。这个方法处理书籍编辑表单的请求。下面是源代码:
def post(self, isbn=None):
import time
book_fields = ['isbn', 'title', 'subtitle', 'image', 'author',
'date_released', 'description']
coll = self.application.db.books
book = dict()
if isbn:
book = coll.find_one({"isbn": isbn})
for key in book_fields:
book[key] = self.get_argument(key, None)
if isbn:
coll.save(book)
else:
book['date_added'] = int(time.time())
coll.insert(book)
self.redirect("/recommended/")
和 get 方法一样,post 方法也有两个任务:处理编辑已存在文档的请求以及添加新文档的请求。如果有 isbn 参数(即,路径的请求类似于/edit/0-123-456),我们假定为编辑给定 ISBN 的文档。如果这个参数没有被提供,则假定为添加一个新文档。
我们先设置一个空的字典变量 book。如果我们正在编辑一个已存在的书籍,我们使用 book 集合的 find_one 方法从数据库中加载和传入的 ISBN 值对应的文档。无论哪种情况,book_fields 列表指定哪些域应该出现在书籍文档中。我们迭代这个列表,使用 RequestHandler 对象的 get_argument 方法从 POST 请求中抓取对应的值。
此时,我们准备好更新数据库了。如果我们有一个 ISBN 码,那么我们调用集合的 save 方法来更新数据库中的书籍文档。如果没有的话,我们调用集合的 insert 方法,此时要注意首先要为 date_added 键添加一个值。(我们没有将其包含在我们的域列表中获取传入的请求,因为在图书被添加到数据库之后 date_added 值不应该再被改变。)当我们完成时,使用 RequestHandler 类的 redirect 方法给用户返回推荐页面。我们所做的任何改变可以立刻显现。图 4-2 所示为更新后的推荐页面。
图 4-2 Burt's Books:带有新添加书籍的推荐列表
你还将注意到我们给每个图书条目添加了一个"Edit"链接,用于链接到列表中每个书籍的编辑表单。下面是修改后的图书模块的源代码:
<div class="book" style="overflow: auto">
<h3 class="book_title">{{ book["title"] }}</h3>
{% if book["subtitle"] != "" %}
<h4 class="book_subtitle">{{ book["subtitle"] }}</h4>
{% end %}
<img src="{{ book["image"] }}" class="book_image"/>
<div class="book_details">
<div class="book_date_released">Released: {{ book["date_released"]}}</div>
<div class="book_date_added">Added: {{ locale.format_date(book["date_added"],
relative=False) }}</div>
<h5>Description:</h5>
<div class="book_body">{% raw book["description"] %}</div>
<p><a href="/edit/{{ book['isbn'] }}">Edit</a></p>
</div>
</div>
其中最重要的一行是:
<p><a href="/edit/{{ book['isbn'] }}">Edit</a></p>
编辑页面的链接是把图书的 isbn 键的值添加到字符串/edit/后面组成的。这个链接将会带你进入这本图书的编辑表单。你可以从图 4-3 中看到结果。
图 4-3 Burt's Books:带有编辑链接的推荐列表
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论