From patchwork Tue Mar 6 20:26:10 2012 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Michael-Doyle Hudson X-Patchwork-Id: 7118 Return-Path: X-Original-To: patchwork@peony.canonical.com Delivered-To: patchwork@peony.canonical.com Received: from fiordland.canonical.com (fiordland.canonical.com [91.189.94.145]) by peony.canonical.com (Postfix) with ESMTP id CAEFB23E76 for ; Tue, 6 Mar 2012 20:52:08 +0000 (UTC) Received: from mail-yx0-f180.google.com (mail-yx0-f180.google.com [209.85.213.180]) by fiordland.canonical.com (Postfix) with ESMTP id 65BE3A183A3 for ; Tue, 6 Mar 2012 20:52:08 +0000 (UTC) Received: by yenl4 with SMTP id l4so2934360yen.11 for ; Tue, 06 Mar 2012 12:52:08 -0800 (PST) Received: by 10.50.158.133 with SMTP id wu5mr9824069igb.50.1331067127601; Tue, 06 Mar 2012 12:52:07 -0800 (PST) X-Forwarded-To: linaro-patchwork@canonical.com X-Forwarded-For: patch@linaro.org linaro-patchwork@canonical.com Delivered-To: patches@linaro.org Received: by 10.231.53.18 with SMTP id k18csp244ibg; Tue, 6 Mar 2012 12:52:06 -0800 (PST) Received: by 10.180.89.66 with SMTP id bm2mr3532778wib.0.1331065570725; Tue, 06 Mar 2012 12:26:10 -0800 (PST) Received: from indium.canonical.com (indium.canonical.com. [91.189.90.7]) by mx.google.com with ESMTPS id r65si14967448weq.131.2012.03.06.12.26.10 (version=TLSv1/SSLv3 cipher=OTHER); Tue, 06 Mar 2012 12:26:10 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) client-ip=91.189.90.7; Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) smtp.mail=bounces@canonical.com Received: from ackee.canonical.com ([91.189.89.26]) by indium.canonical.com with esmtp (Exim 4.71 #1 (Debian)) id 1S50xW-0001d6-4K for ; Tue, 06 Mar 2012 20:26:10 +0000 Received: from ackee.canonical.com (localhost [127.0.0.1]) by ackee.canonical.com (Postfix) with ESMTP id 0C2AAE01ED for ; Tue, 6 Mar 2012 20:26:10 +0000 (UTC) MIME-Version: 1.0 X-Launchpad-Project: lava-scheduler X-Launchpad-Branch: ~linaro-validation/lava-scheduler/trunk X-Launchpad-Message-Rationale: Subscriber X-Launchpad-Branch-Revision-Number: 141 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [Branch ~linaro-validation/lava-scheduler/trunk] Rev 141: Three tweaks to ajax tables: Message-Id: <20120306202610.26480.12942.launchpad@ackee.canonical.com> Date: Tue, 06 Mar 2012 20:26:10 -0000 Reply-To: noreply@launchpad.net Sender: bounces@canonical.com Errors-To: bounces@canonical.com Precedence: bulk X-Generated-By: Launchpad (canonical.com); Revision="14900"; Instance="launchpad-lazr.conf" X-Launchpad-Hash: 74409bc121b3a9efbc4b07698b24313cab7a794e X-Gm-Message-State: ALoCoQl/61qNk0ero/ahNTr++p3b9wjsQntBiLbbR217JruX691ZDphYcTv3HZd2e9UUVIqx//uN 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 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 === 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( + '%s' % ( + 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 '%s' % (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 '%s' % ( - record.actual_device.get_absolute_url(), record.actual_device.pk) + return pklink(record.actual_device) elif record.requested_device: - return '%s' % ( - record.requested_device.get_absolute_url(), record.requested_device.pk) - else: - return '' + record.requested_device_type.pk + '' + return pklink(record.requested_device) + else: + return mark_safe( + '' + escape(record.requested_device_type.pk) + '') + + 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 '%s' % (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 '%s' % (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 \