pvl/verkko/hosts.py
changeset 180 e6bca452ce72
parent 179 706972d09f05
child 183 8fbaaf0564dc
--- 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("&laquo;&laquo; First"))
-                yield html.a(href=url(page=(page - 1)))(html("&laquo; Prev"))
-            
-            yield html.span("Page {page} of {pages}".format(page=(page + 1), pages=(pages or '???')))
-
-            yield html.a(href=url(page=(page + 1)))(html("&raquo; 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('&laquo;'), '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('&laquo;'), 'Back') if self.filters else None,
+            #html.a(href=self.url())(html('&laquo;'), '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 = (