diff mbox

[Branch,~linaro-validation/lava-dashboard/trunk] Rev 326: allow Launchpad bugs to be linked to test runs from the image status views

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

Commit Message

Michael-Doyle Hudson July 18, 2012, 12:08 a.m. UTC
Merge authors:
  Michael Hudson-Doyle (mwhudson)
Related merge proposals:
  https://code.launchpad.net/~mwhudson/lava-dashboard/launchpad-bug-linking/+merge/114782
  proposed by: Michael Hudson-Doyle (mwhudson)
  review: Approve - Andy Doan (doanac)
------------------------------------------------------------
revno: 326 [merge]
committer: Michael Hudson-Doyle <michael.hudson@linaro.org>
branch nick: trunk
timestamp: Wed 2012-07-18 12:01:01 +1200
message:
  allow Launchpad bugs to be linked to test runs from the image status views
added:
  dashboard_app/migrations/0015_auto__add_launchpadbug.py
modified:
  dashboard_app/admin.py
  dashboard_app/models.py
  dashboard_app/static/css/image-report.css
  dashboard_app/static/js/image-report.js
  dashboard_app/templates/dashboard_app/image-report.html
  dashboard_app/urls.py
  dashboard_app/views.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/admin.py'
--- dashboard_app/admin.py	2012-07-12 05:42:59 +0000
+++ dashboard_app/admin.py	2012-07-13 04:00:04 +0000
@@ -34,6 +34,7 @@ 
     Image,
     ImageAttribute,
     ImageSet,
+    LaunchpadBug,
     NamedAttribute,
     SoftwarePackage,
     SoftwareSource,
@@ -179,6 +180,8 @@ 
     filter_horizontal = ['images']
     save_as = True
 
+class LaunchpadBugAdmin(admin.ModelAdmin):
+    raw_id_fields = ['test_runs']
 
 admin.site.register(Attachment)
 admin.site.register(Bundle, BundleAdmin)
@@ -187,6 +190,7 @@ 
 admin.site.register(HardwareDevice, HardwareDeviceAdmin)
 admin.site.register(Image, ImageAdmin)
 admin.site.register(ImageSet, ImageSetAdmin)
+admin.site.register(LaunchpadBug, LaunchpadBugAdmin)
 admin.site.register(SoftwarePackage, SoftwarePackageAdmin)
 admin.site.register(SoftwareSource, SoftwareSourceAdmin)
 admin.site.register(Test, TestAdmin)

