# -*- test-case-name: xmantissa.test.test_scrolltable -*-
from zope.interface import implements
from nevow.athena import LiveElement, expose
from axiom.attributes import timestamp
from xmantissa.ixmantissa import IWebTranslator, IColumn
from xmantissa.tdb import AttributeColumn, Unsortable
TYPE_FRAGMENT = 'fragment'
# these objects aren't for view junk - they allow the model
# to inform the javascript controller about which columns are
# sortable, as well as supporting non-attribute columns
class TimestampAttributeColumn(AttributeColumn):
# timestamps are a special case; we need to get the posix timestamp
# so we can send the attribute value to javascript. we don't register
# an adapter for attributes.timestamp because the TDB model uses
# IColumn.extractValue() to determine the value of the query pivot,
# and so it needs an extime.Time instance, not a float
def extractValue(self, model, item):
val = AttributeColumn.extractValue(self, model, item)
if val is None:
raise AttributeError("%r was None" % (self.attribute,))
return val.asPOSIXTimestamp()
def getType(self):
return 'timestamp'
class UnsortableColumn(AttributeColumn):
def sortAttribute(self):
return None
class UnsortableColumnWrapper:
"""
Wraps an L{AttributeColumn} and makes it unsortable
@ivar col: L{AttributeColumn}
"""
implements(IColumn)
def __init__(self, col):
self.col = col
self.attribute = col.attribute
self.attributeID = col.attributeID
def extractValue(self, model, item):
return self.col.extractValue(model, item)
def sortAttribute(self):
return None
def getType(self):
return self.col.getType()
class Scrollable(object):
"""
Mixin for "model" implementations of an in-browser scrollable list of
elements.
@ivar webTranslator: A L{IWebTranslator} provider for resolving and
creating web links for items.
@ivar columns: A mapping of attribute identifiers to L{IColumn}
providers.
@ivar columnNames: A list of attribute identifiers.
@ivar isAscending: A boolean indicating the current order of the sort.
@ivar currentSortColumn: A L{axiom.attributes.SQLAttribute} representing
the current sort key.
"""
def __init__(self, webTranslator, columns, defaultSortColumn,
defaultSortAscending):
self.webTranslator = webTranslator
self.columns = {}
self.columnNames = []
for col in columns:
# see comment in TimestampAttributeColumn
if isinstance(col, timestamp):
col = TimestampAttributeColumn(col)
else:
col = IColumn(col)
if defaultSortColumn is None:
defaultSortColumn = col.sortAttribute()
attributeID = unicode(col.attributeID, 'ascii')
self.columns[attributeID] = col
self.columnNames.append(attributeID)
self.currentSortColumn = defaultSortColumn
self.isAscending = defaultSortAscending
# Override these two in a subclass
def performCount(self):
"""
Override this in a subclass.
@rtype: C{int}
@return: The total number of elements in this scrollable.
"""
raise NotImplementedError()
def performQuery(self, rangeBegin, rangeEnd):
"""
Override this in a subclass.
@rtype: C{list}
@return: Elements from C{rangeBegin} to C{rangeEnd} of the
underlying data set, as ordered by the value of
C{currentSortColumn} sort column in the order indicated by
C{isAscending}.
"""
raise NotImplementedError()
# The rest takes care of responding to requests from the client.
def getTableMetadata(self):
"""
Retrieve a description of the various properties of this scrolltable.
@return: A sequence containing 5 elements. They are, in order, a
list of the names of the columns present, a mapping of column names
to two-tuples of their type and a boolean indicating their
sortability, the total number of rows in the scrolltable, the name
of the default sort column, and a boolean indicating whether or not
the current sort order is ascending.
"""
coltypes = {}
for (colname, column) in self.columns.iteritems():
sortable = column.sortAttribute() is not None
coltype = column.getType()
if coltype is not None:
coltype = unicode(coltype, 'ascii')
coltypes[colname] = (coltype, sortable)
if self.currentSortColumn:
csc = unicode(self.currentSortColumn.attrname, 'ascii')
else:
csc = None
return [self.columnNames, coltypes, self.requestCurrentSize(),
csc, self.isAscending]
expose(getTableMetadata)
def resort(self, columnName):
"""
Re-sort the table.
@param columnName: the name of the column to sort by. This is a string
because it is passed from the browser.
"""
csc = self.currentSortColumn
newSortColumn = self.columns[columnName].sortAttribute()
if newSortColumn is None:
raise Unsortable('column %r has no sort attribute' % (columnName,))
if csc is newSortColumn:
self.isAscending = not self.isAscending
else:
self.currentSortColumn = newSortColumn
self.isAscending = True
return self.isAscending
expose(resort)
def linkToItem(self, item):
"""
Return a URL that the row for C{item} should link to, by asking the
L{xmantissa.ixmantissa.IWebTranslator} in C{self.store}
@return: C{unicode} URL
"""
return unicode(self.webTranslator.toWebID(item), 'ascii')
def itemFromLink(self, link):
"""
Inverse of L{linkToItem}.
@rtype: L{axiom.item.Item}
"""
return self.webTranslator.fromWebID(link)
def requestRowRange(self, rangeBegin, rangeEnd):
"""
Retrieve display data for the given range of rows.
"""
return self.constructRows(self.performQuery(rangeBegin, rangeEnd))
expose(requestRowRange)
def requestCurrentSize(self):
return self.performCount()
expose(requestCurrentSize)
def performAction(self, name, rowID):
method = getattr(self, 'action_' + name)
item = self.itemFromLink(rowID)
return method(item)
expose(performAction)
class ScrollableView(object):
"""
Mixin for structuring model data in the way expected by
Mantissa.ScrollTable.ScrollingWidget.
"""
jsClass = u'Mantissa.ScrollTable.ScrollingWidget'
fragmentName = 'scroller'
def constructRows(self, items):
rows = []
for item in items:
row = dict((colname, col.extractValue(self, item))
for (colname, col) in self.columns.iteritems())
link = self.linkToItem(item)
if link is not None:
row[u'__id__'] = link
rows.append(row)
return rows
class _ScrollingElement(LiveElement):
def getInitialArguments(self):
return [self.getTableMetadata()]
class ItemQueryScrollingFragment(Scrollable, ScrollableView, _ScrollingElement):
def __init__(self, store, itemType, baseConstraint, columns,
defaultSortColumn=None, defaultSortAscending=True,
*a, **kw):
Scrollable.__init__(self, IWebTranslator(store, None), columns,
defaultSortColumn, defaultSortAscending)
LiveElement.__init__(self, *a, **kw)
self.store = store
self.itemType = itemType
self.baseConstraint = baseConstraint
def performCount(self):
return self.store.query(self.itemType, self.baseConstraint).count()
def performQuery(self, rangeBegin, rangeEnd):
if self.isAscending:
sort = self.currentSortColumn.ascending
else:
sort = self.currentSortColumn.descending
return list(self.store.query(self.itemType,
self.baseConstraint,
offset=rangeBegin,
limit=rangeEnd - rangeBegin,
sort=sort))
ScrollingFragment = ItemQueryScrollingFragment
class SequenceScrollingFragment(Scrollable, ScrollableView, _ScrollingElement):
"""
Scrolltable implementation backed by any Python L{axiom.item.Item}
sequence.
"""
def __init__(self, store, elements, columns, defaultSortColumn=None,
defaultSortAscending=True, *a, **kw):
Scrollable.__init__(self, IWebTranslator(store, None), columns,
defaultSortColumn, defaultSortAscending)
LiveElement.__init__(self, *a, **kw)
self.store = store
self.elements = elements
def getInitialArguments(self):
return [self.getTableMetadata()]
def performCount(self):
return len(self.elements)
def performQuery(self, rangeBegin, rangeEnd):
step = 1
if not self.isAscending:
# The ranges are from the end, not the beginning.
rangeBegin = max(0, len(self.elements) - rangeBegin - 1)
# Python is so very very confusing:
# s[1:0:-1] == []
# s[1:None:-1] == [s[0]]
# s[1:-1:-1] == some crazy thing you don't even want to know
rangeEnd = max(-1, len(self.elements) - rangeEnd - 1)
if rangeEnd == -1:
rangeEnd = None
step = -1
return self.elements[rangeBegin:rangeEnd:step]
class StoreIDSequenceScrollingFragment(SequenceScrollingFragment):
"""
Scrolltable implementation like L{SequenceScrollingFragment} but which is
backed by a sequence of Item storeID values rather than Items themselves.
"""
def performQuery(self, rangeBegin, rangeEnd):
return map(
self.store.getItemByID,
super(
StoreIDSequenceScrollingFragment,
self).performQuery(rangeBegin, rangeEnd))
syntax highlighted by Code2HTML, v. 0.9.1