--- 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 + <table>
+ """
+
+ 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 <table> element. Wrapped in <form> if filters.
-
- query - filter()'d sort()'d SELECT query()
- caption - optional <caption>
- 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 = (