diff mbox

[Branch,~linaro-validation/lava-dashboard/trunk] Rev 266: Release 0.8

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

Commit Message

Paul Larson Sept. 28, 2011, 3:11 a.m. UTC
Merge authors:
  Zygmunt Krynicki (zkrynicki)
Related merge proposals:
  https://code.launchpad.net/~zkrynicki/lava-dashboard/0.8-landing/+merge/77263
  proposed by: Zygmunt Krynicki (zkrynicki)
  review: Approve - Paul Larson (pwlars)
  https://code.launchpad.net/~zkrynicki/lava-dashboard/better-bundle-view/+merge/77168
  proposed by: Zygmunt Krynicki (zkrynicki)
  review: Approve - Paul Larson (pwlars)
  https://code.launchpad.net/~zkrynicki/lava-dashboard/testing-effort/+merge/77166
  proposed by: Zygmunt Krynicki (zkrynicki)
  review: Approve - Paul Larson (pwlars)
  https://code.launchpad.net/~zkrynicki/lava-dashboard/support-1.3-format/+merge/77100
  proposed by: Zygmunt Krynicki (zkrynicki)
  review: Approve - Michael Hudson-Doyle (mwhudson)
  review: Approve - Paul Larson (pwlars)
  https://code.launchpad.net/~zkrynicki/lava-dashboard/budle-list-view-performance/+merge/76980
  proposed by: Zygmunt Krynicki (zkrynicki)
  review: Approve - Paul Larson (pwlars)
  https://code.launchpad.net/~zkrynicki/lava-dashboard/better-front-page-snippet/+merge/76004
  proposed by: Zygmunt Krynicki (zkrynicki)
  https://code.launchpad.net/~zkrynicki/lava-dashboard/fix-attachment-list/+merge/75691
  proposed by: Zygmunt Krynicki (zkrynicki)
  review: Approve - Paul Larson (pwlars)
  review: Approve - Spring Zhang (qzhang)
------------------------------------------------------------
revno: 266 [merge]
tags: release-0.8, 2011.09
committer: Paul Larson <paul.larson@canonical.com>
branch nick: lava-dashboard
timestamp: Tue 2011-09-27 22:08:44 -0500
message:
  Release 0.8
added:
  dashboard_app/migrations/0006_auto__chg_field_bundledeserializationerror_bundle.py
  dashboard_app/migrations/0007_auto__add_tag.py
  dashboard_app/migrations/0008_auto__add_testingeffort.py
  dashboard_app/migrations/0009_auto__add_testrundenormalization.py
  dashboard_app/migrations/0010_denormalize_test_run.py
  dashboard_app/signals.py
  dashboard_app/templates/dashboard_app/testing_effort_detail.html
  dashboard_app/templates/dashboard_app/testing_effort_list.html
modified:
  dashboard_app/__init__.py
  dashboard_app/admin.py
  dashboard_app/helpers.py
  dashboard_app/managers.py
  dashboard_app/models.py
  dashboard_app/templates/dashboard_app/_extension_navigation.html
  dashboard_app/templates/dashboard_app/_test_run_list_table.html
  dashboard_app/templates/dashboard_app/attachment_list.html
  dashboard_app/templates/dashboard_app/bundle_detail.html
  dashboard_app/templates/dashboard_app/bundle_list.html
  dashboard_app/templates/dashboard_app/front_page_snippet.html
  dashboard_app/templates/dashboard_app/test_result_detail.html
  dashboard_app/templates/dashboard_app/test_run_detail.html
  dashboard_app/templates/dashboard_app/test_run_list.html
  dashboard_app/tests/models/bundle.py
  dashboard_app/tests/other/deserialization.py
  dashboard_app/urls.py
  dashboard_app/views.py
  dashboard_app/xmlrpc.py


--
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/__init__.py'
--- dashboard_app/__init__.py	2011-09-14 13:06:05 +0000
+++ dashboard_app/__init__.py	2011-09-28 02:35:42 +0000
@@ -20,4 +20,4 @@ 
 Dashboard Application (package)
 """
 
-__version__ = (0, 7, 2, "final", 0)
+__version__ = (0, 8, 0, "final", 0)

=== modified file 'dashboard_app/admin.py'
--- dashboard_app/admin.py	2011-07-19 21:39:22 +0000
+++ dashboard_app/admin.py	2011-09-27 13:06:33 +0000
@@ -26,19 +26,21 @@ 
 from django.utils.translation import ugettext as _
 
 from dashboard_app.models import (
-        Attachment,
-        Bundle,
-        BundleDeserializationError,
-        BundleStream,
-        HardwareDevice,
-        NamedAttribute,
-        SoftwarePackage,
-        SoftwareSource,
-        Test,
-        TestCase,
-        TestResult,
-        TestRun,
-        )
+    Attachment,
+    Bundle,
+    BundleDeserializationError,
+    BundleStream,
+    HardwareDevice,
+    NamedAttribute,
+    SoftwarePackage,
+    SoftwareSource,
+    Tag,
+    Test,
+    TestCase,
+    TestResult,
+    TestRun,
+    TestingEffort,
+)
 
 
 class BundleAdmin(admin.ModelAdmin):
@@ -147,6 +149,10 @@ 
     inlines = [NamedAttributeInline]
 
 
+class TestingEffortAdmin(admin.ModelAdmin):
+    list_display = ('__unicode__', 'project')
+
+
 admin.site.register(Attachment)
 admin.site.register(Bundle, BundleAdmin)
 admin.site.register(BundleDeserializationError, BundleDeserializationErrorAdmin)
@@ -158,3 +164,5 @@ 
 admin.site.register(TestCase, TestCaseAdmin)
 admin.site.register(TestResult, TestResultAdmin)
 admin.site.register(TestRun, TestRunAdmin)
+admin.site.register(Tag)
+admin.site.register(TestingEffort, TestingEffortAdmin)

=== modified file 'dashboard_app/helpers.py'
--- dashboard_app/helpers.py	2011-07-20 23:08:27 +0000
+++ dashboard_app/helpers.py	2011-09-28 02:29:55 +0000
@@ -100,7 +100,7 @@ 
         Note: This function uses commit_on_success to ensure the database is in
         a consistent state after IntegrityErrors that would clog the
         transaction on pgsql. Since transactions will not rollback any files we
-        created in the meantime there is is a helper that cleans attachments in
+        created in the meantime there is a helper that cleans attachments in
         case something goes wrong
         """
         self._import_document(s_bundle, doc)
@@ -161,6 +161,7 @@ 
         self._log('attributes')
         # collect all the changes that happen before the previous save
         s_test_run.save()
+        s_test_run.denormalize()
         return s_test_run
 
     def _import_software_context(self, c_test_run, s_test_run):
@@ -681,6 +682,24 @@ 
                     ContentFile(content))
 
 
