diff mbox

[Branch,~linaro-validation/lava-dashboard/trunk] Rev 248: Merge 0.6-wip branch

Message ID 20110722013922.8983.41110.launchpad@loganberry.canonical.com
State Accepted
Headers show

Commit Message

Zygmunt Krynicki July 22, 2011, 1:39 a.m. UTC
Merge authors:
  Zygmunt Krynicki (zkrynicki)
Related merge proposals:
  https://code.launchpad.net/~linaro-validation/lava-dashboard/0.6-wip/+merge/68142
  proposed by: Zygmunt Krynicki (zkrynicki)
------------------------------------------------------------
revno: 248 [merge]
committer: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
branch nick: trunk
timestamp: Fri 2011-07-22 03:34:46 +0200
message:
  Merge 0.6-wip branch
  
  * New UI synchronized with lava-server, the UI is going to be changed in the
    next release to be more in line with the official Linaro theme. Currently
    most changes are under-the-hood, sporting more jQuery UI CSS.
  * New test browser that allows to see all the registered tests and their test
    cases.
  * New data view browser, similar to data view browser.
  * New permalink system that allows easy linking to bundles, test runs and test
    results.
  * New image status views that allow for quick inspection of interesting
    hardware pack + root filesystem combinations.
  * New image status detail view with color-coded information about test failures
    affecting current and historic instances of a particular root filesystem +
    hardware pack combination.
  * New image test history view showing all the runs of a particular test on a
    particular combination of root filesystem + hardware pack.
  * New table widget for better table display with support for client side
    sorting and searching.
  * New option to render data reports without any navigation that is suitable for
    embedding inside an iframe (by appending &iframe=yes to the URL)
  * New view for showing text attachments associated with test runs.
  * New view showing test runs associated with a specific bundle.
  * New view showing the raw JSON text of a bundle.
  * New view for inspecting bundle deserialization failures.
  * Integration with lava-server/RPC2/ for web APIs
  * Added support for non-anonymous submissions (test results uploaded by
    authenticated users), including uploading results to personal (owned by
    person), team (owned by group), public (visible) and private (hidden from
    non-owners) bundle streams.
  * Added support for creating non-anonymous bundle streams with
    dashboard.make_stream() (for authenticated users)
removed:
  dashboard_app/dataview.py
  dashboard_app/dispatcher.py
  dashboard_app/tests/other/xml_rpc.py
added:
  dashboard_app/repositories/common.py
  dashboard_app/repositories/data_view.py
  dashboard_app/templates/dashboard_app/image_status_detail.html
  dashboard_app/templates/dashboard_app/image_status_list.html
  dashboard_app/templates/dashboard_app/image_test_history.html
  dashboard_app/templatetags/call.py
  dashboard_app/tests/views/redirects.py
modified:
  dashboard_app/admin.py
  dashboard_app/models.py
  dashboard_app/repositories/__init__.py
  dashboard_app/repositories/data_report.py
  dashboard_app/static/js/jquery.dashboard.js
  dashboard_app/templates/dashboard_app/_extension_navigation.html
  dashboard_app/templates/dashboard_app/bundle_detail.html
  dashboard_app/templates/dashboard_app/data_view_list.html
  dashboard_app/templates/dashboard_app/index.html
  dashboard_app/templates/dashboard_app/report_detail.html
  dashboard_app/templates/dashboard_app/test_result_detail.html
  dashboard_app/templates/dashboard_app/test_run_detail.html
  dashboard_app/tests/__init__.py
  dashboard_app/tests/other/dataview.py
  dashboard_app/tests/utils.py
  dashboard_app/tests/views/test_run_detail_view.py
  dashboard_app/urls.py
  dashboard_app/views.py
  dashboard_app/xmlrpc.py
  doc/changes.rst
  doc/index.rst


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

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

Patch

=== modified file 'dashboard_app/admin.py'
--- dashboard_app/admin.py	2011-04-29 11:54:24 +0000
+++ dashboard_app/admin.py	2011-07-19 21:39:22 +0000
@@ -137,8 +137,13 @@ 
 class TestRunAdmin(admin.ModelAdmin):
     class NamedAttributeInline(generic.GenericTabularInline):
         model = NamedAttribute
-    list_display = ('analyzer_assigned_uuid',
-                    'analyzer_assigned_date', 'import_assigned_date')
+    list_filter = ('test'),
+    list_display = (
+        'test',
+        'analyzer_assigned_uuid',
+        'bundle',
+        'analyzer_assigned_date',
+        'import_assigned_date')
     inlines = [NamedAttributeInline]
 
 

=== removed file 'dashboard_app/dataview.py'
--- dashboard_app/dataview.py	2011-07-09 15:09:48 +0000
+++ dashboard_app/dataview.py	1970-01-01 00:00:00 +0000
@@ -1,284 +0,0 @@ 
-# Copyright (C) 2011 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/>.
-
-"""
-DataViews: Encapsulated SQL query definitions.
-
-Implementation of the following launchpad blueprint:
-https://blueprints.launchpad.net/launch-control/+spec/other-linaro-n-data-views-for-launch-control
-"""
-
-from contextlib import closing
-from xml.sax import parseString
-from xml.sax.handler import ContentHandler
-import logging
-import os
-import re
-
-
-class DataView(object):
-    """
-    Data view, a container for SQL query and optional arguments
-    """
-    
-    def __init__(self, name, backend_queries, arguments, documentation, summary):
-        self.name = name
-        self.backend_queries = backend_queries
-        self.arguments = arguments
-        self.documentation = documentation
-        self.summary = summary
-
-    def _get_connection_backend_name(self, connection):
-        backend = str(type(connection))
-        if "sqlite" in backend:
-            return "sqlite"
-        elif "postgresql" in backend:
-            return "postgresql"
-        else:
-            return ""
-
-    def get_backend_specific_query(self, connection):
-        """
-        Return BackendSpecificQuery for the specified connection
-        """
-        sql_backend_name = self._get_connection_backend_name(connection)
-        try:
-            return self.backend_queries[sql_backend_name]
-        except KeyError:
-            return self.backend_queries.get(None, None)
-
-    def lookup_argument(self, name):
-        """
-        Return Argument with the specified name
-
-        Raises LookupError if the argument cannot be found
-        """
-        for argument in self.arguments:
-            if argument.name == name:
-                return argument
-        raise LookupError(name)
-
-    @classmethod
-    def load_from_xml(self, xml_string):
-        """
-        Load a data view instance from XML description
-
-        This raises ValueError in several error situations.
-        TODO: check what kind of exceptions this can raise
-        """
-        handler = _DataViewHandler()
-        parseString(xml_string, handler)
-        return handler.data_view
-
-    @classmethod
-    def get_connection(cls):
-        """
-        Get the appropriate connection for data views
-        """
-        from django.db import connection, connections
-        from django.db.utils import ConnectionDoesNotExist
-        try:
-            return connections['dataview']
-        except ConnectionDoesNotExist:
-            logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed")
-            return connection  # NOTE: it's connection not connectionS (the default connection)
-
-    def __call__(self, connection, **arguments):
-        # Check if arguments have any bogus names
-        valid_arg_names = frozenset([argument.name for argument in self.arguments])
-        for arg_name in arguments:
-            if arg_name not in valid_arg_names:
-                raise TypeError("Data view %s has no argument %r" % (self.name, arg_name))
-        # Get the SQL template for our database connection
-        query = self.get_backend_specific_query(connection)
-        if query is None:
-            raise LookupError("Specified data view has no SQL implementation "
-                              "for current database")
-        # Replace SQL aruments with django placeholders (connection agnostic)
-        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)
-            for arg_name in query.argument_list]
-        with closing(connection.cursor()) as cursor:
-            # Execute the query with the specified arguments
-            cursor.execute(sql, sql_args)
-            # Get and return the results
-            rows = cursor.fetchall()
-            columns = cursor.description
-            return rows, columns
-
-
-class Argument(object):
-    """
-    Data view argument for SQL prepared statements
-    """
-
-    def __init__(self, name, type, default, help):
-        self.name = name
-        self.type = type
-        self.default = default
-        self.help = help 
-
-
-class BackendSpecificQuery(object):
-    """
-    Backend-specific query and argument list
-    """
-
-    def __init__(self, backend, sql_template, argument_list):
-        self.backend = backend
-        self.sql_template = sql_template
-        self.argument_list = argument_list
-
-
-class _DataViewHandler(ContentHandler):
-    """
-    ContentHandler subclass for parsing DataView documents
-    """
-    
-    def _end_text(self):
-        """
-        Stop collecting text and produce a stripped string with deduplicated whitespace
-        """
-        full_text = re.sub("\s+", " ", u''.join(self._text)).strip()
-        self.text = None
-        return full_text
-        
-    def _start_text(self):
-        """
-        Start collecting text
-        """
-        self._text = []
-
-    def startDocument(self):
-        # Text can be None or a [] that accumulates all detected text
-        self._text = None
-        # Data view object
-        self.data_view = DataView(None, {}, [], None, None)
-        # Internal variables
-        self._current_backend_query = None
-
-    def endDocument(self):
-        # TODO: check if we have anything defined
-        if self.data_view.name is None:
-            raise ValueError("No data view definition found")
-
-    def startElement(self, name, attrs):
-        if name == "data-view":
-            self.data_view.name = attrs["name"]
-        elif name == "summary" or name == "documentation":
-            self._start_text()
-        elif name == "sql":
-            self._start_text()
-            self._current_backend_query = BackendSpecificQuery(attrs.get("backend"), None, [])
-            self.data_view.backend_queries[self._current_backend_query.backend] = self._current_backend_query 
-        elif name == "value":
-            if "name" not in attrs:
-                raise ValueError("<value> requires attribute 'name'")
-            self._text.append("{" + attrs["name"] + "}")
-            self._current_backend_query.argument_list.append(attrs["name"])
-        elif name == "argument":
-            if "name" not in attrs:
-                raise ValueError("<argument> requires attribute 'name'")
-            if "type" not in attrs:
-                raise ValueError("<argument> requires attribute 'type'")
-            if attrs["type"] not in ("string", "number", "boolean", "timestamp"):
-                raise ValueError("invalid value for argument 'type' on <argument>")
-            argument = Argument(name=attrs["name"], type=attrs["type"],
-                                   default=attrs.get("default", None),
-                                   help=attrs.get("help", None))
-            self.data_view.arguments.append(argument)
-                
-    def endElement(self, name):
-        if name == "sql":
-            self._current_backend_query.sql_template = self._end_text()
-            self._current_backend_query = None
-        elif name == "documentation":
-            self.data_view.documentation = self._end_text()
-        elif name == "summary":
-            self.data_view.summary = self._end_text()
-            
-    def characters(self, content):
-        if isinstance(self._text, list):
-            self._text.append(content)
-
-
-class DataViewRepository(object):
-
-    _instance = None
-
-    def __init__(self):
-        self.data_views = []
-
-    def __iter__(self):
-        return iter(self.data_views)
-
-    def __getitem__(self, name):
-        for item in self:
-            if item.name == name:
-                return item
-        else:
-            raise KeyError(name)
-
-    def load_from_directory(self, directory):
-        for name in os.listdir(directory):
-            pathname = os.path.join(directory, name)
-            if os.path.isfile(pathname) and pathname.endswith(".xml"):
-                self.load_from_file(pathname)
-
-    def load_from_file(self, pathname):
-        try:
-            with open(pathname, "rt") as stream:
-                text = stream.read()
-            data_view = DataView.load_from_xml(text)
-            self.data_views.append(data_view)
-        except Exception as exc:
-            logging.error("Unable to load data view from %s: %s", pathname, exc)
-
-    @classmethod
-    def get_instance(cls):
-        from django.conf import settings
-        if cls._instance is None:
-            cls._instance = cls()
-            cls._instance.load_default()
-
-        # I development mode always reload data views
-        if getattr(settings, "DEBUG", False) is True:
-            cls._instance.data_views = []
-            cls._instance.load_default()
-        return cls._instance
-
-    def load_default(self):
-        from django.conf import settings
-        for dirname in getattr(settings, "DATAVIEW_DIRS", []):
-            self.load_from_directory(dirname)
-
-
-__all__ = [
-    "Argument",
-    "BackendSpecificQuery",
-    "DataView", 
-    "DataViewRepository",
-]

