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: 10101 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 5E20424036 for ; Wed, 18 Jul 2012 00:08:15 +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 DA2FEA1870C for ; Wed, 18 Jul 2012 00:08:14 +0000 (UTC) Received: by yhpp61 with SMTP id p61so1141395yhp.11 for ; Tue, 17 Jul 2012 17:08:14 -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=FsxZ/D3ldM8xGR3BiE5zc8fVg728EV4S8rFhOIm5ck0=; b=NADNiA5nUBeSuMT1dTOMSQdS5ZQZtYVksWUXuSMYvWJ1sPuKYKPqqI3u356txhnYFr 1yAo47K4xF5H8gBXXFgY0ieyReaVuGSl5UE+dMFPf5aoZ5x1wc0cNnC3WirnBAt/MI4B dE81D1slezkA+AL5lquurcBQPw0Vvjlc2o3PBMMjQNLQO3IVy1KZMPvn6WQdULS4Fh71 vr95MzXFaF/y/Hg3nT6UPs+3vK+pmuvfVT5oQ/IavQbpw35yGAsaP4JUfq1GWBGfweQ0 6f8oBoE7PKR+yscUsbsG7fjURh0+FSRuFnSYN0WdKOuu/Oj6VCG5JLLf3OtiSX0zUIX4 erQA== Received: by 10.50.160.198 with SMTP id xm6mr458525igb.0.1342570094257; Tue, 17 Jul 2012 17:08:14 -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 lc2csp25737ibb; Tue, 17 Jul 2012 17:08:12 -0700 (PDT) Received: by 10.180.92.129 with SMTP id cm1mr1676039wib.10.1342570092161; 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 r48si26268555wel.144.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-00066T-9X 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 3DE86E029B 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: 325 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [Branch ~linaro-validation/lava-dashboard/trunk] Rev 325: models and views to let the QA services team track image status in LAVA Message-Id: <20120718000811.20915.7547.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: 6bc7efdd2d82e1535aba924c0d5d1b4186b064ee X-Gm-Message-State: ALoCoQlcr58AspyxX1YQwU2Buw+D/ZYz9RKfpIYIwMP1O1x0yLAPTSoWiv83Pik8xW6ZsgFgD8iW Merge authors: Michael Hudson-Doyle (mwhudson) Related merge proposals: https://code.launchpad.net/~mwhudson/lava-dashboard/image-status-view-2/+merge/114768 proposed by: Michael Hudson-Doyle (mwhudson) ------------------------------------------------------------ revno: 325 [merge] committer: Michael Hudson-Doyle branch nick: trunk timestamp: Wed 2012-07-18 12:00:12 +1200 message: models and views to let the QA services team track image status in LAVA added: dashboard_app/migrations/0014_auto__add_imageset__add_image__add_imageattribute.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/templates/dashboard_app/image-reports.html modified: dashboard_app/admin.py dashboard_app/extension.py dashboard_app/models.py 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-03-16 16:57:24 +0000 +++ dashboard_app/admin.py 2012-07-12 05:42:59 +0000 @@ -31,6 +31,9 @@ BundleDeserializationError, BundleStream, HardwareDevice, + Image, + ImageAttribute, + ImageSet, NamedAttribute, SoftwarePackage, SoftwareSource, @@ -160,11 +163,30 @@ list_display = ('__unicode__', 'project') +class ImageAttributeInline(admin.TabularInline): + model = ImageAttribute + verbose_name = 'required metadata attribute' + verbose_name_plural = 'required metadata attributes' + + +class ImageAdmin(admin.ModelAdmin): + filter_horizontal = ['bundle_streams'] + inlines = [ImageAttributeInline] + save_as = True + + +class ImageSetAdmin(admin.ModelAdmin): + filter_horizontal = ['images'] + save_as = True + + admin.site.register(Attachment) admin.site.register(Bundle, BundleAdmin) admin.site.register(BundleDeserializationError, BundleDeserializationErrorAdmin) admin.site.register(BundleStream, BundleStreamAdmin) admin.site.register(HardwareDevice, HardwareDeviceAdmin) +admin.site.register(Image, ImageAdmin) +admin.site.register(ImageSet, ImageSetAdmin) admin.site.register(SoftwarePackage, SoftwarePackageAdmin) admin.site.register(SoftwareSource, SoftwareSourceAdmin) admin.site.register(Test, TestAdmin) === modified file 'dashboard_app/extension.py' --- dashboard_app/extension.py 2012-03-26 04:05:20 +0000 +++ dashboard_app/extension.py 2012-07-13 01:22:03 +0000 @@ -44,7 +44,9 @@ Menu("Bundle Streams", reverse("dashboard_app.views.bundle_stream_list")), Menu("Tests", reverse("dashboard_app.views.test_list")), Menu("Data Views", reverse("dashboard_app.views.data_view_list")), - Menu("Reports", reverse("dashboard_app.views.report_list"))] + Menu("Reports", reverse("dashboard_app.views.report_list")), + Menu("Image Reports", reverse("dashboard_app.views.image_report_list")), + ] return menu @property === added file 'dashboard_app/migrations/0014_auto__add_imageset__add_image__add_imageattribute.py' --- dashboard_app/migrations/0014_auto__add_imageset__add_image__add_imageattribute.py 1970-01-01 00:00:00 +0000 +++ dashboard_app/migrations/0014_auto__add_imageset__add_image__add_imageattribute.py 2012-07-12 04:58:23 +0000 @@ -0,0 +1,280 @@ +# -*- 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 'ImageSet' + db.create_table('dashboard_app_imageset', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=1024)), + )) + db.send_create_signal('dashboard_app', ['ImageSet']) + + # Adding M2M table for field images on 'ImageSet' + db.create_table('dashboard_app_imageset_images', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('imageset', models.ForeignKey(orm['dashboard_app.imageset'], null=False)), + ('image', models.ForeignKey(orm['dashboard_app.image'], null=False)) + )) + db.create_unique('dashboard_app_imageset_images', ['imageset_id', 'image_id']) + + # Adding model 'Image' + db.create_table('dashboard_app_image', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=1024)), + ('build_number_attribute', self.gf('django.db.models.fields.CharField')(max_length=1024)), + )) + db.send_create_signal('dashboard_app', ['Image']) + + # Adding M2M table for field bundle_streams on 'Image' + db.create_table('dashboard_app_image_bundle_streams', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('image', models.ForeignKey(orm['dashboard_app.image'], null=False)), + ('bundlestream', models.ForeignKey(orm['dashboard_app.bundlestream'], null=False)) + )) + db.create_unique('dashboard_app_image_bundle_streams', ['image_id', 'bundlestream_id']) + + # Adding model 'ImageAttribute' + db.create_table('dashboard_app_imageattribute', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=1024)), + ('value', self.gf('django.db.models.fields.CharField')(max_length=1024)), + ('image', self.gf('django.db.models.fields.related.ForeignKey')(related_name='required_attributes', to=orm['dashboard_app.Image'])), + )) + db.send_create_signal('dashboard_app', ['ImageAttribute']) + + + def backwards(self, orm): + # Deleting model 'ImageSet' + db.delete_table('dashboard_app_imageset') + + # Removing M2M table for field images on 'ImageSet' + db.delete_table('dashboard_app_imageset_images') + + # Deleting model 'Image' + db.delete_table('dashboard_app_image') + + # Removing M2M table for field bundle_streams on 'Image' + db.delete_table('dashboard_app_image_bundle_streams') + + # Deleting model 'ImageAttribute' + db.delete_table('dashboard_app_imageattribute') + + + 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.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-03-26 04:00:57 +0000 +++ dashboard_app/models.py 2012-07-16 22:00:15 +0000 @@ -1412,3 +1412,83 @@ return TestRun.objects.order_by( ).filter( tags__in=self.tags.all()) + + +class ImageAttribute(models.Model): + + name = models.CharField(max_length=1024) + value = models.CharField(max_length=1024) + + image = models.ForeignKey("Image", related_name="required_attributes") + + def __unicode__(self): + return '%s = %s' % (self.name, self.value) + +class Image(models.Model): + + name = models.CharField(max_length=1024, unique=True) + + build_number_attribute = models.CharField(max_length=1024) + + bundle_streams = models.ManyToManyField(BundleStream) + + def __unicode__(self): + return self.name + + def get_bundles(self, user): + accessible_bundles = BundleStream.objects.accessible_by_principal( + user) + args = [models.Q(bundle_stream__in=accessible_bundles)] + if self.bundle_streams.exists(): + args += [models.Q(bundle_stream__in=self.bundle_streams.all())] + bundles = Bundle.objects.filter(*args) + + # This is a little tricky. We want to AND together the conditions + # that the attribute matches, but with the Django ORM we can only join + # the attribute table once per query so we put each condition in a + # nested query, so for example instead of something like this: + # + # select * from bundle + # where + # and + # and bundle.id in + # (select * from bundle + # where tbody > tr > td { + padding: 0; + vertical-align: top; +} +#outer-table th { + text-align: left; +} +.inner-table td, .inner-table th { + min-width: 25ex; + padding: 3px 4px; + border: thin solid black; +} +.inner-table td.present a { + color: #00a; + text-decoration: none; +} +.inner-table td.present a:hover { + text-decoration: underline; +} +.inner-table td.pass { + background-color: #4e4; +} +.inner-table td.fail { + background-color: #e44; +} +.inner-table td.missing { + background-color: #bbb; +} +#scroller { + width: 800px; + overflow-x: scroll; + overflow-y: visible; +} +#scroller td:first-child, #scroller th:first-child { + border-left: none; +} === added file 'dashboard_app/static/js/image-report.js' --- dashboard_app/static/js/image-report.js 1970-01-01 00:00:00 +0000 +++ dashboard_app/static/js/image-report.js 2012-07-13 04:21:42 +0000 @@ -0,0 +1,23 @@ +function _resize() { + // I couldn't figure out how to do this in CSS: resize the table + // so that it takes as much space as it can without expanding the + // page horizontally. + var space = parseInt($("#lava-breadcrumbs").outerWidth() - $("#outer-table").outerWidth()); + space -= $("#lava-content").outerWidth() - $("#lava-content").width(); + var table = $("#results-table"), scroller=$("#scroller"); + var atRight = scroller.width() + scroller.scrollLeft() >= table.attr('scrollWidth'); + 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(); + }); +// Because what resize does depends on the final sizes of elements, +// run it again after everything is loaded (things end up wrong in +// chromium if you don't do this). +$(window).load(_resize); === added file 'dashboard_app/templates/dashboard_app/image-report.html' --- dashboard_app/templates/dashboard_app/image-report.html 1970-01-01 00:00:00 +0000 +++ dashboard_app/templates/dashboard_app/image-report.html 2012-07-12 22:42:42 +0000 @@ -0,0 +1,79 @@ +{% extends "dashboard_app/_content.html" %} + +{% block extrahead %} +{{ block.super }} + + +{% endblock %} + +{% block content %} +