+class BundleFormatImporter_1_3(BundleFormatImporter_1_2):
+    """
+    IFormatImporter subclass capable of loading "Dashboard Bundle Format 1.3"
+    """
+
+    def _import_test_run(self, c_test_run, s_bundle):
+        from dashboard_app.models import Tag
+
+        s_test_run = super(BundleFormatImporter_1_3, self)._import_test_run(c_test_run, s_bundle)
+        self._log('tags')
+        for c_tag in c_test_run.get("tags", []):
+            s_tag, created = Tag.objects.get_or_create(name=c_tag)
+            if created:
+                s_tag.save()
+            s_test_run.tags.add(s_tag)
+        return s_test_run
+
+
 class BundleDeserializer(object):
     """
     Helper class for de-serializing JSON bundle content into database models
@@ -691,6 +710,7 @@ 
         "Dashboard Bundle Format 1.0.1": BundleFormatImporter_1_0_1,
         "Dashboard Bundle Format 1.1": BundleFormatImporter_1_1,
         "Dashboard Bundle Format 1.2": BundleFormatImporter_1_2,
+        "Dashboard Bundle Format 1.3": BundleFormatImporter_1_3,
     }
 
     def deserialize(self, s_bundle, prefer_evolution):

=== modified file 'dashboard_app/managers.py'
--- dashboard_app/managers.py	2011-07-09 13:47:36 +0000
+++ dashboard_app/managers.py	2011-09-28 02:28:43 +0000
@@ -51,3 +51,20 @@ 
             raise
         else:
             return bundle 
+
+
+class TestRunDenormalizationManager(models.Manager):
+
+    def create_from_test_run(self, test_run):
+        from dashboard_app.models import TestResult
+        stats = test_run.test_results.values('result').annotate(
+            count=models.Count('result')).order_by()
+        result = dict([
+            (TestResult.RESULT_MAP[item['result']], item['count'])
+            for item in stats])
+        return self.create(
+            test_run=test_run,
+            count_pass=result.get('pass', 0),
+            count_fail=result.get('fail', 0),
+            count_skip=result.get('skip', 0),
+            count_unknown=result.get('unknown', 0))

=== added file 'dashboard_app/migrations/0006_auto__chg_field_bundledeserializationerror_bundle.py'
--- dashboard_app/migrations/0006_auto__chg_field_bundledeserializationerror_bundle.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0006_auto__chg_field_bundledeserializationerror_bundle.py	2011-09-26 12:55:16 +0000
@@ -0,0 +1,176 @@ 
+# encoding: 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):
+        
+        # Changing field 'BundleDeserializationError.bundle'
+        db.alter_column('dashboard_app_bundledeserializationerror', 'bundle_id', self.gf('django.db.models.fields.related.OneToOneField')(unique=True, primary_key=True, to=orm['dashboard_app.Bundle']))
+
+
+    def backwards(self, orm):
+        
+        # Changing field 'BundleDeserializationError.bundle'
+        db.alter_column('dashboard_app_bundledeserializationerror', 'bundle_id', self.gf('django.db.models.fields.related.ForeignKey')(unique=True, primary_key=True, to=orm['dashboard_app.Bundle']))
+
+
+    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'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            '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.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.CharField', [], {'max_length': '32'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        '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.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.CharField', [], {'max_length': '100', '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.CharField', [], {'max_length': '100'}),
+            'units': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        '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'}),
+            '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'}),
+            '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'})
+        }
+    }
+
+    complete_apps = ['dashboard_app']

=== added file 'dashboard_app/migrations/0007_auto__add_tag.py'
--- dashboard_app/migrations/0007_auto__add_tag.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0007_auto__add_tag.py	2011-09-26 21:15:33 +0000
@@ -0,0 +1,197 @@ 
+# encoding: 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 'Tag'
+        db.create_table('dashboard_app_tag', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=256, db_index=True)),
+        ))
+        db.send_create_signal('dashboard_app', ['Tag'])
+
+        # Adding M2M table for field tags on 'TestRun'
+        db.create_table('dashboard_app_testrun_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('testrun', models.ForeignKey(orm['dashboard_app.testrun'], null=False)),
+            ('tag', models.ForeignKey(orm['dashboard_app.tag'], null=False))
+        ))
+        db.create_unique('dashboard_app_testrun_tags', ['testrun_id', 'tag_id'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Tag'
+        db.delete_table('dashboard_app_tag')
+
+        # Removing M2M table for field tags on 'TestRun'
+        db.delete_table('dashboard_app_testrun_tags')
+
+
+    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'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            '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.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.CharField', [], {'max_length': '32'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        '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', 'db_index': 'True'})
+        },
+        '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.CharField', [], {'max_length': '100', '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.CharField', [], {'max_length': '100'}),
+            'units': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        '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'}),
+            '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'})
+        }
+    }
+
+    complete_apps = ['dashboard_app']

=== added file 'dashboard_app/migrations/0008_auto__add_testingeffort.py'
--- dashboard_app/migrations/0008_auto__add_testingeffort.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0008_auto__add_testingeffort.py	2011-09-27 13:06:00 +0000
@@ -0,0 +1,220 @@ 
+# encoding: 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 'TestingEffort'
+        db.create_table('dashboard_app_testingeffort', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='testing_efforts', to=orm['lava_projects.Project'])),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
+            ('description', self.gf('django.db.models.fields.TextField')()),
+        ))
+        db.send_create_signal('dashboard_app', ['TestingEffort'])
+
+        # Adding M2M table for field tags on 'TestingEffort'
+        db.create_table('dashboard_app_testingeffort_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('testingeffort', models.ForeignKey(orm['dashboard_app.testingeffort'], null=False)),
+            ('tag', models.ForeignKey(orm['dashboard_app.tag'], null=False))
+        ))
+        db.create_unique('dashboard_app_testingeffort_tags', ['testingeffort_id', 'tag_id'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'TestingEffort'
+        db.delete_table('dashboard_app_testingeffort')
+
+        # Removing M2M table for field tags on 'TestingEffort'
+        db.delete_table('dashboard_app_testingeffort_tags')
+
+
+    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'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            '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.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.CharField', [], {'max_length': '32'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        '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', 'db_index': 'True'})
+        },
+        '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.CharField', [], {'max_length': '100', '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.CharField', [], {'max_length': '100'}),
+            'units': ('django.db.models.fields.CharField', [], {'max_length': '100', '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'}),
+            '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'})
+        },
+        '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', 'db_index': 'True'}),
+            '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']

=== added file 'dashboard_app/migrations/0009_auto__add_testrundenormalization.py'
--- dashboard_app/migrations/0009_auto__add_testrundenormalization.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0009_auto__add_testrundenormalization.py	2011-09-28 02:28:43 +0000
@@ -0,0 +1,218 @@ 
+# encoding: 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 'TestRunDenormalization'
+        db.create_table('dashboard_app_testrundenormalization', (
+            ('test_run', self.gf('django.db.models.fields.related.OneToOneField')(related_name='denormalization', unique=True, primary_key=True, to=orm['dashboard_app.TestRun'])),
+            ('count_pass', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('count_fail', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('count_skip', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('count_unknown', self.gf('django.db.models.fields.PositiveIntegerField')()),
+        ))
+        db.send_create_signal('dashboard_app', ['TestRunDenormalization'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'TestRunDenormalization'
+        db.delete_table('dashboard_app_testrundenormalization')
+
+
+    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'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            '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.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.CharField', [], {'max_length': '32'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        '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', 'db_index': 'True'})
+        },
+        '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.CharField', [], {'max_length': '100', '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.CharField', [], {'max_length': '100'}),
+            'units': ('django.db.models.fields.CharField', [], {'max_length': '100', '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'}),
+            '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']"})
+        },
+        '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', 'db_index': 'True'}),
+            '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']

=== added file 'dashboard_app/migrations/0010_denormalize_test_run.py'
--- dashboard_app/migrations/0010_denormalize_test_run.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0010_denormalize_test_run.py	2011-09-28 02:29:33 +0000
@@ -0,0 +1,235 @@ 
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        # Result codes
+        RESULT_PASS = 0
+        RESULT_FAIL = 1
+        RESULT_SKIP = 2
+        RESULT_UNKNOWN = 3
+        # Code-to-name mapping
+        RESULT_MAP = {
+            RESULT_PASS: 'pass',
+            RESULT_FAIL: 'fail',
+            RESULT_SKIP: 'skip',
+            RESULT_UNKNOWN: 'unknown'
+        }
+        for test_run in orm.TestRun.objects.all():
+            stats = test_run.test_results.order_by(
+                # Disable sorting
+            ).values(
+                'result'  # only get the result outcome (pass/fail/etc)
+            ).annotate(
+                count=models.Count('result')  # Count number of items with this value
+            )
+            # Translate the 0-4 element array into a 0-4 element dictionary 
+            result = dict([
+                (RESULT_MAP[item['result']], item['count'])
+                for item in stats])
+            # Create a denormalized test run instance
+            orm.TestRunDenormalization.objects.create(
+                test_run=test_run,
+                count_pass=result.get('pass', 0),
+                count_fail=result.get('fail', 0),
+                count_skip=result.get('skip', 0),
+                count_unknown=result.get('unknown', 0))
+
+    def backwards(self, orm):
+        orm.TestRunDenormalization.objects.all().delete()
+
+    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'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            '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.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.CharField', [], {'max_length': '32'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        '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', 'db_index': 'True'})
+        },
+        '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.CharField', [], {'max_length': '100', '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.CharField', [], {'max_length': '100'}),
+            'units': ('django.db.models.fields.CharField', [], {'max_length': '100', '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'}),
+            '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']"})
+        },
+        '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', 'db_index': 'True'}),
+            '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']

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2011-09-14 12:38:36 +0000
+++ dashboard_app/models.py	2011-09-28 02:35:22 +0000
@@ -39,12 +39,16 @@ 
 from django.utils.translation import ungettext
 
 from django_restricted_resource.models  import RestrictedResource
+from lava_projects.models import Project
+from linaro_dashboard_bundle.io import DocumentIO
 
 from dashboard_app.helpers import BundleDeserializer
-from dashboard_app.managers import BundleManager
+from dashboard_app.managers import BundleManager, TestRunDenormalizationManager
 from dashboard_app.repositories import RepositoryItem 
 from dashboard_app.repositories.data_report import DataReportRepository
 from dashboard_app.repositories.data_view import DataViewRepository
+from dashboard_app.signals import bundle_was_deserialized 
+
 
 # Fix some django issues we ran into
 from dashboard_app.patches import patch
@@ -397,6 +401,7 @@ 
             return
         try:
             self._do_deserialize(prefer_evolution)
+            bundle_was_deserialized.send(sender=self, bundle=self)
         except Exception as ex:
             import_error = BundleDeserializationError.objects.get_or_create(
                 bundle=self)[0]
@@ -404,8 +409,10 @@ 
             import_error.traceback = traceback.format_exc()
             import_error.save()
         else:
-            if self.deserialization_error.count():
-                self.deserialization_error.get().delete()
+            try:
+                self.deserialization_error.delete()
+            except BundleDeserializationError.DoesNotExist:
+                pass
             self.is_deserialized = True
             self.save()
 
@@ -446,6 +453,17 @@ 
         finally:
             self.content.close()
 
+    def get_document_format(self):
+        self.content.open('rb')
+        try:
+            fmt, doc = DocumentIO.load(self.content)
+            return fmt
+        finally:
+            self.content.close()
+
+    def get_serialization_format(self):
+        return "JSON"
+
 
 class SanitizedBundle(object):
 
@@ -485,7 +503,7 @@ 
     The relevant logic for managing this is in the Bundle.deserialize()
     """
 
