diff mbox

[Branch,~linaro-validation/lava-scheduler/trunk] Rev 141: Three tweaks to ajax tables:

Message ID 20120306202610.26480.12942.launchpad@ackee.canonical.com
State Accepted
Headers show

Commit Message

Michael-Doyle Hudson March 6, 2012, 8:26 p.m. UTC
Merge authors:
  Michael Hudson-Doyle (mwhudson)
Related merge proposals:
  https://code.launchpad.net/~mwhudson/lava-scheduler/prefill-tables/+merge/96051
  proposed by: Michael Hudson-Doyle (mwhudson)
  review: Approve - Zygmunt Krynicki (zkrynicki)
------------------------------------------------------------
revno: 141 [merge]
committer: Michael Hudson-Doyle <michael.hudson@linaro.org>
branch nick: trunk
timestamp: Wed 2012-03-07 09:22:42 +1300
message:
  Three tweaks to ajax tables:
  * make the way rendering works more in line with how ajax-less tables are
    rendered by django-tables2.
  * render the first page of a table when rendering the html, delaying all ajax
    action until it is needed.
  * fix a couple of bugs with sorting on the job health page.
modified:
  lava_scheduler_app/tables.py
  lava_scheduler_app/views.py


--
lp:lava-scheduler
https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk

You are subscribed to branch lp:lava-scheduler.
To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk/+edit-subscription
diff mbox

Patch

=== modified file 'lava_scheduler_app/tables.py'
--- lava_scheduler_app/tables.py	2012-03-01 22:02:21 +0000
+++ lava_scheduler_app/tables.py	2012-03-06 04:22:34 +0000
@@ -1,48 +1,72 @@ 
 import simplejson
 
-import django_tables2 as tables
+from django.template import compile_string, RequestContext
+
+from django_tables2.columns import BoundColumn
 from django_tables2.rows import BoundRow
+from django_tables2.tables import Table, TableData
 from django_tables2.utils import AttributeDict
 
 from lava.utils.data_tables.views import DataTableView
 from lava.utils.data_tables.backends import QuerySetBackend
 
 
-class AjaxColumn(tables.Column):
-
-    def __init__(self, *args, **kw):
-        sort_expr = kw.pop('sort_expr', None)
-        width = kw.pop('width', None)
-        super(AjaxColumn, self).__init__(*args, **kw)
-        self.sort_expr = sort_expr
-        self.width = width
+simple_nodelist = compile_string('{{ a }}', None)
 
 
 class _ColWrapper(object):
 
-    def __init__(self, name, sort_expr, table):
+    def __init__(self, name, table):
         self.name = name
-        if sort_expr is not None:
-            self.sort_expr = sort_expr
-        else:
-            self.sort_expr = name
+        self.sort_expr = table.columns[name].accessor.replace('.', '__')
         self.table = table
 
     def callback(self, record):
-        # It _might_ make life more convenient to handle certain non-JSONable
-        # datatypes here -- particularly, applying unicode() to model objects
-        # would be more consistent with the way templates work.
-        return BoundRow(self.table, record)[self.name]
-
-
-class AjaxTable(tables.Table):
-    datatable_opts = None
-    searchable_columns = []
-
-    def __init__(self, id, source, **kw):
+        context = self.table.context
+        context.update({"a": BoundRow(self.table, record)[self.name]})
+        try:
+            return simple_nodelist.render(context)
+        finally:
+            context.pop()
+
+
+class _AjaxTableData(TableData):
+    def order_by(self, order_by):
+        if order_by:
+            raise AssertionError(
+                "AjaxTables do not support ordering by Table options")
+        return
+
+
+class AjaxTable(Table):
+    TableDataClass = _AjaxTableData
+
+    def __init__(self, id, source, params=(), _for_rendering=True, **kw):
         if 'template' not in kw:
             kw['template'] = 'lava_scheduler_app/ajax_table.html'