=== removed file 'dashboard_app/dispatcher.py'
--- dashboard_app/dispatcher.py	2011-05-07 23:08:30 +0000
+++ dashboard_app/dispatcher.py	1970-01-01 00:00:00 +0000
@@ -1,188 +0,0 @@ 
-# 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/>.
-
-"""
-XML-RPC Dispatcher for the dashboard
-"""
-import SimpleXMLRPCServer
-import logging
-import sys
-import xmlrpclib
-
-class _NullHandler(logging.Handler):
-    def emit(self, record):
-        pass
-
-
-class FaultCodes:
-    """
-    Common fault codes.
-
-    See: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
-    """
-    class ParseError:
-        NOT_WELL_FORMED = -32700
-        UNSUPPORTED_ENCODING = -32701
-        INVALID_CHARACTER_FOR_ENCODING = -32702
-    class ServerError:
-        INVALID_XML_RPC = -32600
-        REQUESTED_METHOD_NOT_FOUND = -32601
-        INVALID_METHOD_PARAMETERS = -32602
-        INTERNAL_XML_RPC_ERROR = -32603
-    APPLICATION_ERROR = -32500
-    SYSTEM_ERROR = -32400
-    TRANSPORT_ERROR = -32300
-
-
-class DjangoXMLRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
-    """
-    Slightly extended XML-RPC dispatcher class suitable for embedding in
-    django applications.
-    """
-    #TODO: Implement _marshaled_dispatch() and capture XML errors to
-    #      translate them to appropriate standardized fault codes. There
-    #      might be some spill to the view code to make this complete.
-
-    #TODO: Implement and expose system.getCapabilities() and advertise
-    #      support for standardised fault codes.
-    #      See: http://tech.groups.yahoo.com/group/xml-rpc/message/2897
-    def __init__(self):
-        # it's a classic class, no super
-        SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,
-                allow_none=True)
-        self.logger = logging.getLogger(__name__)
-        self.logger.addHandler(_NullHandler())
-
-    def _lookup_func(self, method):
-        """
-        Lookup implementation of method named `method`.
-
-        Returns implementation of `method` or None if the method is not
-        registered anywhere in the dispatcher.
-
-        This new function is taken directly out of the base class
-        implementation of _dispatch. The point is to be able to
-        detect a situation where method is not known and return
-        appropriate XML-RPC fault. With plain dispatcher it is
-        not possible as the implementation raises a plain Exception
-        to signal this error condition and capturing and interpreting
-        arbitrary exceptions is flaky.
-        """
-        func = None
-        try:
-            # check to see if a matching function has been registered
-            func = self.funcs[method]
-        except KeyError:
-            if self.instance is not None:
-                # check for a _dispatch method
-                if hasattr(self.instance, '_dispatch'):
-                    # FIXME: pyflakes, params is undefined
-                    return self.instance._dispatch(method, params)
-                else:
-                    # call instance method directly
-                    try:
-                        func = SimpleXMLRPCServer.resolve_dotted_attribute(
-                            self.instance,
-                            method,
-                            self.allow_dotted_names
-                            )
-                    except AttributeError:
-                        pass
-        return func
-
-    def _dispatch(self, method, params):
-        """
-        Improved dispatch method from the base dispatcher.
-
-        The primary improvement is exception handling:
-            - xml-rpc faults are passed back to the caller
-            - missing methods return standardized fault code (
-              FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND)
-            - all other exceptions in the called method are translated
-              to standardized internal xml-rpc fault code
-              (FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR).  In
-              addition such errors cause _report_incident() to be
-              called. This allows to hook a notification mechanism for
-              deployed servers where exceptions are, for example, mailed
-              to the administrator.
-        """
-        func = self._lookup_func(method)
-        if func is None:
-            raise xmlrpclib.Fault(
-                    FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND,
-                    "No such method: %r" % method)
-        try:
-            # TODO: check parameter types before calling
-            return func(*params)
-        except xmlrpclib.Fault:
-            # Forward XML-RPC Faults to the client
-            raise
-        except:
-            # Treat all other exceptions as internal errors 
-            # This prevents the clients from seeing internals    
-            exc_type, exc_value, exc_tb = sys.exc_info()
-            incident_id = self._report_incident(method, params, exc_type, exc_value, exc_tb)
-            string = ("Dashboard has encountered internal error. "
-                    "Incident ID is: %s" % (incident_id,))
-            raise xmlrpclib.Fault(
-                    FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR,
-                    string)
-
-    def _report_incident(self, method, params, exc_type, exc_value, exc_tb):
-        """
-        Report an exception that happened
-        """
-        self.logger.exception("Internal error when dispatching "
-                "XML-RPC method: %s%r", method, params)
-        # TODO: store the exception somewhere and assign fault codes 
-        return None
-
-    def system_methodSignature(self, method):
-        if method.startswith("_"):
-            return ""
-        if self.instance is not None:
-            func = getattr(self.instance, method, None)
-        else:
-            func = self.funcs.get(method)
-        # When function is not known return empty string
-        if func is None:
-            return ""
-        # When signature is not known return "undef"
-        # See: http://xmlrpc-c.sourceforge.net/introspection.html
-        return getattr(func, 'xml_rpc_signature', "undef")
-
-
-def xml_rpc_signature(*sig):
-    """
-    Small helper that attaches "xml_rpc_signature" attribute to the
-    function. The attribute is a list of values that is then reported
-    by system_methodSignature().
-
-    This is a simplification of the XML-RPC spec that allows to attach a
-    list of variants (like I may accept this set of arguments, or that
-    set or that other one). This version has only one set of arguments.
-
-    Note that it's a purely presentational argument for our
-    implementation. Putting bogus values here won't spoil the day.
-
-    The first element is the signature of the return type.
-    """
-    def decorator(func):
-        func.xml_rpc_signature = sig
-        return func
-    return decorator

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2011-07-12 14:38:26 +0000
+++ dashboard_app/models.py	2011-07-22 00:57:44 +0000
@@ -26,6 +26,7 @@ 
 import os
 import simplejson
 import traceback
+import contextlib
 
 from django.contrib.auth.models import User
 from django.contrib.contenttypes import generic
@@ -43,6 +44,7 @@ 
 from dashboard_app.managers import BundleManager
 from dashboard_app.repositories import RepositoryItem 
 from dashboard_app.repositories.data_report import DataReportRepository