Image Report: {{ image.name }}

+ + + + + + +
+ + + + + + + + + + + {% for test_run_name in test_run_names %} + + + + {% endfor %} + +
+ Build Number +
+ Date +
+ {{ test_run_name }} +
+
+
+ + + + {% for bundle in bundles %} + + {% endfor %} + + + + + {% for bundle in bundles %} + + {% endfor %} + + {% for row_data in table_data %} + + {% for result in row_data %} + + {% endfor %} + + {% endfor %} + +
+ {{ bundle.number }} +
+ {{ bundle.date|date }} +
+ {% if result.present %} + {{ result.passes }}/{{ result.total }} + {% else %} + — + {% endif %} +
+
+
+ +{% endblock %} === added file 'dashboard_app/templates/dashboard_app/image-reports.html' --- dashboard_app/templates/dashboard_app/image-reports.html 1970-01-01 00:00:00 +0000 +++ dashboard_app/templates/dashboard_app/image-reports.html 2012-07-12 06:16:07 +0000 @@ -0,0 +1,21 @@ +{% extends "dashboard_app/_content.html" %} + +{% block content %} +

Image Reports

+ +{% for imageset in imagesets %} +

{{ imageset.name }}

+
    + {% for image in imageset.images %} +
  • + {% if image.bundle_count %} + {{ image.name }} + ({{ image.bundle_count }} results) + {% else %} + {{ image.name }} ({{ image.bundle_count }} results) + {% endif %} +
  • + {% endfor %} +