-        super(AjaxTable, self).__init__(data=[], **kw)
+        self.params = params
+        self.total_length = None
+        if _for_rendering:
+            qs = self.get_queryset()
+            self.total_length = qs.count()
+
+            ordering = self.datatable_opts.get('aaSorting', [[0, 'asc']])
+            # What follows is duplicated from backends.py which isn't ideal.
+            order_by = []
+            for column_index, order in ordering:
+                name, col = self.base_columns.items()[column_index]
+                sort_expr = BoundColumn(self, col, name).accessor.replace('.', '__')
+                order_by.append(
+                    "{asc_desc}{column}".format(
+                        asc_desc="-" if order == 'desc' else '',
+                        column=sort_expr))
+            qs = qs.order_by(*order_by)
+
+            display_length = self.datatable_opts.get('iDisplayLength', 10)
+            qs = qs[:display_length]
+        else:
+            qs = []
+        super(AjaxTable, self).__init__(data=qs, **kw)
         self.source = source
         self.attrs = AttributeDict({
             'id': id,
@@ -50,13 +74,13 @@ 
             })
 
     @classmethod
-    def json(cls, request, queryset):
-        table = cls(None, None)
-        our_cols = [_ColWrapper(name, col.sort_expr, table)
-                    for name, col in cls.base_columns.iteritems()]
+    def json(cls, request, params=()):
+        table = cls(None, None, params, _for_rendering=False)
+        table.context = RequestContext(request)
+        our_cols = [_ColWrapper(name, table) for name in table.columns]
         return DataTableView.as_view(
             backend=QuerySetBackend(
-                queryset=queryset,
+                queryset=table.get_queryset(),
                 columns=our_cols,
                 searching_columns=cls.searchable_columns)
             )(request)
@@ -73,6 +97,8 @@ 
             'sAjaxSource': self.source,
             'bFilter': bool(self.searchable_columns)
             })
+        if self.total_length is not None:
+            opts['iDeferLoading'] = self.total_length
         aoColumnDefs = opts['aoColumnDefs'] = []
         for col in self.columns:
             aoColumnDefs.append({
@@ -80,6 +106,11 @@ 
                 'mDataProp': col.name,
                 'aTargets': [col.name],
                 })
-            if col.column.width:
-                aoColumnDefs[-1]['sWidth'] = col.column.width
         return simplejson.dumps(opts)
+
+    datatable_opts = {}
+    searchable_columns = []
+
+    def get_queryset(self):
+        raise NotImplementedError
+

=== modified file 'lava_scheduler_app/views.py'
--- lava_scheduler_app/views.py	2012-03-01 23:38:16 +0000
+++ lava_scheduler_app/views.py	2012-03-06 04:20:03 +0000
@@ -19,6 +19,10 @@ 
 )
 from django.template import RequestContext
 from django.template import defaultfilters as filters
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+
+from django_tables2 import Attrs, Column
 
 from lava_server.views import index as lava_index
 from lava_server.bread_crumbs import (
@@ -37,7 +41,6 @@ 
     TestJob,
     )
 from lava_scheduler_app.tables import (
-    AjaxColumn,
     AjaxTable,
     )
 
@@ -51,7 +54,7 @@ 
     return decorated
 
 
-class DateColumn(AjaxColumn):
+class DateColumn(Column):
 
     def __init__(self, **kw):
         self._format = kw.get('date_format', settings.DATETIME_FORMAT)
@@ -61,14 +64,20 @@ 
         return filters.date(value, self._format)
 
 
-class IDLinkColumn(AjaxColumn):
+def pklink(record):
+    return mark_safe(
+        '<a href="%s">%s</a>' % (
+            record.get_absolute_url(),
+            escape(record.pk)))
+
+class IDLinkColumn(Column):
 
     def __init__(self, verbose_name="ID", **kw):
         kw['verbose_name'] = verbose_name
         super(IDLinkColumn, self).__init__(**kw)
 
     def render(self, record):
-        return '<a href="%s">%s</a>' % (record.get_absolute_url(), record.pk)
+        return pklink(record)
 
 
 def all_jobs_with_device_sort():
@@ -84,19 +93,24 @@ 
 
     def render_device(self, record):
         if record.actual_device:
-            return '<a href="%s">%s</a>' % (
-                record.actual_device.get_absolute_url(), record.actual_device.pk)
+            return pklink(record.actual_device)
         elif record.requested_device:
