From patchwork Wed Jul 18 00:08:11 2012 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Michael-Doyle Hudson X-Patchwork-Id: 10102 Return-Path: X-Original-To: patchwork@peony.canonical.com Delivered-To: patchwork@peony.canonical.com Received: from fiordland.canonical.com (fiordland.canonical.com [91.189.94.145]) by peony.canonical.com (Postfix) with ESMTP id 1FA7B24036 for ; Wed, 18 Jul 2012 00:08:16 +0000 (UTC) Received: from mail-yw0-f52.google.com (mail-yw0-f52.google.com [209.85.213.52]) by fiordland.canonical.com (Postfix) with ESMTP id CD1F0A18438 for ; Wed, 18 Jul 2012 00:08:15 +0000 (UTC) Received: by mail-yw0-f52.google.com with SMTP id p61so1141395yhp.11 for ; Tue, 17 Jul 2012 17:08:15 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20120113; h=x-forwarded-to:x-forwarded-for:delivered-to:received-spf :content-type:mime-version:x-launchpad-project:x-launchpad-branch :x-launchpad-message-rationale:x-launchpad-branch-revision-number :x-launchpad-notification-type:to:from:subject:message-id:date :reply-to:sender:errors-to:precedence:x-generated-by :x-launchpad-hash:x-gm-message-state; bh=ccfId4h4ZBb2dWZen28l2c3Ba/j+mkCBEG4cogRtWWc=; b=aMZlsm2691JpIYoeO8XjAM87e2Fk3gBFeiE1Cr2FV0OdAWoys6f63XXcZKntDuswRG QX8hPjAuZ/2gaLktmXf9BH7xtu1HJnKUl3qYkqqpR8MA3QScMQtnLubc51erN8SNYQv+ F5oFWoYKxfkcYdFKgwddkh073gK4g6elXF6KySD5xHS/VPsIbSmruQwYEEp3QQunXVjz cieDD3uuzSjauBzhHNG/FpJgpiqFnKYAfV5/nAq9oOGrwTgnIO7NQsqyKPUCWTkOUwEu 10J9thVbVQq8zhLgBbjuu+SW15Oaj30MUpg1IHzEO1uyUTl9oQeDJewoyLfTOEfvs5yF /MvQ== Received: by 10.50.193.196 with SMTP id hq4mr363312igc.57.1342570095178; Tue, 17 Jul 2012 17:08:15 -0700 (PDT) X-Forwarded-To: linaro-patchwork@canonical.com X-Forwarded-For: patch@linaro.org linaro-patchwork@canonical.com Delivered-To: patches@linaro.org Received: by 10.231.241.2 with SMTP id lc2csp25738ibb; Tue, 17 Jul 2012 17:08:12 -0700 (PDT) Received: by 10.216.138.200 with SMTP id a50mr495741wej.155.1342570092131; Tue, 17 Jul 2012 17:08:12 -0700 (PDT) Received: from indium.canonical.com (indium.canonical.com. [91.189.90.7]) by mx.google.com with ESMTPS id gs8si24331551wib.6.2012.07.17.17.08.11 (version=TLSv1/SSLv3 cipher=OTHER); Tue, 17 Jul 2012 17:08:12 -0700 (PDT) Received-SPF: pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) client-ip=91.189.90.7; Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) smtp.mail=bounces@canonical.com Received: from ackee.canonical.com ([91.189.89.26]) by indium.canonical.com with esmtp (Exim 4.71 #1 (Debian)) id 1SrHoJ-00066e-JE for ; Wed, 18 Jul 2012 00:08:11 +0000 Received: from ackee.canonical.com (localhost [127.0.0.1]) by ackee.canonical.com (Postfix) with ESMTP id 8835AE00A6 for ; Wed, 18 Jul 2012 00:08:11 +0000 (UTC) MIME-Version: 1.0 X-Launchpad-Project: lava-dashboard X-Launchpad-Branch: ~linaro-validation/lava-dashboard/trunk X-Launchpad-Message-Rationale: Subscriber X-Launchpad-Branch-Revision-Number: 326 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [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> Date: Wed, 18 Jul 2012 00:08:11 -0000 Reply-To: noreply@launchpad.net Sender: bounces@canonical.com Errors-To: bounces@canonical.com Precedence: bulk X-Generated-By: Launchpad (canonical.com); Revision="15627"; Instance="launchpad-lazr.conf" X-Launchpad-Hash: a048c0dc5424d0b63187cac0a3008b7db8ed4944 X-Gm-Message-State: ALoCoQlhFk1xb35PiiFClVpMOjEuJWeiIDBijr3pE5P4zOYJn2Fyahup6ssigZggFAfibcx6V60n 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 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 === 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 = '

Use a bug previously linked to ' + names.testrun + ':

    '; + for (var i = 0; i < previous.already_linked.length; i++) { + html += '
  • ' + previous.already_linked[i] + ' (already linked)
  • '; + } + for (var i = 0; i < previous.bugs.length; i++) { + html += '
  • ' + + previous.bugs[i] + '
  • '; + } + html += '
'; + html += "

Or enter another bug number:

"; + 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 %} {% for result in row_data %} - + {% if result.present %} {{ result.passes }}/{{ result.total }} + + {% for bug_id in result.bug_ids %} + [{{ bug_id }}] + {% endfor %} + [+] + {% else %} — {% endif %} @@ -76,4 +82,26 @@ + + + + {% 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[a-z0-9-]+)/\+new/$', 'testing_effort_create'), url(r'^image-reports/$', 'image_report_list'), url(r'^image-reports/(?P[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'])