=== modified file 'dashboard_app/__init__.py'
--- dashboard_app/__init__.py	2011-07-01 14:04:59 +0000
+++ dashboard_app/__init__.py	2011-07-12 02:39:22 +0000
@@ -20,4 +20,4 @@
 Dashboard Application (package)
 """
 
-__version__ = (0, 5, 2, "final", 0)
+__version__ = (0, 6, 0, "beta", 1)

=== added file 'dashboard_app/bread_crumbs.py'
--- dashboard_app/bread_crumbs.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/bread_crumbs.py	2011-07-12 02:34:12 +0000
@@ -0,0 +1,86 @@
+# Copyright (C) 2010 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.core.urlresolvers import reverse
+import logging
+
+
+class BreadCrumb(object):
+
+    def __init__(self, name, parent=None, needs=None):
+        self.name = name
+        self.view = None
+        self.parent = parent
+        self.needs = needs or []
+
+    def __repr__(self):
+        return "<BreadCrumb name=%r view=%r parent=%r>" % (
+            self.name, self.view, self.parent)
+
+    def __call__(self, view):
+        self.view = view
+        view._bread_crumb = self
+        return view
+
+    def get_name(self, kwargs):
+        try:
+            return self.name.format(**kwargs)
+        except:
+            logging.exception("Unable to construct breadcrumb name for view %r", self.view)
+            raise
+
+    def get_absolute_url(self, kwargs):
+        try:
+            return reverse(self.view, args=[kwargs[name] for name in self.needs])
+        except:
+            logging.exception("Unable to construct breadcrumb URL for view %r", self.view)
+            raise
+
+
+class LiveBreadCrumb(object):
+
+    def __init__(self, bread_crumb, kwargs):
+        self.bread_crumb = bread_crumb
+        self.kwargs = kwargs
+
+    def get_name(self):
+        return self.bread_crumb.get_name(self.kwargs)
+
+    def get_absolute_url(self):
+        return self.bread_crumb.get_absolute_url(self.kwargs)
+
+
+class BreadCrumbTrail(object):
+
+    def __init__(self, bread_crumb_list, kwargs):
+        self.bread_crumb_list = bread_crumb_list
+        self.kwargs = kwargs
+
+    def __iter__(self):
+        for bread_crumb in self.bread_crumb_list:
+            yield LiveBreadCrumb(bread_crumb, self.kwargs)
+
+    @classmethod
+    def leading_to(cls, view, **kwargs):
+        lst = []
+        while view is not None:
+            lst.append(view._bread_crumb)
+            view = view._bread_crumb.parent
+        lst.reverse()
+        return cls(lst, kwargs or {})
+

=== modified file 'dashboard_app/dataview.py'
--- dashboard_app/dataview.py	2011-05-18 16:00:53 +0000
+++ dashboard_app/dataview.py	2011-07-09 15:09:48 +0000
@@ -110,7 +110,13 @@
             raise LookupError("Specified data view has no SQL implementation "
                               "for current database")
         # Replace SQL aruments with django placeholders (connection agnostic)
-        sql = query.sql_template.format(**dict([(arg_name, "%s") for arg_name in query.argument_list]))
+        template = query.sql_template
+        template = template.replace("%", "%%")
+        # template = template.replace("{", "{{").replace("}", "}}")
+        sql = template.format(
+            **dict([
+                (arg_name, "%s")
+                for arg_name in query.argument_list]))
         # Construct argument list using defaults for missing values
         sql_args = [
             arguments.get(arg_name, self.lookup_argument(arg_name).default)

=== modified file 'dashboard_app/extension.py'
--- dashboard_app/extension.py	2011-07-01 14:03:49 +0000
+++ dashboard_app/extension.py	2011-07-12 02:32:26 +0000
@@ -1,3 +1,4 @@
+import os
 from lava_server.extension import LavaServerExtension
 
 
@@ -13,7 +14,7 @@
 
     @property
     def main_view_name(self):
-        return "dashboard_app.views.bundle_stream_list"
+        return "dashboard_app.views.index"
 
     @property
     def description(self):
@@ -25,19 +26,33 @@
         import dashboard_app 
         return versiontools.format_version(dashboard_app.__version__)
 
-    def contribute_to_settings(self, settings):
-        super(DashboardExtension, self).contribute_to_settings(settings)
-        settings['INSTALLED_APPS'].extend([
+    def contribute_to_settings(self, settings_module):
+        super(DashboardExtension, self).contribute_to_settings(settings_module)
+        settings_module['INSTALLED_APPS'].extend([
             "linaro_django_pagination",
             "south",
         ])
-        settings['MIDDLEWARE_CLASSES'].append(
+        settings_module['MIDDLEWARE_CLASSES'].append(
             'linaro_django_pagination.middleware.PaginationMiddleware')
-        settings['RESTRUCTUREDTEXT_FILTER_SETTINGS'] = {
-            "initial_header_level": 4}
+        root_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
+        settings_module['DATAVIEW_DIRS'] = [
+            os.path.join(root_dir, 'examples/views'),
+            os.path.join(root_dir, 'production/views')]
+        settings_module['DATAREPORT_DIRS'] = [
+            os.path.join(root_dir, 'examples/reports'),
+            os.path.join(root_dir, 'production/reports')]
 
     def contribute_to_settings_ex(self, settings_module, settings_object):
         settings_module['DATAVIEW_DIRS'] = settings_object._settings.get(
             "DATAVIEW_DIRS", [])
         settings_module['DATAREPORT_DIRS'] = settings_object._settings.get(
             "DATAREPORT_DIRS", [])
+
+        # Enable constrained dataview database if requested
+        if settings_object._settings.get("use_dataview_database"):
+            # Copy everything from the default database and append _dataview to user
+            # name. The rest is out of scope (making sure it's actually setup
+            # properly, having permissions to login, permissions to view proper data)
+            settings_module['DATABASES']['dataview'] = dict(settings_module['DATABASES']['default'])
+            settings_module['DATABASES']['dataview']['USER'] += "_dataview"
+

=== modified file 'dashboard_app/managers.py'
--- dashboard_app/managers.py	2011-03-16 21:10:26 +0000
+++ dashboard_app/managers.py	2011-07-09 13:47:36 +0000
@@ -31,6 +31,7 @@
                 bundle_stream=bundle_stream,
                 uploaded_by=uploaded_by,
                 content_filename=content_filename)
+        # XXX: this _can_ fail -- if content_sha1 is a duplicate
         logging.debug("Saving bundle object (this is safe so far)")
         bundle.save()
         try:

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2011-05-30 16:58:20 +0000
+++ dashboard_app/models.py	2011-07-12 02:30:17 +0000
@@ -24,6 +24,7 @@
 import hashlib
 import logging
 import os
+import simplejson
 import traceback
 
 from django.contrib.auth.models import User
@@ -43,6 +44,10 @@
 from dashboard_app.repositories import RepositoryItem 
 from dashboard_app.repositories.data_report import DataReportRepository
 
+# Fix some django issues we ran into
+from dashboard_app.patches import patch
+patch()
+
 
 def _help_max_length(max_length):
     return ungettext(
@@ -184,7 +189,7 @@
 
     @models.permalink
     def get_absolute_url(self):
-        return ("dashboard_app.test_run_list", [self.pathname])
+        return ("dashboard_app.views.bundle_list", [self.pathname])
 
     def get_test_run_count(self):
         return TestRun.objects.filter(bundle__bundle_stream=self).count()
@@ -341,7 +346,7 @@
 
     @models.permalink
     def get_absolute_url(self):
-        return ("dashboard_app.bundle.detail", [self.pk])
+        return ("dashboard_app.views.bundle_detail", [self.bundle_stream.pathname, self.content_sha1])
 
     def save(self, *args, **kwargs):
         if self.content:
@@ -413,6 +418,41 @@
             for attachment in test_run.attachments.all():
                 attachment.content.delete(save=save)
 
+    def get_sanitized_bundle(self):
+        self.content.open()
+        try:
+            return SanitizedBundle(self.content)
+        finally:
+            self.content.close()
+
+
+class SanitizedBundle(object):
+
+    def __init__(self, stream):
+        try:
+            self.bundle_json = simplejson.load(stream)
+            self.deserialization_error = None
+        except simplejson.JSONDeserializationError as ex:
+            self.bundle_json = None
+            self.deserialization_error = ex
+        self.did_remove_attachments = False
+        self._sanitize()
+
+    def get_human_readable_json(self):
+        return simplejson.dumps(self.bundle_json, indent=4)
+
+    def _sanitize(self):
+        for test_run in self.bundle_json.get("test_runs", []):
+            attachments = test_run.get("attachments")
+            if isinstance(attachments, list):
+                for attachment in attachments:
+                    attachment["content"] = None
+                    self.did_remove_attachments = True
+            elif isinstance(attachments, dict):
+                for name in attachments:
+                    attachments[name] = None
+                    self.did_remove_attachments = True
+
 
 class BundleDeserializationError(models.Model):
     """
@@ -466,7 +506,18 @@
 
     @models.permalink
     def get_absolute_url(self):
-        return ('dashboard_app.test.detail', [self.test_id])
+        return ('dashboard_app.views.test_detail', [self.test_id])
+
+    def count_results_without_test_case(self):
+        return TestResult.objects.filter(
+            test_run__test=self,
+            test_case=None).count()
+
+    def count_failures_without_test_case(self):
+        return TestResult.objects.filter(
+            test_run__test=self,
+            test_case=None,
+            result=TestResult.RESULT_FAIL).count()
 
 
 class TestCase(models.Model):
@@ -511,6 +562,9 @@
     def get_absolute_url(self):
         return ("dashboard_app.test_case.details", [self.test.test_id, self.test_case_id])
 
+    def count_failures(self):
+        return self.test_results.filter(result=TestResult.RESULT_FAIL).count()
+
 
 class SoftwareSource(models.Model):
     """
@@ -675,7 +729,9 @@
     @models.permalink
     def get_absolute_url(self):
         return ("dashboard_app.views.test_run_detail",
-                [self.analyzer_assigned_uuid])
+                [self.bundle.bundle_stream.pathname,
+                 self.bundle.content_sha1,
+                 self.analyzer_assigned_uuid])
 
     def get_summary_results(self):
         stats = self.test_results.values('result').annotate(
@@ -723,10 +779,6 @@
     def __unicode__(self):
         return self.content_filename
 
-    @models.permalink
-    def get_absolute_url(self):
-        return ("dashboard_app.views.attachment_detail", [self.pk])
-
     def get_content_if_possible(self, mirror=False):
         if self.content:
             self.content.open()
@@ -752,6 +804,25 @@
             data = None
         return data
 
+    def is_test_run_attachment(self):
+        if (self.content_type.app_label == 'dashboard_app' and
+            self.content_type.model == 'testrun'):
+            return True
+
+    @property
+    def test_run(self):
+        if self.is_test_run_attachment():
+            return self.content_object
+
+    @models.permalink
+    def get_absolute_url(self):
+        if self.is_test_run_attachment():
+            return ("dashboard_app.views.attachment_detail",
+                    [self.test_run.bundle.bundle_stream.pathname,
+                     self.test_run.bundle.content_sha1,
+                     self.test_run.analyzer_assigned_uuid,
+                     self.pk])
+
 
 class TestResult(models.Model):
     """
@@ -888,8 +959,12 @@
 
     @models.permalink
     def get_absolute_url(self):