-    bundle = models.ForeignKey(
+    bundle = models.OneToOneField(
         Bundle,
         primary_key = True,
         unique = True,
@@ -740,6 +758,14 @@ 
 
     attributes = generic.GenericRelation(NamedAttribute)
 
+    # Tags
+
+    tags = models.ManyToManyField(
+        "Tag",
+        blank=True,
+        related_name='test_runs',
+        verbose_name=_(u"Tags"))
+
     # Attachments
 
     attachments = generic.GenericRelation('Attachment')
@@ -757,6 +783,34 @@ 
     def get_permalink(self):
         return reverse("dashboard_app.views.redirect_to_test_run", args=[self.analyzer_assigned_uuid])
 
+    def get_board(self):
+        """
+        Return an associated Board device, if any.
+        """
+        try:
+            return self.devices.filter(device_type="device.board").get()
+        except HardwareDevice.DoesNotExist:
+            pass
+        except HardwareDevice.MultipleObjectsReturned:
+            pass
+
+    def get_results(self):
+        """
+        Get all results efficiently
+        """
+        return self.test_results.select_related(
+            "test_case",  # explicit join on test_case which might be NULL
+            "test_run",  # explicit join on test run, needed by all the get_absolute_url() methods
+            "test_run__bundle",  # explicit join on bundle
+            "test_run__bundle__bundle_stream",  # explicit join on bundle stream
+        ).order_by("relative_index")  # sort as they showed up in the bundle
+
+    def denormalize(self):
+        try:
+            self.denormalization
+        except TestRunDenormalization.DoesNotExist:
+            TestRunDenormalization.objects.create_from_test_run(self)
+
     def _get_summary_results(self, factor=3):
         stats = self.test_results.values('result').annotate(
             count=models.Count('result')).order_by()
@@ -776,6 +830,39 @@ 
         ordering = ['-import_assigned_date']
 
 
+class TestRunDenormalization(models.Model):
+    """
+    Denormalized model for test run
+    """
+
+    test_run = models.OneToOneField(
+        TestRun,
+        primary_key=True,
+        related_name="denormalization")
+
+    count_pass = models.PositiveIntegerField(
+        null=False,
+        blank=False)
+
+    count_fail = models.PositiveIntegerField(
+        null=False,
+        blank=False)
+
+    count_skip = models.PositiveIntegerField(
+        null=False,
+        blank=False)
+
+    count_unknown = models.PositiveIntegerField(
+        null=False,
+        blank=False)
+
+    def count_all(self):
+        return (self.count_pass + self.count_fail + self.count_skip +
+                self.count_unknown)
+
+    objects = TestRunDenormalizationManager()
+
+
 class Attachment(models.Model):
     """
     Model for adding attachments to any other models.
@@ -1302,3 +1389,53 @@ 
             for attr in NamedAttribute.objects.filter(
                 name='hwpack.type').values('value').distinct()]
         return hwpack_list
+
+
+class Tag(models.Model):
+    """
+    Tag used for marking test runs.
+    """
+    name = models.SlugField(
+        verbose_name=_(u"Tag"),
+        max_length=256,
+        db_index=True,
+        unique=True)
+
+    def __unicode__(self):
+        return self.name
+
+
+class TestingEffort(models.Model):
+    """
+    A collaborative effort to test something.
+
+    Uses tags to associate with test runs.
+    """
+    project = models.ForeignKey(
+        Project,
+        related_name="testing_efforts")
+
+    name = models.CharField(
+        verbose_name=_(u"Name"),
+        max_length=100)
+
+    description = models.TextField(
+        verbose_name=_(u"Description"),
+        help_text=_(u"Description of this testing effort"))
+
+    tags = models.ManyToManyField(
+        Tag,
+        verbose_name=_(u"Tags"),
+        related_name="testing_efforts")
+
+    def __unicode__(self):
+        return self.name
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ("dashboard_app.views.testing_effort_detail", [self.pk])
+
+    def get_test_runs(self):
+        return TestRun.objects.order_by(
+        ).filter(
+            tags__in=self.tags.all())

=== added file 'dashboard_app/signals.py'
--- dashboard_app/signals.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/signals.py	2011-09-27 13:13:53 +0000
@@ -0,0 +1,21 @@ 
+# Copyright (C) 2010, 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 django.dispatch import Signal
+
+bundle_was_deserialized = Signal(providing_args=['bundle'])

=== modified file 'dashboard_app/templates/dashboard_app/_extension_navigation.html'
--- dashboard_app/templates/dashboard_app/_extension_navigation.html	2011-07-25 23:19:44 +0000
+++ dashboard_app/templates/dashboard_app/_extension_navigation.html	2011-09-28 00:35:41 +0000
@@ -5,6 +5,8 @@ 
       >{% trans "Back to LAVA" %}</a></li>
     <li><a href="{% url dashboard_app.views.image_status_list %}"
       >{% trans "Image Status" %}</a></li>
+    <li><a href="{% url dashboard_app.views.testing_effort_list %}"
+      >{% trans "Testing efforts" %}</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/_test_run_list_table.html'
--- dashboard_app/templates/dashboard_app/_test_run_list_table.html	2011-09-09 14:30:15 +0000
+++ dashboard_app/templates/dashboard_app/_test_run_list_table.html	2011-09-27 21:22:36 +0000
@@ -6,10 +6,6 @@ 
       <th>{% trans "Test" %}</th>
       <th>{% trans "Uploaded On" %} </th>
       <th>{% trans "Analyzed On" %}</th>
-      <th>{% trans "Pass" %}</th>
-      <th>{% trans "Fail" %}</th>
-      <th>{% trans "Skip" %}</th>
-      <th>{% trans "Unknown" %}</th>
     </tr>
   </thead>
   <tbody>
@@ -19,12 +15,6 @@ 
       <td><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test }}</a></td>
       <td>{{ test_run.bundle.uploaded_on|date:"Y-m-d H:i:s" }}</td>
       <td>{{ test_run.analyzer_assigned_date|date:"Y-m-d H:i:s" }}</td>
-      {% with test_run.get_summary_results as summary %}
-      <td>{{ summary.pass|default:0 }}</td>
-      <td>{{ summary.fail|default:0 }}</td>
-      <td>{{ summary.skip|default:0 }}</td>
-      <td>{{ summary.unknown|default:0 }}</td>
-      {% endwith %}
     </tr>
     {% endfor %}
   </tbody>

=== modified file 'dashboard_app/templates/dashboard_app/attachment_list.html'
--- dashboard_app/templates/dashboard_app/attachment_list.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/attachment_list.html	2011-09-16 09:12:11 +0000
@@ -17,11 +17,17 @@ 
     <tr>
       <td><a href="{{ attachment.get_absolute_url }}"
           ><code>{{ attachment.content_filename }}</code></a></td>
-      <td>{{ attachment.content.size|filesizeformat }}</td>
+      <td>
+        {% if attachment.content %}
+        {{ attachment.content.size|filesizeformat }}
+        {% else %}
+        Not available
+        {% endif %}
+      </td>
       <td><code>{{ attachment.mime_type }}</code></td>
       <td>
         {% if attachment.public_url %}
-        <a href="{{ attachment.public_url }}">public url</a>
+        <a href="{{ attachment.public_url }}">public URL</a>
         {% endif %}
       </td>
     </tr>

=== modified file 'dashboard_app/templates/dashboard_app/bundle_detail.html'
--- dashboard_app/templates/dashboard_app/bundle_detail.html	2011-07-18 16:29:23 +0000
+++ dashboard_app/templates/dashboard_app/bundle_detail.html	2011-09-27 19:29:01 +0000
@@ -1,4 +1,4 @@ 
-{% extends "dashboard_app/_content.html" %}
+{% extends "dashboard_app/_content_with_sidebar.html" %}
 {% load humanize %}
 {% load i18n %}
 {% load stylize %}
@@ -10,21 +10,48 @@ 
 {% endblock %}
 
 
+
+{% block sidebar %}
+<h3>Permalink</h3>
+<p>You can navigate to this bundle, regardless of the bundle stream it is
+located in, by using this <a href="{{bundle.get_permalink}}">permalink</a></p>
+
+<h3>Upload details</h3>
+{% if bundle.uploaded_by %}
+<p>This bundle was uploaded by <strong>{{bundle.uploaded_by}}</strong> on
+{{bundle.uploaded_on}} ({{bundle.uploaded_on|timesince}} ago)</p>
+{% else %}
+<p>This bundle was uploaded by an anonymous contributor on
+{{bundle.uploaded_on}} ({{bundle.uploaded_on|timesince}} ago)</p>
+{% endif %}
+
+<h3>File details</h3>
+<dl>
+  <dt>Declared file name:</dt>
+  <dd><q>{{ bundle.content_filename }}</q></dd>
+  <dt>Content SHA1:</dt>
+  <dd>{{ bundle.content_sha1 }}</dd>
+  <dt>Content size:</dt>
+  <dd>{{ bundle.content.size|filesizeformat }}</dd>
+</dl>
+
+<h3>Storage and format</h3>
+<dl>
+  <dt>Document format:</dt>
+  <dd><q>{{bundle.get_document_format}}</q></dd>
+  <dt>Serialization format:</dt>
+  <dd><q>{{ bundle.get_serialization_format}}</q></dd>
+</dl>
+
+<h3>Tips</h3>
+<p>You can download this bundle with the following command:</p>
+<div class="console">
+  <code>lava-dashboard-tool --dashboard-url=http://{{site.domain}}{% url lava.api_handler %} get {{bundle.content_sha1}}</code>
+</div>
+{% endblock %}
+
+
 {% 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({
@@ -55,7 +82,7 @@ 
     {% if bundle.is_deserialized %}
     <li><a href="#tab-test-runs">{% trans "Test Runs" %}</a></li>
     {% endif %}
-    {% if bundle.deserialization_error.get %}
+    {% if bundle.deserialization_error %}
     <li><a href="#tab-deserialization-error">{% trans "Deserialization Error" %}</a></li>
     {% endif %}
     <li><a href="{% url dashboard_app.views.ajax_bundle_viewer bundle.pk %}">{% trans "Bundle Viewer" %}</a></li>
@@ -68,13 +95,13 @@ 
   </div>
   {% endif %}
 
-  {% if bundle.deserialization_error.get %}
+  {% if bundle.deserialization_error %}
   <div id="tab-deserialization-error">
     <h3>Cause</h3>
-    <p>{{ bundle.deserialization_error.get.error_message }}</p>
+    <p>{{ bundle.deserialization_error.error_message }}</p>
     <h3>Deserialization failure traceback</h3>
     <div style="overflow-x: scroll">
-      {% stylize "pytb" %}{{ bundle.deserialization_error.get.traceback|safe }}{% endstylize %}
+      {% stylize "pytb" %}{{ bundle.deserialization_error.traceback|safe }}{% endstylize %}
     </div>
   </div>
   {% endif %}

=== modified file 'dashboard_app/templates/dashboard_app/bundle_list.html'
--- dashboard_app/templates/dashboard_app/bundle_list.html	2011-09-09 14:30:15 +0000
+++ dashboard_app/templates/dashboard_app/bundle_list.html	2011-09-27 21:30:09 +0000
@@ -55,7 +55,6 @@ 
     $("#master-toolbar-splice").remove();
     $("#master-toolbar").addClass("ui-widget-header ui-corner-tl ui-corner-tr").css(
       "padding", "5pt").css("text-align", "center");
-    new FixedHeader(oTable);
   });
 </script> 
 <table class="demo_jui display" id="bundles">
@@ -81,7 +80,7 @@ 
         {% endif %}
       </td>
       <td>{{ bundle.is_deserialized|yesno }}</td>
-      <td>{% if bundle.deserialization_error.get %}yes{% endif %}</td>
+      <td>{% if bundle.deserialization_error %}yes{% endif %}</td>
     </tr>
     {% endfor %}
   </tbody>

=== modified file 'dashboard_app/templates/dashboard_app/front_page_snippet.html'
--- dashboard_app/templates/dashboard_app/front_page_snippet.html	2011-08-18 15:04:13 +0000
+++ dashboard_app/templates/dashboard_app/front_page_snippet.html	2011-09-19 11:20:07 +0000
@@ -1,10 +1,29 @@ 
 <p>Click on image description to see automatic QA report</p>
 {% regroup dashboard.interesting_images by rootfs_type as rootfs_list %}
 {% for rootfs in rootfs_list %}
-<h4>{{ rootfs.grouper|capfirst }}</h4>
-<ul>
-  {% for image_health in rootfs.list %}
-  <li><a href="{{ image_health.get_absolute_url }}">{{ image_health.rootfs_type }} + {{ image_health.hwpack_type }}</a></li>
-  {% endfor %}
-</ul>
+<style type="text/css">
+  .column {
+    float: left;
+    width: 25em;
+    padding: 2pt; 
+  }
+  .column h4 {
+    margin: 0;
+  }
+  .column ul {
+    padding: 0;
+    margin: 0 0 0 1em;
+    list-style-position: inside;
+  }
+
+</style>
+<div class="column">
+  <h4>{{ rootfs.grouper|capfirst }}</h4>
+  <ul>
+    {% for image_health in rootfs.list %}
+    <li><a href="{{ image_health.get_absolute_url }}">{{ image_health.rootfs_type }} + {{ image_health.hwpack_type }}</a></li>
+    {% endfor %}
+  </ul>
+</div>
 {% endfor %} 
+<div style="clear:both;"></div>

=== modified file 'dashboard_app/templates/dashboard_app/test_result_detail.html'
--- dashboard_app/templates/dashboard_app/test_result_detail.html	2011-07-18 16:29:23 +0000
+++ dashboard_app/templates/dashboard_app/test_result_detail.html	2011-09-27 21:00:31 +0000
@@ -16,11 +16,11 @@ 
   </div>
 </div>
 <br/>
-{% if test_result.test_run.test_results.count > 1 %}
+{% if test_result.test_run.get_results.count > 1 %}
 <h3>Other results</h3>
 <p class="hint">Results from the same test run are available here</p>
 <select id="other_results">
-  {% regroup test_result.test_run.test_results.all by test_case as test_result_group_list %}
+  {% regroup test_result.test_run.get_results by test_case as test_result_group_list %}
   {% for test_result_group in test_result_group_list %}
   {% if test_result_group.list|length > 1 %}<optgroup label="Results for test case {{ test_result_group.grouper }}">{% endif %}
     {% for other_test_result in test_result_group.list %}

=== modified file 'dashboard_app/templates/dashboard_app/test_run_detail.html'
--- dashboard_app/templates/dashboard_app/test_run_detail.html	2011-08-19 04:17:06 +0000
+++ dashboard_app/templates/dashboard_app/test_run_detail.html	2011-09-27 20:58:16 +0000
@@ -21,57 +21,173 @@ 
       <th>{% trans "Result" %}</th>
       <th>{% trans "Measurement" %}</th>
     </tr>
-    <tbody>
-      {% for test_result in test_run.test_results.select_related.all %}
-      <tr>
-        <td width="1%">{{ test_result.relative_index }}</td>
-        <td>{{ test_result.test_case|default_if_none:"<em>Not specified</em>" }}</td>
-        <td>
-          <a href ="{{test_result.get_absolute_url}}">
-            <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ test_result.result_code }}.png"
-            alt="{{ test_result.get_result_display }}" width="16" height="16" border="0"/></a>
-          <a href ="{{test_result.get_absolute_url}}">{{ test_result.get_result_display }}</a>
-        </td>
-        <td>{{ test_result.measurement|default_if_none:"Not specified" }} {{ test_result.units }}</td>
-      </tr>
-      {% endfor %}
-    </tbody>
-  </table>
-  {% endblock %}
+  </thead>
+  <tbody>
+    {% for test_result in test_run.get_results %}
+    <tr>
+      <td width="1%">{{ test_result.relative_index }}</td>
+      <td>{{ test_result.test_case|default_if_none:"<em>Not specified</em>" }}</td>
+      <td>
+        <a href ="{{test_result.get_absolute_url}}">
+          <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ test_result.result_code }}.png"
+          alt="{{ test_result.get_result_display }}" width="16" height="16" border="0"/></a>
+        <a href ="{{test_result.get_absolute_url}}">{{ test_result.get_result_display }}</a>
+      </td>
+      <td>{{ test_result.measurement|default_if_none:"Not specified" }} {{ test_result.units }}</td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}
 
 
 {% block sidebar %}
-<dl>
-  <dt>{% trans "Test Run UUID" %}</dt>
-  <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><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test }}</a></dd>
-  <dt>{% trans "OS Distribution" %}</dt>
+<h3>Permalink</h3>
+<p>You can navigate to this test run, regardless of the bundle stream it is
+located in, by using this <a
+  href="{% url dashboard_app.views.redirect_to_test_run test_run.analyzer_assigned_uuid %}"
+  >permalink</a>.</p>
+
+<h3>Test run details</h3>
+<dl>
+  <dt>{% trans "Test Name:" %}</dt>
+  <dd><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test.test_id }}</a>
+  <p class="help_text">This is the identifier of the test that was invoked. A
+  test is a collection of test cases. Test is also the smallest piece of code
+  that can be invoked by lava-test.</p>
+  </dd>
+  <dt>{% trans "Test Run UUID:" %}</dt>
+  <dd><small>{{ test_run.analyzer_assigned_uuid }}</small>
+  <p class="help_text">This is a globally unique identifier that was assigned
+  by the log analyzer. Running the same test multiple times results in
+  different values of this identifier.  The dashboard uses this identifier to
+  refer to a particular test run. It is preserved across different LAVA
+  installations, that is, if you pull test results (as bundles) from one system
+  to another this identifier remains intact</p>
+  </dd>
+  <dt>{% trans "Bundle SHA1:" %}</dt>
+  <dd><a href="{{ test_run.bundle.get_absolute_url }}"
+    ><small>{{ test_run.bundle.content_sha1 }}</small></a>
+  <p class="help_text">This is the SHA1 hash of the bundle that contains this test run.</p>
+  </dd>
+
+  <dt>{% trans "Attachments:" %}</dt>
+  <dd>
+  <ul>
+    {% for attachment in test_run.attachments.all %}
+    <li><a href="{{ attachment.get_absolute_url }}"
+      >{{ attachment }}</a>
+      {% if attachment.content %}
+      ({{ attachment.content.size|filesizeformat }})
+      {% endif %}
+    </li>
+    {% empty %}
+    <em>{% trans "There are no attachments associated with this test run." %}</em>
+    {% endfor %}
+  </ul>
+  <p class="help_text">LAVA can store attachments associated with a
+  particular test run. Those attachments can be used to store log files, crash
+  dumps, screen shots or other useful test artifacts.</p> 
+  </dd>
+
+  <dt>{% trans "Tags:" %}</dt>
+  <dd>
+  <ul>
+    {% for tag in test_run.tags.all %}
+    <li><code>{{ tag }}</code></li>
+    {% empty %}
+    <em>{% trans "There are no tags associated with this test run." %}</em>
+    {% endfor %}
+  </ul>
+  <p class="help_text">LAVA can store tags associated with a particular
+  test run. Tags are simple strings like <q>project-foo-prerelase-testing</q>
+  or <q>linaro-image-2011-09-27</q>. Tags can be used by the testing effort
+  feature to group results together.</p> 
+  </dd>
+</dl>
+
+<h3>Software context</h3>
+<dl>
+  <dt>{% trans "OS Distribution:" %}</dt>
   <dd>{{ test_run.sw_image_desc|default:"<i>Unspecified</i>" }}</dd>
-  <dt>{% trans "Bundle SHA1" %}</dt>
-  <dd><a href="{{ test_run.bundle.get_absolute_url }}">{{ test_run.bundle.content_sha1 }}</a></dd>
-  <dt>{% trans "Time check performed" %}</dt>
-  <dd>{{ test_run.time_check_performed|yesno }}</dd>
+  <dt>{% trans "Software packages:" %}</dt>
+  <dd><a href="{% url dashboard_app.views.test_run_software_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
+    >See all {{ test_run.packages.all.count }} software packages</a>
+  <p class="help_text">LAVA keeps track of all the software packages (such as
+  Debian packages managed with dpkg) that were installed prior to running a
+  test. This information can help you track down errors caused by a particular
+  buggy dependency</p>
+  </dd>
+  <dt>{% trans "Software sources:" %}</dt>
+  <dd><a href="{% url dashboard_app.views.test_run_software_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
+    >See all {{ test_run.sources.all.count }} source references</a>
+  <p class="help_text">LAVA can track more data than just package name and
+  version. You can track precise software information such as the version
+  control system branch or repository, revision or tag name and more</p>
+  </dd>
+</dl>
+
+<h3>Hardware context</h3>
+<dl>
+  <dt>{% trans "Board:" %}</dt>
+  <dd>{{ test_run.get_board|default_if_none:"There are no boards associated with this test run" }}</dd>
+  <dt>{% trans "Other devices:" %}</dt>
+  <dd><a 
+    href="{% url dashboard_app.views.test_run_hardware_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
+    >See all {{ test_run.devices.all.count }} devices</a>
+  <p class="help_text">LAVA keeps track of the hardware that was used for
+  testing. This can help cross-reference benchmarks and identify
+  hardware-specific issues.</p>
+  </dd>
+</dl>
+
+<h3>Custom attributes</h3>
+<p class="help_text">LAVA can store arbitrary key-value attributes associated
+with each test run (and separately, each test result)</p>
+<ul>
+  {% for attribute in test_run.attributes.all %}
+  <li>{{ attribute.name }} = {{ attribute.value }}</li>
+  {% empty %}
+  <em>{% trans "There are no attributes associated with this test run." %}</em>
+  {% endfor %}
+</ul>
+
+<h3>Time stamps</h3>
+<p class="help_text">There are three different timestamps
+associated with each test run. They are explained below.</p>
+<dl>
   <dt>{% trans "Log analyzed on:" %}</dt>
   <dd>
   {{ test_run.analyzer_assigned_date|naturalday }}
   {{ test_run.analyzer_assigned_date|time }}
-  </dd>
-  <dt>{% trans "Data uploaded on:" %}</dt>
+  ({{ test_run.analyzer_assigned_date|timesince }} ago)
+  <p class="help_text">This is the moment this that this test run's artifacts
+  (such as log files and other output) were processed by the log analyzer.
+  Typically the analyzer is a part of lava-test framework and test output is
+  analyzed on right on the device so this time may not be trusted, see below
+  for the description of <q>time check performed</q></p>
+  </dd>
+  <dt>{% trans "Time check performed" %}</dt>
+  <dd>{{ test_run.time_check_performed|yesno }}
+  <p class="help_text">The value <em>no</em> indicates that the log analyzer
+  was not certain that the time and date is accurate.</p>
+  </dd>
+  <dt>{% trans "Data imported on:" %}</dt>
   <dd>
   {{ test_run.import_assigned_date|naturalday }}
   {{ test_run.import_assigned_date|time }}
+  ({{ test_run.import_assigned_date|timesince }} ago)
+  <p class="help_text">This is the moment this test run entry was created in
+  the LAVA database. It can differ from upload date if there were any initial
+  deserialization problems and the data was deserialized later.</p>
   </dd>
-  <dt>{% trans "Other information:" %}</dt>
-  <dd><a 
-    href="{% url dashboard_app.views.test_run_hardware_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
-    >{% trans "Hardware context" %} ({{ test_run.devices.all.count }} devices)</a></dd>
-  <dd><a 
-    href="{% url dashboard_app.views.test_run_software_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
-    >{% trans "Software context" %} ({{ test_run.packages.all.count }} packages, {{ test_run.sources.all.count }} sources)</a></dd>
-  <dd><a 
-    href="{% url dashboard_app.views.attachment_list test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
-    >{% trans "Attachments" %} ({{ test_run.attachments.count }})</a></dd>
+  <dt>{% trans "Data uploaded on:" %}</dt>
+  <dd>
+  {{ test_run.bundle.uploaded_on|naturalday }}
+  {{ test_run.bundle.uploaded_on|time }}
+  ({{ test_run.bundle.uploaded_on|timesince }} ago)
+  <p class="help_text">This is the moment this data was first uploaded to LAVA
+  (as a serialized bundle).</p>
   </dd>
 </dl>
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_run_list.html'
--- dashboard_app/templates/dashboard_app/test_run_list.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/test_run_list.html	2011-09-27 21:26:42 +0000
@@ -53,7 +53,6 @@ 
     $("#master-toolbar-splice").remove();
     $("#master-toolbar").addClass("ui-widget-header ui-corner-tl ui-corner-tr").css(
       "padding", "5pt").css("text-align", "center");
-    new FixedHeader(oTable);
   });
 </script> 
 {% include "dashboard_app/_test_run_list_table.html" %}

=== added file 'dashboard_app/templates/dashboard_app/testing_effort_detail.html'
--- dashboard_app/templates/dashboard_app/testing_effort_detail.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/testing_effort_detail.html	2011-09-28 02:30:30 +0000
@@ -0,0 +1,105 @@ 
+{% extends "dashboard_app/_content_with_sidebar.html" %}
+{% load humanize %}
+{% load markup %}
+{% load i18n %}
+
+
+{% block extrahead %}
+{{ block.super }}
+<!--[if IE]><script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/excanvas.min.js"></script><![endif]-->
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.min.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.stack.min.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.axislabels.js"></script>
+{% endblock %}
+
+
+{% block sidebar %}
+<h3>{{ effort }}</h3>
+{{ effort.description|markdown }}
+<h3>Tags</h3>
+<p class="help_text">The concept of <q>testing efforts</q> is based on using
+tags to associate test runs with a common goal or task. This testing effort
+will list any test runs that have <strong>any</strong> of the following tags
+present.</p>
+<ul>
+  {% for tag in effort.tags.all %}
+  <li>{{ tag }}</li>
+  {% empty %}
+  <em>This testing effort has not defined any tags yet, tests
+    runs will not show up unless this is done</em>
+  {% endfor %}
+</ul>
+{% endblock %}
+
+
+{% block content %}
+<style type="text/css">
+  .result_pass {
+    background-color: #3aad3a;
+  }
+  
+  .result_fail {
+    background-color: #ff3800;
+  }
+  
+  .result_skip {
+    background-color: yellow;
+  }
+  
+  .result_unknown {
+    background-color: #aaad9c;
+  }
+
+  .helper {
+    float: left;
+    height: 1em;
+    margin-left: 1px;
+  }
+
+  table.special {
+    border-collapse: collapse;
+    width: 100%;
+  }
+
+  table.special th {
+    text-align: left;
+    border-bottom: 1px solid #333;
+    padding: 2pt;
+  }
+</style>
+<table class="special">
+  {% regroup test_run_list|dictsortreversed:"analyzer_assigned_date" by analyzer_assigned_date|date:"Y-m-d" as test_run_cluster_list %}
+  {% for test_run_cluster in test_run_cluster_list %}
+  <tr>
+    <th colspan="2">Tests ran on {{ test_run_cluster.grouper }}</th>
+    <th>Pass</th>
+    <th>Fail</th>
+    <th>Skip</th>
+    <th>Unknown</th>
+  </tr>
+  {% for test_run in test_run_cluster.list %}
+  <tr>
+    <td><a href="{{ test_run.get_absolute_url }}">{{ test_run.test }}</a></td>
+    {% with test_run.denormalization as denormalization %}
+    <td>
+      {% spaceless %}
+      <div class="result_pass helper"
+        style="width: {% widthratio denormalization.count_pass denormalization.count_all 500 %}px;"></div>
+      <div class="result_fail helper"
+        style="width: {% widthratio denormalization.count_fail denormalization.count_all 500 %}px;"></div>
+      <div class="result_skip helper"
+        style="width: {% widthratio denormalization.count_skip denormalization.count_all 500 %}px;"></div>
+      <div class="result_unknown helper"
+        style="width: {% widthratio denormalization.count_unknown denormalization.count_all 500 %}px;"></div>
+      {% endspaceless %}
+    </td>
+    <td>{{ denormalization.count_pass }}</td>
+    <td>{{ denormalization.count_fail }}</td>
+    <td>{{ denormalization.count_skip }}</td>
+    <td>{{ denormalization.count_unknown }}</td>
+    {% endwith %}
+  </tr>
+  {% endfor %}
+  {% endfor %}
+</table>
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/testing_effort_list.html'
--- dashboard_app/templates/dashboard_app/testing_effort_list.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/testing_effort_list.html	2011-09-28 00:35:41 +0000
@@ -0,0 +1,19 @@ 
+{% extends "dashboard_app/_content.html" %}
+{% load humanize %}
+{% load markup %}
+{% load i18n %}
+
+
+{% block content %}
+<h2>Testing efforts</h2>
+{% regroup effort_list by project as effort_group_list %}
+{% for effort_group in effort_group_list %}
+<h3>In project <a href="{{ effort_group.grouper.get_absolute_url }}">{{ effort_group.grouper }}</a></h3>
+<dl>
+  {% for effort in effort_group.list %}
+  <dt><a href="{{ effort.get_absolute_url }}">{{ effort }}</a></dt>
+  <dd>{{ effort.description|markdown }}</dd>
+  {% endfor %}
+</dl>
+{% endfor %}
+{% endblock %}

=== modified file 'dashboard_app/tests/models/bundle.py'
--- dashboard_app/tests/models/bundle.py	2011-06-23 10:09:42 +0000
+++ dashboard_app/tests/models/bundle.py	2011-09-26 12:55:16 +0000
@@ -87,7 +87,7 @@ 
         self.mocker.replay()
         self.bundle.deserialize(False)
         self.assertFalse(self.bundle.is_deserialized)
-        self.assertEqual(self.bundle.deserialization_error.get().error_message, "boom")
+        self.assertEqual(self.bundle.deserialization_error.error_message, "boom")
 
     def test_deserialize_ignores_deserialized_bundles(self):
         # just reply as we're not using mocker in this test case 

=== modified file 'dashboard_app/tests/other/deserialization.py'
--- dashboard_app/tests/other/deserialization.py	2011-07-20 23:06:30 +0000
+++ dashboard_app/tests/other/deserialization.py	2011-09-27 06:52:52 +0000
@@ -516,6 +516,48 @@ 
                 ("attr2", "value2")]))
 
 
+class Bundle13DeserializerSuccessTests(TestCase):
+
+    json_text = '''
+    {
+        "format": "Dashboard Bundle Format 1.3",
+        "test_runs": [
+            {
+                "test_id": "some_test_id",
+                "analyzer_assigned_uuid": "1ab86b36-c23d-11df-a81b-002163936223",
+                "analyzer_assigned_date": "2010-12-31T23:59:59Z",
+                "time_check_performed": true,
+                "test_results": [ ],
+                "tags": [
+                    "tag-1",
+                    "tag-2"
+                ]
+            }
+        ]
+    }
+    '''
+    
+    def setUp(self):
+        super(Bundle13DeserializerSuccessTests, self).setUp()
+        self.s_bundle = fixtures.create_bundle(
+            '/anonymous/', self.json_text, 'bundle.json')
+        # Decompose the data here
+        self.s_bundle.deserialize(prefer_evolution=False)
+        if not self.s_bundle.is_deserialized:
+            raise AssertionError("Deserialzation failed:" + self.s_bundle.deserialization_error.get().traceback)
+        # Link to test run for easier testing
+        self.s_test = self.s_bundle.test_runs.get()
+
+    def tearDown(self):
+        self.s_bundle.delete_files()
+        super(Bundle13DeserializerSuccessTests, self).tearDown()
+
+    def test_deserialize_tags(self):
+        self.assertEqual(self.s_test.tags.count(), 2)
+        self.assertEqual([tag.name for tag in self.s_test.tags.order_by('name').all()],
+                         ["tag-1", "tag-2"])
+
+
 class BundleDeserializerFailureTestCase(TestCaseWithScenarios):
 
     scenarios = [
@@ -690,7 +732,7 @@ 
         # better than not knowing what really happened and hiding other
         # potential bugs that would otherwise be masked here.
         self.assertIn(
-            self.s_bundle.deserialization_error.get().error_message, [
+            self.s_bundle.deserialization_error.error_message, [
                 'A test with UUID 1ab86b36-c23d-11df-a81b-002163936223 already exists',
                 'column analyzer_assigned_uuid is not unique',
                 u'duplicate key value violates unique constraint '

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2011-08-24 04:03:56 +0000
+++ dashboard_app/urls.py	2011-09-28 00:35:41 +0000
@@ -65,4 +65,6 @@ 
     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'),
+    url(r'^efforts/$', 'testing_effort_list'),
+    url(r'^efforts/(?P<pk>[0-9]+)/$', 'testing_effort_detail'),
 )

=== modified file 'dashboard_app/views.py'
--- dashboard_app/views.py	2011-08-24 04:39:48 +0000
+++ dashboard_app/views.py	2011-09-28 02:30:30 +0000
@@ -22,6 +22,7 @@ 
 
 import json
 
+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 Http404, HttpResponse
@@ -39,6 +40,7 @@ 
     Test,
     TestResult,
     TestRun,
+    TestingEffort,
 )
 from dashboard_app.bread_crumbs import BreadCrumb, BreadCrumbTrail
 
@@ -126,7 +128,7 @@ 
     )
     return object_list(
         request,
-        queryset=bundle_stream.bundles.all().order_by('-uploaded_on'),
+        queryset=bundle_stream.bundles.select_related('bundle_stream', 'deserialization_error').order_by('-uploaded_on'),
         template_name="dashboard_app/bundle_list.html",
         template_object_name="bundle",
         extra_context={
@@ -163,6 +165,7 @@ 
                 bundle_detail,
                 pathname=pathname,
                 content_sha1=content_sha1),
+            "site": Site.objects.get_current(),
             "bundle_stream": bundle_stream
         })
 
@@ -226,7 +229,22 @@ 
                 test_run_list,
                 pathname=pathname),
             "test_run_list": TestRun.objects.filter(
-                bundle__bundle_stream=bundle_stream),
+                bundle__bundle_stream=bundle_stream
+            ).order_by(  # clean any implicit ordering
+            ).select_related(
+                "test",
+                "bundle",
+                "bundle__bundle_stream",
+                "test_results"
+            ).only(
+                "analyzer_assigned_uuid",  # needed by TestRun.__unicode__
+                "analyzer_assigned_date",  # used by the view
+                "bundle__uploaded_on",  # needed by Bundle.get_absolute_url
+                "bundle__content_sha1",   # needed by Bundle.get_absolute_url
+                "bundle__bundle_stream__pathname",  # Needed by TestRun.get_absolute_url 
+                "test__name",  # needed by Test.__unicode__
+                "test__test_id",  # needed by Test.__unicode__
+            ),
             "bundle_stream": bundle_stream,
         }, RequestContext(request)
     )
@@ -554,3 +572,37 @@ 
                 test=test,
                 test_id=test_id),
         }, RequestContext(request))
+
+
+@BreadCrumb("Testing efforts", parent=index)
+def testing_effort_list(request):
+    return render_to_response(
+        "dashboard_app/testing_effort_list.html", {
+            'effort_list': TestingEffort.objects.all(
+            ).order_by('name'),
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                testing_effort_list),
+        }, RequestContext(request))
+
+
+@BreadCrumb(
+    "{effort}",
+    parent=testing_effort_list,
+    needs=["pk"])
+def testing_effort_detail(request, pk):
+    effort = get_object_or_404(TestingEffort, pk=pk)
+    return render_to_response(
+        "dashboard_app/testing_effort_detail.html", {
+            'effort': effort,
+            'test_run_list': effort.get_test_runs(
+            ).select_related(
+                'denormalization',
+                'bundle',
+                'bundle__bundle_stream',
+                'test',
+            ),
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                testing_effort_detail,
+                effort=effort,
+                pk=pk),
+        }, RequestContext(request))

=== modified file 'dashboard_app/xmlrpc.py'
--- dashboard_app/xmlrpc.py	2011-08-17 10:33:41 +0000
+++ dashboard_app/xmlrpc.py	2011-09-26 21:12:40 +0000
@@ -443,7 +443,7 @@ 
         if bundle.is_deserialized is False:
             raise xmlrpclib.Fault(
                 errors.CONFLICT,
-                bundle.deserialization_error.get().error_message)
+                bundle.deserialization_error.error_message)
         return True
 
     def make_stream(self, pathname, name):