+from dashboard_app.repositories.data_view import DataViewRepository
 
 # Fix some django issues we ran into
 from dashboard_app.patches import patch
@@ -364,6 +366,9 @@ 
     def get_absolute_url(self):
         return ("dashboard_app.views.bundle_detail", [self.bundle_stream.pathname, self.content_sha1])
 
+    def get_permalink(self):
+        return reverse("dashboard_app.views.redirect_to_bundle", args=[self.content_sha1])
+
     def save(self, *args, **kwargs):
         if self.content:
             try:
@@ -749,6 +754,9 @@ 
                  self.bundle.content_sha1,
                  self.analyzer_assigned_uuid])
 
+    def get_permalink(self):
+        return reverse("dashboard_app.views.redirect_to_test_run", args=[self.analyzer_assigned_uuid])
+
     def get_summary_results(self):
         stats = self.test_results.values('result').annotate(
             count=models.Count('result')).order_by()
@@ -932,6 +940,20 @@ 
     def __unicode__(self):
         return "Result {0}/{1}".format(self.test_run.analyzer_assigned_uuid, self.relative_index)
 
+    @models.permalink
+    def get_absolute_url(self):
+        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 get_permalink(self):
+        return reverse("dashboard_app.views.redirect_to_test_result",
+                       args=[self.test_run.analyzer_assigned_uuid,
+                             self.relative_index])
+
     @property
     def result_code(self):
         """
@@ -973,15 +995,6 @@ 
 
     duration = property(_get_duration, _set_duration)
 
-    @models.permalink
-    def get_absolute_url(self):
-        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):
         """
         Check if there is a log file attached to the test run that has
@@ -1001,6 +1014,105 @@ 
         order_with_respect_to = 'test_run'
 
 
+class DataView(RepositoryItem):
+    """
+    Data view, a container for SQL query and optional arguments
+    """
+
+    repository = DataViewRepository()
+    
+    def __init__(self, name, backend_queries, arguments, documentation, summary):
+        self.name = name
+        self.backend_queries = backend_queries
+        self.arguments = arguments
+        self.documentation = documentation
+        self.summary = summary
+
+    def __unicode__(self):
+        return self.name
+
+    def __repr__(self):
+        return "<DataView name=%r>" % (self.name,)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ("dashboard_app.views.data_view_detail", [self.name])
+
+    def _get_connection_backend_name(self, connection):
+        backend = str(type(connection))
+        if "sqlite" in backend:
+            return "sqlite"
+        elif "postgresql" in backend:
+            return "postgresql"
+        else:
+            return ""
+
+    def get_backend_specific_query(self, connection):
+        """
+        Return BackendSpecificQuery for the specified connection
+        """
+        sql_backend_name = self._get_connection_backend_name(connection)
+        try:
+            return self.backend_queries[sql_backend_name]
+        except KeyError:
+            return self.backend_queries.get(None, None)
+
+    def lookup_argument(self, name):
+        """
+        Return Argument with the specified name
+
+        Raises LookupError if the argument cannot be found
+        """
+        for argument in self.arguments:
+            if argument.name == name:
+                return argument
+        raise LookupError(name)
+
+    @classmethod
+    def get_connection(cls):
+        """
+        Get the appropriate connection for data views
+        """
+        from django.db import connection, connections
+        from django.db.utils import ConnectionDoesNotExist
+        try:
+            return connections['dataview']
+        except ConnectionDoesNotExist:
+            logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed")
+            return connection  # NOTE: it's connection not connectionS (the default connection)
+
+    def __call__(self, connection, **arguments):
+        # Check if arguments have any bogus names
+        valid_arg_names = frozenset([argument.name for argument in self.arguments])
+        for arg_name in arguments:
+            if arg_name not in valid_arg_names:
+                raise TypeError("Data view %s has no argument %r" % (self.name, arg_name))
+        # Get the SQL template for our database connection
+        query = self.get_backend_specific_query(connection)
+        if query is None:
+            raise LookupError("Specified data view has no SQL implementation "
+                              "for current database")
+        # Replace SQL aruments with django placeholders (connection agnostic)
+        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)
+            for arg_name in query.argument_list]
+        with contextlib.closing(connection.cursor()) as cursor:
+            # Execute the query with the specified arguments
+            cursor.execute(sql, sql_args)
+            # Get and return the results
+            rows = cursor.fetchall()
+            columns = cursor.description
+            return rows, columns
+
+
 class DataReport(RepositoryItem):
     """
     Data reports are small snippets of xml that define
@@ -1013,6 +1125,16 @@ 
         self._html = None
         self._data = kwargs
 
+    def __unicode__(self):
+        return self.title
+
+    def __repr__(self):
+        return "<DataReport name=%r>" % (self.name,)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ("dashboard_app.views.report_detail", [self.name])
+
     def _get_raw_html(self):
         pathname = os.path.join(self.base_path, self.path)
         try:
@@ -1041,13 +1163,6 @@ 
             self._html = template.render(context)
         return self._html
 
-    def __unicode__(self):
-        return self.title
-
-    @models.permalink
-    def get_absolute_url(self):
-        return ("dashboard_app.views.report_detail", [self.name])
-
     @property
     def title(self):
         return self._data['title']
@@ -1067,3 +1182,90 @@ 
     @property
     def author(self):
         return self._data.get('author')
+
+
+class ImageHealth(object):
+
+    def __init__(self, rootfs_type, hwpack_type):
+        self.rootfs_type = rootfs_type
+        self.hwpack_type = hwpack_type
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ("dashboard_app.views.image_status_detail", [
+            self.rootfs_type, self.hwpack_type])
+
+    def get_tests(self):
+        return Test.objects.filter(test_runs__in=self.get_test_runs()).distinct()
+
+    def current_health_for_test(self, test):
+        test_run = self.get_current_test_run(test)
+        test_results = test_run.test_results
+        fail_count = test_results.filter(
+            result=TestResult.RESULT_FAIL).count()
+        pass_count = test_results.filter(
+            result=TestResult.RESULT_PASS).count()
+        total_count = test_results.count()
+        return {
+            "test_run": test_run,
+            "total_count": total_count,
+            "fail_count": fail_count,
+            "pass_count": pass_count,
+            "other_count": total_count - (fail_count + pass_count),
+            "fail_percent": fail_count * 100.0 / total_count if total_count > 0 else None,
+        }
+    
+    def overall_health_for_test(self, test):
+        test_run_list = self.get_all_test_runs_for_test(test)
+        test_result_list = TestResult.objects.filter(test_run__in=test_run_list)
+        fail_result_list = test_result_list.filter(result=TestResult.RESULT_FAIL)
+        pass_result_list = test_result_list.filter(result=TestResult.RESULT_PASS)
+
+        total_count = test_result_list.count()
+        fail_count = fail_result_list.count()
+        pass_count = pass_result_list.count()
+        fail_percent = fail_count * 100.0 / total_count if total_count > 0 else None
+        return {
+            "total_count": total_count,
+            "total_run_count": test_run_list.count(),
+            "fail_count": fail_count,
+            "pass_count": pass_count,
+            "other_count": total_count - fail_count - pass_count,
+            "fail_percent": fail_percent,
+        }
+
+    def get_test_runs(self):
+        return TestRun.objects.filter(
+            bundle__bundle_stream__pathname="/anonymous/lava-daily/"
+        ).filter(
+            attributes__name='rootfs.type',
+            attributes__value=self.rootfs_type
+        ).filter(
+            attributes__name='hwpack.type',
+            attributes__value=self.hwpack_type)
+
+    def get_current_test_run(self, test):
+        return self.get_all_test_runs_for_test(test).order_by('-analyzer_assigned_date')[0]
+
+    def get_all_test_runs_for_test(self, test):
+        return self.get_test_runs().filter(test=test)
+
+    @classmethod
+    def get_rootfs_list(self):
+        rootfs_list = [
+            attr['value'] 
+            for attr in NamedAttribute.objects.filter(
+                name='rootfs.type').values('value').distinct()]
+        try:
+            rootfs_list.remove('android')
+        except ValueError:
+            pass
+        return rootfs_list
+
+    @classmethod
+    def get_hwpack_list(self):
+        hwpack_list = [
+            attr['value'] 
+            for attr in NamedAttribute.objects.filter(
+                name='hwpack.type').values('value').distinct()]
+        return hwpack_list

=== modified file 'dashboard_app/repositories/__init__.py'
--- dashboard_app/repositories/__init__.py	2011-05-03 22:07:19 +0000
+++ dashboard_app/repositories/__init__.py	2011-07-22 00:04:21 +0000
@@ -32,7 +32,8 @@ 
     """
 
     def __new__(mcls, name, bases, namespace):
-        cls = super(RepositoryItemMeta, mcls).__new__(mcls, name, bases, namespace)
+        cls = super(RepositoryItemMeta, mcls).__new__(
+            mcls, name, bases, namespace)
         if "repository" in namespace:
             repo = cls.repository
             repo.item_cls = cls
