Run the code in books2.py
and point your web browser to http://localhost:8080.
After logging in (a default username/password combination of admin/admin will be present), you will be presented with a list of entities to browse (books and authors) and after clicking on Books, a screen will present itself that closely resembles the general Browse application (the page still has a Spartan look because no CSS is added at this point):
Thanks to some JavaScript goodness, our browse screen is embedded in the page instead of functioning standalone, yet all functionality is retained, including skipping forward and backward as well as sorting. New books or authors may be added by clicking the Add new button.
When we take a look at the code in books2.py
, we see that its main part consists of definitions of entities, relations, and specific Browse
entities that are combined together to form a CherryPy application:
Chapter7/books2.py
import os import cherrypy from entity import AbstractEntity, Attribute from relation import AbstractRelation from browse import Browse from display import Display from editor import Editor from logondb import LogonDB db="/tmp/book2.db" class Entity(AbstractEntity): database = db class Relation(AbstractRelation): database = db class User(Entity): name = Attribute(notnull=True, unique=True, displayname="Name") class Book(Entity): title = Attribute(notnull=True, displayname="Title") isbn = Attribute(displayname="Isbn") published = Attribute(displayname="Published") class Author(Entity): name = Attribute(notnull=True, unique=True, displayname="Name", primary=True) class OwnerShip(Relation): a = User b = Book class Writer(Relation): a = Book b = Author logon = LogonDB() class AuthorBrowser(Browse): display = Display(Author) edit = Display(Author, edit=True, logon=logon) add = Display(Author, add=True, logon=logon) class BookBrowser(Browse): display = Display(Book) edit = Display(Book, edit=True, logon=logon, columns=Book.columns+[Author]) add = Display(Book, add=True, logon=logon, columns=Book.columns+[Author]) with open('basepage.html') as f: basepage=f.read(-1) books applicationbooks applicationcreatingclass Root(): logon = logon books = BookBrowser(Book, columns=['title','isbn','published',Author]) authors = AuthorBrowser(Author) @cherrypy.expose def index(self): return Root.logon.index(returnpage='../entities') @cherrypy.expose def entities(self): username = self.logon.checkauth() if username is None : raise HTTPRedirect('.') user=User.list(pattern=[('name',username)]) if len(user) < 1 : User(name=username) return basepage%'''<div class="navigation"> <a href="books">Books</a> <a href="authors">Authors</a> </div><div class="content"> </div> <script> ... Javascript omitted ... </script> ''' cherrypy.engine.subscribe('start_thread', lambda thread_index: Root.logon.connect()) current_dir = os.path.dirname(os.path.abspath(__file__)) cherrypy.quickstart(Root(),config={ '/': { 'log.access_file' : os.path.join(current_dir,"access.log"), 'log.screen': False, 'tools.sessions.on': True } })
After importing the modules we need, we define User, Book
, and Author
entities and an OwnerShip
class, to define the relation between a book and a user. Likewise, we define a Writer
class that defines the relation between a book and its author(s).
The next step is to create an instance of a LogonDB
class (highlighted) that will be used in many parts of our CherryPy application to verify that the user is authenticated.
The bulk of the CherryPy application consists of two Browse
classes, one for books and one for authors. Each class has display, edit, and add class variables that point to further branches of our application that are served by Display
instances.
The Root
class we define ties all of this together. It refers to the LogonDb
instance created earlier in its logon
class variable, and its books
and authors
class variables point to the previously defined Browse
instances. It also defines an index()
method that merely presents a logon screen if the user is not yet authenticated and if he/she is, redirects the user to the entities page. The entities()
method which serves this page makes sure there is a corresponding user in the database (highlighted) and presents a base page consisting of a navigation div
and a content div
that will be filled when one of the links in the navigation section is clicked, and some JavaScript to tie everything together.
Before we examine the JavaScript, it might be good to take a look at the illustration to see how the application tree looks:
Path |
Method |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(Note that the edit, add
, and display
branches are each serviced by a different instance of Display)
.
Earlier, we saw that the Browse
class we created was able to function standalone: clicking on any of the buttons referred to the same URL that served up the form in the first place. This setup makes it possible to use different Browse
instances in different parts of an application tree, but here we want to replace a part of a page with the form produced by the Browse
instance using an AJAX call. The problem then is that submitting a form without an action
attribute will result in a request to the current URL, that is, the one referring to the page the form is embedded in, not the one that produces the form.
Fortunately, we can use jQuery to solve this problem by altering the action
attributes of the freshly loaded forms to point to the URL that served those forms:
Chapter7/books2.py
$.ajaxSetup({cache:false,type:"GET"});
$(".navigation a").click(function (){
var rel = $(this).attr('href'),
function shiftforms(){
$(".content form").each(function(i,e){
$(e).attr('action',
rel+'/'+$(e).attr('action'));
$('[type=submit]',e).bind('click',
function(event){
var f = $(this).parents('form'),
var n = $(this).attr('name'),
if (n != ''){
n = '&'+n+'='+$(this).attr('value'),}
$(".content").load(f.attr('action'),
f.serialize()+n,shiftforms);
return false;
});
});
};
// change action attributes of form elements
$(".content").load($(this).attr('href'),shiftforms);
return false;
});
This is accomplished by adding a click event handler to the links in the navigation area. That will not only prevent the default action but load the HTML produced by the URL referred to by the href
attribute and pass a callback function that will alter the action attributes of any freshly loaded<form>
elements (highlighted).
The shiftforms()
function first prepends the original href
contents to the action
attributes and then binds a click handler to each button or input element with a type
attribute equal to submit.
It would not be sufficient to add a submit handler to the form, because we don't want to let the<form>
perform its default action. When a form is submitted, the contents of the page are replaced and this is not what we want. Instead, we want to replace the contents of the content div
so we have to load()
the URL from the form's action
attribute ourselves.
This also means that we have to serialize the contents of the form to add as parameters to this URL, but jQuery's serialize()
function will not serialize submit buttons. We, therefore, end up with adding a click handler to submit buttons in order to be able to determine the submit button that was clicked, so we can construct a complete list of parameters, including the name and value of the submit button.
18.190.239.166