pvl/verkko/hosts.py
changeset 179 706972d09f05
parent 178 f9f5e669bace
child 180 e6bca452ce72
equal deleted inserted replaced
178:f9f5e669bace 179:706972d09f05
    62             return u'???'
    62             return u'???'
    63 
    63 
    64         else :
    64         else :
    65             return unicode(self.mac)
    65             return unicode(self.mac)
    66 
    66 
       
    67     def network (self) :
       
    68         return self.gw
       
    69 
    67     def render_name (self) :
    70     def render_name (self) :
    68         if self.name :
    71         if self.name :
    69             return self.name.decode('ascii', 'replace')
    72             return self.name.decode('ascii', 'replace')
    70         else :
    73         else :
    71             return None
    74             return None
   103         if (datetime.datetime.now() - dt).days :
   106         if (datetime.datetime.now() - dt).days :
   104             return dt.strftime(cls.DATE_FMT)
   107             return dt.strftime(cls.DATE_FMT)
   105 
   108 
   106         else :
   109         else :
   107             return dt.strftime(cls.TIME_FMT)
   110             return dt.strftime(cls.TIME_FMT)
   108 
   111     
   109     def seen (self) :
   112     def seen (self) :
   110         return (
   113         return (
   111                 html.span(title=self.first_seen)(self.format_datetime(self.first_seen)),
   114                 html.span(title=self.first_seen)(self.format_datetime(self.first_seen)),
   112                 '-',
   115                 '-',
   113                 html.span(title=self.last_seen)(self.format_datetime(self.last_seen))
   116                 html.span(title=self.last_seen)(self.format_datetime(self.last_seen))
   138     #id      = db.dhcp_hosts.c.rowid,
   141     #id      = db.dhcp_hosts.c.rowid,
   139     #state   = db.dhcp_hosts.c.,
   142     #state   = db.dhcp_hosts.c.,
   140 ))
   143 ))
   141 
   144 
   142 ## Controller 
   145 ## Controller 
       
   146 def column (attr, title, column, rowhtml=None, sort=True, filter=True, colcss=True, rowfilter=True, rowtitle=None, rowcss=None) :
       
   147     """
       
   148         web.Table column spec.
       
   149     """
       
   150 
       
   151     return (attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss)
       
   152 
   143 class BaseHandler (web.DatabaseHandler) :
   153 class BaseHandler (web.DatabaseHandler) :
   144     """
   154     """
   145         Common controller stuff for DHCP hosts
   155         Common controller stuff for DHCP hosts
   146     """
   156     """
   147 
   157 
   149         "/static/dhcp/hosts.css", 
   159         "/static/dhcp/hosts.css", 
   150     )
   160     )
   151     JS = (
   161     JS = (
   152         #"/static/jquery/jquery.js"
   162         #"/static/jquery/jquery.js"
   153     )
   163     )
   154 
   164     
   155     HOST_ATTRS = {
   165     TABLE = Host
   156         'id':       Host.id,
   166     TABLE_COLUMNS = (
   157         'net':      Host.gw,
   167         #column('id',    "#",        Host.id         ),
   158         'ip':       Host.ip,
   168         column('ip',    "IP",       Host.ip,        ),
   159         'mac':      Host.mac,
   169         column('mac',   "MAC",      Host.mac,       Host.render_mac),
   160         'name':     Host.name,
   170         column('name',  "Hostname", Host.name,      Host.render_name, rowfilter=False),
   161         'seen':     Host.last_seen,
   171         column('gw',    "Network",  Host.gw,        Host.network, rowfilter=False),
   162         'state':    Host.state,
   172         column('seen',  "Seen",     Host.last_seen, Host.seen, rowfilter=False),
   163         'count':    Host.count,
   173         column('state', "State",    Host.count,     rowtitle=Host.state_title, rowcss=Host.state_class, rowfilter=False),
   164     }
   174     )
   165 
   175     
   166     HOST_SORT = Host.last_seen.desc()
   176     # attr -> column
       
   177     TABLE_ATTRS = dict((attr, column) for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in TABLE_COLUMNS)
       
   178     
       
   179     # default sort
       
   180     TABLE_SORT = Host.last_seen.desc()
       
   181     
       
   182     # items per page
       
   183     TABLE_PAGE = 10
       
   184     
       
   185     # target for items
       
   186     TABLE_URL = None
       
   187     TABLE_ITEM_URL = None
   167 
   188 
   168     def query (self) :
   189     def query (self) :
   169         return self.db.query(Host)
   190         """
   170     
   191             Database SELECT query.
   171     def sort (self, hosts, default=HOST_SORT) :
   192         """
       
   193 
       
   194         return self.db.query(self.TABLE)
       
   195     
       
   196     def sort (self, query, default=TABLE_SORT) :
       
   197         """
       
   198             Apply ?sort= from requset args to query.
       
   199 
       
   200             Return { attr: sort }, query
       
   201         """
       
   202 
   172         sort = self.request.args.get('sort')
   203         sort = self.request.args.get('sort')
   173 
   204 
   174         if sort :
   205         if sort :
   175             name = sort.lstrip('+-')
   206             name = sort.lstrip('+-')
   176         else :
   207         else :
   177             name = None
   208             name = None
   178 
   209 
   179         if name :
   210         if name :
   180             order_by = self.HOST_ATTRS[name]
   211             order_by = self.TABLE_ATTRS[name]
   181         else :
   212         else :
   182             order_by = default
   213             order_by = default
   183         
   214         
   184         # prefix
   215         # prefix -> ordering
   185         if not sort :
   216         if not sort :
   186             pass
   217             pass
   187         elif sort.startswith('+') :
   218         elif sort.startswith('+') :
   188             order_by = order_by.asc()
   219             order_by = order_by.asc()
   189         elif sort.startswith('-') :
   220         elif sort.startswith('-') :
   190             order_by = order_by.desc()
   221             order_by = order_by.desc()
   191         else :
   222         else :
   192             pass
   223             pass
   193 
   224         
       
   225         # apply
   194         log.debug("sort: %s", order_by)
   226         log.debug("sort: %s", order_by)
   195         
   227         
   196         hosts = hosts.order_by(order_by)
   228         query = query.order_by(order_by)
   197 
   229 
   198         # k
   230         return sort, query
   199         return sort, hosts
   231     
   200     
   232     def filter_seen (self, value) :
       
   233         """
       
   234             Return filter expression for given attr == value
       
   235         """
       
   236 
       
   237         column = Host.last_seen
       
   238 
       
   239         if value.isdigit() :
       
   240             # specific date
       
   241             date = datetime.datetime.strptime(value, Host.DATE_FMT).date()
       
   242 
       
   243             return db.between(date.strftime(Host.DATE_FMT), 
       
   244                     db.func.strftime(Host.DATE_FMT, Host.first_seen),
       
   245                     db.func.strftime(Host.DATE_FMT, Host.last_seen)
       
   246             )
       
   247         else :
       
   248             # recent
       
   249             timedelta = parse_timedelta(value)
       
   250             
       
   251             return ((db.func.now() - Host.last_seen) < timedelta)
       
   252 
       
   253             # XXX: for sqlite, pgsql should handle this natively?
       
   254             # to seconds
       
   255             #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds
       
   256             
       
   257             # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout
       
   258             #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout)
       
   259         
       
   260     def filter_ip (self, value) :
       
   261         column = Host.ip
       
   262 
       
   263         # column is IPv4 string literal format...
       
   264         if '/' in value :
       
   265             return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value)))
       
   266         else :
       
   267             return (db.func.inet(Host.ip) == db.func.inet(value))
       
   268 
       
   269     def filter_mac (self, value) :
       
   270         return self.filter_attr('mac', Host.normalize_mac(value))
       
   271 
   201     def filter_attr (self, attr, value) :
   272     def filter_attr (self, attr, value) :
   202         """
   273         """
   203             Return filter expression for given attr == value
   274             Return filter expression for given attr == value
   204         """
   275         """
   205 
   276 
   206         if attr == 'seen' :
   277         # preprocess
   207             column = Host.last_seen
   278         like = False
   208 
   279 
   209             if value.isdigit() :
   280         if value.endswith('*') :
   210                 # specific date
   281             like = value.replace('*', '%')
   211                 date = datetime.datetime.strptime(value, Host.DATE_FMT).date()
   282 
   212 
   283         # filter
   213                 return db.between(date.strftime(Host.DATE_FMT), 
   284         column = self.TABLE_ATTRS[attr]
   214                         db.func.strftime(Host.DATE_FMT, Host.first_seen),
   285 
   215                         db.func.strftime(Host.DATE_FMT, Host.last_seen)
   286         if like :
   216                 )
   287             return (column.like(like))
       
   288         else :
       
   289             return (column == value)
       
   290  
       
   291     def _filter (self, attr, values) :
       
   292         """
       
   293             Apply filters for given attr -> (value, expression)
       
   294         """
       
   295 
       
   296         for value in values :
       
   297             value = value.strip()
       
   298             
       
   299             # ignore empty fields
       
   300             if not value :
       
   301                 continue
       
   302 
       
   303             # lookup attr-specific filter
       
   304             filter = getattr(self, 'filter_{attr}'.format(attr=attr), None)
       
   305 
       
   306             if filter :
       
   307                 filter = filter(value)
   217             else :
   308             else :
   218                 # recent
   309                 # use generic
   219                 timedelta = parse_timedelta(value)
   310                 filter = self.filter_attr(attr, value)
   220                 
   311             
   221                 return ((db.func.now() - Host.last_seen) < timedelta)
   312             log.debug("%s: %s: %s", attr, value, filter)
   222 
   313             
   223                 # XXX: for sqlite, pgsql should handle this natively?
   314             yield value, filter
   224                 # to seconds
   315 
   225                 #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds
   316     def filter (self, query) :
   226                 
       
   227                 # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout
       
   228                 #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout)
       
   229         
       
   230         elif attr == 'ip' :
       
   231             column = Host.ip
       
   232 
       
   233             # column is IPv4 string literal format...
       
   234             if '/' in value :
       
   235                 return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value)))
       
   236             else :
       
   237                 return (db.func.inet(Host.ip) == db.func.inet(value))
       
   238 
       
   239         else :
       
   240             # preprocess
       
   241             like = False
       
   242 
       
   243             if value.endswith('*') :
       
   244                 like = value.replace('*', '%')
       
   245 
       
   246             elif attr == 'mac' :
       
   247                 value = Host.normalize_mac(value)
       
   248 
       
   249             # filter
       
   250             column = self.HOST_ATTRS[attr]
       
   251 
       
   252             if like :
       
   253                 return (column.like(like))
       
   254             else :
       
   255                 return (column == value)
       
   256 
       
   257     def filter (self, hosts) :
       
   258         """
   317         """
   259             Apply filters from request.args against given hosts.
   318             Apply filters from request.args against given hosts.
   260 
   319 
   261             Returns (filters, hosts).
   320             Returns (filters, hosts).
   262         """
   321         """
   263 
   322 
   264         # filter?
   323         # filter?
   265         filters = {}
   324         filters = {}
   266 
   325 
   267         for attr in self.HOST_ATTRS :
   326         for attr in self.TABLE_ATTRS :
   268             values = [value.strip() for value in self.request.args.getlist(attr) if value.strip()]
   327             # from request args
       
   328             values = self.request.args.getlist(attr)
       
   329 
       
   330             # lookup attr filters as expressions
       
   331             value_filters = list(self._filter(attr, values))
   269 
   332 
   270             # ignore empty fields
   333             # ignore empty fields
   271             if not values :
   334             if not value_filters :
   272                 continue
   335                 continue
   273             
   336         
   274             # build query expression
   337             # filtering values, and filter expressions
   275             filter = db.or_(*[self.filter_attr(attr, value) for value in values])
   338             values, expressions = zip(*value_filters)
   276 
   339 
   277             log.debug("filter %s: %s", attr, filter)
   340             # apply
   278 
   341             query = query.filter(db.or_(*expressions))
   279             hosts = hosts.filter(filter)
       
   280             filters[attr] = values
   342             filters[attr] = values
   281 
   343 
   282         return filters, hosts
   344         return filters, query
   283 
   345 
   284     def filters_title (self) :
   346     def filters_title (self) :
   285         """
   347         """
   286             Return a string representing the applied filters.
   348             Return a string representing the applied filters.
   287         """
   349         """
   288 
   350 
   289         return ', '.join(value for values in self.filters.itervalues() for value in values)
   351         return ', '.join(value for values in self.filters.itervalues() for value in values)
   290  
   352  
   291     def render_hosts (self, hosts, title=None, filters=False, page=None, hilight=None) :
   353     def render_table (self, query, caption=None, sort=None, filters=None, page=None, hilight=None) :
   292         COLS = (
   354         """
   293             #title          sort        filter      class
   355             Return <table> element. Wrapped in <form> if filters.
   294             ('IP',          'ip',       'ip',       'ip'    ),
   356 
   295             ('MAC',         'mac',      'mac',      'mac'   ),
   357                 query   - filter()'d sort()'d SELECT query()
   296             ('Hostname',    'name',     False,      False   ),
   358                 caption - optional <caption>
   297             ('Network',     'net',      'net',      False   ),
   359                 sort    - None for no sorting ui, sort-attr otherwise.
   298             ('Seen',        'seen',     'seen',     'seen'  ),
   360                 filters - None for no filtering ui, dict of filters otherwise.
   299             ('State',       'state',    'state',    False   ), 
   361                 page    - display pagination for given page
   300         )
   362                 hilight - { attr: value } cells to hilight
   301 
   363         """
   302         def url (**opts) :
   364 
       
   365         def url (filters=filters, sort=sort, **opts) :
       
   366             """
       
   367                 URL for table with given opts, keeping our sorting/filtering unless overriden.
       
   368             """
       
   369 
   303             args = dict()
   370             args = dict()
   304 
   371 
   305             if filters :
   372             if filters :
   306                 args.update(filters)
   373                 args.update(filters)
   307 
   374             
   308             args.update(opts)
   375             if sort :
   309 
   376                 args['sort'] = sort
   310             return self.url(**args)
   377             
   311 
   378             if opts :
   312         def sortlink (attr) :
   379                 args.update(opts)
   313             if not self.sorts :
   380 
       
   381             return self.url(self.TABLE_URL, **args)
       
   382 
       
   383         def sorturl (attr, sort=sort) :
       
   384             """
       
   385                 URL for table sorted by given column, reversing direction if already sorting by given column.
       
   386             """
       
   387 
       
   388             if not sort :
   314                 sort = attr
   389                 sort = attr
   315             elif self.sorts.lstrip('+-') != attr :
   390             elif sort.lstrip('+-') != attr :
   316                 sort = attr
   391                 sort = attr
   317             elif self.sorts.startswith('-') :
   392             elif sort.startswith('-') :
   318                 sort = "+" + attr
   393                 sort = "+" + attr
   319             else :
   394             else :
   320                 sort = "-" + attr
   395                 sort = "-" + attr
   321 
   396 
   322             return html.a(href=url(sort=sort))
   397             return url(sort=sort)
   323 
   398 
   324         def paginate (page, count=None) :
   399         def itemurl (item) :
   325             """
   400             """
   326                 Render pagination.
   401                 URL for given item, by id.
   327             """
   402             """
   328 
   403 
   329             if count is not None :
   404             if self.TABLE_ITEM_URL :
   330                 pages = int(math.ceil(count / self.PAGE)) # XXX: bad self.PAGE
   405                 # separate page
       
   406                 return self.url(self.TABLE_ITEM_URL, id=item.id)
   331             else :
   407             else :
   332                 pages = None
   408                 # to our table
   333 
   409                 return url() + '#{id}'.format(id=item.id)
   334             if page > 0 :
   410 
   335                 yield html.a(href=url(page=0))(html("&laquo;&laquo; First"))
   411         def render_filter (attr) :
   336                 yield html.a(href=url(page=(page - 1)))(html("&laquo; Prev"))
   412             """
   337             
   413                 Render filter-input for column.
   338             yield html.span("Page {page} of {pages}".format(page=(page + 1), pages=(pages or '???')))
   414             """
   339 
   415 
   340             yield html.a(href=url(page=(page + 1)))(html("&raquo; Next"))
   416             value = filters.get(attr)
   341 
       
   342         def render_filter (filter) :
       
   343             value = filters.get(filter)
       
   344 
   417 
   345             if value :
   418             if value :
   346                 # XXX: multi-valued filters?
   419                 # XXX: multi-valued filters?
   347                 value = value[0]
   420                 value = value[0]
   348             else :
   421             else :
   349                 value = None
   422                 value = None
   350 
   423 
   351             return html.input(type='text', name=filter, value=value)
   424             return html.input(type='text', name=attr, value=value)
   352 
   425 
   353         def render_cell (attr, value, cssclass=True, filter=None, htmlvalue=None) :
   426         def render_head () :
   354             if htmlvalue :
   427             """
   355                 cell = htmlvalue
   428                 Yield header, filter rows for columns in table header.
       
   429             """
       
   430             
       
   431             # id
       
   432             yield html.td('#'), html.td(html.input(type='submit', value=u'\u00BF'))
       
   433 
       
   434             for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in self.TABLE_COLUMNS :
       
   435                 header = title
       
   436 
       
   437                 if sort :
       
   438                     header = html.a(href=sorturl(attr))(header)
       
   439 
       
   440                 header = html.th(header)
       
   441                 
       
   442                 if filters is not None and filter :
       
   443                     filter = render_filter(attr)
       
   444                 else  :
       
   445                     filter = None
       
   446                 
       
   447                 if colcss is True :
       
   448                     colcss = attr
       
   449 
       
   450                 filter = html.td(class_=colcss)(filter)
       
   451 
       
   452                 yield header, filter
       
   453 
       
   454         def render_cell (attr, value, rowhtml=None, colcss=True, filter=None, rowtitle=None, rowcss=None, hilight=hilight) :
       
   455             """
       
   456                 Render a single cell.
       
   457 
       
   458                     colcss      - css class for column; True -> attr
       
   459                     filter      - render filter link for value?
       
   460                     htmlvalue   - rendered value?
       
   461                     title       - mouseover title for cell
       
   462                     rowcss      - css class for row
       
   463             """
       
   464 
       
   465             if not rowhtml :
       
   466                 rowhtml = value
       
   467 
       
   468             if filter :
       
   469                 cell = html.a(href=url(filters=None, **{attr: value}))(rowhtml)
   356             else :
   470             else :
   357                 cell = value
   471                 cell = rowhtml
   358 
   472 
   359             if filter :
   473             if colcss is True :
   360                 cell = html.a(href=self.url(ListHandler, **{attr: value}))(cell)
   474                 colcss = attr
   361 
   475 
   362             if cssclass is True :
   476             if hilight :
   363                 cssclass = attr
   477                 hilight = attr in hilight and value in hilight[attr]
   364 
   478 
   365             css = (cssclass, 'hilight' if (hilight and attr in hilight and value in hilight[attr]) else None)
   479             css = (colcss, rowcss, 'hilight' if hilight else None)
   366             css = ' '.join(cls for cls in css if cls)
   480             css = ' '.join(cls for cls in css if cls)
   367             
   481             
   368             return html.td(class_=css)(cell)
   482             return html.td(class_=css, title=rowtitle)(cell)
   369 
   483        
       
   484         def render_row (item) :
       
   485             """
       
   486                 Yield columns for row.
       
   487             """
       
   488             
       
   489             for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in self.TABLE_COLUMNS :
       
   490                 # XXX: this is sometimes broken, figure out how to index by column
       
   491                 value = getattr(item, attr)
       
   492 
       
   493                 if rowhtml :
       
   494                     rowhtml = rowhtml(item)
       
   495                 else :
       
   496                     rowhtml = value
       
   497 
       
   498                 if rowtitle :
       
   499                     rowtitle = rowtitle(item)
       
   500                 else :
       
   501                     rowtitle = None
       
   502 
       
   503                 if rowcss :
       
   504                     rowcss = rowcss(item)
       
   505                 else :
       
   506                     rowcss = None
       
   507 
       
   508                 yield render_cell(attr, value,
       
   509                         rowhtml     = rowhtml,
       
   510                         colcss      = colcss,
       
   511                         filter      = value if rowfilter else None,
       
   512                         rowtitle    = rowtitle,
       
   513                         rowcss      = rowcss,
       
   514                 )
       
   515 
       
   516         def render_body (rows) :
       
   517             """
       
   518                 Yield rows.
       
   519             """
       
   520 
       
   521             for i, item in enumerate(rows) :
       
   522                 yield html.tr(class_=('alternate' if i % 2 else None), id=item.id)(
       
   523                     html.th(
       
   524                         html.a(href=itemurl(item))("#")
       
   525                     ),
       
   526 
       
   527                     render_row(item)
       
   528                 )
       
   529 
       
   530         def render_pagination (page, count=None) :
       
   531             """
       
   532                 Render pagination links.
       
   533             """
       
   534 
       
   535             if count is not None :
       
   536                 pages = int(math.ceil(count / self.TABLE_PAGE))
       
   537             else :
       
   538                 pages = None
       
   539 
       
   540             if page > 0 :
       
   541                 yield html.a(href=url(page=0))(html("&laquo;&laquo; First"))
       
   542                 yield html.a(href=url(page=(page - 1)))(html("&laquo; Prev"))
       
   543             
       
   544             yield html.span("Page {page} of {pages}".format(page=(page + 1), pages=(pages or '???')))
       
   545 
       
   546             yield html.a(href=url(page=(page + 1)))(html("&raquo; Next"))
       
   547 
       
   548 
       
   549         def render_foot () :
       
   550             # XXX: does separate SELECT count()
       
   551             count = query.count()
       
   552 
       
   553             if page :
       
   554                 return render_pagination(page, count)
       
   555             else :
       
   556                 return "{count} hosts".format(count=count)
       
   557 
       
   558         # columns for the two header rows
       
   559         headers, filtering = zip(*list(render_head()))
       
   560         
       
   561         # render table
   370         table = html.table(
   562         table = html.table(
   371             html.caption(title) if title else None,
   563             html.caption(caption) if caption else None,
   372             html.thead(
   564             html.thead(
   373                 html.tr(
   565                 html.tr(headers),
   374                     html.th('#'),
   566                 # filters?
   375                     (
   567                 html.tr(class_='filter')(filtering) if filters is not None else None,
   376                         html.th(
       
   377                             sortlink(sort)(title) if sort else (title)
       
   378                         ) for title, sort, filter, class_ in COLS
       
   379                     )
       
   380                 ),
       
   381                 html.tr(class_='filter')(
       
   382                     html.td(
       
   383                         html.input(type='submit', value=u'\u00BF'),
       
   384                     ),
       
   385                     (
       
   386                         html.td(class_=class_)(
       
   387                             render_filter(filter) if filter else None
       
   388                         ) for title, sort, filter, class_ in COLS
       
   389                     )
       
   390                 ) if filters is not False else None
       
   391             ),
   568             ),
   392             html.tbody(
   569             html.tbody(
   393                 html.tr(class_=('alternate' if i % 2 else None), id=host.id)(
   570                 render_body(query)
   394                     html.th(
       
   395                         html.a(href=self.url(ItemHandler, id=host.id))(
       
   396                             '#' #host.id
       
   397                         )
       
   398                     ),
       
   399 
       
   400                     render_cell('ip', host.ip, filter=True),
       
   401                     render_cell('mac', host.mac, filter=True, htmlvalue=host.render_mac()),
       
   402                     render_cell('name', host.name, htmlvalue=host.render_name()),
       
   403                     render_cell('gw', host.gw),
       
   404 
       
   405                     render_cell('seen', host.seen()),
       
   406                     html.td(class_=host.state_class(), title=host.state_title())(host.state),
       
   407                 ) for i, host in enumerate(hosts)
       
   408             ),
   571             ),
   409             html.tfoot(
   572             html.tfoot(
   410                 html.tr(
   573                 html.tr(
   411                     html.td(colspan=(1 + len(COLS)))(
   574                     html.td(colspan=(1 + len(self.TABLE_COLUMNS)))(
   412                         paginate(page) if page is not None else (
   575                         render_foot()
   413                             # XXX: does separate SELECT count()
       
   414                             "{count} hosts".format(count=hosts.count())
       
   415                         )
       
   416                     )
   576                     )
   417                 )
   577                 )
   418             )
   578             )
   419         )
   579         )
   420         
   580         
   421         if filters is False :
   581         # filters form?
       
   582         if filters is None :
   422             return table
   583             return table
   423         else :
   584         else :
   424             return html.form(method='get', action=self.url())(
   585             return html.form(method='get', action=url(filters=None, sort=None))(
   425                 html.input(type='hidden', name='sort', value=self.sorts),
   586                 html.input(type='hidden', name='sort', value=sort),
   426                 table,
   587                 table,
   427             )
   588             )
   428 
   589 
   429 class ItemHandler (BaseHandler) :
   590 class ItemHandler (BaseHandler) :
   430     """
   591     """
   431         A specific DHCP host, along with a list of related hosts.
   592         A specific DHCP host, along with a list of related hosts.
   432     """
   593     """
   433 
   594     
   434     def process (self, id) :
   595     def process (self, id) :
   435         self.hosts = self.query()
   596         self.hosts = self.query()
   436         self.host = self.hosts.get(id)
   597         self.host = self.hosts.get(id)
   437         
   598         
   438         if not self.host :
   599         if not self.host :
   470         return (
   631         return (
   471             html.h2('Host'),
   632             html.h2('Host'),
   472             self.render_host(self.host),
   633             self.render_host(self.host),
   473 
   634 
   474             html.h2('Related'),
   635             html.h2('Related'),
   475             self.render_hosts(self.hosts, hilight=dict(ip=self.host.ip, mac=self.host.mac)),
   636             self.render_table(self.hosts, sort=self.sorts, hilight=dict(ip=self.host.ip, mac=self.host.mac)),
   476 
   637 
   477             html.a(href=self.url(ListHandler))(html('&laquo;'), 'Back'),
   638             html.a(href=self.url(ListHandler))(html('&laquo;'), 'Back'),
   478         )
   639         )
   479 
   640 
   480 
   641 
   481 class ListHandler (BaseHandler) :
   642 class ListHandler (BaseHandler) :
   482     """
   643     """
   483         List of DHCP hosts for given filter.
   644         List of DHCP hosts for given filter.
   484     """
   645     """
   485 
   646 
   486     # pagination
   647     TABLE_PAGE = 10
   487     PAGE = 10
       
   488 
   648 
   489     def process (self) :
   649     def process (self) :
   490         hosts = self.query()
   650         hosts = self.query()
   491 
   651 
   492         # filter
   652         # filter
   511         else :
   671         else :
   512             return "DHCP Hosts"
   672             return "DHCP Hosts"
   513     
   673     
   514     def render (self) :
   674     def render (self) :
   515         return (
   675         return (
   516             self.render_hosts(self.hosts, filters=self.filters, page=self.page),
   676             self.render_table(self.hosts, filters=self.filters, sort=self.sorts, page=self.page),
   517 
   677 
   518             html.a(href=self.url())(html('&laquo;'), 'Back') if self.filters else None,
   678             html.a(href=self.url())(html('&laquo;'), 'Back') if self.filters else None,
   519         )
   679         )
       
   680 
       
   681 # XXX:
       
   682 BaseHandler.TABLE_URL = ListHandler
       
   683 BaseHandler.TABLE_ITEM_URL = ItemHandler
   520 
   684 
   521 class RealtimeHandler (BaseHandler) :
   685 class RealtimeHandler (BaseHandler) :
   522     TITLE = "DHCP Pseudo-Realtime hosts.."
   686     TITLE = "DHCP Pseudo-Realtime hosts.."
   523     CSS = BaseHandler.CSS + (
   687     CSS = BaseHandler.CSS + (
   524         'http://code.jquery.com/ui/1.9.0/themes/base/jquery-ui.css',
   688         'http://code.jquery.com/ui/1.9.0/themes/base/jquery-ui.css',