Run browse.py
and point your browser to http://localhost:8080
. A small sample application is started that shows lists of random data, as can be seen in the following image:
This rather Spartan looking interface may lack most visual adornments, but it is fully functional nevertheless. You may page through the list of data by clicking the appropriate buttons in the button bar at the bottom, change the sort order of the list by clicking one or more times on a header (which will cycle through ascending, descending, or no sort at all, however, without any visual feedback at the moment) or reduce the list of items shown by clicking on a value in a column, that will result in a list of items that share the same value in this column. All items may be shown again by clicking the Clear button.
The browse
module (which is available as browse.py)
contains more than the sample application. It also defines a reusable Browse
class that can be initialized with a reference to an Entity
and used as a CherryPy application. The Browse
class can also be given arguments that specify which, if any, columns should be shown.
Its intended use is best illustrated by taking a look at the sample application:
Chapter7/browse.py
from random import randint import os current_dir = os.path.dirname(os.path.abspath(__file__)) class Entity(AbstractEntity): database='/tmp/browsetest.db' class Number(Entity): n = Attribute(displayname="Size") n=len(Number.listids()) if n<100: for i in range(100-n): Number(n=randint(0,1000000)) root = Browse(Number, columns=['id','n'], sortorder=[('n','asc'),('id','desc')]) cherrypy.quickstart(root,config={ '/': { 'log.access_file' : os.path.join(current_dir,"access.log"), 'log.screen': False, 'tools.sessions.on': True } })
It initializes an instance of the Browse
class with a single mandatory argument as a subclass of Entity
, in this case, Number
. It also takes a columns
argument that takes a list that specifies which attributes to show in the table's columns and in which order. It also takes a sortorder
argument, a list of tuples that specifies on which columns to sort and in which direction.
This instance of the Browse
class is then passed to CherryPy's quickstart()
function to deliver the functionality to the client. It would be just as simple to mount two different Browse
instances, each servicing a different Entity
class within a custom root application.
How is all this implemented? Let's first take a look at the __init__()
method:
Chapter7/browse.py
class Browse:
def __init__(self,entity,columns=None,
sortorder=None,pattern=None,page=10,show="show"):
if not issubclass(entity,AbstractEntity) :
raise TypeError()
self.entity = entity
self.columns = entity.columns if columns is None else
columns
self.sortorder = [] if sortorder is None else sortorder
self.pattern = [] if pattern is None else pattern
self.page = page
self.show = show
self.cache= {}
self.cachelock=threading.Lock()
self.cachesize=3
for c in self.columns:
if not (c in entity.columns or c == 'id') and not (
hasattr(self.entity,'get'+c.__name__)) :
raise ValueError('column %s not defined'%c)
if len(self.sortorder) > len(self.columns) :
raise ValueError()
for s in self.sortorder:
if s[0] not in self.columns and s[0]!='id':
raise ValueError(
'sorting on column %s not
possible'%s[0])
if s[1] not in ('asc','desc'):
raise ValueError(
'column %s, %s is not a valid sort
order'%s)
for s in self.pattern:
if s[0] not in self.columns and s[0]!='id':
raise ValueError(
'filtering on column %s not
possible'%s[0])
if self.page < 5 :
raise ValueError()
The __init__()
method takes quite a number of arguments and only the entity
argument is mandatory. It should be a subclass of AbstractEntity
and this is checked in the highlighted code.
All parameters are stored and initialized to suitable defaults if missing.
The columns
argument defaults to a list of all columns defined for the entity, and we verify that any column we want to display is actually defined for the entity.
Likewise, we verify that the sortorder
argument (a list of tuples containing the column name and its sort direction) contains no more items than there are columns (as it is not sensible to sort more than once on the same column) and that the sort directions specified are either asc
or desc
(for ascending and descending respectively).
The pattern
argument, a list of tuples containing the column name and a value to filter on, is treated in a similar manner to see if only defined columns are filtered on. Note that it is perfectly valid to filter or sort on a column or columns that are themselves not shown. This way, we can display subsets of a large dataset without bothering with too many columns.
The final sanity check is done on the page
argument which specifies the number of rows to show on each page. Very few rows feels awkward and negative values are meaningless, so we settle for a lower limit of five rows per page:
Chapter7/browse.py
@cherrypy.expose def index(self, _=None, start=0, pattern=None, sortorder=None, cacheid=None, next=None,previous=None, first=None, last=None, clear=None): if not clear is None : pattern=None if sortorder is None : sortorder = self.sortorder elif type(sortorder)==str: sortorder=[tuple(sortorder.split(','))] elif type(sortorder)==list: sortorder=[tuple(s.split(',')) for s in sortorder] else: sortorder=None if pattern is None : pattern = self.pattern elif type(pattern)==str: pattern=[tuple(pattern.split(','))] elif type(pattern)==list: pattern=[tuple(s.split(',',1)) for s in pattern] else: pattern=None ids = self.entity.listids( pattern=pattern,sortorder=sortorder) start=int(start) if not next is None : start+=self.page elif not previous is None : start-=self.page elif not first is None : start=0 elif not last is None : start=len(ids)-self.page if start >= len(ids) : start=len(ids)-1 if start<0 : start=0 yield '<table class="entitylist" start="%d" page="%d"> n'%(start,self.page) yield '<thead><tr>' for col in self.columns: if type(col) == str : sortclass="notsorted" for s in sortorder: if s[0]==col : sortclass='sorted-'+s[1] break yield '<th class="%s">'%sortclass+self.entity. displaynames[col]+'</th>' else : yield '<th>'+col.__name__+'</th>' yield '</tr></thead> <tbody> ' entities = [self.entity(id=i) for i in ids[start:start+self.page]] for e in entities: vals=[] for col in self.columns: if not type(col) == str: vals.append( "".join( ['<span class="related" entity="%s" >%s</span> ' % (r.__ class__.__name__, r.primary) for r in e.get(col)])) else: vals.append(str(getattr(e,col))) yield ('<tr id="%d"><td>' + '</td><td>'.join(vals)+'</td></tr> ')%(e.id,) yield '</tbody> </table> ' yield '<form method="GET" action=".">' yield '<div class="buttonbar">' yield '<input name="start" type="hidden" value="%d"> '%start for s in sortorder: yield '<input name="sortorder" type="hidden" value="%s,%s"> '%s for f in pattern: yield '<input name="pattern" type="hidden" value="%s,%s"> '%f yield '<input name="cacheid" type="hidden" value="%s">'%cacheid yield '<p class="info">items %d-%d/%d</p>'%(start+1,start+len (entities),len(ids)) yield '<button name="first" type="submit">First</button> ' yield '<button name="previous" type="submit">Previous</button> ' yield '<button name="next" type="submit">Next</button> ' yield '<button name="last" type="submit">Last</button> ' yield '<button name="clear" type="submit">Clear</button> ' yield '</div>' yield '</form>' # no name attr on the following button otherwise it may be sent as an argument! yield '<form method="GET" action="add"><button type="submit">Add new</button></form>'
Both the initial display of the table as well as paging, sorting, and filtering are taken care of by the same index()
method. To understand all the parameters it may take, it might be helpful to look at the HTML markup it produces for our sample application.
The index()
method of the Browse
class is not the only place where we encounter a fair amount of HTML to be delivered to the client. This might become difficult to read and therefore difficult to maintain and using templates might be a better solution. A good start point for choosing a template solution that works well with CherryPy is http://www.cherrypy.org/wiki/ChoosingATemplatingLanguage.
3.135.201.217