@@ -46,7 +47,7 @@ 
     Each repository item is loaded from a XML file.
     """
 
-    __metaclass__ = RepositoryItemMeta 
+    __metaclass__ = RepositoryItemMeta
 
     _base_path = None
 
@@ -57,7 +58,6 @@ 
     def base_path(self):
         return self._base_path
 
-
     class DoesNotExist(Exception):
         pass
 
@@ -89,12 +89,12 @@ 
 
     def all(self):
         return self
-    
+
     def get(self, **kwargs):
         query = self.filter(**kwargs)
         if len(query) == 1:
             return query[0]
-        if not query: 
+        if not query:
             raise self.model.DoesNotExist()
         else:
             raise self.model.MultipleValuesReturned()
@@ -127,7 +127,7 @@ 
     __metaclass__ = abc.ABCMeta
 
     def __init__(self):
-        self.item_cls = None # later patched by RepositoryItemMeta
+        self.item_cls = None  # later patched by RepositoryItemMeta
         self._items = []
         self._did_load = False
 
@@ -154,9 +154,10 @@ 
         try:
             items = os.listdir(directory)
         except (OSError, IOError) as exc:
-            logging.exception("Unable to enumreate directory: %s: %s", directory, exc)
+            logging.exception("Unable to enumreate directory: %s: %s",
+                              directory, exc)
         else:
-            for name in items: 
+            for name in items:
                 pathname = os.path.join(directory, name)
                 if os.path.isfile(pathname) and pathname.endswith(".xml"):
                     self.load_from_file(pathname)
@@ -176,7 +177,8 @@ 
             item._load_from_external_representation(pathname)
             self._items.append(item)
         except Exception as exc:
-            logging.exception("Unable to load object into repository %s: %s", pathname, exc)
+            logging.exception("Unable to load object into repository %s: %s",
+                              pathname, exc)
 
     @abc.abstractproperty
     def settings_variable(self):

=== added file 'dashboard_app/repositories/common.py'
--- dashboard_app/repositories/common.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/repositories/common.py	2011-07-22 00:05:03 +0000
@@ -0,0 +1,47 @@ 
+# Copyright (C) 2011 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 xml.sax.handler import ContentHandler
+import re
+
+
+class BaseContentHandler(ContentHandler):
+
+    def _end_text(self):
+        """
+        Stop collecting text and produce a stripped string with de-duplicated
+        whitespace
+        """
+        full_text = re.sub("\s+", " ", u''.join(self._text)).strip()
+        self.text = None
+        return full_text
+        
+    def _start_text(self):
+        """
+        Start collecting text
+        """
+        self._text = []
+            
+    def characters(self, content):
+        if isinstance(self._text, list):
+            self._text.append(content)
+
+    def startDocument(self):
+        # Text can be None or a [] that accumulates all detected text
+        self._text = None

=== modified file 'dashboard_app/repositories/data_report.py'
--- dashboard_app/repositories/data_report.py	2011-05-03 18:16:04 +0000
+++ dashboard_app/repositories/data_report.py	2011-07-22 00:07:21 +0000
@@ -18,34 +18,19 @@ 
 
 
 from xml.sax import parseString
-from xml.sax.handler import ContentHandler
-import re
 
 from dashboard_app.repositories import Repository, Undefined, Object
-
-
-class _DataReportHandler(ContentHandler):
+from dashboard_app.repositories.common import BaseContentHandler
+
+
+class _DataReportHandler(BaseContentHandler):
     """
