In Chapter 3, we presented Burt’s Books as an example of how to build a sophisticated web application with Tornado’s template tools. In this section, we’ll show you a version of the Burt’s Books example that uses MongoDB as a data store. (You’ll want to review the Burt’s Books example from Chapter 3 before you continue.)
Let’s start with something simple: a version of Burt’s Books that reads its list of books from the database. The first thing we’ll need to do is create a database and a collection on our MongoDB server and populate it with book documents, like so:
>>>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')
(We’ve omitted the descriptions of these books to save space.) Once we have these documents in the database, we’re ready to roll. Example 4-3 shows the source code for the modified version of the Burt’s Books web application, called burts_books_db.py.
Example 4-3. Reading from the database: burts_books_db.py
import os.path import tornado.auth import tornado.escape 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()
As you can see, this program is almost exactly identical to the
original Burt’s Books web application presented in Chapter 3. There are two differences. First,
we’ve added a db
attribute to our
Application
connected to a MongoDB
server:
conn = pymongo.Connection("localhost", 27017) self.db = conn["bookstore"]
Second, we use the connection’s find
method to get a list of book documents
from the database, and pass that list in when rendering recommended.html in the get
method of RecommendedHandler
. Here’s the relevant
code:
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 )
Previously, the list of books had been hardcoded into the get
method. However, because the documents we
added to MongoDB have the same fields as the original hardcoded
dictionaries, the template code we wrote works without any
modification.
Run the application like so:
$ python burts_books_db.py
And then point your web browser to
http://localhost:8000/recommended/
. At this point, it should
look almost exactly like the hardcoded version of Burt’s Books (see
Figure 3-6).
The next step is to make an interface for editing books that are already in the database, and to add new books to the database. In order to do this, we need to make a form for the user to fill out with book information, a handler to serve that form, and a handler to process the results of that form and put them in the database.
The source code for this version of Burt’s Books is nearly identical to the code previously presented, with a few additions that we’ll discuss below. You can follow along with the full source code that came with the book; the relevant program is burts_books_rwdb.py.
Here’s the source code for BookEditHandler
, which performs two
jobs:
A
GET
request to the handler renders an HTML form (in the template book_edit.html), potentially with data for an existing book.A
POST
request to the handler takes data from the form and either updates an existing book record in the database, or adds a new one, depending on the data supplied.
Here’s the source code for the handler:
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/")
We’ll talk about the details in a second, but first let’s
discuss how we’ve set up our Application
class to route requests to this
handler. Here’s the relevant section from the Application
’s __init__
method:
handlers = [ (r"/", MainHandler), (r"/recommended/", RecommendedHandler), (r"/edit/([0-9Xx\-]+)", BookEditHandler), (r"/add", BookEditHandler) ]
As you can see, BookEditHandler
handles requests for
two different path patterns. One of these,
/add
, serves up the edit form with
no existing information, so you can add a new book to the database;
the other, /edit/([0-9Xx\-]+)
,
renders the form with information for a pre-existing book, according
to the book’s ISBN.
Let’s look at the get
method
in BookEditHandler
to see how it
works:
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)
If the method is invoked as a result of a request to /add
, Tornado will call the get
method without a second argument (as
there’s no corresponding group in the regular expression for the
path). In this case, the default, an empty book
dictionary is passed to the book_edit.html template.
If the method was called as a result of a request to, for
example, /edit/0-123-456
, the
isbn
parameter is set to the value
0-123-456
. In this case, we get the
books
collection from our Application
instance and use it to look up
the book with the corresponding ISBN. Then we pass the resulting
book
dictionary into the
template.
Here’s the template (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 %}
This is a fairly conventional HTML form. We’re using the
book
dictionary passed in from the
request handler to prepopulate the form with data from the existing
book, if any; we use the Python dictionary object’s get
method to supply a default value for a
key if the key isn’t present in the dictionary. Note that the name
attributes of the input
tags are set to the corresponding key
of the book
dictionary; this will
make it easy to associate the data from the form with the data we want
to put into the database.
Also note that, because the form
tag lacks an action
attribute, the form’s POST
will be directed to the current URL,
which is precisely what we want (e.g., if the page was loaded as
/edit/0-123-456
, the POST
request will go to /edit/0-123-456
; if the page was loaded as
/add
, the POST
will go to /add
). Figure 4-1 shows what the page looks like
when rendered.
Let’s take a look at the post
method of BookEditHandler
. This
method handles requests that come from the book edit form. Here’s the
source code:
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/")
Like the get
method, the
post
method does double duty: it
handles requests to edit existing documents and requests to add a new
document. If there’s an isbn
argument (i.e., the path of the request was something like /edit/0-123-456
), we assume that we’re
editing the document with the given ISBN. If such an argument is not
present, we assume that we’re adding a new document.
We begin with an empty dictionary variable called book
. If we’re editing an existing book, we
load the document corresponding to the incoming ISBN from the database
using the book
collection’s
find_one
method. In either case,
the book_fields
list specifies what
fields should be present in a book document. We iterate over this
list, grabbing the corresponding values from the POST
request using the get_argument
method of the RequestHandler
object.
At this point, we’re ready to update the database. If we have an
ISBN, we call the collection’s save
method to update the book document in the database. If not, we call
the collection’s insert
method,
taking care to first add a value for the date_added
key. (We didn’t include this in
our list of fields to fetch from the incoming request, as it doesn’t
make sense to be able to edit the date_added
value after the book has been
added to the database.) When we’re done,
we use the redirect
method of
the RequestHandler
class to send the user back to
the Recommendations page. Any changes that we made should be visible
there immediately. Figure 4-2 shows
what the updated Recommendations page might look like.
You’ll also notice that we’ve added an “Edit” link to each book entry, which links to the Edit form for each book in the list. Here’s the source code for the modified Book module:
<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>
The important line is this one:
<p><a href="/edit/{{ book['isbn'] }}">Edit</a></p>
The link to the Edit page is made by appending the value of the
book’s isbn
key to the string
/edit/
. This link will lead to the
Edit form for the book in question. You can see the results in Figure 4-3.
Get Introduction to Tornado now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.