-            return '<a href="%s">%s</a>' % (
-                record.requested_device.get_absolute_url(), record.requested_device.pk)
-        else:
-            return '<i>' + record.requested_device_type.pk + '</i>'
+            return pklink(record.requested_device)
+        else:
+            return mark_safe(
+                '<i>' + escape(record.requested_device_type.pk) + '</i>')
+
+    def render_description(self, value):
+        if value:
+            return value
+        else:
+            return ''
 
     id = IDLinkColumn()
-    status = AjaxColumn()
-    device = AjaxColumn(sort_expr='device_sort')
-    description = AjaxColumn(width="30%")
-    submitter = AjaxColumn(accessor='submitter.username')
+    status = Column()
+    device = Column(accessor='device_sort')
+    description = Column(attrs=Attrs(width="30%"))
+    submitter = Column()
     submit_time = DateColumn()
     end_time = DateColumn()
 
@@ -107,29 +121,33 @@ 
 
 
 class IndexJobTable(JobTable):
+    def get_queryset(self):
+        return all_jobs_with_device_sort().filter(
+            status__in=[TestJob.SUBMITTED, TestJob.RUNNING])
+
     class Meta:
         exclude = ('end_time',)
 
 
 def index_active_jobs_json(request):
-    return IndexJobTable.json(
-        request, all_jobs_with_device_sort().filter(
-            status__in=[TestJob.SUBMITTED, TestJob.RUNNING]))
+    return IndexJobTable.json(request)
 
 
 class DeviceTable(AjaxTable):
 
+    def get_queryset(self):
+        return Device.objects.select_related("device_type")
+
     hostname = IDLinkColumn("hostname")
-    device_type = AjaxColumn(accessor='device_type.pk')
-    status = AjaxColumn()
-    health_status = AjaxColumn()
+    device_type = Column()
+    status = Column()
+    health_status = Column()
 
     searchable_columns=['hostname']
 
 
 def index_devices_json(request):
-    return DeviceTable.json(
-        request, Device.objects.select_related("device_type"))
+    return DeviceTable.json(request)
 
 
 @BreadCrumb("Scheduler", parent=lava_index)
@@ -147,20 +165,26 @@ 
 
 class DeviceHealthTable(AjaxTable):
 
+    def get_queryset(self):
+        return Device.objects.select_related(
+            "hostname", "last_health_report_job")
+
     def render_hostname(self, record):
-        return '<a href="%s">%s</a>' % (record.get_device_health_url(), record.pk)
+        return pklink(record)
 
-    def render_last_report_job(self, record):
+    def render_last_health_report_job(self, record):
         report = record.last_health_report_job
         if report is None:
             return ''
         else:
-            return '<a href="%s">%s</a>' % (report.get_absolute_url(), report.pk)
+            return pklink(report)
 