-    ContentHandler subclass for parsing DataView documents
+    ContentHandler subclass for parsing DataReport documents
     """
     
-    def _end_text(self):
-        """
-        Stop collecting text and produce a stripped string with deduplicated whitespace
-        """
-        full_text = re.sub("\s+", " ", u''.join(self._text)).strip()
-        self.text = None
-        return full_text
-        
-    def _start_text(self):
-        """
-        Start collecting text
-        """
-        self._text = []
-
     def startDocument(self):
-        # Text can be None or a [] that accumulates all detected text
-        self._text = None
+        # Classic-classes 
+        BaseContentHandler.startDocument(self)
         # Data report object
         self.obj = Object()
 
@@ -70,10 +55,6 @@ 
             self.obj.title = self._end_text()
         elif name == "path":
             self.obj.path = self._end_text()
-            
-    def characters(self, content):
-        if isinstance(self._text, list):
-            self._text.append(content)
 
 
 class DataReportRepository(Repository):

=== added file 'dashboard_app/repositories/data_view.py'
--- dashboard_app/repositories/data_view.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/repositories/data_view.py	2011-07-22 00:11:36 +0000
@@ -0,0 +1,133 @@ 
+# Copyright (C) 2011 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/>.
+
+"""
+DataViews: Encapsulated SQL query definitions.
+
+Implementation of the following launchpad blueprint:
+https://blueprints.launchpad.net/launch-control/+spec/other-linaro-n-data-views-for-launch-control
+"""
+
+
+from xml.sax import parseString
+
+from dashboard_app.repositories import Repository, Undefined, Object
+from dashboard_app.repositories.common import BaseContentHandler
+
+
+class _DataViewHandler(BaseContentHandler):
+
+    """
+    ContentHandler subclass for parsing DataView documents
+    """
+    
+    def startDocument(self):
+        # Classic-classes 
+        BaseContentHandler.startDocument(self)
+        # Data view object
+        self.obj = Object()
+        # Set default values
+        self.obj.name = Undefined
+        self.obj.backend_queries = {}
+        self.obj.arguments = []
+        self.obj.documentation = None
+        self.obj.summary = None
+        # Internal variables
+        self._current_backend_query = None
+
+    def endDocument(self):
+        # TODO: check if we have anything defined
+        if self.obj.name is Undefined:
+            raise ValueError("No data view definition found")
+
+    def startElement(self, name, attrs):
+        if name == "data-view":
+            self.obj.name = attrs["name"]
+        elif name == "summary" or name == "documentation":
+            self._start_text()
+        elif name == "sql":
+            self._start_text()
+            self._current_backend_query = BackendSpecificQuery(
+                attrs.get("backend"), None, [])
+            self.obj.backend_queries[
+                self._current_backend_query.backend] = self._current_backend_query
+        elif name == "value":
+            if "name" not in attrs:
+                raise ValueError("<value> requires attribute 'name'")
+            self._text.append("{" + attrs["name"] + "}")
+            self._current_backend_query.argument_list.append(attrs["name"])
+        elif name == "argument":
+            if "name" not in attrs:
+                raise ValueError("<argument> requires attribute 'name'")
+            if "type" not in attrs:
+                raise ValueError("<argument> requires attribute 'type'")
+            if attrs["type"] not in ("string", "number", "boolean", "timestamp"):
+                raise ValueError("invalid value for argument 'type' on <argument>")
+            argument = Argument(name=attrs["name"], type=attrs["type"],
+                                   default=attrs.get("default", None),
+                                   help=attrs.get("help", None))
+            self.obj.arguments.append(argument)
+                
+    def endElement(self, name):
+        if name == "sql":
+            self._current_backend_query.sql_template = self._end_text()
+            self._current_backend_query = None
+        elif name == "documentation":
+            self.obj.documentation = self._end_text()
+        elif name == "summary":
+            self.obj.summary = self._end_text()
+
+
+class Argument(object):
+    """
+    Data view argument for SQL prepared statements
+    """
+
+    def __init__(self, name, type, default, help):
+        self.name = name
+        self.type = type
+        self.default = default
+        self.help = help 
+
+
+class BackendSpecificQuery(object):
+    """
+    Backend-specific query and argument list
+    """
+
+    def __init__(self, backend, sql_template, argument_list):
+        self.backend = backend
+        self.sql_template = sql_template
+        self.argument_list = argument_list
+
+
+class DataViewRepository(Repository):
+
+    @property
+    def settings_variable(self):
+        return "DATAVIEW_DIRS"
+
+    def load_from_xml_string(self, text):
+        handler = _DataViewHandler()
+        parseString(text, handler)
+        return self.item_cls(**handler.obj.__dict__)
+
+
+__all__ = [
+    "DataViewRepository",
+]

=== modified file 'dashboard_app/static/js/jquery.dashboard.js'
--- dashboard_app/static/js/jquery.dashboard.js	2011-07-09 15:09:19 +0000
+++ dashboard_app/static/js/jquery.dashboard.js	2011-07-19 12:20:02 +0000
@@ -76,7 +76,7 @@ 
           }
           if (column.name == "UUID") {
             /* This is a bit hacky but will work for now */
-            cell_link = _url + ".." + "/test-runs/" + cell + "/";
+            cell_link = _url + ".." + "/permalink/test-run/" + cell + "/";
           }
           html += "<td>";
           if (cell_link) {

=== modified file 'dashboard_app/templates/dashboard_app/_extension_navigation.html'
--- dashboard_app/templates/dashboard_app/_extension_navigation.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/_extension_navigation.html	2011-07-22 00:57:44 +0000
@@ -1,6 +1,8 @@ 
 {% load i18n %}
 <div id="lava-server-extension-navigation" class="lava-server-sub-toolbar">
   <ul>
+    <li><a href="{% url dashboard_app.views.image_status_list %}"
+      >{% trans "Image Status" %}</a></li>
     <li><a href="{% url dashboard_app.views.bundle_stream_list %}"
       >{% trans "Bundle Streams" %}</a></li>
     <li><a href="{% url dashboard_app.views.test_list %}"

=== modified file 'dashboard_app/templates/dashboard_app/bundle_detail.html'
--- dashboard_app/templates/dashboard_app/bundle_detail.html	2011-07-13 11:07:18 +0000
+++ dashboard_app/templates/dashboard_app/bundle_detail.html	2011-07-18 16:29:23 +0000
@@ -11,6 +11,20 @@ 
 
 
 {% block content %}
+<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>{% trans "Note:" %}</strong>
+    {% blocktrans %}
+    You can navigate to this bundle, regardless of the bundle stream it is
+    located in, by using this
+    {% endblocktrans %}
+    <a href="{{ bundle.get_permalink }}" >{% trans "permalink" %}</a>
+  </div>
+</div>
+<br/>
 <script type="text/javascript">
   $(document).ready(function() {
     $("#tabs").tabs({

=== modified file 'dashboard_app/templates/dashboard_app/data_view_list.html'
--- dashboard_app/templates/dashboard_app/data_view_list.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/data_view_list.html	2011-07-22 00:17:19 +0000
@@ -34,7 +34,7 @@ 
   <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><a href="{{ data_view.get_absolute_url }}">{{ data_view.name }}</a></td>
       <td>{{ data_view.summary }}</td>
     </tr>
     {% endfor %}

=== added file 'dashboard_app/templates/dashboard_app/image_status_detail.html'
--- dashboard_app/templates/dashboard_app/image_status_detail.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/image_status_detail.html	2011-07-22 00:57:44 +0000
@@ -0,0 +1,71 @@ 
+{% extends "dashboard_app/_content.html" %}
+{% load call %}
+
+{% 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 rowspan="2">Test</th>
+      <th colspan="5">Totals</th>
+      <th colspan="4" style="border-left: 1px solid black">Most Recent Test Run</th>
+      <th rowspan="2" style="width: 30%">Description</th>
+    </th>
+    <tr>
+      <th>PASS</th>
+      <th>FAIL</th>
+      <th>FAIL rate</th>
+      <th>Test Runs</th>
+      <th>Test Results</th>
+      <th>PASS</th>
+      <th>FAIL</th>
+      <th>FAIL rate</th>
+      <th>Test Results</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for test in image_health.get_tests %}
+    {% call image_health.current_health_for_test test as current_test_health %}
+    {% call image_health.overall_health_for_test test as overall_test_health %}
+    <tr
+      {% if current_test_health.fail_count > 0 %}
+        {% if current_test_health.pass_count == 0 %}
+          style="background-color: rgba(255, 0, 0, 0.5)"
+        {% else %}
+          style="background-color: rgba(255, 165, 0, 0.5)"
+        {% endif %}
+      {% else %}
+        {% if current_test_health.pass_count > 0 %}
+          style="background-color: rgba(173, 255, 47, 0.5)"
+        {% endif %}
+      {% endif %}
+      >
+      <td>{{ test.test_id }}</td>
+      <td>{{ overall_test_health.pass_count }}</td>
+      <td>{{ overall_test_health.fail_count }}</td>
+      <td>{{ overall_test_health.fail_percent|default_if_none:0|floatformat }}%</td>
+      <td><a 
+          href="{% url dashboard_app.views.image_test_history image_health.rootfs_type image_health.hwpack_type test.test_id %}"
+          >{{ overall_test_health.total_run_count }}</a></td>
+      <td>{{ overall_test_health.total_count }}</td>
+      <td>{{ current_test_health.pass_count|default:0 }}</td>
+      <td>{{ current_test_health.fail_count|default:0 }}</td>
+      <td>{{ current_test_health.fail_percent|default_if_none:0|floatformat }}%</td>
+      <td><a 
+          href="{{ current_test_health.test_run.get_absolute_url }}"
+          >{{ current_test_health.total_count|default:0 }}</a></td>
+      <td>{{ test.name|default:"<em>not set</em>" }}</td>
+    </tr>
+    {% endcall %}
+    {% endcall %}
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/image_status_list.html'
--- dashboard_app/templates/dashboard_app/image_status_list.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/image_status_list.html	2011-07-22 00:57:44 +0000
@@ -0,0 +1,38 @@ 
+{% extends "dashboard_app/_content.html" %}
+{% load call %}
+
+
+{% block content %}
+<script type="text/javascript" charset="utf-8"> 
+  $(document).ready(function() {
+    oTable = $('#LEBs').dataTable({
+      "bJQueryUI": true,
+      "bPaginate": false,
+    });
+  });
+</script> 
+<table class="demo_jui display" id="LEBs">
+  <thead>
+    <tr>
+      <th></th>
+      {% for hwpack in hwpack_list %}
+      <th>{{ hwpack }}</th>
+      {% endfor %}
+    </tr>
+  </thead>
+  <tbody>
+    {% for rootfs in rootfs_list %}
+    <tr>
+      <th>{{ rootfs }}</th>
+      {% for hwpack in hwpack_list %}
+      <td>
+        {% call ImageHealth rootfs hwpack as image_health %}
+        <a href="{{ image_health.get_absolute_url }}">{{ image_health.get_test_runs.count }} test runs</a>
+        {% endcall %}
+      </td>
+      {% endfor %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/image_test_history.html'
--- dashboard_app/templates/dashboard_app/image_test_history.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/image_test_history.html	2011-07-22 00:57:44 +0000
@@ -0,0 +1,15 @@ 
+{% extends "dashboard_app/_content.html" %}
+
+
+{% block content %}
+<script type="text/javascript">
+  $(document).ready(function() {
+    $('#test_runs').dataTable({
+      bJQueryUI: true,
+      sPaginationType: "full_numbers",
+      aaSorting: [[0, "desc"]],
+    });
+  });
+</script>
+{% include "dashboard_app/_test_run_list_table.html" %}
+{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/index.html'
--- dashboard_app/templates/dashboard_app/index.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/index.html	2011-07-22 01:09:09 +0000
@@ -1,10 +1,58 @@ 
 {% 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>
+<h1>Welcome</h1>
+<p>The <em>Validation Dashboard</em> is your window to
+test results, regardless of how your run your tests you
+can upload the results here and analyze them with simple
+built-in reports as well as arbitrary custom reports and
+data mining queries.</p>
+
+<h2>Key Features</h2>
+<ul>
+  <li>Online repository of test results, with simple to use, web APIs and
+  command line tools for uploading test results.</li>
+  <li>Test results are packaged in documents (bundles) that you can easily sync
+  across systems, model is similar to the one used by git</li>
+  <li>Test results can refer to software and hardware context so that you know
+  exactly what software and hardware combination fails</li>
+  <li>Data mining and reporting allows users to create custom tailored reports
+  based on the data in the system</li>
+  <li>Distributed work-flow model, with some data privacy out of the box, fully
+  private installation can be deployed in minutes.</li>
+</ul>
+
+<h2>Documentation &amp; Get Started</h2>
+<p>To get started quickly follow the link below, if you feel that an important
+content is missing please <a
+  href="https://bugs.launchpad.net/lava-dashboard/+filebug"
+  >report a bug</a> or <a
+  href="https://answers.launchpad.net/lava-dashboard/+addquestion"
+  >ask a question</a>. Please make sure to report dashboard version (you are
+currently using version {{lava.extensions.as_mapping.dashboard_app.version}})</p>
+<p>All documentation is hosted on <a
+  href="http://readthedocs.org/docs/lava-dashboard/en/latest/">ReadTheDocs.org</a>.</p>
+
+<h3>Developers</h3>
+<ul>
+  <li>How to put test results of my test suite into the Dashboard?</li>
+  <li>How to integrate my testing toolkit with the Dashboard?</li>
+  <li>How to allow users of my application to send anonymous qualitative and
+  quantitative (tests and benchmarks) data from their systems?</li>
+</ul>
+
+<h3>Managers</h3>
+<ul>
+  <li>What kind of reporting features are available out of the box?</li>
+  <li>How to create additional reports?</li>
+  <li>What kind of data is available in the system</li>
+</ul>
+
+<h3>System Administrators</h3>
+<ul>
+  <li>System requirements</li>
+  <li>How to deploy or upgrade the dashboard?</li>
+  <li>How to backup and restore the data</li>
 </ul>
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/report_detail.html'
--- dashboard_app/templates/dashboard_app/report_detail.html	2011-07-13 11:07:18 +0000
+++ dashboard_app/templates/dashboard_app/report_detail.html	2011-07-19 21:46:36 +0000
@@ -48,3 +48,12 @@ 
   });
 </script>
 {% endblock %}
+
+{% block body %}
+{% if is_iframe %}
+{{ report.get_html|safe}}
+{% else %}
+{{ block.super }}
+{% endif %}
+{% endblock %}
+

=== modified file 'dashboard_app/templates/dashboard_app/test_result_detail.html'
--- dashboard_app/templates/dashboard_app/test_result_detail.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/test_result_detail.html	2011-07-18 16:29:23 +0000
@@ -4,15 +4,18 @@ 
 
 
 {% block sidebar %}
-<h3>{% trans "Hints" %}</h3>
-<p class="hint">
-{% blocktrans %}
-This is all the information that launch control has about this result. Log
-analyzers can provide additional information by scrubbing it from the log file.
-Information that is global to a test run can be attached to test run attributes
-instead.
-{% endblocktrans %}
-</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> This is all the information that the dashboard has
+    about this result. Log analyzers can provide additional information by
+    scrubbing it from the log file.  Information that is global to a test run
+    can be attached to test run attributes instead.
+  </div>
+</div>
+<br/>
 {% 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>
@@ -42,11 +45,25 @@ 
 
 
 {% block content %}
-<h2>{% trans "Detailed information about test result" %}</h2>
-<p>{% trans "Launch Control has the following information about this test result" %}</p>
+<h2>{% trans "Test Result Details" %}</h2>
 <dl>
   <dt>{% trans "Result ID:" %}</dt>
-  <dd>{{ test_result }}</dd>
+  <dd>
+  {{ test_result }}
+  <div class="ui-widget" style="width: 30em">
+    <div class="ui-state-highlight ui-corner-all" style="padding: 0.7em">
+      <span
+        class="ui-icon ui-icon-info"
+        style="float: left; margin-right: 0.3em;"></span>
+      <strong>{% trans "Note:" %}</strong>
+      {% blocktrans %}
+      You can navigate to this test result, regardless of the bundle stream it is
+      located in, by using this
+      {% endblocktrans %}
+      <a href="{{ test_result.get_permalink }}" >{% trans "permalink" %}</a>
+    </div>
+  </div>
+  </dd> 
   <dt>{% trans "Test case:" %}</dt>
   <dd>
   {% if test_result.test_case %}
@@ -54,7 +71,7 @@ 
   {% else %}
   <i>{% trans "unknown test case" %}</i>
   {% endif %}
-  {% trans "from test" %} <b>{{ test_result.test_run.test }}</b>
+  {% trans "from test" %} <b><a href="{{ test_result.test_run.test.get_absolute_url }}">{{ test_result.test_run.test }}</a></b>
   </dd>
   <dt>{% trans "Test outcome:" %}</dt>
   <dd>{{ test_result.get_result_display }}</dd>

=== modified file 'dashboard_app/templates/dashboard_app/test_run_detail.html'
--- dashboard_app/templates/dashboard_app/test_run_detail.html	2011-07-13 17:28:46 +0000
+++ dashboard_app/templates/dashboard_app/test_run_detail.html	2011-07-18 16:29:23 +0000
@@ -43,9 +43,9 @@ 
 {% block sidebar %}
 <dl>
   <dt>{% trans "Test Run UUID" %}</dt>
-  <dd>{{ test_run.analyzer_assigned_uuid }}</dd>
+  <dd>{{ test_run.analyzer_assigned_uuid }} <a href="{% url dashboard_app.views.redirect_to_test_run test_run.analyzer_assigned_uuid %}">{% trans "permalink" %}</a></dd>
   <dt>{% trans "Test Name" %}</dt>
-  <dd>{{ test_run.test }}</dd>
+  <dd><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test }}</a></dd>
   <dt>{% trans "OS Distribution" %}</dt>
   <dd>{{ test_run.sw_image_desc|default:"<i>Unspecified</i>" }}</dd>
   <dt>{% trans "Bundle SHA1" %}</dt>

=== added file 'dashboard_app/templatetags/call.py'
--- dashboard_app/templatetags/call.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/templatetags/call.py	2011-07-22 00:29:54 +0000
@@ -0,0 +1,64 @@ 
+from django import template
+
+
+register = template.Library()
+
+
+class CallNode(template.Node):
+    def __init__(self, func, args, name, nodelist):
+        self.func = func
+        self.args = args
+        self.name = name
+        self.nodelist = nodelist
+
+    def __repr__(self):
+        return "<CallNode>"
+
+    def _lookup_func(self, context):
+        parts = self.func.split('.')
+        current = context[parts[0]] 
+        for part in parts[1:]:
+            if part.startswith("_"):
+                raise ValueError(
+                    "Function cannot traverse private implementation attributes")
+            current = getattr(current, part)
+        return current
+
+    def render(self, context):
+        try:
+            func = self._lookup_func(context) 
+            values = [template.Variable(arg).resolve(context) for arg in self.args]
+            context.push()
+            context[self.name] = func(*values)
+            output = self.nodelist.render(context)
+            context.pop()
+            return output
+        except Exception as ex:
+            import logging
+            logging.exception("Unable to call %s with %r: %s",
+                              self.func, self.args, ex)
+            raise
+
+def do_call(parser, token):
+    """
+    Adds a value to the context (inside of this block) for caching and easy
+    access.
+
+    For example::
+
+        {% call func 1 2 3 as result %}
+            {{ result }}
+        {% endcall %}
+    """
+    bits = list(token.split_contents())
+    if len(bits) < 2 or bits[-2] != "as":
+        raise template.TemplateSyntaxError(
+            "%r expected format is 'call func [args] [as name]'" % bits[0])
+    func = bits[1]
+    args = bits[2:-2]
+    name = bits[-1]
+    nodelist = parser.parse(('endcall',))
+    parser.delete_first_token()
+    return CallNode(func, args, name, nodelist)
+
+do_call = register.tag('call', do_call)

=== modified file 'dashboard_app/tests/__init__.py'
--- dashboard_app/tests/__init__.py	2011-07-13 11:24:08 +0000
+++ dashboard_app/tests/__init__.py	2011-07-22 00:55:51 +0000
@@ -24,11 +24,11 @@ 
     'other.deserialization',
     'other.login',
     'other.test_client',
-    'other.xml_rpc',
     'regressions.LP658917',
     'views.bundle_stream_list_view',
     'views.test_run_detail_view',
     'views.test_run_list_view',
+    'views.redirects',
 ]
 
 def load_tests_from_submodules(_locals):

=== modified file 'dashboard_app/tests/other/dataview.py'
--- dashboard_app/tests/other/dataview.py	2011-07-07 11:38:53 +0000
+++ dashboard_app/tests/other/dataview.py	2011-07-22 00:29:54 +0000
@@ -16,12 +16,10 @@ 
 # 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/>.
 
-import unittest
-
 from mocker import Mocker, expect
 from testtools import TestCase
 
-from dashboard_app.dataview import DataView
+from dashboard_app.models import DataView
 
 
 class DataViewHandlerTests(TestCase):
@@ -45,7 +43,7 @@ 
 
     def setUp(self):
         super(DataViewHandlerTests, self).setUp()
-        self.dataview = DataView.load_from_xml(self.text) 
+        self.dataview = DataView.repository.load_from_xml_string(self.text) 
 
     def test_name_parsed_ok(self):
         self.assertEqual(self.dataview.name, "foo")
@@ -97,7 +95,6 @@ 
         Test for DataView.get_connection()
         """
         # Mock connections['dataview'] to return special connection 