-        return ("dashboard_app.views.test_result_detail",
-                [self.pk])
+        return ("dashboard_app.views.test_result_detail", [
+            self.test_run.bundle.bundle_stream.pathname,
+            self.test_run.bundle.content_sha1,
+            self.test_run.analyzer_assigned_uuid,
+            self.relative_index,
+        ])
 
     def related_attachment_available(self):
         """
@@ -920,7 +995,7 @@
 
     def __init__(self, **kwargs):
         self._html = None
-        self.__dict__.update(kwargs)
+        self._data = kwargs
 
     def _get_raw_html(self):
         pathname = os.path.join(self.base_path, self.path)
@@ -935,7 +1010,9 @@
         return Template(self._get_raw_html())
 
     def _get_html_template_context(self):
-        return Context({"API_URL": reverse("dashboard_app.views.dashboard_xml_rpc_handler")})
+        return Context({
+            "API_URL": reverse("dashboard_app.views.dashboard_xml_rpc_handler")
+        })
 
     def get_html(self):
         from django.conf import settings
@@ -952,3 +1029,23 @@
     @models.permalink
     def get_absolute_url(self):
         return ("dashboard_app.views.report_detail", [self.name])
+
+    @property
+    def title(self):
+        return self._data['title']
+
+    @property
+    def path(self):
+        return self._data['path']
+
+    @property
+    def name(self):
+        return self._data['name']
+
+    @property
+    def bug_report_url(self):
+        return self._data.get('bug_report_url')
+
+    @property
+    def author(self):
+        return self._data.get('author')

=== added file 'dashboard_app/patches.py'
--- dashboard_app/patches.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/patches.py	2011-07-09 15:08:42 +0000
@@ -0,0 +1,112 @@
+"""
+Patches for django bugs that affect this package
+"""
+
+class PatchDjangoTicket1476(object):
+    """
+    Patch for bug http://code.djangoproject.com/ticket/1476
+    """
+
+    @classmethod
+    def apply_if_needed(patch):
+        import django
+        if django.VERSION[0:3] <= (1, 2, 4):
+            patch.apply()
+
+    @classmethod
+    def apply(patch):
+        from django.utils.decorators import method_decorator
+        from django.views.decorators.csrf import csrf_protect
+
+        @method_decorator(csrf_protect)
+        def __call__(self, request, *args, **kwargs):
+            """
+            Main method that does all the hard work, conforming to the Django view
+            interface.
+            """
+            if 'extra_context' in kwargs:
+                self.extra_context.update(kwargs['extra_context'])
+            current_step = self.determine_step(request, *args, **kwargs)
+            self.parse_params(request, *args, **kwargs)
+
+            # Sanity check.
+            if current_step >= self.num_steps():
+                raise Http404('Step %s does not exist' % current_step)
+
+            # Process the current step. If it's valid, go to the next step or call
+            # done(), depending on whether any steps remain.
+            if request.method == 'POST':
+                form = self.get_form(current_step, request.POST)
+            else:
+                form = self.get_form(current_step)
+
+            if form.is_valid():
+                # Validate all the forms. If any of them fail validation, that
+                # must mean the validator relied on some other input, such as
+                # an external Web site.
+
+                # It is also possible that validation might fail under certain
+                # attack situations: an attacker might be able to bypass previous
+                # stages, and generate correct security hashes for all the
+                # skipped stages by virtue of:
+                #  1) having filled out an identical form which doesn't have the
+                #     validation (and does something different at the end),
+                #  2) or having filled out a previous version of the same form
+                #     which had some validation missing,
+                #  3) or previously having filled out the form when they had
+                #     more privileges than they do now.
+                #
+                # Since the hashes only take into account values, and not other
+                # other validation the form might do, we must re-do validation
+                # now for security reasons.
+                previous_form_list = [self.get_form(i, request.POST) for i in range(current_step)]
+
+                for i, f in enumerate(previous_form_list):
+                    if request.POST.get("hash_%d" % i, '') != self.security_hash(request, f):
+                        return self.render_hash_failure(request, i)
+
+                    if not f.is_valid():
+                        return self.render_revalidation_failure(request, i, f)
+                    else:
+                        self.process_step(request, f, i)
+
+                # Now progress to processing this step:
+                self.process_step(request, form, current_step)
+                next_step = current_step + 1
+
+
+                if next_step == self.num_steps():
+                    return self.done(request, previous_form_list + [form])
+                else:
+                    form = self.get_form(next_step)
+                    self.step = current_step = next_step
+
+            return self.render(form, request, current_step)
+
+        from django.contrib.formtools.wizard import FormWizard
+        FormWizard.__call__ = __call__
+
+
+class PatchDjangoTicket15155(object):
+    """
+    Patch for bug http://code.djangoproject.com/ticket/15155
+    """
+
+    PROPER_FORMAT = r'(?<!%)%s'
+
+    @classmethod
+    def apply_if_needed(patch):
+        from django.db.backends.sqlite3 import base
+        if base.FORMAT_QMARK_REGEX != patch.PROPER_FORMAT:
+            patch.apply()
+
+    @classmethod
+    def apply(cls):
+        from django.db.backends.sqlite3 import base
+        import re
+        base.FORMAT_QMARK_REGEX = re.compile(cls.PROPER_FORMAT)
+
+
+def patch():
+    PatchDjangoTicket1476.apply_if_needed()
+    PatchDjangoTicket15155.apply_if_needed()

=== added file 'dashboard_app/static/css/demo_table_jui.css'
--- dashboard_app/static/css/demo_table_jui.css	1970-01-01 00:00:00 +0000
+++ dashboard_app/static/css/demo_table_jui.css	2011-07-08 04:20:37 +0000
@@ -0,0 +1,516 @@
+/*
+ *  File:         demo_table_jui.css
+ *  CVS:          $Id$
+ *  Description:  CSS descriptions for DataTables demo pages
+ *  Author:       Allan Jardine
+ *  Created:      Tue May 12 06:47:22 BST 2009
+ *  Modified:     $Date$ by $Author$
+ *  Language:     CSS
+ *  Project:      DataTables
+ *
+ *  Copyright 2009 Allan Jardine. All Rights Reserved.
+ *
+ * ***************************************************************************
+ * DESCRIPTION
+ *
+ * The styles given here are suitable for the demos that are used with the standard DataTables
+ * distribution (see www.datatables.net). You will most likely wish to modify these styles to
+ * meet the layout requirements of your site.
+ *
+ * Common issues:
+ *   'full_numbers' pagination - I use an extra selector on the body tag to ensure that there is
+ *     no conflict between the two pagination types. If you want to use full_numbers pagination
+ *     ensure that you either have "example_alt_pagination" as a body class name, or better yet,
+ *     modify that selector.
+ *   Note that the path used for Images is relative. All images are by default located in
+ *     ../images/ - relative to this CSS file.
+ */
+
+
+/*
+ * jQuery UI specific styling
+ */
+
+.paging_two_button .ui-button {
+	float: left;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+.paging_full_numbers .ui-button {
+	padding: 2px 6px;
+	margin: 0;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+.dataTables_paginate .ui-button {
+	margin-right: -0.1em !important;
+}
+
+.paging_full_numbers {
+	width: 350px !important;
+}
+
+.dataTables_wrapper .ui-toolbar {
+	padding: 5px;
+}
+
+.dataTables_paginate {
+	width: auto;
+}
+
+.dataTables_info {
+	padding-top: 3px;
+}
+
+table.display thead th {
+	padding: 3px 0px 3px 10px;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+div.dataTables_wrapper .ui-widget-header {
+	font-weight: normal;
+}
+
+
+/*
+ * Sort arrow icon positioning
+ */
+table.display thead th div.DataTables_sort_wrapper {
+	position: relative;
+	padding-right: 20px;
+	padding-right: 20px;
+}
+
+table.display thead th div.DataTables_sort_wrapper span {
+	position: absolute;
+	top: 50%;
+	margin-top: -8px;
+	right: 0;
+}
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * Everything below this line is the same as demo_table.css. This file is
+ * required for 'cleanliness' of the markup
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables features
+ */
+
+.dataTables_wrapper {
+	position: relative;
+	min-height: 302px;
+	_height: 302px;
+	clear: both;
+}
+
+.dataTables_processing {
+	position: absolute;
+	top: 0px;
+	left: 50%;
+	width: 250px;
+	margin-left: -125px;
+	border: 1px solid #ddd;
+	text-align: center;
+	color: #999;
+	font-size: 11px;
+	padding: 2px 0;
+}
+
+.dataTables_length {
+	width: 40%;
+	float: left;
+}
+
+.dataTables_filter {
+	width: 50%;
+	float: right;
+	text-align: right;
+}
+
+.dataTables_info {
+	width: 50%;
+	float: left;
+}
+
+.dataTables_paginate {
+	float: right;
+	text-align: right;
+}
+
+/* Pagination nested */
+.paginate_disabled_previous, .paginate_enabled_previous, .paginate_disabled_next, .paginate_enabled_next {
+	height: 19px;
+	width: 19px;
+	margin-left: 3px;
+	float: left;
+}
+
+.paginate_disabled_previous {
+	background-image: url('../images/back_disabled.jpg');
+}
+
+.paginate_enabled_previous {
+	background-image: url('../images/back_enabled.jpg');
+}
+
+.paginate_disabled_next {
+	background-image: url('../images/forward_disabled.jpg');
+}
+
+.paginate_enabled_next {
+	background-image: url('../images/forward_enabled.jpg');
+}
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables display
+ */
+table.display {
+	margin: 0 auto;
+	width: 100%;
+	clear: both;
+	border-collapse: collapse;
+}
+
+table.display tfoot th {
+	padding: 3px 0px 3px 10px;
+	font-weight: bold;
+	font-weight: normal;
+}
+
+table.display tr.heading2 td {
+	border-bottom: 1px solid #aaa;
+}
+
+table.display td {
+	padding: 3px 10px;
+}
+
+table.display td.center {
+	text-align: center;
+}
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables sorting
+ */
+
+.sorting_asc {
+	background: url('../images/sort_asc.png') no-repeat center right;
+}
+
+.sorting_desc {
+	background: url('../images/sort_desc.png') no-repeat center right;
+}
+
+.sorting {
+	background: url('../images/sort_both.png') no-repeat center right;
+}
+
+.sorting_asc_disabled {
+	background: url('../images/sort_asc_disabled.png') no-repeat center right;
+}
+
+.sorting_desc_disabled {
+	background: url('../images/sort_desc_disabled.png') no-repeat center right;
+}
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables row classes
+ */
+table.display tr.odd.gradeA {
+	background-color: #ddffdd;
+}
+
+table.display tr.even.gradeA {
+	background-color: #eeffee;
+}
+
+
+
+
+table.display tr.odd.gradeA {
+	background-color: #ddffdd;
+}
+
+table.display tr.even.gradeA {
+	background-color: #eeffee;
+}
+
+table.display tr.odd.gradeC {
+	background-color: #ddddff;
+}
+
+table.display tr.even.gradeC {
+	background-color: #eeeeff;
+}
+
+table.display tr.odd.gradeX {
+	background-color: #ffdddd;
+}
+
+table.display tr.even.gradeX {
+	background-color: #ffeeee;
+}
+
+table.display tr.odd.gradeU {
+	background-color: #ddd;
+}
+
+table.display tr.even.gradeU {
+	background-color: #eee;
+}
+
+
+tr.odd {
+	background-color: #E2E4FF;
+}
+
+tr.even {
+	background-color: white;
+}
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Misc
+ */
+.dataTables_scroll {
+	clear: both;
+}
+
+.top, .bottom {
+	padding: 15px;
+	background-color: #F5F5F5;
+	border: 1px solid #CCCCCC;
+}
+
+.top .dataTables_info {
+	float: none;
+}
+
+.clear {
+	clear: both;
+}
+
+.dataTables_empty {
+	text-align: center;
+}
+
+tfoot input {
+	margin: 0.5em 0;
+	width: 100%;
+	color: #444;
+}
+
+tfoot input.search_init {
+	color: #999;
+}
+
+td.group {
+	background-color: #d1cfd0;
+	border-bottom: 2px solid #A19B9E;
+	border-top: 2px solid #A19B9E;
+}
+
+td.details {
+	background-color: #d1cfd0;
+	border: 2px solid #A19B9E;
+}
+
+
+.example_alt_pagination div.dataTables_info {
+	width: 40%;
+}
+
+.paging_full_numbers span.paginate_button,
+ 	.paging_full_numbers span.paginate_active {
+	border: 1px solid #aaa;
+	-webkit-border-radius: 5px;
+	-moz-border-radius: 5px;
+	padding: 2px 5px;
+	margin: 0 3px;
+	cursor: pointer;
+	*cursor: hand;
+}
+
+.paging_full_numbers span.paginate_button {
+	background-color: #ddd;
+}
+
+.paging_full_numbers span.paginate_button:hover {
+	background-color: #ccc;
+}
+
+.paging_full_numbers span.paginate_active {
+	background-color: #99B3FF;
+}
+
+table.display tr.even.row_selected td {
+	background-color: #B0BED9;
+}
+
+table.display tr.odd.row_selected td {
+	background-color: #9FAFD1;
+}
+
+
+/*
+ * Sorting classes for columns
+ */
+/* For the standard odd/even */
+tr.odd td.sorting_1 {
+	background-color: #D3D6FF;
+}
+
+tr.odd td.sorting_2 {
+	background-color: #DADCFF;
+}
+
+tr.odd td.sorting_3 {
+	background-color: #E0E2FF;
+}
+
+tr.even td.sorting_1 {
+	background-color: #EAEBFF;
+}
+
+tr.even td.sorting_2 {
+	background-color: #F2F3FF;
+}
+
+tr.even td.sorting_3 {
+	background-color: #F9F9FF;
+}
+
+
+/* For the Conditional-CSS grading rows */
+/*
+ 	Colour calculations (based off the main row colours)
+  Level 1:
+		dd > c4
+		ee > d5
+	Level 2:
+	  dd > d1
+	  ee > e2
+ */
+tr.odd.gradeA td.sorting_1 {
+	background-color: #c4ffc4;
+}
+
+tr.odd.gradeA td.sorting_2 {
+	background-color: #d1ffd1;
+}
+
+tr.odd.gradeA td.sorting_3 {
+	background-color: #d1ffd1;
+}
+
+tr.even.gradeA td.sorting_1 {
+	background-color: #d5ffd5;
+}
+
+tr.even.gradeA td.sorting_2 {
+	background-color: #e2ffe2;
+}
+
+tr.even.gradeA td.sorting_3 {
+	background-color: #e2ffe2;
+}
+
+tr.odd.gradeC td.sorting_1 {
+	background-color: #c4c4ff;
+}
+
+tr.odd.gradeC td.sorting_2 {
+	background-color: #d1d1ff;
+}
+
+tr.odd.gradeC td.sorting_3 {
+	background-color: #d1d1ff;
+}
+
+tr.even.gradeC td.sorting_1 {
+	background-color: #d5d5ff;
+}
+
+tr.even.gradeC td.sorting_2 {
+	background-color: #e2e2ff;
+}
+
+tr.even.gradeC td.sorting_3 {
+	background-color: #e2e2ff;
+}
+
+tr.odd.gradeX td.sorting_1 {
+	background-color: #ffc4c4;
+}
+
+tr.odd.gradeX td.sorting_2 {
+	background-color: #ffd1d1;
+}
+
+tr.odd.gradeX td.sorting_3 {
+	background-color: #ffd1d1;
+}
+
+tr.even.gradeX td.sorting_1 {
+	background-color: #ffd5d5;
+}
+
+tr.even.gradeX td.sorting_2 {
+	background-color: #ffe2e2;
+}
+
+tr.even.gradeX td.sorting_3 {
+	background-color: #ffe2e2;
+}
+
+tr.odd.gradeU td.sorting_1 {
+	background-color: #c4c4c4;
+}
+
+tr.odd.gradeU td.sorting_2 {
+	background-color: #d1d1d1;
+}
+
+tr.odd.gradeU td.sorting_3 {
+	background-color: #d1d1d1;
+}
+
+tr.even.gradeU td.sorting_1 {
+	background-color: #d5d5d5;
+}
+
+tr.even.gradeU td.sorting_2 {
+	background-color: #e2e2e2;
+}
+
+tr.even.gradeU td.sorting_3 {
+	background-color: #e2e2e2;
+}
+
+/*
+ * Row highlighting example
+ */
+.ex_highlight #example tbody tr.even:hover, #example tbody tr.even td.highlighted {
+	background-color: #ECFFB3;
+}
+
+.ex_highlight #example tbody tr.odd:hover, #example tbody tr.odd td.highlighted {
+	background-color: #E6FF99;
+}

=== added file 'dashboard_app/static/images/details_close.png'
Binary files dashboard_app/static/images/details_close.png	1970-01-01 00:00:00 +0000 and dashboard_app/static/images/details_close.png	2011-07-08 04:20:37 +0000 differ
=== added file 'dashboard_app/static/images/details_open.png'
Binary files dashboard_app/static/images/details_open.png	1970-01-01 00:00:00 +0000 and dashboard_app/static/images/details_open.png	2011-07-08 04:20:37 +0000 differ
=== added file 'dashboard_app/static/js/FixedHeader.min.js'
--- dashboard_app/static/js/FixedHeader.min.js	1970-01-01 00:00:00 +0000
+++ dashboard_app/static/js/FixedHeader.min.js	2011-07-08 04:20:37 +0000
@@ -0,0 +1,115 @@
+/*
+ * File:        FixedHeader.min.js
+ * Version:     2.0.4
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * 
+ * Copyright 2009-2011 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD (3 point) style license, as supplied with this software.
+ * 
+ * This source file is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+ */
+var FixedHeader=function(b,a){if(typeof this.fnInit!="function"){alert("FixedHeader warning: FixedHeader must be initialised with the 'new' keyword.");
+return}var c=this;var d={aoCache:[],oSides:{top:true,bottom:false,left:false,right:false},oZIndexes:{top:104,bottom:103,left:102,right:101},oMes:{iTableWidth:0,iTableHeight:0,iTableLeft:0,iTableRight:0,iTableTop:0,iTableBottom:0},nTable:null,bUseAbsPos:false,bFooter:false};
+this.fnGetSettings=function(){return d};this.fnUpdate=function(){this._fnUpdateClones();
+this._fnUpdatePositions()};this.fnInit(b,a)};FixedHeader.prototype={fnInit:function(b,a){var c=this.fnGetSettings();
+var d=this;this.fnInitSettings(c,a);if(typeof b.fnSettings=="function"){if(typeof b.fnVersionCheck=="functon"&&b.fnVersionCheck("1.6.0")!==true){alert("FixedHeader 2 required DataTables 1.6.0 or later. Please upgrade your DataTables installation");
+return}var e=b.fnSettings();if(e.oScroll.sX!=""||e.oScroll.sY!=""){alert("FixedHeader 2 is not supported with DataTables' scrolling mode at this time");
+return}c.nTable=e.nTable;e.aoDrawCallback.push({fn:function(){FixedHeader.fnMeasure();
+d._fnUpdateClones.call(d);d._fnUpdatePositions.call(d)},sName:"FixedHeader"})}else{c.nTable=b
+}c.bFooter=($(">tfoot",c.nTable).length>0)?true:false;c.bUseAbsPos=(jQuery.browser.msie&&(jQuery.browser.version=="6.0"||jQuery.browser.version=="7.0"));
+if(c.oSides.top){c.aoCache.push(d._fnCloneTable("fixedHeader","FixedHeader_Header",d._fnCloneThead))
+}if(c.oSides.bottom){c.aoCache.push(d._fnCloneTable("fixedFooter","FixedHeader_Footer",d._fnCloneTfoot))
+}if(c.oSides.left){c.aoCache.push(d._fnCloneTable("fixedLeft","FixedHeader_Left",d._fnCloneTLeft))
+}if(c.oSides.right){c.aoCache.push(d._fnCloneTable("fixedRight","FixedHeader_Right",d._fnCloneTRight))
+}FixedHeader.afnScroll.push(function(){d._fnUpdatePositions.call(d)});jQuery(window).resize(function(){FixedHeader.fnMeasure();
+d._fnUpdateClones.call(d);d._fnUpdatePositions.call(d)});FixedHeader.fnMeasure();
+d._fnUpdateClones();d._fnUpdatePositions()},fnInitSettings:function(b,a){if(typeof a!="undefined"){if(typeof a.top!="undefined"){b.oSides.top=a.top
+}if(typeof a.bottom!="undefined"){b.oSides.bottom=a.bottom}if(typeof a.left!="undefined"){b.oSides.left=a.left
+}if(typeof a.right!="undefined"){b.oSides.right=a.right}if(typeof a.zTop!="undefined"){b.oZIndexes.top=a.zTop
+}if(typeof a.zBottom!="undefined"){b.oZIndexes.bottom=a.zBottom}if(typeof a.zLeft!="undefined"){b.oZIndexes.left=a.zLeft
+}if(typeof a.zRight!="undefined"){b.oZIndexes.right=a.zRight}}b.bUseAbsPos=(jQuery.browser.msie&&(jQuery.browser.version=="6.0"||jQuery.browser.version=="7.0"))
+},_fnCloneTable:function(f,e,d){var b=this.fnGetSettings();var a;if(jQuery(b.nTable.parentNode).css("position")!="absolute"){b.nTable.parentNode.style.position="relative"
+}a=b.nTable.cloneNode(false);var c=document.createElement("div");c.style.position="absolute";
+c.className+=" FixedHeader_Cloned "+f+" "+e;if(f=="fixedHeader"){c.style.zIndex=b.oZIndexes.top
+}if(f=="fixedFooter"){c.style.zIndex=b.oZIndexes.bottom}if(f=="fixedLeft"){c.style.zIndex=b.oZIndexes.left
+}else{if(f=="fixedRight"){c.style.zIndex=b.oZIndexes.right}}c.appendChild(a);document.body.appendChild(c);
+return{nNode:a,nWrapper:c,sType:f,sPosition:"",sTop:"",sLeft:"",fnClone:d}},_fnMeasure:function(){var d=this.fnGetSettings(),a=d.oMes,c=jQuery(d.nTable),b=c.offset(),f=this._fnSumScroll(d.nTable.parentNode,"scrollTop"),e=this._fnSumScroll(d.nTable.parentNode,"scrollLeft");
+a.iTableWidth=c.outerWidth();a.iTableHeight=c.outerHeight();a.iTableLeft=b.left+d.nTable.parentNode.scrollLeft;
+a.iTableTop=b.top+f;a.iTableRight=a.iTableLeft+a.iTableWidth;a.iTableRight=FixedHeader.oDoc.iWidth-a.iTableLeft-a.iTableWidth;
+a.iTableBottom=FixedHeader.oDoc.iHeight-a.iTableTop-a.iTableHeight},_fnSumScroll:function(c,b){var a=c[b];
+while(c=c.parentNode){if(c.nodeName!="HTML"&&c.nodeName!="BODY"){break}a=c[b]}return a
+},_fnUpdatePositions:function(){var c=this.fnGetSettings();this._fnMeasure();for(var b=0,a=c.aoCache.length;
+b<a;b++){if(c.aoCache[b].sType=="fixedHeader"){this._fnScrollFixedHeader(c.aoCache[b])
+}else{if(c.aoCache[b].sType=="fixedFooter"){this._fnScrollFixedFooter(c.aoCache[b])
+}else{if(c.aoCache[b].sType=="fixedLeft"){this._fnScrollHorizontalLeft(c.aoCache[b])
+}else{this._fnScrollHorizontalRight(c.aoCache[b])}}}}},_fnUpdateClones:function(){var c=this.fnGetSettings();
+for(var b=0,a=c.aoCache.length;b<a;b++){c.aoCache[b].fnClone.call(this,c.aoCache[b])
+}},_fnScrollHorizontalRight:function(g){var e=this.fnGetSettings(),f=e.oMes,b=FixedHeader.oWin,a=FixedHeader.oDoc,d=g.nWrapper,c=jQuery(d).outerWidth();
+if(b.iScrollRight<f.iTableRight){this._fnUpdateCache(g,"sPosition","absolute","position",d.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",d.style);this._fnUpdateCache(g,"sLeft",(f.iTableLeft+f.iTableWidth-c)+"px","left",d.style)
+}else{if(f.iTableLeft<a.iWidth-b.iScrollRight-c){if(e.bUseAbsPos){this._fnUpdateCache(g,"sPosition","absolute","position",d.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",d.style);this._fnUpdateCache(g,"sLeft",(a.iWidth-b.iScrollRight-c)+"px","left",d.style)
+}else{this._fnUpdateCache(g,"sPosition","fixed","position",d.style);this._fnUpdateCache(g,"sTop",(f.iTableTop-b.iScrollTop)+"px","top",d.style);
+this._fnUpdateCache(g,"sLeft",(b.iWidth-c)+"px","left",d.style)}}else{this._fnUpdateCache(g,"sPosition","absolute","position",d.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",d.style);this._fnUpdateCache(g,"sLeft",f.iTableLeft+"px","left",d.style)
+}}},_fnScrollHorizontalLeft:function(g){var e=this.fnGetSettings(),f=e.oMes,b=FixedHeader.oWin,a=FixedHeader.oDoc,c=g.nWrapper,d=jQuery(c).outerWidth();
+if(b.iScrollLeft<f.iTableLeft){this._fnUpdateCache(g,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",c.style);this._fnUpdateCache(g,"sLeft",f.iTableLeft+"px","left",c.style)
+}else{if(b.iScrollLeft<f.iTableLeft+f.iTableWidth-d){if(e.bUseAbsPos){this._fnUpdateCache(g,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",c.style);this._fnUpdateCache(g,"sLeft",b.iScrollLeft+"px","left",c.style)
+}else{this._fnUpdateCache(g,"sPosition","fixed","position",c.style);this._fnUpdateCache(g,"sTop",(f.iTableTop-b.iScrollTop)+"px","top",c.style);
+this._fnUpdateCache(g,"sLeft","0px","left",c.style)}}else{this._fnUpdateCache(g,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",c.style);this._fnUpdateCache(g,"sLeft",(f.iTableLeft+f.iTableWidth-d)+"px","left",c.style)
+}}},_fnScrollFixedFooter:function(h){var f=this.fnGetSettings(),g=f.oMes,b=FixedHeader.oWin,a=FixedHeader.oDoc,c=h.nWrapper,e=jQuery("thead",f.nTable).outerHeight(),d=jQuery(c).outerHeight();
+if(b.iScrollBottom<g.iTableBottom){this._fnUpdateCache(h,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(h,"sTop",(g.iTableTop+g.iTableHeight-d)+"px","top",c.style);this._fnUpdateCache(h,"sLeft",g.iTableLeft+"px","left",c.style)
+}else{if(b.iScrollBottom<g.iTableBottom+g.iTableHeight-d-e){if(f.bUseAbsPos){this._fnUpdateCache(h,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(h,"sTop",(a.iHeight-b.iScrollBottom-d)+"px","top",c.style);this._fnUpdateCache(h,"sLeft",g.iTableLeft+"px","left",c.style)
+}else{this._fnUpdateCache(h,"sPosition","fixed","position",c.style);this._fnUpdateCache(h,"sTop",(b.iHeight-d)+"px","top",c.style);
+this._fnUpdateCache(h,"sLeft",(g.iTableLeft-b.iScrollLeft)+"px","left",c.style)}}else{this._fnUpdateCache(h,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(h,"sTop",(g.iTableTop+d)+"px","top",c.style);this._fnUpdateCache(h,"sLeft",g.iTableLeft+"px","left",c.style)
+}}},_fnScrollFixedHeader:function(g){var d=this.fnGetSettings(),f=d.oMes,b=FixedHeader.oWin,a=FixedHeader.oDoc,c=g.nWrapper,e=d.nTable.getElementsByTagName("tbody")[0].offsetHeight;
+if(f.iTableTop>b.iScrollTop){this._fnUpdateCache(g,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(g,"sTop",f.iTableTop+"px","top",c.style);this._fnUpdateCache(g,"sLeft",f.iTableLeft+"px","left",c.style)
+}else{if(b.iScrollTop>f.iTableTop+e){this._fnUpdateCache(g,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(g,"sTop",(f.iTableTop+e)+"px","top",c.style);this._fnUpdateCache(g,"sLeft",f.iTableLeft+"px","left",c.style)
+}else{if(d.bUseAbsPos){this._fnUpdateCache(g,"sPosition","absolute","position",c.style);
+this._fnUpdateCache(g,"sTop",b.iScrollTop+"px","top",c.style);this._fnUpdateCache(g,"sLeft",f.iTableLeft+"px","left",c.style)
+}else{this._fnUpdateCache(g,"sPosition","fixed","position",c.style);this._fnUpdateCache(g,"sTop","0px","top",c.style);
+this._fnUpdateCache(g,"sLeft",(f.iTableLeft-b.iScrollLeft)+"px","left",c.style)}}}},_fnUpdateCache:function(e,c,b,d,a){if(e[c]!=b){a[d]=b;
+e[c]=b}},_fnCloneThead:function(d){var c=this.fnGetSettings();var a=d.nNode;d.nWrapper.style.width=jQuery(c.nTable).outerWidth()+"px";
+while(a.childNodes.length>0){jQuery("thead th",a).unbind("click");a.removeChild(a.childNodes[0])
+}var b=jQuery("thead",c.nTable).clone(true)[0];a.appendChild(b);jQuery("thead:eq(0)>tr th",c.nTable).each(function(e){jQuery("thead:eq(0)>tr th:eq("+e+")",a).width(jQuery(this).width())
+});jQuery("thead:eq(0)>tr td",c.nTable).each(function(e){jQuery("thead:eq(0)>tr th:eq("+e+")",a)[0].style.width(jQuery(this).width())
+})},_fnCloneTfoot:function(d){var c=this.fnGetSettings();var a=d.nNode;d.nWrapper.style.width=jQuery(c.nTable).outerWidth()+"px";
+while(a.childNodes.length>0){a.removeChild(a.childNodes[0])}var b=jQuery("tfoot",c.nTable).clone(true)[0];
+a.appendChild(b);jQuery("tfoot:eq(0)>tr th",c.nTable).each(function(e){jQuery("tfoot:eq(0)>tr th:eq("+e+")",a).width(jQuery(this).width())
+});jQuery("tfoot:eq(0)>tr td",c.nTable).each(function(e){jQuery("tfoot:eq(0)>tr th:eq("+e+")",a)[0].style.width(jQuery(this).width())
+})},_fnCloneTLeft:function(f){var c=this.fnGetSettings();var b=f.nNode;var e=jQuery("tbody tr:eq(0) td",c.nTable).length;
+var a=($.browser.msie&&($.browser.version=="6.0"||$.browser.version=="7.0"));while(b.childNodes.length>0){b.removeChild(b.childNodes[0])
+}b.appendChild(jQuery("thead",c.nTable).clone(true)[0]);b.appendChild(jQuery("tbody",c.nTable).clone(true)[0]);
+if(c.bFooter){b.appendChild(jQuery("tfoot",c.nTable).clone(true)[0])}jQuery("thead tr th:gt(0)",b).remove();
+jQuery("tfoot tr th:gt(0)",b).remove();$("tbody tr",b).each(function(g){$("td:gt(0)",this).remove();
+if($.browser.mozilla||$.browser.opera){$("td",this).height($("tbody tr:eq("+g+")",that.dom.body).outerHeight())
+}else{$("td",this).height($("tbody tr:eq("+g+")",that.dom.body).outerHeight()-iBoxHack)
+}if(!a){$("tbody tr:eq("+g+")",that.dom.body).height($("tbody tr:eq("+g+")",that.dom.body).outerHeight())
+}});var d=jQuery("thead tr th:eq(0)",c.nTable).outerWidth();b.style.width=d+"px";
+f.nWrapper.style.width=d+"px"},_fnCloneTRight:function(f){var c=this.fnGetSettings();
+var b=f.nNode;var e=jQuery("tbody tr:eq(0) td",c.nTable).length;var a=($.browser.msie&&($.browser.version=="6.0"||$.browser.version=="7.0"));
+while(b.childNodes.length>0){b.removeChild(b.childNodes[0])}b.appendChild(jQuery("thead",c.nTable).clone(true)[0]);
+b.appendChild(jQuery("tbody",c.nTable).clone(true)[0]);if(c.bFooter){b.appendChild(jQuery("tfoot",c.nTable).clone(true)[0])
+}jQuery("thead tr th:not(:nth-child("+e+"n))",b).remove();jQuery("tfoot tr th:not(:nth-child("+e+"n))",b).remove();
+$("tbody tr",b).each(function(g){$("td:lt("+e-1+")",this).remove();if($.browser.mozilla||$.browser.opera){$("td",this).height($("tbody tr:eq("+g+")",that.dom.body).outerHeight())
+}else{$("td",this).height($("tbody tr:eq("+g+")",that.dom.body).outerHeight()-iBoxHack)
+}if(!a){$("tbody tr:eq("+g+")",that.dom.body).height($("tbody tr:eq("+g+")",that.dom.body).outerHeight())
+}});var d=jQuery("thead tr th:eq("+(e-1)+")",c.nTable).outerWidth();b.style.width=d+"px";
+f.nWrapper.style.width=d+"px"}};FixedHeader.oWin={iScrollTop:0,iScrollRight:0,iScrollBottom:0,iScrollLeft:0,iHeight:0,iWidth:0};
+FixedHeader.oDoc={iHeight:0,iWidth:0};FixedHeader.afnScroll=[];FixedHeader.fnMeasure=function(){var d=jQuery(window),c=jQuery(document),b=FixedHeader.oWin,a=FixedHeader.oDoc;
+a.iHeight=c.height();a.iWidth=c.width();b.iHeight=d.height();b.iWidth=d.width();b.iScrollTop=d.scrollTop();
+b.iScrollLeft=d.scrollLeft();b.iScrollRight=a.iWidth-b.iScrollLeft-b.iWidth;b.iScrollBottom=a.iHeight-b.iScrollTop-b.iHeight
+};jQuery(window).scroll(function(){FixedHeader.fnMeasure();for(var b=0,a=FixedHeader.afnScroll.length;
+b<a;b++){FixedHeader.afnScroll[b]()}});
\ No newline at end of file

=== modified file 'dashboard_app/static/js/jquery.dashboard.js'
--- dashboard_app/static/js/jquery.dashboard.js	2011-05-05 02:25:24 +0000
+++ dashboard_app/static/js/jquery.dashboard.js	2011-07-09 15:09:19 +0000
@@ -2,6 +2,7 @@
 (function($) {
   var _server = null;
   var _url = null;
+  var _global_table_id = 0;
 
   function query_data_view(data_view_name, data_view_arguments, callback) {
     _server.query_data_view(callback, data_view_name, data_view_arguments);
@@ -40,25 +41,30 @@
           $this.data('dashboard', plot_data);
         }
         query_data_view(query.data_view.name, query.data_view.args, function(response) {
-          plot_data.series.push({
-            data: response.result.rows,
-            label: query.label
-          });
-          $.plot($this, plot_data.series, plot_data.options);
+          if (response.result) {
+            plot_data.series.push({
+              data: response.result.rows,
+              label: query.label
+            });
+            $.plot($this, plot_data.series, plot_data.options);
+          } else {
+            alert("Query failed: "+ response.error.faultString + " (code: " + response.error.faultCode + ")");
+          }
         });
       });
     },
 
     render_table: function(dataset, options) {
-      var html = "<table class='data'>";
+      var table_id = _global_table_id++;
+      var html = "<table class='demo_jui display' id='dashboard_table_" + table_id + "'>";
       if (options != undefined && options.caption != undefined) {
         html += "<caption>" + options.caption + "</caption>";
       }
-      html += "<tr>";
+      html += "<thead><tr>";
       $.each(dataset.columns, function (index, column) {
         html += "<th>" + column.name + "</th>";
       });
-      html += "</tr>";
+      html += "</tr></thead><tbody>";
       $.each(dataset.rows, function (index, row) {
         html += "<tr>";
         $.each(row, function (index, cell) {
@@ -84,8 +90,12 @@
         });
         html += "</tr>";
       });
-      html += "</table>";
+      html += "</tbody></table>";
       this.html(html);
+      $("#dashboard_table_" + table_id).dataTable({
+        "bJQueryUI": true,
+        "sPaginationType": "full_numbers",
+      });
     },
 
     render_to_table: function(data_view_name, data_view_arguments, options) {

=== added file 'dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html'
--- dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,12 @@
+{% load i18n %}
+{% if lines %}
+<ol class="file_listing">
+  {% for line in lines %}
+  <li id="L{{forloop.counter}}">
+  <a href="#L{{forloop.counter}}">{{line}}</a>
+  </li>
+  {% endfor %}
+</ol>
+{% else %}
+<h1>{% trans "Viewer not available" %}</h1>
+{% endif %}

=== added file 'dashboard_app/templates/dashboard_app/_ajax_bundle_viewer.html'
--- dashboard_app/templates/dashboard_app/_ajax_bundle_viewer.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_ajax_bundle_viewer.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,17 @@
+{% load i18n %}
+{% load stylize %}
+
+
+{% with bundle.get_sanitized_bundle as sanitized_bundle %}
+{% if sanitized_bundle.did_remove_attachments %}
+<div class="ui-widget">
+  <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
+  <span class="ui-icon ui-icon-info" style="float: left; margin-right: 0.3em;"></span>
+  <strong>Note:</strong> Inline attachments were removed to make this page more readable.
+  </div>
+</div>
+{% endif %}
+<div style="overflow-x: scroll">
+  {% stylize "js" %}{{ sanitized_bundle.get_human_readable_json|safe }}{% endstylize %}
+</div>
+{% endwith %}

=== added file 'dashboard_app/templates/dashboard_app/_breadcrumbs.html'
--- dashboard_app/templates/dashboard_app/_breadcrumbs.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_breadcrumbs.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,5 @@
+{% for bread_crumb in bread_crumb_trail %}
+<li><a 
+  href="{{ bread_crumb.get_absolute_url }}"
+  >{{ bread_crumb.get_name }}</a></li>
+{% endfor %}

=== added file 'dashboard_app/templates/dashboard_app/_bundle_stream_sidebar.html'
--- dashboard_app/templates/dashboard_app/_bundle_stream_sidebar.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_bundle_stream_sidebar.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,74 @@
+{% load i18n %}
+<h3>{% trans "About" %}</h3>
+<dl>
+  <dt>{% trans "Pathname:" %}</dt>
+  <dd>{{ bundle_stream.pathname }}</dd>
+  <dt>{% trans "Name:" %}</dt>
+  <dd>{{ bundle_stream.name|default:"<i>not set</i>" }}</dd>
+</dl>
+<h3>{% trans "Ownership" %}</h3>
+{% if bundle_stream.user %}
+<p>{% trans "This stream is owned by" %} <q>{{ bundle_stream.user }}</q></p>
+{% else %}
+<p>{% trans "This stream is owned by group called" %} <q>{{ bundle_stream.group }}</q></p>
+{% endif %}
+<h3>{% trans "Access rights" %}</h3>
+<dl>
+  <dt>{% trans "Stream type:" %}</dt>
+  {% if bundle_stream.is_anonymous %}
+  <dd>
+  Anonymous stream <a href="#" id="what-are-anonymous-streams">(what is this?)</a>
+  <div id="dialog-message" title="{% trans "About anonymous streams" %}">
+    <p>The dashboard has several types of containers for test results. One of the
+    most common and the oldest one is an <em>anonymous stream</em>. Anonymous
+    streams act like public FTP servers. Anyone can download or upload files at
+    will. There are some restrictions, nobody can change or remove existing
+    files</p>
+
+    <p>When a stream is anonymous anonyone can upload new test results and the
+    identity of the uploading user is not recorded in the system. Anonymous
+    streams have to be public (granting read access to everyone)</p>
+
+    <div class="ui-widget">
+      <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
+      <span class="ui-icon ui-icon-info" style="float: left; margin-right: 0.3em;"></span>
+      <strong>Note:</strong> A stream can be marked as anonymous in the administration panel
+      </div>
+    </div>
+  </div>
+  <script type="text/javascript">
+    $(function() {
+        $( "#dialog-message" ).dialog({
+          autoOpen: false,
+          modal: true,
+          minWidth: 500,
+          buttons: {
+            Ok: function() {
+              $( this ).dialog( "close" );
+            }
+          }
+        });
+        $( "#what-are-anonymous-streams" ).click(function(e) {
+          $( "#dialog-message").dialog('open');
+        });
+      });
+  </script>
+  </dd>
+  <dt>{% trans "Read access:" %}</dt>
+  <dd>{% trans "Anyone can download or read test results uploaded here" %}</dd>
+  <dt>{% trans "Write access:" %}</dt>
+  <dd>{% trans "Anyone can upload test results here" %}</dt>
+  {% else %}
+    {% if  bundle_stream.is_public %}
+    <dd>{% trans "Public stream" %}</dd>
+    <dt>{% trans "Read access:" %}</dt>
+    <dd>{% trans "Anyone can download or read test results uploaded here" %}</dd>
+    {% else %}
+    <dd>{% trans "Private stream" %}</dd>
+    <dt>{% trans "Read access:" %}</dt>
+    <dd>{% trans "Only the owner can download or read test results uploaded here" %}</dd>
+    {% endif %}
+    <dt>{% trans "Write access:" %}</dt>
+    <dd>{% trans "Only the owner can upload test results here" %}</dd>
+  {% endif %}
+</dl>

=== added file 'dashboard_app/templates/dashboard_app/_content.html'
--- dashboard_app/templates/dashboard_app/_content.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_content.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,22 @@
+{% extends "layouts/content.html" %}
+
+
+{% block extrahead %}
+{{ block.super }}
+{% include "dashboard_app/_extrahead.html" %}
+{% endblock %}
+
+
+{% block title %}
+{{ block.super }}{% include "dashboard_app/_title.html" %}
+{% endblock %}
+
+
+{% block breadcrumbs %}
+{% include "dashboard_app/_breadcrumbs.html" %}
+{% endblock %}
+
+
+{% block extension_navigation %}
+{% include "dashboard_app/_extension_navigation.html" %}
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/_content_with_sidebar.html'
--- dashboard_app/templates/dashboard_app/_content_with_sidebar.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_content_with_sidebar.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,22 @@
+{% extends "layouts/content_with_sidebar.html" %}
+
+
+{% block extrahead %}
+{{ block.super }}
+{% include "dashboard_app/_extrahead.html" %}
+{% endblock %}
+
+
+{% block title %}
+{{ block.super }}{% include "dashboard_app/_title.html" %}
+{% endblock %}
+
+
+{% block breadcrumbs %}
+{% include "dashboard_app/_breadcrumbs.html" %}
+{% endblock %}
+
+
+{% block extension_navigation %}
+{% include "dashboard_app/_extension_navigation.html" %}
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/_extension_navigation.html'
--- dashboard_app/templates/dashboard_app/_extension_navigation.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_extension_navigation.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,13 @@
+{% load i18n %}
+<div id="lava-server-extension-navigation" class="lava-server-sub-toolbar">
+  <ul>
+    <li><a href="{% url dashboard_app.views.bundle_stream_list %}"
+      >{% trans "Bundle Streams" %}</a></li>
+    <li><a href="{% url dashboard_app.views.test_list %}"
+      >{% trans "Tests" %}</a></li>
+    <li><a href="{% url dashboard_app.views.data_view_list %}"
+      >{% trans "Data Views" %}</a></li>
+    <li><a href="{% url dashboard_app.views.report_list %}"
+      >{% trans "Reports" %}</a></li>
+  </ul>
+</div>

=== added file 'dashboard_app/templates/dashboard_app/_extrahead.html'
--- dashboard_app/templates/dashboard_app/_extrahead.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_extrahead.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,3 @@
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava/css/demo_table_jui.css"/>
+<script type="text/javascript" src="{{ STATIC_URL }}js/FixedHeader.min.js"></script> 
+<script type="text/javascript" src="{{ STATIC_URL }}lava/js/jquery.dataTables.min.js"></script> 

=== added file 'dashboard_app/templates/dashboard_app/_test_run_list_table.html'
--- dashboard_app/templates/dashboard_app/_test_run_list_table.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_test_run_list_table.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,31 @@
+{% load i18n %}
+<table class="demo_jui display" id="test_runs">
+  <thead>
+    <tr>
+      <th>{% trans "Test Run" %}</th>
+      <th>{% trans "Test" %}</th>
+      <th>{% trans "Uploaded On" %} </th>
+      <th>{% trans "Analyzed" %}</th>
+      <th>{% trans "Pass" %}</th>
+      <th>{% trans "Fail" %}</th>
+      <th>{% trans "Skip" %}</th>
+      <th>{% trans "Unknown" %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for test_run in test_run_list %}
+    <tr>
+      <td><a href="{{ test_run.get_absolute_url }}"><code>{{ test_run.analyzer_assigned_uuid }}<code/></a></td>
+      <td><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test }}</a></td>
+      <td>{{ test_run.bundle.uploaded_on }}</td>
+      <td>{{ test_run.analyzer_assigned_date }}</td>
+      {% with test_run.get_summary_results as summary %}
+      <td>{{ summary.pass|default:0 }}</td>
+      <td>{{ summary.fail|default:0 }}</td>
+      <td>{{ summary.skip|default:0 }}</td>
+      <td>{{ summary.unknown|default:0 }}</td>
+      {% endwith %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>

=== added file 'dashboard_app/templates/dashboard_app/_title.html'
--- dashboard_app/templates/dashboard_app/_title.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_title.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,1 @@
+{% for bread_crumb in bread_crumb_trail %} | {{ bread_crumb.get_name }}{% endfor %}

=== modified file 'dashboard_app/templates/dashboard_app/api.html'
--- dashboard_app/templates/dashboard_app/api.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/api.html	2011-07-08 04:19:47 +0000
@@ -1,4 +1,4 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load markup %}
 {% load i18n %}
 
@@ -7,6 +7,7 @@
 {{ block.super }} | {% trans "XML-RPC API" %}
 {% endblock %}
 
+
 {% block extrahead %}
 {{ block.super }}
 <script type="text/javascript" src="{{ STATIC_URL }}lava/js/jquery-1.5.1.min.js"></script>

=== modified file 'dashboard_app/templates/dashboard_app/attachment_detail.html'
--- dashboard_app/templates/dashboard_app/attachment_detail.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/attachment_detail.html	2011-07-12 02:34:12 +0000
@@ -1,51 +1,46 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load i18n %}
 {% load humanize %}
 
 
-{% block title %}
-{{ block.super }} | {% trans "Attachment" %} | {{ attachment.pk }}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-{{ block.super }}
-<li><a href="{{ attachment.get_absolute_url }}">{{ attachment }}</a></li>
-{% endblock %}
-
-
-{% block sidebar %}
-<h3>{% trans "Attachment Information" %}</h3>
-<dl>
-    <dt>{% trans "Pathname" %}</dt>
-    <dd>{{ attachment.content_filename }}</dd>
-    <dt>{% trans "MIME type"%}</dt>
-    <dd>{{ attachment.mime_type }}</dd>
-    <dt>{% trans "Stored in dashboard" %}</dt>
-    <dd>{{ attachment.content|yesno }}</dd>
-    {% if attachment.content %}
-    <dt>{% trans "File size" %}</dt>
-    <dd>{{ attachment.content.size|filesizeformat }}</dd>
-    {% endif %}
-    <dt>{% trans "Stored on 3rd party server" %}</dt>
-    <dd>{{ attachment.public_url|yesno }}</dd>
-    <dt>{% trans "Public URL" %}</dt>
-    <dd><a href="{{ attachment.public_url }}">{{ attachment.public_url }}</a></dd>
-</dl>
-{% endblock %}
-
-
 {% block content %}
-{% if lines %}
-<h3>Inline viewer</h3>
-<ol class="file_listing">
-    {% for line in lines %}
-    <li id="L{{forloop.counter}}">
-        <a href="#L{{forloop.counter}}">{{line}}</a>
-    </li>
-    {% endfor %}
-</ol>
-{% else %}
-<h1>Viewer not available</h1>
-{% endif %}
+<div id="tabs">
+  <ul>
+    <li><a href="#tab-attachment-information">{% trans "Attachment Information" %}</a></li>
+    <li><a href="{% url dashboard_app.views.ajax_attachment_viewer attachment.pk %}">{% trans "Inline Viewer" %}</a></li>
+  </ul>
+  <div id="tab-attachment-information">
+    <dl>
+      <dt>{% trans "Pathname" %}</dt>
+      <dd>{{ attachment.content_filename }}</dd>
+      <dt>{% trans "MIME type"%}</dt>
+      <dd>{{ attachment.mime_type }}</dd>
+      <dt>{% trans "Stored in dashboard" %}</dt>
+      <dd>{{ attachment.content|yesno }}</dd>
+      {% if attachment.content %}
+      <dt>{% trans "File size" %}</dt>
+      <dd>{{ attachment.content.size|filesizeformat }}</dd>
+      {% endif %}
+      <dt>{% trans "Stored on 3rd party server" %}</dt>
+      <dd>{{ attachment.public_url|yesno }}</dd>
+      {% if attachment.public_url %}
+      <dt>{% trans "Public URL" %}</dt>
+      <dd><a href="{{ attachment.public_url }}">{{ attachment.public_url }}</a></dd>
+      {% endif %}
+    </dl>
+  </div>
+</div>
+<script type="text/javascript">
+  $(document).ready(function() {
+    $("#tabs").tabs({
+      ajaxOptions: {
+        dataType: "html",
+        error: function( xhr, status, index, anchor ) {
+          $( anchor.hash ).html(
+          "Couldn't load this tab. We'll try to fix this as soon as possible.");
+        }
+      }
+    });
+  });
+</script>
 {% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/attachment_list.html'
--- dashboard_app/templates/dashboard_app/attachment_list.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/attachment_list.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,40 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+
+
+{% block content %}
+<table id="attachments" class="demo_jui display">
+  <thead>
+    <tr>
+      <th>Name</th>
+      <th>Size</th>
+      <th>MIME type</th>
+      <th>Public URL</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for attachment in attachment_list %}
+    <tr>
+      <td><a href="{{ attachment.get_absolute_url }}"
+          ><code>{{ attachment.content_filename }}</code></a></td>
+      <td>{{ attachment.content.size|filesizeformat }}</td>
+      <td><code>{{ attachment.mime_type }}</code></td>
+      <td>
+        {% if attachment.public_url %}
+        <a href="{{ attachment.public_url }}">public url</a>
+        {% endif %}
+      </td>
+    </tr>
+    {% endfor %}
+  </body>
+</table>
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    $('#attachments').dataTable({
+      bJQueryUI: true,
+      bPaginate: false,
+      aaSorting: [[1, "desc"]],
+    });
+  });
+</script>
+{% endblock %}

=== removed file 'dashboard_app/templates/dashboard_app/base.html'
--- dashboard_app/templates/dashboard_app/base.html	2011-06-29 16:41:01 +0000
+++ dashboard_app/templates/dashboard_app/base.html	1970-01-01 00:00:00 +0000
@@ -1,12 +0,0 @@
-{% extends "base.html" %}
-{% load i18n %}
-
-
-{% block extension_navigation %}
-<ul>
-  <li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-  <li><a href="{% url dashboard_app.views.data_view_list %}">{% trans "Data Views" %}</a></li>
-  <li><a href="{% url dashboard_app.views.report_list %}">{% trans "Reports" %}</a></li>
-  <li><a href="{% url dashboard_app.views.dashboard_xml_rpc_handler %}">{% trans "XML-RPC (dashboard only)" %}</a></li>
-</ul>
-{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/bundle_detail.html'
--- dashboard_app/templates/dashboard_app/bundle_detail.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/bundle_detail.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,68 @@
+{% extends "dashboard_app/_content.html" %}
+{% load humanize %}
+{% load i18n %}
+{% load stylize %}
+
+
+{% block extrahead %}
+{{ block.super }}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/pygments.css"/>
+{% endblock %}
+
+
+{% block content %}
+<script type="text/javascript">
+  $(document).ready(function() {
+    $("#tabs").tabs({
+      cache: true,
+      show: function(event, ui) {
+        var oTable = $('div.dataTables_scrollBody>table.display', ui.panel).dataTable();
+        if ( oTable.length > 0 ) {
+          oTable.fnAdjustColumnSizing();
+        }
+      },
+      ajaxOptions: {
+        dataType: "html",
+        error: function( xhr, status, index, anchor ) {
+          $( anchor.hash ).html(
+          "Couldn't load this tab. We'll try to fix this as soon as possible.");
+        }
+      }
+    });
+    $('#test_runs').dataTable({
+      bJQueryUI: true,
+      sPaginationType: "full_numbers",
+      aaSorting: [[0, "desc"]],
+    });
+  });
+</script>
+<div id="tabs">
+  <ul>
+    {% if bundle.is_deserialized %}
+    <li><a href="#tab-test-runs">{% trans "Test Runs" %}</a></li>
+    {% endif %}
+    {% if bundle.deserialization_error.get %}
+    <li><a href="#tab-deserialization-error">{% trans "Deserialization Error" %}</a></li>
+    {% endif %}
+    <li><a href="{% url dashboard_app.views.ajax_bundle_viewer bundle.pk %}">{% trans "Bundle Viewer" %}</a></li>
+  </ul>
+  {% if bundle.is_deserialized %}
+  <div id="tab-test-runs">
+    {% with bundle.test_runs.all as test_run_list %}
+    {% include "dashboard_app/_test_run_list_table.html" %}
+    {% endwith %}
+  </div>
+  {% endif %}
+
+  {% if bundle.deserialization_error.get %}
+  <div id="tab-deserialization-error">
+    <h3>Cause</h3>
+    <p>{{ bundle.deserialization_error.get.error_message }}</p>
+    <h3>Deserialization failure traceback</h3>
+    <div style="overflow-x: scroll">
+      {% stylize "pytb" %}{{ bundle.deserialization_error.get.traceback|safe }}{% endstylize %}
+    </div>
+  </div>
+  {% endif %}
+</div>
+{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/bundle_list.html'
--- dashboard_app/templates/dashboard_app/bundle_list.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/bundle_list.html	2011-07-12 02:34:12 +0000
@@ -1,51 +1,78 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load i18n %}
 {% load humanize %}
-{% load pagination_tags %}
-
-{% block title %}
-{{ block.super }} | {% trans "Streams" %} | {% trans "Bundle Stream" %} {{ bundle_stream.pathname }} | {% trans "Bundles" %}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-<li><a href="{{ bundle_stream.get_absolute_url }}">{{ bundle_stream }}</a></li>
-<li><a href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}">{% trans "Bundles" %}</a></li>
-{% endblock %}
-
-
-{% block sidebar %}
-{% endblock %}
 
 
 {% block content %}
-
-{% autopaginate bundle_list %}
-{% if invalid_page %}
-  <h3>{% trans "There are no bundles on this page." %}</h3>
-  <p>{% trans "Try the" %} <a href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}">{% trans "first page" %}</a> {% trans "instead." %}</p>
-{% else %}
-  {% if bundle_list.count %}
-  <table class="data">
+<div id="master-toolbar-splice" >
+  <span
+    class="length"
+    style="float:left; display:inline-block; text-align:left;"></span>
+  <span
+    class="view-as"
+    style="text-align:center; display: inline-block; width:auto">
+    <input name="radio" checked type="radio" id="as_bundles"/>
+    <label for="as_bundles">
+      <a
+        id="as_bundles_link"
+        href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}"
+        >{% trans "Bundles" %}</a>
+    </label>
+    <input name="radio" type="radio" id="as_test_runs"/>
+    <label for="as_test_runs">
+      <a
+        id="as_test_runs_link"
+        href="{% url dashboard_app.views.test_run_list bundle_stream.pathname %}"
+        >{% trans "Test Runs" %}</a>
+    </label>
+  </span>
+  <span
+    class="search"
+    style="float:right; display:inline-block; text-align:right"></span>
+</div>
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#bundles').dataTable({
+      bJQueryUI: true,
+      sPaginationType: "full_numbers",
+      aaSorting: [[1, "desc"]],
+      iDisplayLength: 25,
+      aLengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
+      sDom: 'lfr<"#master-toolbar">t<"F"ip>'
+    });
+    // Try hard to make our radio boxes behave as links
+    $("#master-toolbar-splice span.view-as").buttonset();
+    $("#master-toolbar-splice span.view-as input").change(function(event) {
+      var link = $("#" + event.target.id + "_link");
+      location.href=link.attr("href");
+    });
+    // Insane splicing below
+    $("div.dataTables_length").children().appendTo("#master-toolbar-splice span.length");
+    $("div.dataTables_filter").children().appendTo("#master-toolbar-splice span.search");
+    $("#master-toolbar-splice").children().appendTo($("#master-toolbar"));
+    $("div.dataTables_length").remove();
+    $("div.dataTables_filter").remove();
+    $("#master-toolbar-splice").remove();
+    $("#master-toolbar").addClass("ui-widget-header ui-corner-tl ui-corner-tr").css(
+      "padding", "5pt").css("text-align", "center");
+    new FixedHeader(oTable);
+  });
+</script> 
+<table class="demo_jui display" id="bundles">
+  <thead>
     <tr>
-      <th>
-        {% trans "Uploaded On" %}
-        <div style="font-size:smaller">
-          {% trans "most recent first" %}
-        </div>
-      </th>
-      <th>{% trans "Uploaded by" %}</th>
-      <th>{% trans "Content filename" %}</th>
-      <th>{% trans "Content SHA1" %}</th>
-      <th>{% trans "Deserialized" %}</th>
+      <th>{% trans "Bundle SHA1" %}</th>
+      <th>{% trans "Uploaded On" %}</th>
+      <th>{% trans "Uploaded By" %}</th>
+      <th>{% trans "Deserialized?" %}</th>
+      <th>{% trans "Problems?" %}</th>
     </tr>
+  </thead>
+  <tbody>
     {% for bundle in bundle_list %}
     <tr>
-      <td>
-        {{ bundle.uploaded_on|naturalday }}
-        {{ bundle.uploaded_on|time }}
-      </td>
+      <td><a href="{{ bundle.get_absolute_url }}"><code>{{ bundle.content_sha1 }}</code></a></td>
+      <td>{{ bundle.uploaded_on }}</td>
       <td>
         {% if bundle.uploaded_by %}
         {{ bundle.uploaded_by }}
@@ -53,16 +80,15 @@
         <em>{% trans "anonymous user" %}</em>
         {% endif %}
       </td>
-      <td>{{ bundle.content_filename }}</td>
-      <td>{{ bundle.content_sha1 }}</td>
       <td>{{ bundle.is_deserialized|yesno }}</td>
+      <td>{% if bundle.deserialization_error.get %}yes{% endif %}</td>
     </tr>
     {% endfor %}
-  </table>
-  {% else %}
-  <p>{% trans "There are no bundles in this stream yet." %}</p>
-  {% endif %}
-{% paginate %}
-{% endif %}
-
+  </tbody>
+</table>
+{% endblock %}
+
+
+{% block sidebar %}
+{% include "dashboard_app/_bundle_stream_sidebar.html" %}
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/bundle_stream_list.html'
--- dashboard_app/templates/dashboard_app/bundle_stream_list.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/bundle_stream_list.html	2011-07-12 02:34:12 +0000
@@ -1,15 +1,38 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load i18n %}
 {% load pagination_tags %}
 
 
-{% block title %}
-{{ block.super }} | {% trans "Streams" %}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
+{% block content %}
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#bundle_streams').dataTable({
+      bJQueryUI: true,
+      sPaginationType: "full_numbers",
+      iDisplayLength: 25,
+    });
+  });
+</script> 
+<table class="demo_jui display" id="bundle_streams">
+  <thead>
+    <tr>
+      <th width="50%">Pathname</th>
+      <th>Name</th>
+      <th>Number of test runs</th>
+      <th>Number of bundles</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for bundle_stream in bundle_stream_list %}
+    <tr>
+      <td style="vertical-align: top;"><a href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}"><code>{{ bundle_stream.pathname }}</code></a></td>
+      <td>{{ bundle_stream.name|default:"<i>not set</i>" }}</td>
+      <td style="vertical-align: top;"><a href="{% url dashboard_app.views.test_run_list bundle_stream.pathname %}">{{ bundle_stream.get_test_run_count }}</a></td>
+      <td style="vertical-align: top;"><a href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}">{{ bundle_stream.bundles.count}}</a></td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
 {% endblock %}
 
 
@@ -34,34 +57,3 @@
 <p>{% trans "You must" %} <a href="{% url django.contrib.auth.views.login %}">{% trans "sign in" %}</a> {% trans "to get more access" %}</p>
 {% endif %}
 {% endblock %}
-
-
-{% block content %}
-{% autopaginate bundle_stream_list %}
-{% if bundle_stream_list.count %}
-<table class="data">
-  <tr>
-    <th>Pathname</th>
-    <th>Name</th>
-    <th>Number of test runs</th>
-    <th>Number of bundles</th>
-  </tr>
-  {% for bundle_stream in bundle_stream_list %}
-  <tr>
-    <td>{{ bundle_stream.pathname }}</td>
-    <td>{{ bundle_stream.name|default:"<i>not set</i>" }}</td>
-    <td><a href="{% url dashboard_app.views.test_run_list bundle_stream.pathname %}">{{ bundle_stream.get_test_run_count }}</a></td>
-    <td><a href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}">{{ bundle_stream.bundles.count}}</a></td>
-  </tr>
-  {% endfor %}
-</table>
-{% else %}
-{% if user.is_staff %}
-<p>There are no streams yet, you can create one in the <a
-  href="{% url admin:dashboard_app_bundlestream_add %}">admin</a> panel.</p>
-{% else %}
-<p>There are no streams yet.</p>
-{% endif %}
-{% endif %}
-{% paginate %}
-{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/data_view_detail.html'
--- dashboard_app/templates/dashboard_app/data_view_detail.html	2011-06-29 16:41:01 +0000
+++ dashboard_app/templates/dashboard_app/data_view_detail.html	2011-07-12 02:34:12 +0000
@@ -1,16 +1,6 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load i18n %}
 
-{% block title %}
-{{ block.super }} | {% trans "Data Views" %} | {{ data_view.name }}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.data_view_list %}">{% trans "Data Views" %}</a></li>
-<li><a href="{% url dashboard_app.views.data_view_detail data_view.name %}">{{ data_view.name }}</a></li>
-{% endblock %}
-
 
 {% block content %}
 <dl>
@@ -21,6 +11,7 @@
   <dt>Documentation:</dt>
   <dd>{{ data_view.documentation }}</dd>
 </dl>
+{% if data_view.arguments %}
 <table class="data">
   <caption>Aruments</caption>
   <tr>
@@ -38,4 +29,5 @@
   </tr>
   {% endfor %}
 </table>
+{% endif %}
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/data_view_list.html'
--- dashboard_app/templates/dashboard_app/data_view_list.html	2011-06-29 16:41:01 +0000
+++ dashboard_app/templates/dashboard_app/data_view_list.html	2011-07-12 02:34:12 +0000
@@ -1,34 +1,43 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load i18n %}
 
-{% block title %}
-{{ block.super }} | {% trans "Data Views" %}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.data_view_list %}">{% trans "Data Views" %}</a></li>
-{% endblock %}
-
-
-{% block sidebar %}
-<h3>Hint:</h3>
-<p>To call a data view use the <code>lava-dashboard-tool</code> command. See
-<code>lava-dashboard-tool query-data-view --help</code> to get started.</p>
-{% endblock %}
-
 
 {% block content %}
-<table class="data">
-  <tr>
-    <th>{% trans "Name" %}</th>
-    <th>{% trans "Summary" %}</th>
-  </tr>
-  {% for data_view in data_view_list %}
-  <tr>
-    <td><a href="{% url dashboard_app.views.data_view_detail data_view.name %}">{{ data_view.name }}</a></td>
-    <td>{{ data_view.summary }}</td>
-  </tr>
-{% endfor %}
+<div class="ui-widget">
+  <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
+    <span
+      class="ui-icon ui-icon-info"
+      style="float: left; margin-right: 0.3em;"></span>
+    <strong>Hint:</strong> To call a data view use the
+    <code>lava-dashboard-tool</code> command. See 
+    <code>lava-dashboard-tool query-data-view --help</code>
+    to get started.
+  </div>
+</div>
+<br/>
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#data_views').dataTable({
+      "bJQueryUI": true,
+      "sPaginationType": "full_numbers",
+      "aaSorting": [[0, "desc"]],
+    });
+  });
+</script> 
+<table class="demo_jui display" id="data_views">
+  <thead>
+    <tr>
+      <th>{% trans "Name" %}</th>
+      <th>{% trans "Summary" %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for data_view in data_view_list %}
+    <tr>
+      <td><a href="{% url dashboard_app.views.data_view_detail data_view.name %}">{{ data_view.name }}</a></td>
+      <td>{{ data_view.summary }}</td>
+    </tr>
+    {% endfor %}
+  </tbody>
 </table>
 {% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/index.html'
--- dashboard_app/templates/dashboard_app/index.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/index.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,10 @@
+{% extends "dashboard_app/_content.html" %}
+
+{% block content %}
+<h1>TODO</h1>
+<ul>
+  <li>Briefly mention key dashboard features</li>
+  <li>Link to readthedocs dashboard manual</li>
+  <li>Add sensible dashboard index page (recent/interesting stuff)</li>
+</ul>
+{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/report_detail.html'
--- dashboard_app/templates/dashboard_app/report_detail.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/report_detail.html	2011-07-12 02:34:12 +0000
@@ -1,37 +1,31 @@
-{% extends "dashboard_app/base.html" %}
-
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load i18n %}
 {% load stylize %}
 
-{% block title %}
-{{ block.super }} | {% trans "Reports" %} | {{ report.title }}
-{% endblock %}
+
 
 {% block extrahead %}
 {{ block.super }}
 <!--[if IE]><script type="text/javascript" src="{{ STATIC_URL }}js/excanvas.min.js"></script><![endif]-->
-<script type="text/javascript" src="{{ STATIC_URL }}lava/js/jquery-1.5.1.min.js"></script>
-<script type="text/javascript" src="{{ STATIC_URL }}lava/js/jquery-ui-1.8.12.custom.min.js"></script>
 <script type="text/javascript" src="{{ STATIC_URL }}js/jquery.rpc.js"></script>
 <script type="text/javascript" src="{{ STATIC_URL }}js/jquery.flot.min.js"></script>
 <script type="text/javascript" src="{{ STATIC_URL }}js/jquery.dashboard.js"></script>
-<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava/css/Aristo/jquery-ui-1.8.7.custom.css"/>
 <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/pygments.css"/>
 {% endblock %}
 
 
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.report_list %}">{% trans "Reports" %}</a></li>
-<li><a href="{{ report.get_absolute_url }}">{{ report.title }}</a></li>
+{% block content %}
+{{ report.get_html|safe}}
 {% endblock %}
 
+
 {% block sidebar %}
 <h3>Basic information</h3>
 <dl>
   <dt>Title</dt>
   <dd>{{report.title}}</dd>
   <dt>Author</dt>
-  <dd>{{report.author|default:"Unspecified"}}</dd>
+  <dd>{{report.author|default_if_none:"Unspecified"}}</dd>
   <dt>Bug report URL</dt>
   <dd>{{report.bug_report_url|default:"Unspecified"}}</dd>
 </dl>
@@ -42,6 +36,9 @@
 maintainer to debug the problem.</p> 
 <h3>Source Code</h3>
 <button id="show-source">Show source</button>
+<div id="report-source" style="display: none">
+  {% stylize "html" %}{{ report.get_html }}{% endstylize %}
+</div>
 <script type="text/javascript">
   $(function() {
     $("#report-source").dialog({ autoOpen: false, modal: true, width: "auto", title: "Source code"});
@@ -51,11 +48,3 @@
   });
 </script>
 {% endblock %}
-
-
-{% block content %}
-{{ report.get_html|safe}}
-<div id="report-source" style="display: none">
-  {% stylize "html" %}{{ report.get_html }}{% endstylize %}
-</div>
-{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/report_list.html'
--- dashboard_app/templates/dashboard_app/report_list.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/report_list.html	2011-07-12 02:34:12 +0000
@@ -1,27 +1,31 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load i18n %}
 
-{% block title %}
-{{ block.super }} | {% trans "Reports" %}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.report_list %}">{% trans "Reports" %}</a></li>
-{% endblock %}
-
 
 {% block content %}
-<table class="data">
-  <tr>
-    <th>{% trans "Report title" %}</th>
-    <th>{% trans "Path of HTML file" %}</th>
-  </tr>
-  {% for report in report_list %}
-  <tr>
-    <td><a href="{{ report.get_absolute_url }}">{{ report.title }}</a></td>
-    <td><pre>{{ report.path }}</pre></td>
-  </tr>
-{% endfor %}
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#reports').dataTable({
+      "bJQueryUI": true,
+      "sPaginationType": "full_numbers",
+      "aaSorting": [[0, "desc"]],
+    });
+  });
+</script> 
+<table class="demo_jui display" id="reports">
+  <thead>
+    <tr>
+      <th>{% trans "Report title" %}</th>
+      <th>{% trans "Author" %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for report in report_list %}
+    <tr>
+      <td><a href="{{ report.get_absolute_url }}">{{ report.title }}</a></td>
+      <td>{{ report.author|default_if_none:"<em>unspecified</em>" }}</td>
+    </tr>
+    {% endfor %}
+  </tbody>
 </table>
 {% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/test_detail.html'
--- dashboard_app/templates/dashboard_app/test_detail.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/test_detail.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,46 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+
+
+{% block content %}
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#test_cases').dataTable({
+      bJQueryUI: true,
+      sPaginationType: "full_numbers",
+      aLengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
+    });
+  });
+</script> 
+<table class="demo_jui display" id="test_cases">
+  <thead>
+    <tr>
+      <th>ID</th>
+      <th>Name</th>
+      <th>Units</th>
+      <th>Total Results</th>
+      <th>Total Failures</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for test_case in test.test_cases.all %}
+    <tr>
+      <td>{{ test_case.test_case_id }}</td>
+      <td>{{ test_case.name|default:"<i>not set</i>" }}</td>
+      <td>{{ test_case.units|default:"<i>not set</i>" }}</td>
+      <td>{{ test_case.test_results.all.count }}</td>
+      <td>{{ test_case.count_failures }}</td>
+    </tr>
+    {% endfor %}
+    {% if test.count_results_without_test_case %}
+    <tr>
+      <td><em>Results without test case</em></td>
+      <td><em>N/A</em></td>
+      <td><em>N/A</em></td>
+      <td>{{ test.count_results_without_test_case }}</td>
+      <td>{{ test.count_failures_without_test_case }}</td>
+    </tr>
+    {% endif %}
+  </tbody>
+</table>
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/test_list.html'
--- dashboard_app/templates/dashboard_app/test_list.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/test_list.html	2011-07-12 02:34:12 +0000
@@ -0,0 +1,34 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+
+
+{% block content %}
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#tests').dataTable({
+      "bJQueryUI": true,
+      "bPaginate": false,
+    });
+  });
+</script> 
+<table class="demo_jui display" id="tests">
+  <thead>
+    <tr>
+      <th>ID</th>
+      <th>Name</th>
+      <th>Test Cases</th>
+      <th>Test Runs</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for test in test_list %}
+    <tr>
+      <td><a href="{{ test.get_absolute_url }}">{{ test.test_id }}</a></td>
+      <td>{{ test.name|default:"<i>not set</i>" }}</td>
+      <td>{{ test.test_cases.all.count }}</td>
+      <td>{{ test.test_runs.all.count }}</td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_result_detail.html'
--- dashboard_app/templates/dashboard_app/test_result_detail.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/test_result_detail.html	2011-07-12 02:34:12 +0000
@@ -1,20 +1,6 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load i18n %}
 {% load humanize %}
-{% load pagination_tags %}
-
-
-{% block title %}
-{{ block.super }} | {% trans "Test Results" %} | {{ test_result }}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-<li><a href="{{ test_result.test_run.bundle.bundle_stream.get_absolute_url }}">{{ test_result.test_run.bundle.bundle_stream }}</a></li>
-<li><a href="{{ test_result.test_run.get_absolute_url }}">{{ test_result.test_run }}</a></li>
-<li><a href="{{ test_result.get_absolute_url }}">{{ test_result }}</a></li>
-{% endblock %}
 
 
 {% block sidebar %}
@@ -30,26 +16,27 @@
 {% if test_result.test_run.test_results.count > 1 %}
 <h3>Other results</h3>
 <p class="hint">Results from the same test run are available here</p>
-<ul>
-{% for another_test_result in test_result.test_run.test_results.all %}
-{% if another_test_result.relative_index != test_result.relative_index %}
-<li><a 
-  href="{{ another_test_result.get_absolute_url }}"
-  >Result {{ another_test_result.relative_index }}
-  {% if another_test_result.test_case %}
-  ({{another_test_result.test_case}})
-  {% endif %}
-  </a></li>
-{% else %}
-<li><b>
-  Result {{ another_test_result.relative_index }}
-  {% if another_test_result.test_case %}
-  ({{another_test_result.test_case}})
-  {% endif %}
-</b></li>
-{% endif %}
-{% endfor %}
-</ul>
+<select id="other_results">
+  {% regroup test_result.test_run.test_results.all by test_case as test_result_group_list %}
+  {% for test_result_group in test_result_group_list %}
+  {% if test_result_group.list|length > 1 %}<optgroup label="Results for test case {{ test_result_group.grouper }}">{% endif %}
+    {% for other_test_result in test_result_group.list %}
+    <option value="{{ other_test_result.get_absolute_url }}"
+    {% if other_test_result.pk == test_result.pk %}disabled selected{% endif %}
+    >
+      Result #{{ other_test_result.relative_index }}: {{ other_test_result.get_result_display }}
+      {% if test_result_group.list|length == 1 %} from test case {{ test_result_group.grouper }}{% endif %}
+      {% if other_test_result.measurement != None %} ({{ other_test_result.measurement }}){% endif %}
+    </option>
+    {% endfor %}
+  {% if test_result_group.list|length > 1 %}</optgroup>{% endif %}
+  {% endfor %}
+</select>
+<script type="text/javascript">
+  $("#other_results").change(function (event) {
+    location.href=$(this).val();
+  });
+</script>
 {% endif %}
 {% endblock %}
 

=== modified file 'dashboard_app/templates/dashboard_app/test_run_detail.html'
--- dashboard_app/templates/dashboard_app/test_run_detail.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/test_run_detail.html	2011-07-12 02:34:12 +0000
@@ -1,19 +1,43 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load i18n %}
 {% load humanize %}
-{% load pagination_tags %}
-
-
-{% block title %}
-{{ block.super }} | {% trans "Test Runs" %} | {{ test_run }}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-<li><a href="{{ test_run.bundle.bundle_stream.get_absolute_url }}">{{ test_run.bundle.bundle_stream }}</a></li>
-<li><a href="{{ test_run.get_absolute_url }}">{{ test_run }}</a></li>
-{% endblock %}
+
+
+{% block content %}
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#test_results').dataTable({
+      "bJQueryUI": true,
+      "sPaginationType": "full_numbers",
+      "aaSorting": [[0, "asc"]],
+    });
+  });
+</script> 
+<table class="demo_jui display" id="test_results">
+  <thead>
+    <tr>
+      <th>#</th>
+      <th>{% trans "Test case" %}</th>
+      <th>{% trans "Result" %}</th>
+      <th>{% trans "Measurement" %}</th>
+    </tr>
+    <tbody>
+      {% for test_result in test_run.test_results.all %}
+      <tr>
+        <td width="1%">{{ test_result.relative_index }}</td>
+        <td>{{ test_result.test_case|default_if_none:"<em>Not specified</em>" }}</td>
+        <td>
+          <a href ="{{test_result.get_absolute_url}}">
+            <img src="{{ STATIC_URL }}images/icon-{{ test_result.result_code }}.png"
+            alt="{{ test_result.get_result_display }}" width="16" height="16" border="0"/></a>
+          <a href ="{{test_result.get_absolute_url}}">{{ test_result.get_result_display }}</a>
+        </td>
+        <td>{{ test_result.measurement|default_if_none:"Not specified" }} {{ test_result.units }}</td>
+      </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+  {% endblock %}
 
 
 {% block sidebar %}
@@ -24,8 +48,8 @@
   <dd>{{ test_run.test }}</dd>
   <dt>{% trans "OS Distribution" %}</dt>
   <dd>{{ test_run.sw_image_desc|default:"<i>Unspecified</i>" }}</dd>
-  <dt>{% trans "Bundle information" %}</dt>
-  <dd>{{ test_run.bundle.content_sha1 }}</dd>
+  <dt>{% trans "Bundle SHA1" %}</dt>
+  <dd><a href="{{ test_run.bundle.get_absolute_url }}">{{ test_run.bundle.content_sha1 }}</a></dd>
   <dt>{% trans "Time check performed" %}</dt>
   <dd>{{ test_run.time_check_performed|yesno }}</dd>
   <dt>{% trans "Log analyzed on:" %}</dt>
@@ -38,51 +62,16 @@
   {{ test_run.import_assigned_date|naturalday }}
   {{ test_run.import_assigned_date|time }}
   </dd>
-  <dt>{% trans "Software Context" %}</dt>
-  <dd>
-  <a
-    href="{% url dashboard_app.views.test_run_software_context test_run.analyzer_assigned_uuid %}"
-    >{% trans "more" %}</a>
-  </dd>
-  <dt>{% trans "Hardware Context" %}</dt>
-  <dd>
-  <a 
-    href="{% url dashboard_app.views.test_run_hardware_context test_run.analyzer_assigned_uuid %}"
-    >{% trans "more" %}</a>
+  <dt>{% trans "Other information:" %}</dt>
+  <dd><a 
+    href="{% url dashboard_app.views.test_run_hardware_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
+    >{% trans "Hardware context" %} ({{ test_run.devices.all.count }} devices)</a></dd>
+  <dd><a 
+    href="{% url dashboard_app.views.test_run_software_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
+    >{% trans "Software context" %} ({{ test_run.packages.all.count }} packages, {{ test_run.sources.all.count }} sources)</a></dd>
+  <dd><a 
+    href="{% url dashboard_app.views.attachment_list test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
+    >{% trans "Attachments" %} ({{ test_run.attachments.count }})</a></dd>
   </dd>
 </dl>
 {% endblock %}
-
-
-{% block content %}
-{% autopaginate test_run.test_results.all 50 as test_results %}
-{% if invalid_page %}
-<h3>{% trans "There is no content on this page." %}</h3>
-<p>{% trans "Try the" %} <a href="{{ test_run.get_absolute_url }}">{% trans "first page" %}</a> {% trans "instead." %}</p>
-{% else %}
-<table class="data">
-  <tr>
-    <th>#</th>
-    <th>{% trans "Test case" %}</th>
-    <th>{% trans "Result" %}</th>
-    <th>{% trans "Measurement" %}</th>
-  </tr>
-  {% for test_result in test_results %}
-  <tr>
-    <td width="1%">{{ test_result.relative_index }}</td>
-    <td>{{ test_result.test_case }}</td>
-    <td>
-      <a href ="{{test_result.get_absolute_url}}">
-      <img src="{{ STATIC_URL }}images/icon-{{ test_result.result_code }}.png"
-      alt="{{ test_result.get_result_display }}" width="16" height="16" border="0"/></a>
-      <a href ="{{test_result.get_absolute_url}}">{{ test_result.get_result_display }}</a>
-    </td>
-    {% if test_result.measurement %}
-    <td>{{ test_result.measurement|default_if_none:"" }} {{ test_result.units }}</td>
-    {% endif %}
-  </tr>
-  {% endfor %}
-</table>
-{% paginate %}
-{% endif %}
-{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_run_hardware_context.html'
--- dashboard_app/templates/dashboard_app/test_run_hardware_context.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/test_run_hardware_context.html	2011-07-12 02:34:12 +0000
@@ -1,45 +1,79 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load i18n %}
-{% load humanize %}
-{% load pagination_tags %}
-
-
-{% block title %}
-{{ block.super }} | {% trans "Test Runs" %} | {{ test_run }} | {% trans "Hardware Context" %}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-<li><a href="{{ test_run.bundle.bundle_stream.get_absolute_url }}">{{ test_run.bundle.bundle_stream }}</a></li>
-<li><a href="{{ test_run.get_absolute_url }}">{{ test_run }}</a></li>
-<li><a href="{% url dashboard_app.views.test_run_hardware_context test_run.analyzer_assigned_uuid %}">{% trans "Hardware Context" %}</a></li>
-{% endblock %}
-
-
-{% block sidebar %}
-{% endblock %}
 
 
 {% block content %}
-<h2>Hardware Devices</h2>
-<dl>
-{% autopaginate test_run.devices.all as hardware_devices %}
-{% for hardware_device in hardware_devices %}
-<dt>{{ hardware_device.description }}</dt>
-<dd>
-<table>
-  {% for attribute in hardware_device.attributes.all %}
-  <tr>
-    <th>{{ attribute.name }}</th>
-    <td>{{ attribute.value }}</td>
-  </tr>
-  {% endfor %}
+<table class="demo_jui display" id="hardware_devices">
+  <thead>
+    <tr>
+      <th>Description</th>
+      <th>Device Type</th>
+      <th>Attributes</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for hardware_device in test_run.devices.all %} 
+    <tr>
+      <td>{{ hardware_device.description }}</td>
+      <td>{{ hardware_device.get_device_type_display }}</td>
+      <td>
+        <dl>
+          {% for attribute in hardware_device.attributes.all %}
+          <dt>{{ attribute.name }}</dt>
+          <dd>{{ attribute.value }}</dd>
+          {% endfor %}
+        </dl>
+      </td>
+    </tr>
+    {% endfor %}
+  </tbody>
 </table>
-</dd>
-{% empty %}
-<em>There are no hardware devices associated with this test run</em>
-{% endfor %}
-</dl>
-{% paginate %}
+<script type="text/javascript" charset="utf-8"> 
+  function fnFormatDetails(oTable, nTr) {
+    var aData = oTable.fnGetData(nTr);
+    return aData[3];
+  }
+
+  $(document).ready(function() {
+    /* Insert a 'details' column to the table */
+    var nCloneTh = document.createElement('th');
+    var nCloneTd = document.createElement('td');
+    nCloneTd.innerHTML = '<img src="{{ STATIC_URL }}images/details_open.png">';
+    nCloneTd.className = "center";
+
+    $('#hardware_devices thead tr').each( function () {
+      this.insertBefore(nCloneTh, this.childNodes[0]);
+    });
+
+    $('#hardware_devices tbody tr').each( function () {
+      this.insertBefore(nCloneTd.cloneNode(true), this.childNodes[0]);
+    });
+
+    /* Initialse DataTables, with no sorting on the 'details' column */
+    var oTable = $('#hardware_devices').dataTable({
+      aoColumnDefs: [
+        { bSortable: false, aTargets: [0] },
+        { bVisible: false, aTargets: [3] }
+      ],
+      aaSorting: [[2, 'asc']],
+      bPaginate: false,
+      bJQueryUI: true
+    });
+    /* Add event listener for opening and closing details. Note that the
+    indicator for showing which row is open is not controlled by DataTables,
+    rather it is done here */
+    $('#hardware_devices tbody td img').live('click', function () {
+      var nTr = this.parentNode.parentNode;
+      if (this.src.match('details_close')) {
+        /* This row is already open - close it */
+        this.src = "{{ STATIC_URL }}images/details_open.png";
+        oTable.fnClose(nTr);
+      } else {
+        /* Open this row */
+        this.src = "{{ STATIC_URL }}images/details_close.png";
+        oTable.fnOpen(nTr, fnFormatDetails(oTable, nTr), 'details');
+      }
+    });
+  });
+</script> 
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_run_list.html'
--- dashboard_app/templates/dashboard_app/test_run_list.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/test_run_list.html	2011-07-12 02:34:12 +0000
@@ -1,152 +1,65 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load i18n %}
 {% load humanize %}
-{% load pagination_tags %}
-
-{% block extrahead %}
-{{ block.super }}
-<script type="text/javascript" src="{{ STATIC_URL }}lava/js/jquery-1.5.1.min.js"></script>
-<script type="text/javascript" src="{{ STATIC_URL }}lava/js/jquery-ui-1.8.12.custom.min.js"></script>
-<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava/css/Aristo/jquery-ui-1.8.7.custom.css"/>
-{% endblock %}
-
-{% block title %}
-{{ block.super }} | {% trans "Streams" %} | {% trans "Bundle Stream" %} {{ bundle_stream.pathname }}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-<li><a href="{{ bundle_stream.get_absolute_url }}">{{ bundle_stream }}</a></li>
+
+
+{% block content %}
+<div id="master-toolbar-splice" >
+  <span
+    class="length"
+    style="float:left; display:inline-block; width:30%; text-align:left;"></span>
+  <span class="view-as">
+    <input name="radio" type="radio" id="as_bundles"/>
+    <label for="as_bundles">
+      <a
+        id="as_bundles_link"
+        href="{% url dashboard_app.views.bundle_list bundle_stream.pathname %}"
+        >{% trans "Bundles" %}</a>
+    </label>
+    <input name="radio" checked type="radio" id="as_test_runs"/>
+    <label for="as_test_runs">
+      <a
+        id="as_test_runs_link"
+        href="{% url dashboard_app.views.test_run_list bundle_stream.pathname %}"
+        >{% trans "Test Runs" %}</a>
+    </label>
+  </span>
+  <span
+    class="search"
+    style="float:right; display:inline-block; width: 30%; text-align:right"></span>
+</div>
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#test_runs').dataTable({
+      bJQueryUI: true,
+      sPaginationType: "full_numbers",
+      aaSorting: [[1, "desc"]],
+      iDisplayLength: 25,
+      aLengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
+      sDom: 'lfr<"#master-toolbar">t<"F"ip>'
+    });
+    // Try hard to make our radio boxes behave as links
+    $("#master-toolbar-splice span.view-as").buttonset();
+    $("#master-toolbar-splice span.view-as input").change(function(event) {
+      var link = $("#" + event.target.id + "_link");
+      location.href=link.attr("href");
+    });
+    // Insane splicing below
+    $("div.dataTables_length").children().appendTo("#master-toolbar-splice span.length");
+    $("div.dataTables_filter").children().appendTo("#master-toolbar-splice span.search");
+    $("#master-toolbar-splice").children().appendTo($("#master-toolbar"));
+    $("div.dataTables_length").remove();
+    $("div.dataTables_filter").remove();
+    $("#master-toolbar-splice").remove();
+    $("#master-toolbar").addClass("ui-widget-header ui-corner-tl ui-corner-tr").css(
+      "padding", "5pt").css("text-align", "center");
+    new FixedHeader(oTable);
+  });
+</script> 
+{% include "dashboard_app/_test_run_list_table.html" %}
 {% endblock %}
 
 
 {% block sidebar %}
-<h3>{% trans "About" %}</h3>
-<dl>
-  <dt>{% trans "Pathname:" %}</dt>
-  <dd>{{ bundle_stream.pathname }}</dd>
-  <dt>{% trans "Name:" %}</dt>
-  <dd>{{ bundle_stream.name|default:"<i>not set</i>" }}</dd>
-</dl>
-<h3>{% trans "Ownership" %}</h3>
-{% if bundle_stream.user %}
-<p>{% trans "This stream is owned by" %} <q>{{ bundle_stream.user }}</q></p>
-{% else %}
-<p>{% trans "This stream is owned by group called" %} <q>{{ bundle_stream.group }}</q></p>
-{% endif %}
-<h3>{% trans "Access rights" %}</h3>
-<dl>
-  <dt>{% trans "Stream type:" %}</dt>
-  {% if bundle_stream.is_anonymous %}
-  <dd>
-  Anonymous stream <a href="#" id="what-are-anonymous-streams">(what is this?)</a>
-  <div id="dialog-message" title="{% trans "About anonymous streams" %}">
-    <p>The dashboard has several types of containers for test results. One of the
-    most common and the oldest one is an <em>anonymous stream</em>. Anonymous
-    streams act like public FTP servers. Anyone can download or upload files at
-    will. There are some restrictions, nobody can change or remove existing
-    files</p>
-
-    <p>When a stream is anonymous anonyone can upload new test results and the
-    identity of the uploading user is not recorded in the system. Anonymous
-    streams have to be public (granting read access to everyone)</p>
-
-    <div class="ui-widget">
-      <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
-      <span class="ui-icon ui-icon-info" style="float: left; margin-right: 0.3em;"></span>
-      <strong>Note:</strong> A stream can be marked as anonymous in the administration panel
-      </div>
-    </div>
-  </div>
-  <script type="text/javascript">
-    $(function() {
-        $( "#dialog-message" ).dialog({
-          autoOpen: false,
-          modal: true,
-          minWidth: 500,
-          buttons: {
-            Ok: function() {
-              $( this ).dialog( "close" );
-            }
-          }
-        });
-        $( "#what-are-anonymous-streams" ).click(function(e) {
-          $( "#dialog-message").dialog('open');
-        });
-      });
-  </script>
-  </dd>
-  <dt>{% trans "Read access:" %}</dt>
-  <dd>{% trans "Anyone can download or read test results uploaded here" %}</dd>
-  <dt>{% trans "Write access:" %}</dt>
-  <dd>{% trans "Anyone can upload test results here" %}</dt>
-  {% else %}
-    {% if  bundle_stream.is_public %}
-    <dd>{% trans "Public stream" %}</dd>
-    <dt>{% trans "Read access:" %}</dt>
-    <dd>{% trans "Anyone can download or read test results uploaded here" %}</dd>
-    {% else %}
-    <dd>{% trans "Private stream" %}</dd>
-    <dt>{% trans "Read access:" %}</dt>
-    <dd>{% trans "Only the owner can download or read test results uploaded here" %}</dd>
-    {% endif %}
-    <dt>{% trans "Write access:" %}</dt>
-    <dd>{% trans "Only the owner can upload test results here" %}</dd>
-  {% endif %}
-</dl>
-{% endblock %}
-
-
-{% block content %}
-
-{% autopaginate test_run_list %}
-{% if invalid_page %}
-  <h3>{% trans "There are no test runs on this page." %}</h3>
-  <p>{% trans "Try the" %} <a href="{{ bundle_stream.get_absolute_url }}">{% trans "first page" %}</a> {% trans "instead." %}</p>
-{% else %}
-  {% if test_run_list.count %}
-  <table class="data">
-    <tr>
-      <th>
-        {% trans "Uploaded On" %}
-        <div style="font-size:smaller">
-          {% trans "most recent first" %}
-        </div>
-      </th>
-      <th>{% trans "Analyzed" %}</th>
-      <th>{% trans "Test" %}</th>
-      <th>{% trans "Run" %}</th>
-      <th>{% trans "Pass" %}</th>
-      <th>{% trans "Fail" %}</th>
-      <th>{% trans "Skip" %}</th>
-      <th>{% trans "Unknown" %}</th>
-    </tr>
-    {% for test_run in test_run_list %}
-    <tr>
-      <td>
-        {{ test_run.bundle.uploaded_on|naturalday }}
-        {{ test_run.bundle.uploaded_on|time }}
-      </td>
-      <td>
-        {{ test_run.analyzer_assigned_date|timesince }}
-        {% trans "ago" %}
-      </td>
-      <td>{{ test_run.test }}</td>
-      <td><a href="{{ test_run.get_absolute_url }}">{{ test_run }}</a></td>
-      {% with test_run.get_summary_results as summary %}
-      <td>{{ summary.pass|default:0 }}</td>
-      <td>{{ summary.fail|default:0 }}</td>
-      <td>{{ summary.skip|default:0 }}</td>
-      <td>{{ summary.unknown|default:0 }}</td>
-      {% endwith %}
-    </tr>
-    {% endfor %}
-  </table>
-  {% else %}
-  <p>{% trans "There are no test runs in this stream yet." %}</p>
-  {% endif %}
-{% paginate %}
-{% endif %}
-
+{% include "dashboard_app/_bundle_stream_sidebar.html" %}
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_run_software_context.html'
--- dashboard_app/templates/dashboard_app/test_run_software_context.html	2011-06-29 14:00:01 +0000
+++ dashboard_app/templates/dashboard_app/test_run_software_context.html	2011-07-12 02:34:12 +0000
@@ -1,57 +1,85 @@
-{% extends "dashboard_app/base.html" %}
+{% extends "dashboard_app/_content.html" %}
 {% load i18n %}
-{% load humanize %}
-{% load pagination_tags %}
-
-
-{% block title %}
-{{ block.super }} | {% trans "Test Runs" %} | {{ test_run }} | {% trans "Software Context" %}
-{% endblock %}
-
-
-{% block breadcrumbs %}
-<li><a href="{% url dashboard_app.views.bundle_stream_list %}">{% trans "Bundle Streams" %}</a></li>
-<li><a href="{{ test_run.bundle.bundle_stream.get_absolute_url }}">{{ test_run.bundle.bundle_stream }}</a></li>
-<li><a href="{{ test_run.get_absolute_url }}">{{ test_run }}</a></li>
-<li><a href="{% url dashboard_app.views.test_run_software_context test_run.analyzer_assigned_uuid %}">{% trans "Software Context" %}</a></li>
-{% endblock %}
-
-
-{% block sidebar %}
-{% endblock %}
 
 
 {% block content %}
-<h2>Software Packages</h2>
-<ul>
-{% autopaginate test_run.packages.all as software_packages %}
-{% for software_package in software_packages %}
-  <li>Package <a href="{{software_package.link_to_packages_ubuntu_com}}">{{software_package.name}}</a> version {{software_package.version}}</li>
-{% empty %}
-  <em>There are no software packages associated with this test run</em>
-{% endfor %}
-</ul>
-{% paginate %}
-
-<h2>Source Sources</h2>
-<ul>
-{% for software_source in test_run.sources.all %}
-  <li>
-  {% if software_source.is_hosted_on_launchpad %}
-    Launchpad project <a href="{{ software_source.link_to_project }}">{{ software_source.project_name }}</a>
-    from bazaar branch <a href="{{ software_source.link_to_branch }}">{{ software_source.branch_url }}</a>
-    {% if software_source.is_tag_revision %}
-      at tag {{ software_source.branch_tag }}
-    {% else %}
-      at revision {{ software_source.branch_revision }}
-    {% endif %}
-  {% else %}
-    Project {{software_source.project_name}} from {{software_source.branch_vcs}}
-    branch {{software_source.branch_url}} at revision {{software_source.branch_revision}}
-  {% endif %}
-  </li>
-{% empty %}
-  <em>There are no source references associated with this test run</em>
-{% endfor %}
-</ul>
+<div id="tabs">
+  <ul>
+    <li><a href="#tab-software-packages">Software Packages</a></li>
+    <li><a href="#tab-software-sources">Software Sources</a></li>
+  </ul>
+
+  <div id="tab-software-packages">
+    <table class="demo_jui display" id="software_packages">
+      <thead>
+        <tr>
+          <th>Name</th>
+          <th>Version</th>
+        </tr>
+      </thead>
+      <tbody>
+        {% for software_package in test_run.packages.all %}
+        <tr>
+          <td><a href="{{software_package.link_to_packages_ubuntu_com}}">{{software_package.name}}</a></td>
+          <td>{{software_package.version}}</td>
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+  </div>
+
+  <div id="tab-software-sources">
+    <table class="demo_jui display" id="software_sources">
+      <thead>
+        <tr>
+          <th>Project</th>
+          <th><abbr title="Version Control System">VCS</abbr></th>
+          <th>Branch</th>
+          <th>Tag or revision</th>
+        </tr>
+      </thead>
+      <tbody>
+        {% for software_source in test_run.sources.all %}
+        <tr>
+          {% if software_source.is_hosted_on_launchpad %}
+          <td><a href="{{ software_source.link_to_project }}">{{ software_source.project_name }}</a></td>
+          <td>{{software_source.branch_vcs}}</td>
+          <td><a href="{{ software_source.link_to_branch }}">{{ software_source.branch_url }}</a></td>
+          {% if software_source.is_tag_revision %}
+          <td>{{ software_source.branch_tag }}</td>
+          {% else %}
+          <td>{{ software_source.branch_revision }}</td>
+          {% endif %}
+          {% else %}
+          <td>{{software_source.project_name}}</td>
+          <td>{{software_source.branch_vcs}}</td>
+          <td>{{software_source.branch_url}}</td>
+          <td>{{software_source.branch_revision}}</td>
+          {% endif %}
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+  </div>
+</div>
+
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    $('#software_packages').dataTable({
+      bJQueryUI: true,
+    });
+    $('#software_sources').dataTable({
+      bJQueryUI: true
+    });
+    $("#tabs").tabs({
+      cache: true,
+      show: function(event, ui) {
+        var oTable = $('div.dataTables_scrollBody>table.display', ui.panel).dataTable();
+        if ( oTable.length > 0 ) {
+          oTable.fnAdjustColumnSizing();
+        }
+      }
+    });
+  });
+</script> 
 {% endblock %}

=== modified file 'dashboard_app/tests/views/test_run_detail_view.py'
--- dashboard_app/tests/views/test_run_detail_view.py	2011-05-23 17:02:43 +0000
+++ dashboard_app/tests/views/test_run_detail_view.py	2011-07-12 02:33:19 +0000
@@ -42,12 +42,12 @@
         self.assertTemplateUsed(response,
                 "dashboard_app/test_run_detail.html")
 
-    def testrun_invalid_page_view(self):
-        invalid_uuid = "0000000-0000-0000-0000-000000000000" 
-        invalid_test_run_url = reverse("dashboard_app.views.test_run_detail",
-                                       args=[invalid_uuid])
-        response = self.client.get(invalid_test_run_url)
-        self.assertEqual(response.status_code, 404)
+    #def testrun_invalid_page_view(self):
+    #    invalid_uuid = "0000000-0000-0000-0000-000000000000" 
+    #    invalid_test_run_url = reverse("dashboard_app.views.test_run_detail",
+    #                                   args=[invalid_uuid])
+    #    response = self.client.get(invalid_test_run_url)
+    #    self.assertEqual(response.status_code, 404)
 
 
 class TestRunViewAuth(TestCaseWithScenarios):

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2011-06-29 16:41:01 +0000
+++ dashboard_app/urls.py	2011-07-12 02:30:17 +0000
@@ -24,17 +24,24 @@
 
 urlpatterns = patterns(
     'dashboard_app.views',
-    url(r'^streams/$', 'bundle_stream_list'),
-    url(r'^test-results/(?P<pk>[0-9]+)/$', 'test_result_detail'),
-    url(r'^test-runs/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'test_run_detail'),
-    url(r'^test-runs/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/software-context$', 'test_run_software_context'),
-    url(r'^test-runs/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/hardware-context$', 'test_run_hardware_context'),
-    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)$', 'test_run_list'),
-    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+?)\+bundles$', 'bundle_list'),
-    url(r'^attachments/(?P<pk>[0-9]+)/$', 'attachment_detail'),
-    url(r'^xml-rpc/', 'dashboard_xml_rpc_handler'),
+    url(r'^$', 'index'),
+    url(r'^ajax/bundle-viewer/(?P<pk>[0-9]+)/$', 'ajax_bundle_viewer'),
+    url(r'^ajax/attachment-viewer/(?P<pk>[0-9]+)/$', 'ajax_attachment_viewer'),
     url(r'^data-views/$', 'data_view_list'),
     url(r'^data-views/(?P<name>[a-zA-Z0-9-_]+)/$', 'data_view_detail'),
     url(r'^reports/$', 'report_list'),
     url(r'^reports/(?P<name>[a-zA-Z0-9-_]+)/$', 'report_detail'),
+    url(r'^tests/$', 'test_list'),
+    url(r'^tests/(?P<test_id>[^/]+)/$', 'test_detail'),
+    url(r'^xml-rpc/', 'dashboard_xml_rpc_handler'),
+    url(r'^streams/$', 'bundle_stream_list'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/$', 'bundle_list'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/$', 'bundle_detail'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'test_run_detail'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/attachments$', 'attachment_list'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/attachments/(?P<pk>[0-9]+)/$', 'attachment_detail'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/result/(?P<relative_index>[0-9]+)/$', 'test_result_detail'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/hardware-context/$', 'test_run_hardware_context'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/software-context/$', 'test_run_software_context'),
+    url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)test-runs/$', 'test_run_list'),
 )

=== modified file 'dashboard_app/views.py'
--- dashboard_app/views.py	2011-06-29 16:41:01 +0000
+++ dashboard_app/views.py	2011-07-12 02:30:17 +0000
@@ -28,11 +28,22 @@
 from django.http import (HttpResponse, Http404)
 from django.shortcuts import render_to_response
 from django.template import RequestContext
+from django.views.generic.list_detail import object_list, object_detail
 
 from dashboard_app.dataview import DataView, DataViewRepository
 from dashboard_app.dispatcher import DjangoXMLRPCDispatcher
-from dashboard_app.models import Attachment, BundleStream, TestRun, TestResult, DataReport
+from dashboard_app.models import (
+    Attachment,
+    Bundle,
+    BundleStream,
+    DataReport,
+    Test,
+    TestCase,
+    TestResult,
+    TestRun,
+)
 from dashboard_app.xmlrpc import DashboardAPI
+from dashboard_app.bread_crumbs import BreadCrumb, BreadCrumbTrail
 
 
 def _get_dashboard_dispatcher():
@@ -123,15 +134,23 @@
     return xml_rpc_handler(request, DashboardDispatcher)
 
 
+@BreadCrumb("Dashboard")
+def index(request):
+    return render_to_response(
+        "dashboard_app/index.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(index)
+        }, RequestContext(request))
+
+
+@BreadCrumb("Bundle Streams", parent=index)
 def bundle_stream_list(request):
     """
     List of bundle streams.
