=== modified file 'dashboard_app/admin.py'
--- dashboard_app/admin.py	2012-09-25 22:40:20 +0000
+++ dashboard_app/admin.py	2013-01-09 02:57:13 +0000
@@ -36,6 +36,7 @@
     ImageSet,
     LaunchpadBug,
     NamedAttribute,
+    PMQABundleStream,
     SoftwarePackage,
     SoftwareSource,
     Tag,
@@ -95,7 +96,7 @@
     the bundle stream itself.
     """
     my_modeladmin = BundleAdmin(Bundle, modeladmin.admin_site)
-    my_modeladmin.delete_selected_confirmation_template = 'admin/dashboard_app/cleanup_selected_bundle_confirmation.html' 
+    my_modeladmin.delete_selected_confirmation_template = 'admin/dashboard_app/cleanup_selected_bundle_confirmation.html'
     my_queryset = None
     if request.POST.get('post'):  # handle bundles
         selected_bundles = request.POST.getlist('_selected_action')
@@ -106,7 +107,7 @@
                 my_queryset = bundle_stream.bundles.all()
             else:
                 my_queryset = my_queryset | bundle_stream.bundles.all()
-    return delete_selected(my_modeladmin, request, my_queryset)    
+    return delete_selected(my_modeladmin, request, my_queryset)
 cleanup_bundle_stream_selected.short_description = "Clean up selected %(verbose_name_plural)s"
 
 
@@ -215,6 +216,7 @@
 admin.site.register(Image, ImageAdmin)
 admin.site.register(ImageSet, ImageSetAdmin)
 admin.site.register(LaunchpadBug, LaunchpadBugAdmin)
+admin.site.register(PMQABundleStream)
 admin.site.register(SoftwarePackage, SoftwarePackageAdmin)
 admin.site.register(SoftwareSource, SoftwareSourceAdmin)
 admin.site.register(Test, TestAdmin)

=== modified file 'dashboard_app/filters.py'
--- dashboard_app/filters.py	2013-01-08 01:27:34 +0000
+++ dashboard_app/filters.py	2013-01-09 00:15:10 +0000
@@ -111,7 +111,7 @@
     pass_count = None # Only filled out for filters that dont specify a test
     result_count = None # Ditto
 
-    def serializable(self):
+    def serializable(self, include_links=True):
         cases_by_test = {}
         for test in self.filter_data['tests']:
             # Not right if filter specifies a test more than once...
@@ -136,8 +136,9 @@
                 'skip': 0,
                 'unknown': 0,
                 'total': 0,
-                'link': url_prefix + tr.get_absolute_url(),
                 }
+            if include_links:
+                d['link'] =  url_prefix + tr.get_absolute_url()
             if tr.test in cases_by_test:
                 results = d['specific_results'] = []
                 for result in self.specific_results:
@@ -146,8 +147,9 @@
                         result_data = {
                             'test_case_id': result.test_case.test_case_id,
                             'result': result_str,
-                            'link': url_prefix + result.get_absolute_url()
                             }
+                        if include_links:
+                            result_data['link'] =  url_prefix + result.get_absolute_url()
                         if result.measurement is not None:
                             result_data['measurement'] = str(result.measurement)
                         if result.units is not None:
@@ -221,7 +223,7 @@
         self.queryset = queryset
         self.filter_data = filter_data
         self.prefetch_related = prefetch_related
-        if filter_data['build_number_attribute']:
+        if filter_data.get('build_number_attribute'):
             self.key = 'build_number'
             self.key_name = 'Build'
         else:
@@ -372,9 +374,9 @@
                 ).values('object_id')))
 
     test_condition = None
-    for test in filter_data['tests']:
+    for test in filter_data.get('tests', []):
         case_ids = set()
-        for test_case in test['test_cases']:
+        for test_case in test.get('test_cases', []):
             case_ids.add(test_case.id)
         if case_ids:
             q = models.Q(
@@ -389,12 +391,12 @@
     if test_condition:
         conditions.append(test_condition)
 
-    if filter_data['uploaded_by']:
+    if filter_data.get('uploaded_by'):
         conditions.append(models.Q(bundle__uploaded_by=filter_data['uploaded_by']))
 
     testruns = TestRun.objects.filter(*conditions)
 
-    if filter_data['build_number_attribute']:
+    if filter_data.get('build_number_attribute'):
         if descending:
             ob = ['-build_number']
         else:

=== added file 'dashboard_app/migrations/0026_auto__add_pmqabundlestream.py'
--- dashboard_app/migrations/0026_auto__add_pmqabundlestream.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0026_auto__add_pmqabundlestream.py	2013-01-09 02:52:50 +0000
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'PMQABundleStream'
+        db.create_table('dashboard_app_pmqabundlestream', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('bundle_stream', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['dashboard_app.BundleStream'])),
+        ))
+        db.send_create_signal('dashboard_app', ['PMQABundleStream'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'PMQABundleStream'
+        db.delete_table('dashboard_app_pmqabundlestream')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'dashboard_app.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'public_url': ('django.db.models.fields.URLField', [], {'max_length': '512', 'blank': 'True'})
+        },
+        'dashboard_app.bundle': {
+            'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
+            '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
+            '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
+        },
+        'dashboard_app.bundledeserializationerror': {
+            'Meta': {'object_name': 'BundleDeserializationError'},
+            'bundle': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'deserialization_error'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.Bundle']"}),
+            'error_message': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'traceback': ('django.db.models.fields.TextField', [], {'max_length': '32768'})
+        },
+        'dashboard_app.bundlestream': {
+            'Meta': {'object_name': 'BundleStream'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'dashboard_app.hardwaredevice': {
+            'Meta': {'object_name': 'HardwareDevice'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'device_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'dashboard_app.image': {
+            'Meta': {'object_name': 'Image'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.imageset': {
+            'Meta': {'object_name': 'ImageSet'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'images': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.Image']", 'symmetrical': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.launchpadbug': {
+            'Meta': {'object_name': 'LaunchpadBug'},
+            'bug_id': ('django.db.models.fields.PositiveIntegerField', [], {'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'test_runs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'launchpad_bugs'", 'symmetrical': 'False', 'to': "orm['dashboard_app.TestRun']"})
+        },
+        'dashboard_app.namedattribute': {
+            'Meta': {'unique_together': "(('object_id', 'name'),)", 'object_name': 'NamedAttribute'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.TextField', [], {})
+        },
+        'dashboard_app.pmqabundlestream': {
+            'Meta': {'object_name': 'PMQABundleStream'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'dashboard_app.softwarepackage': {
+            'Meta': {'unique_together': "(('name', 'version'),)", 'object_name': 'SoftwarePackage'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'dashboard_app.softwarepackagescratch': {
+            'Meta': {'object_name': 'SoftwarePackageScratch'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'dashboard_app.softwaresource': {
+            'Meta': {'object_name': 'SoftwareSource'},
+            'branch_revision': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'branch_url': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'branch_vcs': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
+            'commit_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project_name': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'dashboard_app.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '256'})
+        },
+        'dashboard_app.test': {
+            'Meta': {'object_name': 'Test'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'test_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'})
+        },
+        'dashboard_app.testcase': {
+            'Meta': {'unique_together': "(('test', 'test_case_id'),)", 'object_name': 'TestCase'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_cases'", 'to': "orm['dashboard_app.Test']"}),
+            'test_case_id': ('django.db.models.fields.TextField', [], {}),
+            'units': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        'dashboard_app.testingeffort': {
+            'Meta': {'object_name': 'TestingEffort'},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'testing_efforts'", 'to': "orm['lava_projects.Project']"}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'testing_efforts'", 'symmetrical': 'False', 'to': "orm['dashboard_app.Tag']"})
+        },
+        'dashboard_app.testresult': {
+            'Meta': {'ordering': "('_order',)", 'object_name': 'TestResult'},
+            '_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'filename': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'lineno': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'measurement': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '20', 'decimal_places': '10', 'blank': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'relative_index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'result': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'test_case': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'test_results'", 'null': 'True', 'to': "orm['dashboard_app.TestCase']"}),
+            'test_run': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_results'", 'to': "orm['dashboard_app.TestRun']"}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'dashboard_app.testrun': {
+            'Meta': {'ordering': "['-import_assigned_date']", 'object_name': 'TestRun'},
+            'analyzer_assigned_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'analyzer_assigned_uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}),
+            'bundle': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Bundle']"}),
+            'devices': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.HardwareDevice']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'import_assigned_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'packages': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwarePackage']"}),
+            'sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwareSource']"}),
+            'sw_image_desc': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.Tag']"}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Test']"}),
+            'time_check_performed': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'dashboard_app.testrundenormalization': {
+            'Meta': {'object_name': 'TestRunDenormalization'},
+            'count_fail': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'count_pass': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'count_skip': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'count_unknown': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test_run': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'denormalization'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.TestRun']"})
+        },
+        'dashboard_app.testrunfilter': {
+            'Meta': {'unique_together': "(('owner', 'name'),)", 'object_name': 'TestRunFilter'},
+            'build_number_attribute': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'bundle_streams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.BundleStream']", 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'max_length': '1024'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"})
+        },
+        'dashboard_app.testrunfilterattribute': {
+            'Meta': {'object_name': 'TestRunFilterAttribute'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
+        },
+        'dashboard_app.testrunfiltersubscription': {
+            'Meta': {'unique_together': "(('user', 'filter'),)", 'object_name': 'TestRunFilterSubscription'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'dashboard_app.testrunfiltertest': {
+            'Meta': {'object_name': 'TestRunFilterTest'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tests'", 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.Test']"})
+        },
+        'dashboard_app.testrunfiltertestcase': {
+            'Meta': {'object_name': 'TestRunFilterTestCase'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cases'", 'to': "orm['dashboard_app.TestRunFilterTest']"}),
+            'test_case': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.TestCase']"})
+        },
+        'lava_projects.project': {
+            'Meta': {'object_name': 'Project'},
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'identifier': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}),
+            'is_aggregate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'registered_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}),
+            'registered_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['dashboard_app']
\ No newline at end of file

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2012-12-16 20:23:44 +0000
+++ dashboard_app/models.py	2013-01-09 02:52:50 +0000
@@ -1772,3 +1772,8 @@
 
 
 bundle_was_deserialized.connect(send_bundle_notifications)
+
+
+class PMQABundleStream(models.Model):
+
+    bundle_stream = models.ForeignKey(BundleStream, related_name='+')

=== added file 'dashboard_app/templates/dashboard_app/pmqa-view.html'
--- dashboard_app/templates/dashboard_app/pmqa-view.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/pmqa-view.html	2013-01-10 01:56:51 +0000
@@ -0,0 +1,87 @@
+{% extends "dashboard_app/_content.html" %}
+
+{% block extrahead %}
+{{ block.super }}
+<style type="text/css">
+table {
+    border-collapse: collapse;
+}
+td, th {
+    min-width: 25ex;
+    padding: 3px 4px;
+    border: thin solid black;
+}
+td.pass {
+    background-color: #4e4;
+}
+td.fail {
+    background-color: #e44;
+}
+td.missing {
+    background-color: #bbb;
+}
+
+</style>
+{% endblock %}
+
+{% block content %}
+<h1>PMQA view</h1>
+
+<table>
+  <thead>
+    <tr>
+      <th>
+      </th>
+      {% for device_type in device_types_with_results %}
+      {% if device_type.width %}
+      <th colspan="{{ device_type.width }}">
+        {{ device_type.sn }}
+      </th>
+      {% endif %}
+      {% endfor %}
+    </tr>
+    <tr>
+      <th>
+      </th>
+      {% for device_type in device_types_with_results %}
+      <th>
+        <a href="{{ device_type.filter_link }}">{{ device_type.device_type }}</a>
+      </th>
+      {% endfor %}
+    </tr>
+    <tr>
+      <th>
+        Test prefix
+      </th>
+      {% for device_type in device_types_with_results %}
+      <th>
+        <a href="{{ device_type.link }}">{{ device_type.date }} (build {{device_type.build}})</a>
+        <br />
+        {% if device_type.last_difference %}
+        A different result was last seen in build <a href="{{ device_type.last_difference.1 }}">{{ device_type.last_difference.0 }}</a>
+        {% else %}
+        No different result has been seen
+        {% endif %}
+      </th>
+      {% endfor %}
+    </tr>
+  </thead>
+  <tbody>
+    {% for prefix, board_results in results %}
+    <tr>
+      <td>
+        {{ prefix }}
+      </td>
+      {% for board_result in board_results %}
+      <td class="{{ board_result.css_class }}">
+        {% if board_result.present %}
+        {{ board_result.pass }} / {{ board_result.total }}
+        {% endif %}
+      </td>
+      {% endfor %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/pmqa_filter.html'
--- dashboard_app/templates/dashboard_app/pmqa_filter.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/pmqa_filter.html	2013-01-10 01:56:51 +0000
@@ -0,0 +1,29 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+{% load django_tables2 %}
+
+{% block extrahead %}
+{{ block.super }}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-detail.css"/>
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-detail.js"></script>
+{% endblock %}
+
+{% block content %}
+
+<h1>PMQA results for {{ bundle_stream }} on {{ device_type }}</h1>
+{% render_table filter_table %}
+
+<p>
+  <button id="compare-button">Compare builds</button>
+  <span id="first-prompt" style="display:none">
+    Click a build to compare.
+  </span>
+  <span id="second-prompt" style="display:none">
+    Click build to compare with build <span id="p2-build">XXX</span>.
+  </span>
+  <span id="third-prompt" style="display:none">
+    Click <a href="#">here</a> to compare with build <span id="p3-build-1">XXX</span> with build <span id="p3-build-2">XXX</span>.
+  </span>
+</p>
+
+{% endblock %}

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2013-01-08 23:44:15 +0000
+++ dashboard_app/urls.py	2013-01-11 02:26:37 +0000
@@ -82,6 +82,10 @@
     url(r'^efforts/(?P<pk>[0-9]+)/update/$', 'testing_effort_update'),
     url(r'^efforts/(?P<project_identifier>[a-z0-9-]+)/\+new/$', 'testing_effort_create'),
     url(r'^image-reports/$', 'images.image_report_list'),
+    url(r'^pmqa$', 'pmqa.pmqa_view'),
+    url(r'^pmqa(?P<pathname>/[a-zA-Z0-9/._-]+/)(?P<device_type>[a-zA-Z0-9-_]+)$', 'pmqa.pmqa_filter_view'),
+    url(r'^pmqa(?P<pathname>/[a-zA-Z0-9/._-]+/)(?P<device_type>[a-zA-Z0-9-_]+)/json$', 'pmqa.pmqa_filter_view_json'),
+    url(r'^pmqa(?P<pathname>/[a-zA-Z0-9/._-]+/)(?P<device_type>[a-zA-Z0-9-_]+)/\+compare/(?P<build1>[0-9]+)/(?P<build2>[0-9]+)$', 'pmqa.compare_pmqa_results'),
     url(r'^image-reports/(?P<name>[A-Za-z0-9_-]+)$', 'images.image_report_detail'),
     url(r'^api/link-bug-to-testrun', 'images.link_bug_to_testrun'),
     url(r'^api/unlink-bug-and-testrun', 'images.unlink_bug_and_testrun'),

=== modified file 'dashboard_app/views/filters/tables.py'
--- dashboard_app/views/filters/tables.py	2013-01-08 23:11:01 +0000
+++ dashboard_app/views/filters/tables.py	2013-01-10 01:56:51 +0000
@@ -216,8 +216,8 @@
     passes = Column(accessor='pass_count')
     total = Column(accessor='result_count')
 
-    def get_queryset(self, user, filter):
-        return evaluate_filter(user, filter.as_data())
+    def get_queryset(self, user, filter_data):
+        return evaluate_filter(user, filter_data)
 
     datatable_opts = {
         "sPaginationType": "full_numbers",
@@ -227,9 +227,6 @@
 
 
 class FilterPreviewTable(FilterTable):
-    def get_queryset(self, user, form):
-        return evaluate_filter(user, form.as_data())
-
     datatable_opts = FilterTable.datatable_opts.copy()
     datatable_opts.update({
         "iDisplayLength": 10,

=== modified file 'dashboard_app/views/filters/views.py'
--- dashboard_app/views/filters/views.py	2013-01-10 00:34:58 +0000
+++ dashboard_app/views/filters/views.py	2013-01-10 01:56:51 +0000
@@ -83,7 +83,7 @@
 
 def filter_json(request, username, name):
     filter = TestRunFilter.objects.get(owner__username=username, name=name)
-    return FilterTable.json(request, params=(request.user, filter))
+    return FilterTable.json(request, params=(request.user, filter.as_data()))
 
 
 
@@ -95,7 +95,7 @@
     form = TestRunFilterForm(request.user, request.GET, instance=filter)
     if not form.is_valid():
         raise ValidationError(str(form.errors))
-    return FilterPreviewTable.json(request, params=(request.user, form))
+    return FilterPreviewTable.json(request, params=(request.user, form.as_data()))
 
 
 @BreadCrumb("Filter ~{username}/{name}", parent=filters_list, needs=['username', 'name'])
@@ -118,7 +118,7 @@
             'filter_table': FilterTable(
                 "filter-table",
                 reverse(filter_json, kwargs=dict(username=username, name=name)),
-                params=(request.user, filter)),
+                params=(request.user, filter.as_data())),
             'bread_crumb_trail': BreadCrumbTrail.leading_to(
                 filter_detail, name=name, username=username),
         }, RequestContext(request)
@@ -177,7 +177,7 @@
                         'table': FilterPreviewTable(
                             'filter-preview',
                             reverse(filter_preview_json) + '?' + c.urlencode(),
-                            params=(request.user, form)),
+                            params=(request.user, form.as_data())),
                     }, RequestContext(request))
     else:
         form = TestRunFilterForm(request.user, instance=instance)
@@ -323,16 +323,8 @@
     return differences
 
 
-@BreadCrumb(
-    "Comparing builds {tag1} and {tag2}",
-    parent=filter_detail,
-    needs=['username', 'name', 'tag1', 'tag2'])
-def compare_matches(request, username, name, tag1, tag2):
-    filter = TestRunFilter.objects.get(owner__username=username, name=name)
-    if not filter.public and filter.owner != request.user:
-        raise PermissionDenied()
-    filter_data = filter.as_data()
-    matches = evaluate_filter(request.user, filter_data)
+def compare_filter_matches(user, filter_data, tag1, tag2):
+    matches = evaluate_filter(user, filter_data)
     match1, match2 = matches.with_tags(tag1, tag2)
     test_cases_for_test_id = {}
     for test in filter_data['tests']:
@@ -388,6 +380,19 @@
             tr=tr,
             tag=tag,
             cases=cases))
+    return test_run_info
+
+
+@BreadCrumb(
+    "Comparing builds {tag1} and {tag2}",
+    parent=filter_detail,
+    needs=['username', 'name', 'tag1', 'tag2'])
+def compare_matches(request, username, name, tag1, tag2):
+    filter = TestRunFilter.objects.get(owner__username=username, name=name)
+    if not filter.public and filter.owner != request.user:
+        raise PermissionDenied()
+    filter_data = filter.as_data()
+    test_run_info = compare_filter_matches(request.user, filter_data, tag1, tag2)
     return render_to_response(
         "dashboard_app/filter_compare_matches.html", {
             'test_run_info': test_run_info,

=== added file 'dashboard_app/views/pmqa.py'
--- dashboard_app/views/pmqa.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/views/pmqa.py	2013-01-11 02:26:37 +0000
@@ -0,0 +1,203 @@
+# Copyright (C) 2010-2012 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@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.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from lava_server.bread_crumbs import (
+    BreadCrumb,
+    BreadCrumbTrail,
+)
+
+from dashboard_app.filters import evaluate_filter
+from dashboard_app.models import (
+    BundleStream,
+    PMQABundleStream,
+    Test,
+)
+from dashboard_app.views import index
+from dashboard_app.views.filters.tables import (
+    FilterTable,
+    )
+from dashboard_app.views.filters.views import (
+    compare_filter_matches,
+    )
+
+
+@BreadCrumb("PM QA view", parent=index)
+@login_required
+def pmqa_view(request):
+    test = Test.objects.get(test_id='pwrmgmt')
+    device_types_with_results = []
+    prefix__device_type_result = {}
+
+    from lava_scheduler_app.models import DeviceType
+    device_types = list(DeviceType.objects.filter(display=True).values_list('name', flat=True))
+    bundle_streams = [pmqabs.bundle_stream for pmqabs in PMQABundleStream.objects.all()]
+    bundle_streams.sort(key=lambda bs:bs.pathname)
+
+    for bs in bundle_streams:
+        c = len(device_types_with_results)
+        for device_type in device_types:
+            if device_type.startswith('rtsm'):
+                continue
+            filter_data = {
+                'bundle_streams': [bs],
+                'attributes': [('target.device_type', device_type)],
+                'tests': [{'test':test, 'test_cases':[]}],
+                'build_number_attribute': 'build.id',
+                }
+            matches = list(evaluate_filter(request.user, filter_data)[:50])
+            if matches:
+                match = matches[0]
+                m0 = match.serializable(include_links=False)
+                del m0['tag']
+                last_difference = None
+                for m in matches[1:]:
+                    m1 = m.serializable(include_links=False)
+                    del m1['tag']
+                    if m1 != m0:
+                        last_difference = (
+                            m.tag,
+                            reverse(compare_pmqa_results,
+                                    kwargs={
+                                        'pathname': bs.pathname,
+                                        'device_type': device_type,
+                                        'build1': str(m.tag),
+                                        'build2': str(match.tag),
+                                        }))
+                        break
+                tr = match.test_runs[0]
+                device_types_with_results.append({
+                    'sn': bs.slug,
+                    'device_type': device_type,
+                    'date': tr.bundle.uploaded_on,
+                    'build': match.tag,
+                    'link': tr.get_absolute_url(),
+                    'width': 0,
+                    'last_difference': last_difference,
+                    'filter_link': reverse(pmqa_filter_view, kwargs=dict(
+                        pathname=bs.pathname, device_type=device_type)),
+                    })
+                for result in tr.test_results.all().select_related('test_case'):
+                    prefix = result.test_case.test_case_id.split('.')[0]
+                    device_type__result = prefix__device_type_result.setdefault(prefix, {})
+                    d = device_type__result.setdefault(device_type, {'pass': 0, 'total': 0, 'present':True})
+                    if result.result == result.RESULT_PASS:
+                        d['pass'] += 1
+                    d['total'] += 1
+            if len(device_types_with_results) > c:
+                device_types_with_results[c]['width'] = len(device_types_with_results) - c
+    results = []
+    prefixes = sorted(prefix__device_type_result)
+    for prefix in prefixes:
+        board_results = []
+        for d in device_types_with_results:
+            cell_data = prefix__device_type_result[prefix].get(d['device_type'])
+            if cell_data is not None:
+                if cell_data['total'] == cell_data['pass']:
+                    cell_data['css_class'] = 'pass'
+                else:
+                    cell_data['css_class'] = 'fail'
+            else:
+                cell_data = {
+                    'css_class': 'missing',
+                    'present': False,
+                    }
+            board_results.append(cell_data)
+        results.append((prefix, board_results))
+    return render_to_response(
+        "dashboard_app/pmqa-view.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(pmqa_view),
+            'device_types_with_results': device_types_with_results,
+            'results': results,
+        }, RequestContext(request))
+
+
+def pmqa_filter_view_json(request, pathname, device_type):
+    test = Test.objects.get(test_id='pwrmgmt')
+    bs = BundleStream.objects.get(pathname=pathname)
+    filter_data = {
+        'bundle_streams': [bs],
+        'attributes': [('target.device_type', device_type)],
+        'tests': [{'test':test, 'test_cases':[]}],
+        'build_number_attribute': 'build.id',
+        }
+    return FilterTable.json(request, params=(request.user, filter_data))
+
+
+@BreadCrumb(
+    "PMQA results for {pathname} on {device_type}",
+    parent=pmqa_view,
+    needs=['pathname', 'device_type'])
+def pmqa_filter_view(request, pathname, device_type):
+    test = Test.objects.get(test_id='pwrmgmt')
+    bs = BundleStream.objects.get(pathname=pathname)
+    filter_data = {
+        'bundle_streams': [bs],
+        'attributes': [('target.device_type', device_type)],
+        'tests': [{'test':test, 'test_cases':[]}],
+        'build_number_attribute': 'build.id',
+        }
+    return render_to_response(
+        "dashboard_app/pmqa_filter.html", {
+            'filter_table': FilterTable(
+                "filter-table",
+                reverse(
+                    pmqa_filter_view_json,
+                    kwargs=dict(
+                        pathname=pathname,
+                        device_type=device_type)),
+                params=(request.user, filter_data)),
+            'bundle_stream': bs.slug,
+            'device_type': device_type,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                pmqa_filter_view,
+                pathname=pathname,
+                device_type=device_type),
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "Comparing builds {build1} and {build2}",
+    parent=pmqa_filter_view,
+    needs=['pathname', 'device_type', 'build1', 'build2'])
+def compare_pmqa_results(request, pathname, device_type, build1, build2):
+    test = Test.objects.get(test_id='pwrmgmt')
+    bs = BundleStream.objects.get(pathname=pathname)
+    filter_data = {
+        'bundle_streams': [bs],
+        'attributes': [('target.device_type', device_type)],
+        'tests': [{'test':test, 'test_cases':[]}],
+        'build_number_attribute': 'build.id',
+        }
+    test_run_info = compare_filter_matches(request.user, filter_data, build1, build2)
+    return render_to_response(
+        "dashboard_app/filter_compare_matches.html", {
+            'test_run_info': test_run_info,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                compare_pmqa_results,
+                pathname=pathname,
+                device_type=device_type,
+                build1=build1,
+                build2=build2),
+        }, RequestContext(request))
+