-        from django.db.utils import ConnectionDoesNotExist
         mocker = Mocker()
         connections = mocker.replace("django.db.connections")
         special_connection = mocker.mock()

=== removed file 'dashboard_app/tests/other/xml_rpc.py'
--- dashboard_app/tests/other/xml_rpc.py	2011-05-23 17:02:43 +0000
+++ dashboard_app/tests/other/xml_rpc.py	1970-01-01 00:00:00 +0000
@@ -1,132 +0,0 @@ 
-# 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/>.
-
-"""
-Unit tests of the Dashboard application
-"""
-import xmlrpclib
-
-from django_testscenarios.ubertest import TestCaseWithScenarios
-
-from dashboard_app.dispatcher import (
-        DjangoXMLRPCDispatcher,
-        FaultCodes,
-        xml_rpc_signature,
-        )
-
-
-class TestAPI(object):
-    """
-    Test API that gets exposed by the dispatcher for test runs.
-    """
-
-    @xml_rpc_signature()
-    def ping(self):
-        """
-        Return "pong" message
-        """
-        return "pong"
-
-    def echo(self, arg):
-        """
-        Return the argument back to the caller
-        """
-        return arg
-
-    def boom(self, code, string):
-        """
-        Raise a Fault exception with the specified code and string
-        """
-        raise xmlrpclib.Fault(code, string)
-
-    def internal_boom(self):
-        """
-        Raise a regular python exception (this should be hidden behind
-        an internal error fault)
-        """
-        raise Exception("internal boom")
-
-
-class DjangoXMLRPCDispatcherTestCase(TestCaseWithScenarios):
-
-    def setUp(self):
-        super(DjangoXMLRPCDispatcherTestCase, self).setUp()
-        self.dispatcher = DjangoXMLRPCDispatcher()
-        self.dispatcher.register_instance(TestAPI())
-
-    def xml_rpc_call(self, method, *args):
-        """
-        Perform XML-RPC call on our internal dispatcher instance
-
-        This calls the method just like we would have normally from our view.
-        All arguments are marshaled and un-marshaled. XML-RPC fault exceptions
-        are raised like normal python exceptions (by xmlrpclib.loads)
-        """
-        request = xmlrpclib.dumps(tuple(args), methodname=method)
-        response = self.dispatcher._marshaled_dispatch(request)
-        # This returns return value wrapped in a tuple and method name
-        # (which we don't have here as this is a response message).
-        return xmlrpclib.loads(response)[0][0]
-
-
-class DjangoXMLRPCDispatcherTests(DjangoXMLRPCDispatcherTestCase):
-
-    def test_standard_fault_code_for_missing_method(self):
-        try:
-            self.xml_rpc_call("method_that_hopefully_does_not_exist")
-        except xmlrpclib.Fault as ex:
-            self.assertEqual(
-                    ex.faultCode,
-                    FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND)
-        else:
-            self.fail("Calling missing method did not raise an exception")
-
-    def test_ping(self):
-        retval = self.xml_rpc_call("ping")
-        self.assertEqual(retval, "pong")
-
-    def test_echo(self):
-        self.assertEqual(self.xml_rpc_call("echo", 1), 1)
-        self.assertEqual(self.xml_rpc_call("echo", "string"), "string")
-        self.assertEqual(self.xml_rpc_call("echo", 1.5), 1.5)
-
-    def test_boom(self):
-        self.assertRaises(xmlrpclib.Fault,
-                self.xml_rpc_call, "boom", 1, "str")
-
-
-class DjangoXMLRPCDispatcherFaultCodeTests(DjangoXMLRPCDispatcherTestCase):
-
-    scenarios = [
-            ('method_not_found', {
-                'method': "method_that_hopefully_does_not_exist",
-                'faultCode': FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND,
-                }),
-            ('internal_error', {
-                'method': "internal_boom",
-                'faultCode': FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR,
-                }),
-            ]
-
-    def test_standard_fault_codes(self):
-        try:
-            self.xml_rpc_call(self.method)
-        except xmlrpclib.Fault as ex:
-            self.assertEqual(ex.faultCode, self.faultCode)
-        else:
-            self.fail("Exception not raised")