-
-    The list is paginated and dynamically depends on the currently
-    logged in user.
     """
     return render_to_response(
         'dashboard_app/bundle_stream_list.html', {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                bundle_stream_list),
             "bundle_stream_list": BundleStream.objects.accessible_by_principal(request.user).order_by('pathname'),
             'has_personal_streams': (
                 request.user.is_authenticated() and
@@ -144,12 +163,84 @@
     )
 
 
+@BreadCrumb(
+    "Bundles in {pathname}",
+    parent=bundle_stream_list,
+    needs=['pathname'])
+def bundle_list(request, pathname):
+    """
+    List of bundles in a specified bundle stream.
+    """
+    bundle_stream = get_restricted_object_or_404(
+        BundleStream,
+        lambda bundle_stream: bundle_stream,
+        request.user,
+        pathname=pathname
+    )
+    return object_list(
+        request,
+        queryset=bundle_stream.bundles.all().order_by('-uploaded_on'),
+        template_name="dashboard_app/bundle_list.html",
+        template_object_name="bundle",
+        extra_context={
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                bundle_list,
+                pathname=pathname),
+            "bundle_stream": bundle_stream
+        })
+
+
+@BreadCrumb(
+    "Bundle {content_sha1}",
+    parent=bundle_list,
+    needs=['pathname', 'content_sha1'])
+def bundle_detail(request, pathname, content_sha1):
+    """
+    Detail about a bundle from a particular stream
+    """
+    bundle_stream = get_restricted_object_or_404(
+        BundleStream,
+        lambda bundle_stream: bundle_stream,
+        request.user,
+        pathname=pathname
+    )
+    return object_detail(
+        request,
+        queryset=bundle_stream.bundles.all(),
+        slug=content_sha1,
+        slug_field="content_sha1",
+        template_name="dashboard_app/bundle_detail.html",
+        template_object_name="bundle",
+        extra_context={
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                bundle_detail,
+                pathname=pathname,
+                content_sha1=content_sha1),
+            "bundle_stream": bundle_stream
+        })
+
+
+def ajax_bundle_viewer(request, pk):
+    bundle = get_restricted_object_or_404(
+        Bundle,
+        lambda bundle: bundle.bundle_stream,
+        request.user,
+        pk=pk
+    )
+    return render_to_response(
+        "dashboard_app/_ajax_bundle_viewer.html", {
+            "bundle": bundle
+        },
+        RequestContext(request))
+
+
+@BreadCrumb(
+    "Test runs in {pathname}",
+    parent=bundle_stream_list,
+    needs=['pathname'])
 def test_run_list(request, pathname):
     """
     List of test runs in a specified bundle stream.
