hosts: refactor ListHandler filter support up into BaseHandler, adding support for filters in RealtimeHandler
authorTero Marttila <terom@paivola.fi>
Wed, 24 Oct 2012 20:46:17 +0300
changeset 30 841d856293a1
parent 29 38265b7d8f62
child 31 3e6d0feb115c
hosts: refactor ListHandler filter support up into BaseHandler, adding support for filters in RealtimeHandler
pvl/verkko/hosts.py
static/hosts.js
--- a/pvl/verkko/hosts.py	Wed Oct 24 20:27:39 2012 +0300
+++ b/pvl/verkko/hosts.py	Wed Oct 24 20:46:17 2012 +0300
@@ -150,20 +150,108 @@
         return self.db.query(Host)
     
     def sort (self, hosts, default=HOST_SORT) :
-        self.sort = self.request.args.get('sort')
+        sort = self.request.args.get('sort')
 
-        if self.sort :
-            sort = self.HOST_ATTRS[self.sort]
+        if sort :
+            order_by = self.HOST_ATTRS[sort]
         else :
-            sort = default
+            order_by = default
 
-        log.debug("sort: %s", sort)
+        log.debug("sort: %s", order_by)
         
-        hosts = hosts.order_by(sort)
+        hosts = hosts.order_by(order_by)
 
         # k
-        return hosts
+        return sort, hosts
     
+    def filter_attr (self, attr, value) :
+        """
+            Return filter expression for given attr == value
+        """
+
+        if attr == 'seen' :
+            column = Host.last_seen
+
+            if value.isdigit() :
+                # specific date
+                date = datetime.datetime.strptime(value, Host.DATE_FMT).date()
+
+                return db.between(date.strftime(Host.DATE_FMT), 
+                        db.func.strftime(Host.DATE_FMT, Host.first_seen),
+                        db.func.strftime(Host.DATE_FMT, Host.last_seen)
+                )
+            else :
+                # recent
+                timedelta = parse_timedelta(value)
+                
+                return ((db.func.now() - Host.last_seen) < timedelta)
+
+                # XXX: for sqlite, pgsql should handle this natively?
+                # to seconds
+                #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds
+                
+                # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout
+                #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout)
+        
+        elif attr == 'ip' :
+            column = Host.ip
+
+            # column is IPv4 string literal format...
+            if '/' in value :
+                return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value)))
+            else :
+                return (db.func.inet(Host.ip) == db.func.inet(value))
+
+        else :
+            # preprocess
+            like = False
+
+            if value.endswith('*') :
+                like = value.replace('*', '%')
+
+            elif attr == 'mac' :
+                value = Host.normalize_mac(value)
+
+            # filter
+            column = self.HOST_ATTRS[attr]
+
+            if like :
+                return (column.like(like))
+            else :
+                return (column == value)
+
+    def filter (self, hosts) :
+        """
+            Apply filters from request.args against given hosts.
+
+            Returns (filters, hosts).
+        """
+
+        # filter?
+        filters = {}
+
+        for attr in self.HOST_ATTRS :
+            values = self.request.args.getlist(attr)
+
+            if not values :
+                continue
+            
+            filter = db.or_(*[self.filter_attr(attr, value) for value in values])
+
+            log.debug("filter %s: %s", attr, filter)
+
+            hosts = hosts.filter(filter)
+            filters[attr] = values
+
+        return filters, hosts
+
+    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_hosts (self, hosts, title=None, filters=False, page=None, hilight=None) :
         COLS = (
             #title          sort        filter      class
@@ -286,7 +374,7 @@
             return table
         else :
             return html.form(method='get', action=self.url())(
-                html.input(type='hidden', name='sort', value=self.sort),
+                html.input(type='hidden', name='sort', value=self.sorts),
                 table,
             )
 
@@ -297,8 +385,8 @@
         
         if not self.host :
             raise web.NotFound("No such host: {id}".format(id=id))
-
-        self.hosts = self.sort(self.hosts.filter((Host.ip == self.host.ip) | (Host.mac == self.host.mac)))
+        
+        self.sorts, self.hosts = self.sort(self.hosts.filter((Host.ip == self.host.ip) | (Host.mac == self.host.mac)))
     
     def title (self) :
         return u"DHCP Host: {self.host}".format(self=self)
@@ -362,83 +450,14 @@
         ("Invalid",     dict(state=('DHCPNAK', ))),
     )
 
