Time for action using a table-based Entity browser

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:

Time for action using a table-based Entity browser

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.

What just happened?

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.201.217