=== added file 'dashboard_app/migrations/0015_auto__add_launchpadbug.py'
--- dashboard_app/migrations/0015_auto__add_launchpadbug.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0015_auto__add_launchpadbug.py	2012-07-13 03:46:23 +0000
@@ -0,0 +1,252 @@ 
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'LaunchpadBug'
+        db.create_table('dashboard_app_launchpadbug', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('bug_id', self.gf('django.db.models.fields.PositiveIntegerField')(unique=True)),
+        ))
+        db.send_create_signal('dashboard_app', ['LaunchpadBug'])
+
+        # Adding M2M table for field test_runs on 'LaunchpadBug'
+        db.create_table('dashboard_app_launchpadbug_test_runs', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('launchpadbug', models.ForeignKey(orm['dashboard_app.launchpadbug'], null=False)),
+            ('testrun', models.ForeignKey(orm['dashboard_app.testrun'], null=False))
+        ))
+        db.create_unique('dashboard_app_launchpadbug_test_runs', ['launchpadbug_id', 'testrun_id'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'LaunchpadBug'
+        db.delete_table('dashboard_app_launchpadbug')
+
+        # Removing M2M table for field test_runs on 'LaunchpadBug'
+        db.delete_table('dashboard_app_launchpadbug_test_runs')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'dashboard_app.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'public_url': ('django.db.models.fields.URLField', [], {'max_length': '512', 'blank': 'True'})
+        },
+        'dashboard_app.bundle': {
+            'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
+            '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
+            '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
+        },
+        'dashboard_app.bundledeserializationerror': {
+            'Meta': {'object_name': 'BundleDeserializationError'},
+            'bundle': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'deserialization_error'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.Bundle']"}),
+            'error_message': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'traceback': ('django.db.models.fields.TextField', [], {'max_length': '32768'})
+        },
+        'dashboard_app.bundlestream': {
+            'Meta': {'object_name': 'BundleStream'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'dashboard_app.hardwaredevice': {
+            'Meta': {'object_name': 'HardwareDevice'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'device_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'dashboard_app.image': {
+            'Meta': {'object_name': 'Image'},
+            'build_number_attribute': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'bundle_streams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.BundleStream']", 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.imageattribute': {
+            'Meta': {'object_name': 'ImageAttribute'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'required_attributes'", 'to': "orm['dashboard_app.Image']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
+        },
+        'dashboard_app.imageset': {
+            'Meta': {'object_name': 'ImageSet'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'images': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.Image']", 'symmetrical': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.launchpadbug': {
+            'Meta': {'object_name': 'LaunchpadBug'},
+            'bug_id': ('django.db.models.fields.PositiveIntegerField', [], {'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'test_runs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.TestRun']", 'symmetrical': 'False'})
+        },
+        'dashboard_app.namedattribute': {
+            'Meta': {'unique_together': "(('object_id', 'name'),)", 'object_name': 'NamedAttribute'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.TextField', [], {})
+        },
+        'dashboard_app.softwarepackage': {
+            'Meta': {'unique_together': "(('name', 'version'),)", 'object_name': 'SoftwarePackage'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'dashboard_app.softwarepackagescratch': {
+            'Meta': {'object_name': 'SoftwarePackageScratch'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'dashboard_app.softwaresource': {
+            'Meta': {'object_name': 'SoftwareSource'},
+            'branch_revision': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'branch_url': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'branch_vcs': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
+            'commit_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project_name': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'dashboard_app.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '256'})
+        },
+        'dashboard_app.test': {
+            'Meta': {'object_name': 'Test'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'test_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'})
+        },
+        'dashboard_app.testcase': {
+            'Meta': {'unique_together': "(('test', 'test_case_id'),)", 'object_name': 'TestCase'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_cases'", 'to': "orm['dashboard_app.Test']"}),
+            'test_case_id': ('django.db.models.fields.TextField', [], {}),
+            'units': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        'dashboard_app.testingeffort': {
+            'Meta': {'object_name': 'TestingEffort'},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'testing_efforts'", 'to': "orm['lava_projects.Project']"}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'testing_efforts'", 'symmetrical': 'False', 'to': "orm['dashboard_app.Tag']"})
+        },
+        'dashboard_app.testresult': {
+            'Meta': {'ordering': "('_order',)", 'object_name': 'TestResult'},
+            '_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'filename': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'lineno': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'measurement': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '20', 'decimal_places': '10', 'blank': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'relative_index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'result': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'test_case': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'test_results'", 'null': 'True', 'to': "orm['dashboard_app.TestCase']"}),
+            'test_run': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_results'", 'to': "orm['dashboard_app.TestRun']"}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'dashboard_app.testrun': {
+            'Meta': {'ordering': "['-import_assigned_date']", 'object_name': 'TestRun'},
+            'analyzer_assigned_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'analyzer_assigned_uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}),
+            'bundle': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Bundle']"}),
+            'devices': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.HardwareDevice']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'import_assigned_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            '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'}),
+            'is_aggregate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'registered_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}),
+            'registered_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['dashboard_app']
\ No newline at end of file

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2012-07-16 22:00:15 +0000
+++ dashboard_app/models.py	2012-07-16 22:49:16 +0000
@@ -1492,3 +1492,13 @@ 
 
     def __unicode__(self):
         return self.name
+
+
+class LaunchpadBug(models.Model):
+
+    bug_id = models.PositiveIntegerField(unique=True)
+
+    test_runs = models.ManyToManyField(TestRun, related_name='launchpad_bugs')
+
+    def __unicode__(self):
+        return unicode(self.bug_id)

=== modified file 'dashboard_app/static/css/image-report.css'
--- dashboard_app/static/css/image-report.css	2012-07-12 22:36:31 +0000
+++ dashboard_app/static/css/image-report.css	2012-07-13 04:51:22 +0000
@@ -37,3 +37,9 @@ 
 #scroller td:first-child, #scroller th:first-child {
     border-left: none;
 }
+.bug-links {
+    float: right;
+}
+#go-to-bug-dialog {
+    text-align: center;
+}

=== modified file 'dashboard_app/static/js/image-report.js'
--- dashboard_app/static/js/image-report.js	2012-07-13 04:21:42 +0000
+++ dashboard_app/static/js/image-report.js	2012-07-16 22:51:19 +0000
@@ -9,13 +9,116 @@ 
     scroller.width(scroller.width() + space);
     if (atRight) scroller.scrollLeft(table.attr('scrollWidth'));
 }
-// Hook up the event and run resize ASAP (looks jumpy in FF if you
-// don't run it here).
 $(window).ready(
     function () {
-      $(window).resize(_resize);
-      $("#scroller").scrollLeft(100000);
-      _resize();
+        // Hook up the event and run resize ASAP (looks jumpy in FF if you
+        // don't run it here).
+        $(window).resize(_resize);
+        $("#scroller").scrollLeft(100000);
+        _resize();
+
+        function _submit() {
+            $(this).submit();
+        }
+        var add_bug_dialog = $('#add-bug-dialog').dialog(
+            {
+                autoOpen: false,
+                buttons: {'Cancel': function () {$(this).dialog('close');}, 'OK': _submit },
+                modal: true,
+                title: "Link bug to XXX"
+            });
+        var go_to_bug_dialog = $("#go-to-bug-dialog").dialog(
+            {
+                autoOpen: false,
+                buttons: {'Cancel': function () {$(this).dialog('close');}, 'Remove link': _submit},
+                modal: true,
+                title: "Link bug to XXX"
+            });
+
+        function get_testrun_and_buildnumber (element) {
+            var cell = element.closest('td');
+            var row = cell.closest('tr');
+            var testrun = $($("#test-run-names > tbody > tr")[row.index()]).text();
+            var header_cells = element.closest('table').find('thead > tr > th');
+            var buildnumber = $(header_cells[cell.index()]).text();
+            return {testrun: $.trim(testrun), buildnumber: $.trim(buildnumber)};
+        }
+
+        function find_previous_bugs (element) {
+            var td = $(element).closest('td');
+            var bugs = [];
+            var start = td;
+            while ((td = td.prev()) && td.size()) {
+                td.find(".bug-link").each(
+                    function (index, link) {
+                        var bug_id = $(link).data('bug-id');
+                        if (bugs.indexOf(bug_id) < 0) bugs.push(bug_id);
+                    });
+            }
+            var already_linked = [];
+            start.find(".bug-link").each(
+                function (index, link) {
+                    var bug_id = $(link).data('bug-id');
+                    if (bugs.indexOf(bug_id) >= 0) {
+                        bugs.splice(bugs.indexOf(bug_id), 1);
+                        already_linked.push(bug_id);
+                    }
+                });
+            return {bugs:bugs, already_linked:already_linked};
+        }
+
+        $('a.add-bug-link').click(
+            function (e) {
+                e.preventDefault();
+
+                var previous = find_previous_bugs($(this));
+                var prev_div = add_bug_dialog.find('div.prev');
+                var names = get_testrun_and_buildnumber($(this));
+
+                if (previous.bugs.length) {
+                    var html = '';
+                    prev_div.show();
+                    html = '<p>Use a bug previously linked to ' + names.testrun + ':</p><ul>';
+                    for (var i = 0; i < previous.already_linked.length; i++) {
+                        html += '<li><span style="text-decoration: line-through">' + previous.already_linked[i] + '</span> (already linked)</li>';
+                    }
+                    for (var i = 0; i < previous.bugs.length; i++) {
+                        html += '<li><a href="#" data-bug-id="' + previous.bugs[i] + '">' +
+                            previous.bugs[i] + '</a></li>';
+                    }
+                    html += '</ul>';
+                    html += "<p>Or enter another bug number:</p>";
+                    prev_div.html(html);
+                    prev_div.find('a').click(
+                        function (e) {
+                            e.preventDefault();
+                            add_bug_dialog.find('input[name=bug]').val($(this).data('bug-id'));
+                            add_bug_dialog.submit();
+                        });
+                } else {
+                    prev_div.hide();
+                }
+
+                var title = "Link a bug to the '" + names.testrun +
+                    "' run of build " + names.buildnumber;
+                add_bug_dialog.find('input[name=uuid]').val($(this).closest('td').data('uuid'));
+                add_bug_dialog.dialog('option', 'title', title);
+                add_bug_dialog.dialog('open');
+            });
+
+        $("a.bug-link").click(
+            function (e) {
+                e.preventDefault();
+                var names = get_testrun_and_buildnumber($(this));
+                var title = "Bug linked to the '" + names.testrun +
+                    "' run of build " + names.buildnumber;
+                go_to_bug_dialog.find('input[name=uuid]').val($(this).closest('td').data('uuid'));
+                go_to_bug_dialog.find('input[name=bug]').val($(this).data('bug-id'));
+                go_to_bug_dialog.find('a').attr('href', $(this).attr('href'));
+                go_to_bug_dialog.find('a').text('View bug ' + $(this).data('bug-id'));
+                go_to_bug_dialog.dialog('option', 'title', title);
+                go_to_bug_dialog.dialog('open');
+            });
     });
 // Because what resize does depends on the final sizes of elements,
 // run it again after everything is loaded (things end up wrong in

=== modified file 'dashboard_app/templates/dashboard_app/image-report.html'
--- dashboard_app/templates/dashboard_app/image-report.html	2012-07-12 22:42:42 +0000
+++ dashboard_app/templates/dashboard_app/image-report.html	2012-07-16 22:51:19 +0000
@@ -59,9 +59,15 @@ 
             {% for row_data in table_data %}
             <tr>
               {% for result in row_data %}
-              <td class="{{ result.cls }}">
+              <td class="{{ result.cls }}" data-uuid="{{ result.uuid }}">
                 {% if result.present %}
                 <a href="{{ result.link }}"> {{ result.passes }}/{{ result.total }} </a>
+                <span class="bug-links">
+                  {% for bug_id in result.bug_ids %}
+                  <a class="bug-link" href="https://bugs.launchpad.net/bugs/{{ bug_id }}" data-bug-id="{{ bug_id }}">[{{ bug_id }}]</a>
+                  {% endfor %}
+                  <a href="#" class="add-bug-link">[+]</a>
+                </span>
                 {% else %}
                 &mdash;
                 {% endif %}
@@ -76,4 +82,26 @@ 
   </tr>
 </table>
 
+<form method="POST"
+      action="{% url dashboard_app.views.link_bug_to_testrun %}"
+      id="add-bug-dialog" style="display: none">
+  {% csrf_token %}
+  <input type="hidden" name="back" value="{{ request.path }}"/>
+  <input type="hidden" name="uuid"/>
+  <div class="prev" style="display:none">
+    XXX
+  </div>
+  <input name="bug" style="width: 100%"/>
+</form>
+
+<form method="POST"
+      action="{% url dashboard_app.views.unlink_bug_and_testrun %}"
+      id="go-to-bug-dialog" style="display: none">
+  {% csrf_token %}
+  <input type="hidden" name="back" value="{{ request.path }}"/>
+  <input type="hidden" name="bug"/>
+  <input type="hidden" name="uuid"/>
+  <a href="">View bug XXX</a>
+</form>
+
 {% endblock %}

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2012-07-12 05:40:54 +0000
+++ dashboard_app/urls.py	2012-07-13 04:12:52 +0000
@@ -72,4 +72,6 @@ 
     url(r'^efforts/(?P<project_identifier>[a-z0-9-]+)/\+new/$', 'testing_effort_create'),
     url(r'^image-reports/$', 'image_report_list'),
     url(r'^image-reports/(?P<name>[A-Za-z0-9_-]+)$', 'image_report_detail'),
+    url(r'^api/link-bug-to-testrun', 'link_bug_to_testrun'),
+    url(r'^api/unlink-bug-and-testrun', 'unlink_bug_and_testrun'),
 )

=== modified file 'dashboard_app/views.py'
--- dashboard_app/views.py	2012-07-13 01:20:45 +0000
+++ dashboard_app/views.py	2012-07-13 04:23:58 +0000
@@ -32,6 +32,7 @@ 
 from django.shortcuts import render_to_response, redirect, get_object_or_404
 from django.template import RequestContext, loader
 from django.utils.safestring import mark_safe
+from django.views.decorators.http import require_POST
 from django.views.generic.list_detail import object_list, object_detail
 
 from django_tables2 import Attrs, Column, TemplateColumn
@@ -51,6 +52,7 @@ 
     DataView,
     Image,
     ImageSet,
+    LaunchpadBug,
     Tag,
     Test,
     TestResult,
@@ -917,7 +919,8 @@ 
     test_runs = TestRun.objects.filter(
         bundle_id__in=list(bundle_id_to_data),
         ).select_related(
-        'bundle', 'denormalization', 'test')
+        'bundle', 'denormalization', 'test').prefetch_related(
+        'launchpad_bugs')
 
     test_run_names = set()
     for test_run in test_runs:
@@ -927,6 +930,7 @@ 
             cls = 'present pass'
         else:
             cls = 'present fail'
+        bug_ids = sorted([b.bug_id for b in test_run.launchpad_bugs.all()])
         test_run_data = dict(
             present=True,
             cls=cls,
@@ -934,6 +938,7 @@ 
             passes=denorm.count_pass,
             total=denorm.count_all(),
             link=test_run.get_permalink(),
+            bug_ids=bug_ids,
             )
         bundle_id_to_data[test_run.bundle.id]['test_runs'][name] = test_run_data
         if name != 'lava':
@@ -967,3 +972,23 @@ 
             'table_data': table_data,
             'test_run_names': test_run_names,
         }, RequestContext(request))
+
+
+@require_POST
+def link_bug_to_testrun(request):
+    testrun = get_object_or_404(TestRun, analyzer_assigned_uuid=request.POST['uuid'])
+    bug_id = request.POST['bug']
+    lpbug = LaunchpadBug.objects.get_or_create(bug_id=int(bug_id))[0]
+    testrun.launchpad_bugs.add(lpbug)
+    testrun.save()
+    return HttpResponseRedirect(request.POST['back'])
+
+
+@require_POST
+def unlink_bug_and_testrun(request):
+    testrun = get_object_or_404(TestRun, analyzer_assigned_uuid=request.POST['uuid'])
+    bug_id = request.POST['bug']
+    lpbug = LaunchpadBug.objects.get_or_create(bug_id=int(bug_id))[0]
+    testrun.launchpad_bugs.remove(lpbug)
+    testrun.save()
+    return HttpResponseRedirect(request.POST['back'])