-    hostname = AjaxColumn("hostname")
-    health_status = AjaxColumn()
-    last_report_time = DateColumn(accessor="last_health_report_job.end_time")
-    last_report_job = AjaxColumn()
+    hostname = Column("hostname")
+    health_status = Column()
+    last_report_time = DateColumn(
+        verbose_name="last report time",
+        accessor="last_health_report_job.end_time")
+    last_health_report_job = Column("last report job")
 
     searchable_columns=['hostname']
     datatable_opts = {
@@ -169,9 +193,7 @@ 
 
 
 def lab_health_json(request):
-    return DeviceHealthTable.json(
-        request, Device.objects.select_related(
-            "hostname", "last_health_report_job"))
+    return DeviceHealthTable.json(request)
 
 
 @BreadCrumb("All Device Health", parent=index)
@@ -187,19 +209,22 @@ 
 
 
 class HealthJobTable(JobTable):
+
+    def get_queryset(self):
+        device, = self.params
+        TestJob.objects.select_related(
+            "submitter",
+            ).filter(
+            actual_device=device,
+            health_check=True)
+
     class Meta:
         exclude = ('description', 'device')
 
 
-
 def health_jobs_json(request, pk):
     device = get_object_or_404(Device, pk=pk)
-    return HealthJobTable.json(
-        request, TestJob.objects.select_related(
-            "submitter",
-        ).filter(
-            actual_device=device,
-            health_check=True))
+    return HealthJobTable.json(params=(device,))
 
 
 @BreadCrumb("All Health Jobs on Device {pk}", parent=index, needs=['pk'])
@@ -211,7 +236,8 @@ 
         {
             'device': device,
             'health_job_table': HealthJobTable(
-                'health_jobs', reverse(health_jobs_json, kwargs=dict(pk=pk))),
+                'health_jobs', reverse(health_jobs_json, kwargs=dict(pk=pk)),
+                params=(device,)),
             'show_maintenance': device.can_admin(request.user) and \
                 device.status in [Device.IDLE, Device.RUNNING],
             'show_online': device.can_admin(request.user) and \
@@ -223,6 +249,9 @@ 
 
 class AllJobsTable(JobTable):
 
+    def get_queryset(self):
+        return all_jobs_with_device_sort()
+
     datatable_opts = JobTable.datatable_opts.copy()
 
     datatable_opts.update({
@@ -231,8 +260,7 @@ 
 
 
 def alljobs_json(request):
-    return AllJobsTable.json(
-        request, all_jobs_with_device_sort())
+    return AllJobsTable.json(request)
 
 
 @BreadCrumb("All Jobs", parent=index)
@@ -420,17 +448,34 @@ 
 
 
 class RecentJobsTable(JobTable):
+
+    def get_queryset(self):
+        device, = self.params
+        return device.recent_jobs()
+
     class Meta:
         exclude = ('device',)
 
 
 def recent_jobs_json(request, pk):
     device = get_object_or_404(Device, pk=pk)
-    return RecentJobsTable.json(request, device.recent_jobs())
+    return RecentJobsTable.json(request, params=(device,))
 
 
 class DeviceTransitionTable(AjaxTable):
 
+    def get_queryset(self):
+        device, = self.params
+        qs = device.transitions.select_related('created_by')
+        qs = qs.extra(select={'prev': """
+        select t.created_on
+          from lava_scheduler_app_devicestatetransition as t
+         where t.device_id=%s and t.created_on < lava_scheduler_app_devicestatetransition.created_on
+         order by t.created_on desc
+         limit 1 """},
+                      select_params=[device.pk])
+        return qs
+
     def render_created_on(self, record):
         t = record
         base = filters.date(t.created_on, "Y-m-d H:i")
@@ -448,10 +493,10 @@ 
         else:
             return value
 
-    created_on = AjaxColumn('when', width="40%")
-    transition = AjaxColumn('transition', sortable=False)
-    created_by = AjaxColumn('by', accessor='created_by.username')
-    message = AjaxColumn('reason')
+    created_on = Column('when', attrs=Attrs(width="40%"))
+    transition = Column('transition', sortable=False)
+    created_by = Column('by')
+    message = Column('reason')
 
     datatable_opts = {
         'aaSorting': [[0, 'desc']],
@@ -460,16 +505,7 @@ 
 
 def transition_json(request, pk):
     device = get_object_or_404(Device, pk=pk)
-    qs = device.transitions.select_related('created_by')
-    qs = qs.extra(select={'prev': """
-    select t.created_on
-      from lava_scheduler_app_devicestatetransition as t
-     where t.device_id=%s and t.created_on < lava_scheduler_app_devicestatetransition.created_on
-     order by t.created_on desc
-     limit 1 """},
-                  select_params=[device.pk])
-    return DeviceTransitionTable.json(request, qs)
-
+    return DeviceTransitionTable.json(request, params=(device,))
 
 
 @BreadCrumb("Device {pk}", parent=index, needs=['pk'])
@@ -488,9 +524,11 @@ 
             'device': device,
             'transition': transition,
             'transition_table': DeviceTransitionTable(
-                'transitions', reverse(transition_json, kwargs=dict(pk=device.pk))),
+                'transitions', reverse(transition_json, kwargs=dict(pk=device.pk)),
+                params=(device,)),
             'recent_job_table': RecentJobsTable(
-                'jobs', reverse(recent_jobs_json, kwargs=dict(pk=device.pk))),
+                'jobs', reverse(recent_jobs_json, kwargs=dict(pk=device.pk)),
+                params=(device,)),
             'show_maintenance': device.can_admin(request.user) and \
                 device.status in [Device.IDLE, Device.RUNNING],
             'show_online': device.can_admin(request.user) and \