Learning Django by Example(1): Start the Engine

This is yet another Django tutorial, it makes a difference as:

Translations are freely permitted as long as they are released under Attribution-Noncommercial-Share Alike Creative Common License. Learning Django by Example has already been fully or partially translated into several languages. If you translate it into another language and would like to be listed here, just let me know.

Think Big

I really love the intuitive interface of Delicious Library, unfortunately, it is Mac-only, and it may not scale due to the lack of database server support. Personally I prefer a small daemon running silently in my old Gentoo box to serve contents to allclients, cross-platform and it is supposed to scale when the library grows, and I may share the resources with my friends. So I decide to brew my tea: Gelman, named after the library in the main campus of the George Washington University.

I choose Django for the python on rail

NOTE: In the following tutorial, I would show the code snapshot in each step, when I mention check r#, you can checkout the code via:

svn checkout http://gelman.googlecode.com/svn/trunk/gelman -r3 gelman

in the case, # is 3.

Start the Engine

Installation of django-0.96 in Gentoo is like a breeze, just emerge it. And follow the official tutorial to initialize the project and setup the database, sqlite3 is used in the rest of tutorial for the sake of simplicity. When it is in production use, we would migrate it to MySQL . Check r5.

Next, add an application library:

python manage.py startapp library

Gelman is designed as a lightweight eBook management system, so it make senses to reuse existed components and Web services. For example, the meta data is from Amazon Web Service(aka AWS), the tag suggestion may come from Yahoo. Only the essential information is stored locally, such as the mapping between the meta data and eBook. The following models, Author, Publisher and Book are added.

class Book(models.Model):
    isbn13 = models.CharField(maxlength=13, primary_key=True)
    name = models.CharField(maxlength=255)
    authors = models.ManyToManyField(Author)
    pages = models.IntegerField()
    publisher = models.ForeignKey(Publisher)
    pub_date = models.DateTimeField('data published')

ISBN-13 is the latest standard, we may add ISBN-10 later if necessary. Author and Book is many to many related, so use models.ManyToManyField as suggested. Finish the following boilerplate work to make it run:

Now we can login to http://localhost:8000/admin using bookstack and gelman as the Username and Password, check r7.

Initially, books are added manually for a test drive, we would automate this tedious procedure later. Notice the link between the Book and Publisher, it is really cool that the admin would help us to handle the relation of models.

Add book
Add book

Add one line to the urls.py to enable the user interface for normal users:

 (r'^library/(?P\d+)/$', 'gelman.library.views.detail'),

Add the detail.html in template/booksas the view template, don’t forget to set TEMPLATE_DIRS variable in settings.py, then implement detail in library/views.py:

def detail(request, isbn):
    book = get_object_or_404(Book, isbn13=isbn)
    return render_to_response('books/detail.html', {'book': book})

Now we can access the data via: http://localhost:8000/library/9780596009250/, it is just a boring raw text, we would refine it later. Check r8.

Make Librarian happier

In a real library, the user may browser, search, borrow, return books, but only librarian manage the collection. It is less fun to type all the meta data of the book while we can fetch it from Amazon. As a librarian instead of bar code reader, I prefer title/keyword than boring 13 digital numbers.

And AJAX of course, who is not using it? I will eat my own dog food, The Dojo Javascript Toolkit. You can either use dojo-0.9 release to take full advantage of AOL’s CDN(thanks, Alex) or the SVN version just as I did.

how to server the static file is probably the most frequently asked question in #django since the regular expression based URLConf more or less overkill, :-)

 (r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': '/home/bookstack/projects/media'}),

And prepare the files as well:

cd /home/bookstack/projects/media;
mkdir scripts
sudo mount -o bind $DOJO_ROOT/trunk scripts

Now http://localhost:8000/static/scripts/dojo/dojo.js would point to the dojo.js.

Consider users may input dash and space in ISBN, we need to override the default maxlength attribute of Isbn13and add a validation for ISBN using JavaScript:

dojo.addOnLoad(function() {
    var p = dojo.byId("id_isbn13");
    var n = document.createElement("div")
    p.parentNode.appendChild(n);

    dojo.connect(p, 'onblur', function() {
        if(isValidISBN(p.value)) {
            n.innerHTML = "Searching..."
        } else {
            n.innerHTML = "Invalid ISBN"
        }
    });
    p.removeAttribute("maxlength");
});

NOTE: isValidISBN is merged into dojox.validatie.isbn, renamed as dojox.validate.isValidIsbn. That is just one of benefits to run dojo SVN.

It is a good chance to customize the admin page, a new template add.html is added:

{% block content %}
    <div id="content-main">
    <h1>Add Book<h1>
        <input type="text" id="id_isbn13" class="vTextField" name="isbn13" size="30" value=""/>
        <input type="button" id="search_isbn" value="Search ISBN" />
        <div id="isbn_valid"></div>
        <input type="text" id="id_title" class="vTextField" name="title" size="30" value=""/>
        <input type="button" id="search_title" value="Search Title"/>
        <form action="" method="post" id="book_form"> <div>
            <fieldset class="module aligned ()" id="books">
            <div class="form-row" id="firstrow"> No book found
            </div>
            </fieldset>
                <div class="submit-row">
                    <input type="submit" value="Save and add another" name="_addanother"  />
                    <input type="submit" value="Save and continue editing" name="_continue" />
                    <input type="submit" value="Save" class="default" />
                </div>

        </div></form>
    </div>
{% endblock %}

If we use XMLHttpRequest, we have to go through the boring XML parsing and deploy a proxy to bypass cross-domain AJAX. Neither of them is pleasant. AWS’s JSONP interface is the rescue. In dojo 0.9, dojo.io.script.get replaces the magic dojo.io.bind with Twisted-like syntax, that make it even sweeter.

dojo.connect(searchISBN, 'onclick', function() {
    dojo.io.script.get({
        url: 'http://xml-us.amznxslt.com/onca/xml',
        content: {
            Service: 'AWSECommerceService',
            SubscriptionId: '19267494ZR5A8E2CGPR2',
            AssociateTag: 'kokogiak7-20',
            Operation: 'ItemLookup',
            Style: 'http://kokogiak.com/amazon/JSON/ajsonSingleAsin.xsl',
            ContentType: 'text/javascript',
            IdType: 'ISBN',
            ResponseGroup: 'Medium',
            SearchIndex: 'Books',
            ItemId: isbn.value,
        },
        callbackParamName: "CallBack"
        }).addCallback(function(response) {
            var item = response.Item;
            var row = dojo.byId("firstrow");
            row.textContent = null;
            var node = document.createElement("div");
            node.innerHTML = '<div> <img src="' + item.thumburl + '"/> ' + item.title;
            row.appendChild(node);
        });
});

NOTE: All the parameters are case-sensitive, especially CallBack for callbackParamName. Check r11 for the updated version, further explanation is in section 2.

The above code is dojoized Kokogiak’s idea, and we still use his ajsonSingleAsin.xsl for quick prototype, we would develop a XSLT to meet our requirement in the next section.

Add book by ISBN search
Add book by ISBN search