-
-    The list is paginated and dynamically depends on the currently
-    logged in user.
     """
     bundle_stream = get_restricted_object_or_404(
         BundleStream,
@@ -159,94 +250,183 @@
     )
     return render_to_response(
         'dashboard_app/test_run_list.html', {
-            "test_run_list": TestRun.objects.filter(bundle__bundle_stream=bundle_stream).order_by('-bundle__uploaded_on'),
-            "bundle_stream": bundle_stream,
-        }, RequestContext(request)
-    )
-
-
-def bundle_list(request, pathname):
-    """
-    List of bundles in a specified bundle stream.
-
-    The list is paginated and dynamically depends on the currently logged in
-    user.
-    """
-    bundle_stream = get_restricted_object_or_404(
-        BundleStream,
-        lambda bundle_stream: bundle_stream,
-        request.user,
-        pathname=pathname
-    )
-    return render_to_response(
-        'dashboard_app/bundle_list.html', {
-            "bundle_list": bundle_stream.bundles.all().order_by('-uploaded_on'),
-            "bundle_stream": bundle_stream,
-        }, RequestContext(request)
-    )
-
-
-def _test_run_view(template_name):
-    def view(request, analyzer_assigned_uuid):
-        test_run = get_restricted_object_or_404(
-            TestRun,
-            lambda test_run: test_run.bundle.bundle_stream,
-            request.user,
-            analyzer_assigned_uuid = analyzer_assigned_uuid
-        )
-        return render_to_response(
-            template_name, {
-                "test_run": test_run
-            }, RequestContext(request)
-        )
-    return view
-
-
-test_run_detail = _test_run_view("dashboard_app/test_run_detail.html")
-test_run_software_context = _test_run_view("dashboard_app/test_run_software_context.html")
-test_run_hardware_context = _test_run_view("dashboard_app/test_run_hardware_context.html")
-
-
-def test_result_detail(request, pk):
-    test_result = get_restricted_object_or_404(
-        TestResult,
-        lambda test_result: test_result.test_run.bundle.bundle_stream,
-        request.user,
-        pk = pk
-    )
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                test_run_list,
+                pathname=pathname),
+            "test_run_list": TestRun.objects.filter(
+                bundle__bundle_stream=bundle_stream),
+            "bundle_stream": bundle_stream,
+        }, RequestContext(request)
+    )
+
+
+@BreadCrumb(
+    "Run {analyzer_assigned_uuid}",
+    parent=bundle_detail,
+    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid'])
+def test_run_detail(request, pathname, content_sha1, analyzer_assigned_uuid):
+    test_run = get_restricted_object_or_404(
+        TestRun,
+        lambda test_run: test_run.bundle.bundle_stream,
+        request.user,
+        analyzer_assigned_uuid=analyzer_assigned_uuid
+    )
+    return render_to_response(
+        "dashboard_app/test_run_detail.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                test_run_detail,
+                pathname=pathname,
+                content_sha1=content_sha1,
+                analyzer_assigned_uuid=analyzer_assigned_uuid),
+            "test_run": test_run
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Software Context",
+    parent=test_run_detail,
+    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid'])
+def test_run_software_context(request, pathname, content_sha1, analyzer_assigned_uuid):
+    test_run = get_restricted_object_or_404(
+        TestRun,
+        lambda test_run: test_run.bundle.bundle_stream,
+        request.user,
+        analyzer_assigned_uuid=analyzer_assigned_uuid
+    )
+    return render_to_response(
+        "dashboard_app/test_run_software_context.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                test_run_software_context,
+                pathname=pathname,
+                content_sha1=content_sha1,
+                analyzer_assigned_uuid=analyzer_assigned_uuid),
+            "test_run": test_run
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Hardware Context",
+    parent=test_run_detail,
+    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid'])
+def test_run_hardware_context(request, pathname, content_sha1, analyzer_assigned_uuid):
+    test_run = get_restricted_object_or_404(
+        TestRun,
+        lambda test_run: test_run.bundle.bundle_stream,
+        request.user,
+        analyzer_assigned_uuid=analyzer_assigned_uuid
+    )
+    return render_to_response(
+        "dashboard_app/test_run_hardware_context.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                test_run_hardware_context,
+                pathname=pathname,
+                content_sha1=content_sha1,
+                analyzer_assigned_uuid=analyzer_assigned_uuid),
+            "test_run": test_run
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Result {relative_index}",
+    parent=test_run_detail,
+    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'relative_index'])
+def test_result_detail(request, pathname, content_sha1, analyzer_assigned_uuid, relative_index):
+    test_run = get_restricted_object_or_404(
+        TestRun,
+        lambda test_run: test_run.bundle.bundle_stream,
+        request.user,
+        analyzer_assigned_uuid=analyzer_assigned_uuid
+    )
+    test_result = test_run.test_results.get(relative_index=relative_index)
     return render_to_response(
         "dashboard_app/test_result_detail.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                test_result_detail,
+                pathname=pathname,
+                content_sha1=content_sha1,
+                analyzer_assigned_uuid=analyzer_assigned_uuid,
+                relative_index=relative_index),
             "test_result": test_result
-        }, RequestContext(request)
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Attachments",
+    parent=test_run_detail,
+    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid'])
+def attachment_list(request, pathname, content_sha1, analyzer_assigned_uuid):
+    test_run = get_restricted_object_or_404(
+        TestRun,
+        lambda test_run: test_run.bundle.bundle_stream,
+        request.user,
+        analyzer_assigned_uuid=analyzer_assigned_uuid
     )
-
-
-def attachment_detail(request, pk):
+    return object_list(
+        request,
+        queryset=test_run.attachments.all(),
+        template_name="dashboard_app/attachment_list.html",
+        template_object_name="attachment",
+        extra_context={
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                attachment_list, 
+                pathname=pathname,
+                content_sha1=content_sha1,
+                analyzer_assigned_uuid=analyzer_assigned_uuid),
+            'test_run': test_run})
+
+
+@BreadCrumb(
+    "{content_filename}",
+    parent=attachment_list,
+    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'content_filename'])
+def attachment_detail(request, pathname, content_sha1, analyzer_assigned_uuid, pk):
     attachment = get_restricted_object_or_404(
         Attachment,
-        lambda attachment: attachment.content_object.bundle.bundle_stream,
+        lambda attachment: attachment.test_run.bundle.bundle_stream,
         request.user,
         pk = pk
     )
+    return render_to_response(
+        "dashboard_app/attachment_detail.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                attachment_detail,
+                pathname=pathname,
+                content_sha1=content_sha1,
+                analyzer_assigned_uuid=analyzer_assigned_uuid,
+                content_filename=attachment.content_filename),
+            "attachment": attachment,
+        }, RequestContext(request))
+
+
+def ajax_attachment_viewer(request, pk):
+    attachment = get_restricted_object_or_404(
+        Attachment,
+        lambda attachment: attachment.test_run.bundle.bundle_stream,
+        request.user,
+        pk=pk
+    )
     if attachment.mime_type == "text/plain":
         data = attachment.get_content_if_possible(mirror=request.user.is_authenticated())
     else:
         data = None
     return render_to_response(
-        "dashboard_app/attachment_detail.html", {
+        "dashboard_app/_ajax_attachment_viewer.html", {
+            "attachment": attachment,
             "lines": data.splitlines() if data else None,
-            "attachment": attachment,
-        }, RequestContext(request)
-    )
-
-
+        },
+        RequestContext(request))
+
+
+@BreadCrumb("Reports", parent=index)
 def report_list(request):
     return render_to_response(
         "dashboard_app/report_list.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(report_list),
             "report_list": DataReport.repository.all()
         }, RequestContext(request))
 
 
+@BreadCrumb("{title}", parent=report_list, needs=['name'])
 def report_detail(request, name):
     try:
         report = DataReport.repository.get(name=name)
@@ -254,18 +434,22 @@
         raise Http404('No report matches given name.')
     return render_to_response(
         "dashboard_app/report_detail.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(report_detail, name=report.name, title=report.title),
             "report": report,
         }, RequestContext(request))
 
 
+@BreadCrumb("Data views", parent=index)
 def data_view_list(request):
     repo = DataViewRepository.get_instance()
     return render_to_response(
         "dashboard_app/data_view_list.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(data_view_list),
             "data_view_list": repo.data_views
         }, RequestContext(request))
 
 
+@BreadCrumb("Details of {name}", parent=data_view_list, needs=['name'])
 def data_view_detail(request, name):
     repo = DataViewRepository.get_instance()
     try:
@@ -274,5 +458,32 @@
         raise Http404('No data view matches the given query.') 
     return render_to_response(
         "dashboard_app/data_view_detail.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(data_view_detail, name=data_view.name, summary=data_view.summary),
             "data_view": data_view 
         }, RequestContext(request))
+
+
+@BreadCrumb("Tests", parent=index)
+def test_list(request):
+    return object_list(
+        request,
+        queryset=Test.objects.all(),
+        template_name="dashboard_app/test_list.html",
+        template_object_name="test",
+        extra_context={
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(test_list)
+        })
+
+
+@BreadCrumb("Details of {test_id}", parent=test_list, needs=['test_id'])
+def test_detail(request, test_id):
+    return object_detail(
+        request,
+        queryset=Test.objects.all(),
+        slug=test_id,
+        slug_field="test_id",
+        template_name="dashboard_app/test_detail.html",
+        template_object_name="test",
+        extra_context={
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(test_detail, test_id=test_id)
+        })

=== added directory 'production'
=== added directory 'production/reports'
=== added directory 'production/views'