+{% endfor %} +{% endblock %} === modified file 'dashboard_app/urls.py' --- dashboard_app/urls.py 2012-03-26 04:00:57 +0000 +++ dashboard_app/urls.py 2012-07-12 05:40:54 +0000 @@ -70,4 +70,6 @@ url(r'^efforts/(?P[0-9]+)/$', 'testing_effort_detail'), url(r'^efforts/(?P[0-9]+)/update/$', 'testing_effort_update'), 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'), ) === modified file 'dashboard_app/views.py' --- dashboard_app/views.py 2012-06-14 05:50:23 +0000 +++ dashboard_app/views.py 2012-07-13 01:20:45 +0000 @@ -49,6 +49,8 @@ BundleStream, DataReport, DataView, + Image, + ImageSet, Tag, Test, TestResult, @@ -849,3 +851,119 @@ pk=effort.pk) }) return HttpResponse(t.render(c)) + + +@BreadCrumb("Image Reports", parent=index) +def image_report_list(request): + imagesets = ImageSet.objects.all() + imagesets_data = [] + for imageset in imagesets: + images_data = [] + for image in imageset.images.all(): + image_data = { + 'name': image.name, + 'bundle_count': image.get_bundles(request.user).count(), + } + images_data.append(image_data) + imageset_data = { + 'name': imageset, + 'images': images_data, + } + imagesets_data.append(imageset_data) + return render_to_response( + "dashboard_app/image-reports.html", { + 'bread_crumb_trail': BreadCrumbTrail.leading_to(image_report_list), + 'imagesets': imagesets_data, + }, RequestContext(request)) + + +@BreadCrumb("{name}", parent=image_report_list, needs=['name']) +def image_report_detail(request, name): + + image = Image.objects.get(name=name) + + # We are aiming to produce a table like this: + + # Build Number | 23 | ... | 40 | + # Date | YYYY-MM-DD | ... | YYYY-MM-DD | + # lava | 1/3 | ... | 4/5 | + # cts | 100/100 | ... | 88/100 | + # ... | ... | ... | ... | + # skia | 1/2 | ... | 3/3 | + + # Data processing proceeds in 3 steps: + + # 1) Get the bundles/builds. Image.get_latest_bundles() does the hard + # work here and then we just peel off the data we need from the bundles. + + # 2) Get all the test runs we are interested in, extract the data we + # need from them and associate them with the corresponding bundles. + + # 3) Organize the data so that it's natural for rendering the table + # (basically transposing it from being bundle -> testrun -> result to + # testrun -> bundle -> result). + + bundles = image.get_latest_bundles(request.user, 50) + + bundle_id_to_data = {} + for bundle in bundles: + bundle_id_to_data[bundle.id] = dict( + number=bundle.build_number, + date=bundle.uploaded_on, + test_runs={}, + link=bundle.get_permalink(), + ) + + test_runs = TestRun.objects.filter( + bundle_id__in=list(bundle_id_to_data), + ).select_related( + 'bundle', 'denormalization', 'test') + + test_run_names = set() + for test_run in test_runs: + name = test_run.test.test_id + denorm = test_run.denormalization + if denorm.count_pass == denorm.count_all(): + cls = 'present pass' + else: + cls = 'present fail' + test_run_data = dict( + present=True, + cls=cls, + uuid=test_run.analyzer_assigned_uuid, + passes=denorm.count_pass, + total=denorm.count_all(), + link=test_run.get_permalink(), + ) + bundle_id_to_data[test_run.bundle.id]['test_runs'][name] = test_run_data + if name != 'lava': + test_run_names.add(name) + + test_run_names = sorted(test_run_names) + test_run_names.insert(0, 'lava') + + bundles = sorted(bundle_id_to_data.values(), key=lambda d:d['number']) + + table_data = [] + + for test_run_name in test_run_names: + row_data = [] + for bundle in bundles: + test_run_data = bundle['test_runs'].get(test_run_name) + if not test_run_data: + test_run_data = dict( + present=False, + cls='missing', + ) + row_data.append(test_run_data) + table_data.append(row_data) + + return render_to_response( + "dashboard_app/image-report.html", { + 'bread_crumb_trail': BreadCrumbTrail.leading_to( + image_report_detail, name=image.name), + 'image': image, + 'bundles': bundles, + 'table_data': table_data, + 'test_run_names': test_run_names, + }, RequestContext(request))