diff -r 706972d09f05 -r e6bca452ce72 pvl/verkko/hosts.py --- a/pvl/verkko/hosts.py Sat Jan 26 19:40:24 2013 +0200 +++ b/pvl/verkko/hosts.py Sat Jan 26 21:06:00 2013 +0200 @@ -1,4 +1,4 @@ -from pvl.verkko import web, db +from pvl.verkko import web, db, table from pvl.verkko.utils import parse_timedelta, IPv4Network from pvl.web import html @@ -143,92 +143,56 @@ )) ## Controller -def column (attr, title, column, rowhtml=None, sort=True, filter=True, colcss=True, rowfilter=True, rowtitle=None, rowcss=None) : +class HostsTable (table.Table) : """ - web.Table column spec. + Table of hosts. """ - return (attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss) - -class BaseHandler (web.DatabaseHandler) : - """ - Common controller stuff for DHCP hosts - """ - - CSS = ( - "/static/dhcp/hosts.css", - ) - JS = ( - #"/static/jquery/jquery.js" + COLUMNS = ( + table.Column('ip', "IP", Host.ip, + rowfilter = True, + ), + table.Column('mac', "MAC", Host.mac, Host.render_mac, + rowfilter = True, + ), + table.Column('name', "Hostname", Host.name, Host.render_name, ), + table.Column('gw', "Network", Host.gw, Host.network, ), + table.Column('seen', "Seen", Host.last_seen, Host.seen, ), + table.Column('state', "State", Host.count, + rowtitle = Host.state_title, + rowcss = Host.state_class, + ), ) - TABLE = Host - TABLE_COLUMNS = ( - #column('id', "#", Host.id ), - column('ip', "IP", Host.ip, ), - column('mac', "MAC", Host.mac, Host.render_mac), - column('name', "Hostname", Host.name, Host.render_name, rowfilter=False), - column('gw', "Network", Host.gw, Host.network, rowfilter=False), - column('seen', "Seen", Host.last_seen, Host.seen, rowfilter=False), - column('state', "State", Host.count, rowtitle=Host.state_title, rowcss=Host.state_class, rowfilter=False), + # XXX: have to set again + ATTRS = dict((col.attr, col) for col in COLUMNS) + + # XXX: set later + TABLE_URL = ITEM_URL = None + + # default + SORT = Host.last_seen.desc() + PAGE = 10 + +class HostsHandler (table.TableHandler, web.DatabaseHandler) : + """ + Combined database + + """ + + CSS = web.DatabaseHandler.CSS + table.TableHandler.CSS + ( + "/static/dhcp/hosts.css", ) - # attr -> column - TABLE_ATTRS = dict((attr, column) for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in TABLE_COLUMNS) - - # default sort - TABLE_SORT = Host.last_seen.desc() - - # items per page - TABLE_PAGE = 10 - - # target for items - TABLE_URL = None - TABLE_ITEM_URL = None + # model + TABLE = HostsTable def query (self) : """ Database SELECT query. """ - return self.db.query(self.TABLE) - - def sort (self, query, default=TABLE_SORT) : - """ - Apply ?sort= from requset args to query. - - Return { attr: sort }, query - """ - - sort = self.request.args.get('sort') - - if sort : - name = sort.lstrip('+-') - else : - name = None + return self.db.query(Host) - if name : - order_by = self.TABLE_ATTRS[name] - else : - order_by = default - - # prefix -> ordering - if not sort : - pass - elif sort.startswith('+') : - order_by = order_by.asc() - elif sort.startswith('-') : - order_by = order_by.desc() - else : - pass - - # apply - log.debug("sort: %s", order_by) - - query = query.order_by(order_by) - - return sort, query - def filter_seen (self, value) : """ Return filter expression for given attr == value @@ -269,325 +233,7 @@ def filter_mac (self, value) : return self.filter_attr('mac', Host.normalize_mac(value)) - def filter_attr (self, attr, value) : - """ - Return filter expression for given attr == value - """ - - # preprocess - like = False - - if value.endswith('*') : - like = value.replace('*', '%') - - # filter - column = self.TABLE_ATTRS[attr] - - if like : - return (column.like(like)) - else : - return (column == value) - - def _filter (self, attr, values) : - """ - Apply filters for given attr -> (value, expression) - """ - - for value in values : - value = value.strip() - - # ignore empty fields - if not value : - continue - - # lookup attr-specific filter - filter = getattr(self, 'filter_{attr}'.format(attr=attr), None) - - if filter : - filter = filter(value) - else : - # use generic - filter = self.filter_attr(attr, value) - - log.debug("%s: %s: %s", attr, value, filter) - - yield value, filter - - def filter (self, query) : - """ - Apply filters from request.args against given hosts. - - Returns (filters, hosts). - """ - - # filter? - filters = {} - - for attr in self.TABLE_ATTRS : - # from request args - values = self.request.args.getlist(attr) - - # lookup attr filters as expressions - value_filters = list(self._filter(attr, values)) - - # ignore empty fields - if not value_filters : - continue - - # filtering values, and filter expressions - values, expressions = zip(*value_filters) - - # apply - query = query.filter(db.or_(*expressions)) - filters[attr] = values - - return filters, query - - def filters_title (self) : - """ - Return a string representing the applied filters. - """ - - return ', '.join(value for values in self.filters.itervalues() for value in values) - - def render_table (self, query, caption=None, sort=None, filters=None, page=None, hilight=None) : - """ - Return
element. Wrapped in if filters. - - query - filter()'d sort()'d SELECT query() - caption - optional
- sort - None for no sorting ui, sort-attr otherwise. - filters - None for no filtering ui, dict of filters otherwise. - page - display pagination for given page - hilight - { attr: value } cells to hilight - """ - - def url (filters=filters, sort=sort, **opts) : - """ - URL for table with given opts, keeping our sorting/filtering unless overriden. - """ - - args = dict() - - if filters : - args.update(filters) - - if sort : - args['sort'] = sort - - if opts : - args.update(opts) - - return self.url(self.TABLE_URL, **args) - - def sorturl (attr, sort=sort) : - """ - URL for table sorted by given column, reversing direction if already sorting by given column. - """ - - if not sort : - sort = attr - elif sort.lstrip('+-') != attr : - sort = attr - elif sort.startswith('-') : - sort = "+" + attr - else : - sort = "-" + attr - - return url(sort=sort) - - def itemurl (item) : - """ - URL for given item, by id. - """ - - if self.TABLE_ITEM_URL : - # separate page - return self.url(self.TABLE_ITEM_URL, id=item.id) - else : - # to our table - return url() + '#{id}'.format(id=item.id) - - def render_filter (attr) : - """ - Render filter-input for column. - """ - - value = filters.get(attr) - - if value : - # XXX: multi-valued filters? - value = value[0] - else : - value = None - - return html.input(type='text', name=attr, value=value) - - def render_head () : - """ - Yield header, filter rows for columns in table header. - """ - - # id - yield html.td('#'), html.td(html.input(type='submit', value=u'\u00BF')) - - for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in self.TABLE_COLUMNS : - header = title - - if sort : - header = html.a(href=sorturl(attr))(header) - - header = html.th(header) - - if filters is not None and filter : - filter = render_filter(attr) - else : - filter = None - - if colcss is True : - colcss = attr - - filter = html.td(class_=colcss)(filter) - - yield header, filter - - def render_cell (attr, value, rowhtml=None, colcss=True, filter=None, rowtitle=None, rowcss=None, hilight=hilight) : - """ - Render a single cell. - - colcss - css class for column; True -> attr - filter - render filter link for value? - htmlvalue - rendered value? - title - mouseover title for cell - rowcss - css class for row - """ - - if not rowhtml : - rowhtml = value - - if filter : - cell = html.a(href=url(filters=None, **{attr: value}))(rowhtml) - else : - cell = rowhtml - - if colcss is True : - colcss = attr - - if hilight : - hilight = attr in hilight and value in hilight[attr] - - css = (colcss, rowcss, 'hilight' if hilight else None) - css = ' '.join(cls for cls in css if cls) - - return html.td(class_=css, title=rowtitle)(cell) - - def render_row (item) : - """ - Yield columns for row. - """ - - for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in self.TABLE_COLUMNS : - # XXX: this is sometimes broken, figure out how to index by column - value = getattr(item, attr) - - if rowhtml : - rowhtml = rowhtml(item) - else : - rowhtml = value - - if rowtitle : - rowtitle = rowtitle(item) - else : - rowtitle = None - - if rowcss : - rowcss = rowcss(item) - else : - rowcss = None - - yield render_cell(attr, value, - rowhtml = rowhtml, - colcss = colcss, - filter = value if rowfilter else None, - rowtitle = rowtitle, - rowcss = rowcss, - ) - - def render_body (rows) : - """ - Yield rows. - """ - - for i, item in enumerate(rows) : - yield html.tr(class_=('alternate' if i % 2 else None), id=item.id)( - html.th( - html.a(href=itemurl(item))("#") - ), - - render_row(item) - ) - - def render_pagination (page, count=None) : - """ - Render pagination links. - """ - - if count is not None : - pages = int(math.ceil(count / self.TABLE_PAGE)) - else : - pages = None - - if page > 0 : - yield html.a(href=url(page=0))(html("«« First")) - yield html.a(href=url(page=(page - 1)))(html("« Prev")) - - yield html.span("Page {page} of {pages}".format(page=(page + 1), pages=(pages or '???'))) - - yield html.a(href=url(page=(page + 1)))(html("» Next")) - - - def render_foot () : - # XXX: does separate SELECT count() - count = query.count() - - if page : - return render_pagination(page, count) - else : - return "{count} hosts".format(count=count) - - # columns for the two header rows - headers, filtering = zip(*list(render_head())) - - # render table - table = html.table( - html.caption(caption) if caption else None, - html.thead( - html.tr(headers), - # filters? - html.tr(class_='filter')(filtering) if filters is not None else None, - ), - html.tbody( - render_body(query) - ), - html.tfoot( - html.tr( - html.td(colspan=(1 + len(self.TABLE_COLUMNS)))( - render_foot() - ) - ) - ) - ) - - # filters form? - if filters is None : - return table - else : - return html.form(method='get', action=url(filters=None, sort=None))( - html.input(type='hidden', name='sort', value=sort), - table, - ) - -class ItemHandler (BaseHandler) : +class ItemHandler (HostsHandler) : """ A specific DHCP host, along with a list of related hosts. """ @@ -638,53 +284,33 @@ html.a(href=self.url(ListHandler))(html('«'), 'Back'), ) - -class ListHandler (BaseHandler) : +class ListHandler (HostsHandler) : """ List of DHCP hosts for given filter. """ - TABLE_PAGE = 10 + TABLE_ITEM_URL = ItemHandler def process (self) : - hosts = self.query() - - # filter - self.filters, hosts = self.filter(hosts) - - # sort XXX: default per filter column? - self.sorts, hosts = self.sort(hosts) - - # page? - self.page = self.request.args.get('page') - - if self.page : - self.page = int(self.page) - - hosts = hosts.offset(self.page * self.PAGE).limit(self.PAGE) - - self.hosts = hosts - + # super + table.TableHandler.process(self) + def title (self) : if self.filters : return "DHCP Hosts: {filters}".format(filters=self.filters_title()) else : return "DHCP Hosts" - + def render (self) : return ( - self.render_table(self.hosts, filters=self.filters, sort=self.sorts, page=self.page), + self.render_table(self.query, filters=self.filters, sort=self.sorts, page=self.page), - html.a(href=self.url())(html('«'), 'Back') if self.filters else None, + #html.a(href=self.url())(html('«'), 'Back') if self.filters else None, ) -# XXX: -BaseHandler.TABLE_URL = ListHandler -BaseHandler.TABLE_ITEM_URL = ItemHandler - -class RealtimeHandler (BaseHandler) : +class RealtimeHandler (HostsHandler) : TITLE = "DHCP Pseudo-Realtime hosts.." - CSS = BaseHandler.CSS + ( + CSS = HostsHandler.CSS + ( 'http://code.jquery.com/ui/1.9.0/themes/base/jquery-ui.css', ) JS = (