=== modified file 'dashboard_app/tests/utils.py'
--- dashboard_app/tests/utils.py	2011-05-30 16:53:27 +0000
+++ dashboard_app/tests/utils.py	2011-07-22 00:29:54 +0000
@@ -1,7 +1,6 @@ 
 """
 Django-specific test utilities
 """
-import os
 import xmlrpclib
 
 from django.conf import settings

=== added file 'dashboard_app/tests/views/redirects.py'
--- dashboard_app/tests/views/redirects.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/tests/views/redirects.py	2011-07-18 16:29:23 +0000
@@ -0,0 +1,75 @@ 
+# 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
+from django_testscenarios.ubertest import TestCase
+
+from dashboard_app.tests import fixtures
+
+
+class RedirectTests(TestCase):
+
+    _PATHNAME = "/anonymous/"
+    _BUNDLE_TEXT = """
+{
+  "test_runs": [
+    {
+      "test_results": [
+        {
+          "test_case_id": "test-case-0", 
+          "result": "pass"
+        } 
+      ], 
+      "analyzer_assigned_date": "2010-10-15T22:04:46Z", 
+      "time_check_performed": false, 
+      "analyzer_assigned_uuid": "00000000-0000-0000-0000-000000000001",
+      "test_id": "examples"
+    }
+  ], 
+  "format": "Dashboard Bundle Format 1.0"
+}
+    """
+    _BUNDLE_NAME = "whatever.json"
+
+    def setUp(self):
+        super(RedirectTests, self).setUp()
+        self.bundle = fixtures.create_bundle(self._PATHNAME, self._BUNDLE_TEXT, self._BUNDLE_NAME)
+        self.bundle.deserialize()
+        self.assertTrue(self.bundle.is_deserialized)
+
+    def test_bundle_permalink(self):
+        response = self.client.get(
+            reverse("dashboard_app.views.redirect_to_bundle",
+                    args=(self.bundle.content_sha1, )))
+        self.assertRedirects(response, self.bundle.get_absolute_url())
+
+    def test_test_run_permalink(self):
+        test_run = self.bundle.test_runs.all()[0]
+        response = self.client.get(
+            reverse("dashboard_app.views.redirect_to_test_run",
+                    args=(test_run.analyzer_assigned_uuid, )))
+        self.assertRedirects(response, test_run.get_absolute_url())
+
+    def test_test_result_permalink(self):
+        test_run = self.bundle.test_runs.all()[0]
+        test_result = test_run.test_results.all()[0]
+        response = self.client.get(
+            reverse("dashboard_app.views.redirect_to_test_result",
+                    args=(test_run.analyzer_assigned_uuid,
+                          test_result.relative_index)))
+        self.assertRedirects(response, test_result.get_absolute_url())

=== modified file 'dashboard_app/tests/views/test_run_detail_view.py'
--- dashboard_app/tests/views/test_run_detail_view.py	2011-07-12 02:33:19 +0000
+++ dashboard_app/tests/views/test_run_detail_view.py	2011-07-22 00:29:54 +0000
@@ -20,7 +20,6 @@ 
 from django_testscenarios.ubertest import (TestCase, TestCaseWithScenarios)
 from dashboard_app.models import BundleStream, TestRun
 from django.contrib.auth.models import (User, Group)
-from django.core.urlresolvers import reverse
 
 from dashboard_app.tests.utils import TestClient
 

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2011-07-13 12:29:53 +0000
+++ dashboard_app/urls.py	2011-07-22 00:57:44 +0000
@@ -55,4 +55,10 @@ 
     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'),
+    url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'redirect_to_test_run'),
+    url(r'^permalink/test-result/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<relative_index>[0-9]+)/$', 'redirect_to_test_result'),
+    url(r'^permalink/bundle/(?P<content_sha1>[0-9a-z]+)/$', 'redirect_to_bundle'),
+    url(r'^image_status/$', 'image_status_list'),
+    url(r'^image_status/(?P<rootfs_type>[a-zA-Z0-9_-]+)\+(?P<hwpack_type>[a-zA-Z0-9_-]+)/$', 'image_status_detail'),
+    url(r'^image_status/(?P<rootfs_type>[a-zA-Z0-9_-]+)\+(?P<hwpack_type>[a-zA-Z0-9_-]+)/test-history/(?P<test_id>[^/]+)/$', 'image_test_history'),
 )

=== modified file 'dashboard_app/views.py'
--- dashboard_app/views.py	2011-07-13 17:28:57 +0000
+++ dashboard_app/views.py	2011-07-22 00:57:44 +0000
@@ -20,25 +20,21 @@ 
 Views for the Dashboard application
 """
 
-from django.contrib.auth.decorators import login_required
-from django.contrib.csrf.middleware import csrf_exempt
-from django.contrib.sites.models import Site
 from django.db.models.manager import Manager
 from django.db.models.query import QuerySet
-from django.http import (HttpResponse, Http404)
-from django.shortcuts import render_to_response
+from django.http import Http404
+from django.shortcuts import render_to_response, redirect, get_object_or_404
 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,
     Bundle,
     BundleStream,
     DataReport,
+    DataView,
+    ImageHealth,
     Test,
-    TestCase,
     TestResult,
     TestRun,
 )
@@ -276,7 +272,7 @@ 
 
 
 @BreadCrumb(
-    "Result {relative_index}",
+    "Details of 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):
@@ -384,31 +380,39 @@ 
         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),
+            "is_iframe": request.GET.get("iframe") == "yes",
+            '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
+            "data_view_list": DataView.repository.all(),
         }, RequestContext(request))
 
 
-@BreadCrumb("Details of {name}", parent=data_view_list, needs=['name'])
+@BreadCrumb(
+    "Details of {name}",
+    parent=data_view_list,
+    needs=['name'])
 def data_view_detail(request, name):
-    repo = DataViewRepository.get_instance()
     try:
-        data_view = repo[name]
-    except KeyError:
+        data_view = DataView.repository.get(name=name)
+    except DataView.DoesNotExist:
         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),
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                data_view_detail,
+                name=data_view.name,
+                summary=data_view.summary),
             "data_view": data_view 
         }, RequestContext(request))
 
@@ -437,3 +441,80 @@ 
         extra_context={
             'bread_crumb_trail': BreadCrumbTrail.leading_to(test_detail, test_id=test_id)
         })
+
+
+def redirect_to_test_run(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 redirect(test_run.get_absolute_url())
+
+
+def redirect_to_test_result(request, analyzer_assigned_uuid, relative_index):
+    test_result = get_restricted_object_or_404(
+        TestResult,
+        lambda test_result: test_result.test_run.bundle.bundle_stream,
+        request.user,
+        test_run__analyzer_assigned_uuid=analyzer_assigned_uuid,
+        relative_index=relative_index)
+    return redirect(test_result.get_absolute_url())
+
+
+def redirect_to_bundle(request, content_sha1): 
+    bundle = get_restricted_object_or_404(
+        Bundle,
+        lambda bundle: bundle.bundle_stream,
+        request.user,
+        content_sha1=content_sha1)
+    return redirect(bundle.get_absolute_url())
+
+
+@BreadCrumb("Image Status Matrix", parent=index)
+def image_status_list(request):
+    return render_to_response(
+        "dashboard_app/image_status_list.html", {
+            'hwpack_list': ImageHealth.get_hwpack_list(),
+            'rootfs_list': ImageHealth.get_rootfs_list(),
+            'ImageHealth': ImageHealth,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(image_status_list)
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Image Status for {rootfs_type} + {hwpack_type}",
+    parent=image_status_list,
+    needs=["rootfs_type", "hwpack_type"])
+def image_status_detail(request, rootfs_type, hwpack_type):
+    image_health = ImageHealth(rootfs_type, hwpack_type)
+    return render_to_response(
+        "dashboard_app/image_status_detail.html", {
+            'image_health': image_health,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                image_status_detail,
+                rootfs_type=rootfs_type,
+                hwpack_type=hwpack_type),
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Test history for {test_id}",
+    parent=image_status_detail,
+    needs=["rootfs_type", "hwpack_type", "test_id"])
+def image_test_history(request, rootfs_type, hwpack_type, test_id):
+    image_health = ImageHealth(rootfs_type, hwpack_type)
+    test = get_object_or_404(Test, test_id=test_id)
+    test_run_list = image_health.get_test_runs().filter(test=test)
+    return render_to_response(
+        "dashboard_app/image_test_history.html", {
+            'test_run_list': test_run_list,
+            'test': test,
+            'image_health': image_health,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                image_test_history,
+                rootfs_type=rootfs_type,
+                hwpack_type=hwpack_type,
+                test=test,
+                test_id=test_id),
+        }, RequestContext(request))

=== modified file 'dashboard_app/xmlrpc.py'
--- dashboard_app/xmlrpc.py	2011-07-07 11:39:05 +0000
+++ dashboard_app/xmlrpc.py	2011-07-22 01:04:53 +0000
@@ -24,15 +24,20 @@ 
 import logging
 import xmlrpclib
 
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, Group
 from django.db import IntegrityError, DatabaseError
-from linaro_django_xmlrpc.models import ExposedAPI
-from linaro_django_xmlrpc.models import Mapper
+from linaro_django_xmlrpc.models import (
+    ExposedAPI,
+    Mapper,
+    xml_rpc_signature,
+)
 
 from dashboard_app import __version__
-from dashboard_app.dataview import DataView, DataViewRepository
-from dashboard_app.dispatcher import xml_rpc_signature
-from dashboard_app.models import Bundle, BundleStream
+from dashboard_app.models import (
+    Bundle,
+    BundleStream,
+    DataView,
+)
 
 
 class errors:
@@ -57,10 +62,8 @@ 
     All public methods are automatically exposed as XML-RPC methods
     """
 