-    def filter (self, attr, value) :
-        """
-            Return filter expression for given attr == value
-        """
-
-        if attr == 'seen' :
-            column = Host.last_seen
-
-            if value.isdigit() :
-                # specific date
-                date = datetime.datetime.strptime(value, Host.DATE_FMT).date()
-
-                return db.between(date.strftime(Host.DATE_FMT), 
-                        db.func.strftime(Host.DATE_FMT, Host.first_seen),
-                        db.func.strftime(Host.DATE_FMT, Host.last_seen)
-                )
-            else :
-                # recent
-                timedelta = parse_timedelta(value)
-                
-                return ((db.func.now() - Host.last_seen) < timedelta)
-
-                # XXX: for sqlite, pgsql should handle this natively?
-                # to seconds
-                #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds
-                
-                # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout
-                #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout)
-        
-        elif attr == 'ip' :
-            column = Host.ip
-
-            # column is IPv4 string literal format...
-            if '/' in value :
-                return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value)))
-            else :
-                return (db.func.inet(Host.ip) == db.func.inet(value))
-
-        else :
-            # preprocess
-            like = False
-
-            if value.endswith('*') :
-                like = value.replace('*', '%')
-
-            elif attr == 'mac' :
-                value = Host.normalize_mac(value)
-
-            # filter
-            column = self.HOST_ATTRS[attr]
-
-            if like :
-                return (column.like(like))
-            else :
-                return (column == value)
-
     def process (self) :
         hosts = self.query()
 
-        # filter?
-        self.filters = {}
-
-        for attr in self.HOST_ATTRS :
-            values = self.request.args.getlist(attr)
-
-            if not values :
-                continue
-            
-            filter = db.or_(*[self.filter(attr, value) for value in values])
-
-            log.debug("filter %s: %s", attr, filter)
-
-            hosts = hosts.filter(filter)
-            self.filters[attr] = values
+        # filter
+        self.filters, hosts = self.filter(hosts)
 
         # sort XXX: default per filter column?
-        hosts = self.sort(hosts)
+        self.sorts, hosts = self.sort(hosts)
         
         # page?
         self.page = self.request.args.get('page')
@@ -449,10 +468,10 @@
             hosts = hosts.offset(self.page * self.PAGE).limit(self.PAGE)
 
         self.hosts = hosts
-
+  
     def title (self) :
         if self.filters :
-            return "DHCP Hosts: {filters}".format(filters=', '.join(value for values in self.filters.itervalues() for value in values))
+            return "DHCP Hosts: {filters}".format(filters=self.filters_title())
         else :
             return "DHCP Hosts"
     
@@ -472,7 +491,7 @@
 def ts2dt (ts) :
     return datetime.datetime.fromtimestamp(ts)
 
-class RealtimeHandler (web.Handler) :
+class RealtimeHandler (BaseHandler) :
     TITLE = "Pseudo-Realtime hosts.."
     CSS = web.Handler.CSS + (
         'http://code.jquery.com/ui/1.9.0/themes/base/jquery-ui.css',
@@ -494,10 +513,19 @@
         ( 'state',  'State',        lambda host: host.state ),
     )
 
-
     def process (self) :
-        hosts = self.db.query(Host).order_by(Host.last_seen.desc())
+        """
+            Either return JSON (if ?t=...), or fetch hosts/t for rendering.
+        """
+
+        hosts = self.db.query(Host)
         t = self.request.args.get('t')
+        
+        # always sorted by last_seen
+        hosts = hosts.order_by(Host.last_seen.desc())
+
+        # filter
+        self.filters, hosts = self.filter(hosts)
 
         def host_params (host) :
             yield 'id', host.id
@@ -545,7 +573,17 @@
 
             self.t = self.hosts[0].last_seen
 
+    def title (self) :
+        if self.filters :
+            return "{title}: {filters}".format(title=self.TITLE, filters=self.filters_title())
+        else :
+            return self.TITLE
+
     def render (self) :
+        """
+            Render page HTML and initial <table>, along with bootstrap JS (t0, configuration).
+        """
+
         def column (name, title, fvalue, host) :
             cls = name
 
@@ -556,6 +594,7 @@
 
         params = dict(
             url     = self.url(),
+            filters = self.filters,
             t       = dt2ts(self.t),
             host    = self.url(ItemHandler, id='0'),
             columns = [name for name, title, fvalue in self.COLUMNS]
--- a/static/hosts.js	Wed Oct 24 20:27:39 2012 +0300
+++ b/static/hosts.js	Wed Oct 24 20:46:17 2012 +0300
@@ -77,7 +77,10 @@
         var refresh = $('#refresh').disable(true);
         var spinner = $('#wrapper').spin();
 
-        $.getJSON(params.url, { t: t }, function (data) {
+        var url = params.url;
+        var query = $.param($.extend(params.filters, {t: t }), true);   // using traditional encoding for multi-values
+
+        $.getJSON(url, query, function (data) {
             var t1 = data.t;
             var hosts = data.hosts;