Burt’s Books

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.)

Reading Books (From the Database)

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).

Editing and Adding Books

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.

Rendering the edit form

Here’s the source code for BookEditHandler, which performs two jobs:

  1. A GET request to the handler renders an HTML form (in the template book_edit.html), potentially with data for an existing book.

  2. 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.

Retrieving book information from the database

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.

Burt’s Books: Form for adding a new book

Figure 4-1. Burt’s Books: Form for adding a new book

Saving to the database

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.

Burt’s Books: Recommended list with newly added book

Figure 4-2. Burt’s Books: Recommended list with newly added book

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.

Burt’s Books: Recommended list with edit links

Figure 4-3. Burt’s Books: Recommended list with edit links

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.