-
     data_view_connection = DataView.get_connection()
 
-
     @xml_rpc_signature('str')
     def version(self):
         """
@@ -137,8 +140,8 @@ 
         ------------------------------
         The following rules govern bundle stream upload access rights:
             - all anonymous streams are accessible
-            - personal streams are accessible by owners
-            - team streams are accessible by team members
+            - personal streams are accessible to owners
+            - team streams are accessible to team members
 
         """
         try:
@@ -201,8 +204,8 @@ 
         ------------------------------
         The following rules govern bundle stream download access rights:
             - all anonymous streams are accessible
-            - personal streams are accessible by owners
-            - team streams are accessible by team members
+            - personal streams are accessible to owners
+            - team streams are accessible to team members
         """
         try:
             bundle = Bundle.objects.get(content_sha1=content_sha1)
@@ -256,8 +259,8 @@ 
         ------------------------------
         The following rules govern bundle stream download access rights:
             - all anonymous streams are accessible
-            - personal streams are accessible by owners
-            - team streams are accessible by team members
+            - personal streams are accessible to owners
+            - team streams are accessible to team members
         """
         bundle_streams = BundleStream.objects.accessible_by_principal(self.user)
         return [{
@@ -319,8 +322,8 @@ 
         ------------------------------
         The following rules govern bundle stream download access rights:
             - all anonymous streams are accessible
-            - personal streams are accessible by owners
-            - team streams are accessible by team members
+            - personal streams are accessible to owners
+            - team streams are accessible to team members
         """
         try:
             bundle_stream = BundleStream.objects.accessible_by_principal(self.user).get(pathname=pathname)
@@ -418,20 +421,55 @@ 
         if name is None:
             name = ""
         try:
-            user, group, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname)
+            user_name, group_name, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname)
         except ValueError as ex:
             raise xmlrpclib.Fault(errors.FORBIDDEN, str(ex))
-        if user is None and group is None:
+
+        # Start with those to simplify the logic below
+        user = None
+        group = None
+        if is_anonymous is False:
+            if self.user is not None:
+                assert is_anonymous is False
+                assert self.user is not None
+                if user_name is not None:
+                    if user_name != self.user.username:
+                        raise xmlrpclib.Fault(
+                            errors.FORBIDDEN,
+                            "Only user {user!r} could create this stream".format(user=user_name))
+                    user = self.user  # map to real user object
+                elif group_name is not None:
+                    try:
+                        group = self.user.groups.get(name=group_name)
+                    except Group.DoesNotExist:
+                        raise xmlrpclib.Fault(
+                            errors.FORBIDDEN,
+                            "Only a member of group {group!r} could create this stream".format(group=group_name))
+            else:
+                assert is_anonymous is False
+                assert self.user is None
+                raise xmlrpclib.Fault(
+                    errors.FORBIDDEN, "Only anonymous streams can be constructed (you are not signed in)")
+        else:
+            assert is_anonymous is True
+            assert user_name is None
+            assert group_name is None
             # Hacky but will suffice for now
             user = User.objects.get_or_create(username="anonymous-owner")[0]
-            try:
-                bundle_stream = BundleStream.objects.create(user=user, group=group, slug=slug, is_public=is_public, is_anonymous=is_anonymous, name=name)
-            except IntegrityError:
-                raise xmlrpclib.Fault(errors.CONFLICT, "Stream with the specified pathname already exists")
+        try:
+            bundle_stream = BundleStream.objects.create(
+                user=user,
+                group=group,
+                slug=slug,
+                is_public=is_public,
+                is_anonymous=is_anonymous,
+                name=name)
+        except IntegrityError:
+            raise xmlrpclib.Fault(
+                errors.CONFLICT,
+                "Stream with the specified pathname already exists")
         else:
-            # TODO: Make this constraint unnecessary
-            raise xmlrpclib.Fault(errors.FORBIDDEN, "Only anonymous streams can be constructed")
-        return bundle_stream.pathname
+            return bundle_stream.pathname
 
     def data_views(self):
         """
@@ -461,7 +499,6 @@ 
         -----------------
         None
         """
-        repo = DataViewRepository.get_instance()
         return [{
             'name': data_view.name,
             'summary': data_view.summary,
@@ -472,7 +509,7 @@ 
                 "help": arg.help,
                 "default": arg.default
             } for arg in data_view.arguments]
-        } for data_view in repo]
+        } for data_view in DataView.repository.all()]
 
     def data_view_info(self, name):
         """
@@ -516,10 +553,9 @@ 
         404
             Name does not designate a data view
         """
-        repo = DataViewRepository.get_instance()
         try:
-            data_view = repo[name]
-        except KeyError:
+            data_view = DataView.repository.get(name=name)
+        except DataView.DoesNotExist:
             raise xmlrpclib.Fault(errors.NOT_FOUND, "Data view not found")
         else:
             query = data_view.get_backend_specific_query(self.data_view_connection)
@@ -564,10 +600,9 @@ 
         -----------------
         TBD
         """
-        repo = DataViewRepository.get_instance()
         try:
-            data_view = repo[name]
-        except KeyError:
+            data_view = DataView.repository.get(name=name)
+        except DataView.DoesNotExist:
             raise xmlrpclib.Fault(errors.NOT_FOUND, "Data view not found")
         try:
             rows, columns = data_view(self.data_view_connection, **arguments)
@@ -584,7 +619,6 @@ 
             }
 
 
-
 # Mapper used by the legacy URL
 legacy_mapper = Mapper()
 legacy_mapper.register_introspection_methods()

=== modified file 'doc/changes.rst'
--- doc/changes.rst	2011-06-29 22:11:18 +0000
+++ doc/changes.rst	2011-07-22 01:31:19 +0000
@@ -1,6 +1,45 @@ 
 Version History
 ***************
 
+.. _version_0_6:
+
+Version 0.6
+===========
+
+This version was released as 2011.07 in the Linaro monthly release process.
+
+Release highlights:
+
+* New UI synchronized with lava-server, the UI is going to be changed in the
+  next release to be more in line with the official Linaro theme. Currently
+  most changes are under-the-hood, sporting more jQuery UI CSS.
+* New test browser that allows to see all the registered tests and their test
+  cases. 
+* New data view browser, similar to data view browser.
+* New permalink system that allows easy linking to bundles, test runs and test results.
+* New image status views that allow for quick inspection of interesting
+  hardware pack + root filesystem combinations.
+* New image status detail view with color-coded information about test failures
+  affecting current and historic instances of a particular root filesystem +
+  hardware pack combination. 
+* New image test history view showing all the runs of a particular test on a
+  particular combination of root filesystem + hardware pack.
+* New table widget for better table display with support for client side
+  sorting and searching.
+* New option to render data reports without any navigation that is suitable for
+  embedding inside an iframe (by appending &iframe=yes to the URL)
+* New view for showing text attachments associated with test runs.
+* New view showing test runs associated with a specific bundle.
+* New view showing the raw JSON text of a bundle.
+* New view for inspecting bundle deserialization failures.
+* Integration with lava-server/RPC2/ for web APIs
+* Added support for non-anonymous submissions (test results uploaded by
+  authenticated users), including uploading results to personal (owned by
+  person), team (owned by group), public (visible) and private (hidden from
+  non-owners) bundle streams.
+* Added support for creating non-anonymous bundle streams with
+  dashboard.make_stream() (for authenticated users)
+
 .. _version_0_5:
 
 Version 0.5

=== modified file 'doc/index.rst'
--- doc/index.rst	2011-06-29 22:11:18 +0000
+++ doc/index.rst	2011-07-22 01:31:19 +0000
@@ -5,7 +5,7 @@ 
 .. automodule:: dashboard_app 
 
 .. seealso:: To get started quickly see :ref:`usage`
-.. seealso:: See what's new in :ref:`version_0_5`
+.. seealso:: See what's new in :ref:`version_0_6`
 
 Features
 ========