diff mbox

[Branch,~linaro-validation/lava-dashboard/trunk] Rev 350: a better way of basing image reports on filters

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

Commit Message

Michael-Doyle Hudson Sept. 26, 2012, 8:56 p.m. UTC
Merge authors:
  Michael Hudson-Doyle (mwhudson)
Related merge proposals:
  https://code.launchpad.net/~mwhudson/lava-dashboard/proper-filter-based-image-reports/+merge/126355
  proposed by: Michael Hudson-Doyle (mwhudson)
  review: Approve - Andy Doan (doanac)
------------------------------------------------------------
revno: 350 [merge]
committer: Michael Hudson-Doyle <michael.hudson@linaro.org>
branch nick: trunk
timestamp: Thu 2012-09-27 08:54:46 +1200
message:
  a better way of basing image reports on filters
added:
  dashboard_app/migrations/0024_auto__del_imageattribute__del_field_image_uploaded_by__del_field_image.py
  dashboard_app/static/css/wider-filter-horizontal.css
modified:
  dashboard_app/admin.py
  dashboard_app/models.py
  dashboard_app/templates/dashboard_app/image-report.html
  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-09-24 03:38:56 +0000
+++ dashboard_app/admin.py	2012-09-25 22:40:20 +0000
@@ -33,7 +33,6 @@ 
     BundleStream,
     HardwareDevice,
     Image,
-    ImageAttribute,
     ImageSet,
     LaunchpadBug,
     NamedAttribute,
@@ -183,27 +182,16 @@ 
     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 = ['filters']
-    def formfield_for_manytomany(self, db_field, request, **kwargs):
-        field = super(ImageSetAdmin, self).formfield_for_manytomany(
-            db_field, request, **kwargs)
-        print db_field
-        if db_field.name == 'filters':
-            field.queryset = TestRunFilter.objects.filter(enable_as_image=True).order_by('name')
-        return field
+    class Media:
+        css = {
+            "all": ("dashboard_app/css/wider-filter-horizontal.css",)
+        }
+    filter_horizontal = ['images']
     save_as = True
 
 

=== added file 'dashboard_app/migrations/0024_auto__del_imageattribute__del_field_image_uploaded_by__del_field_image.py'
--- dashboard_app/migrations/0024_auto__del_imageattribute__del_field_image_uploaded_by__del_field_image.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0024_auto__del_imageattribute__del_field_image_uploaded_by__del_field_image.py	2012-09-25 22:21:40 +0000
@@ -0,0 +1,325 @@ 
+# -*- 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):
+        # Deleting model 'ImageAttribute'
+        db.delete_table('dashboard_app_imageattribute')
+
+        # Deleting field 'Image.uploaded_by'
+        db.delete_column('dashboard_app_image', 'uploaded_by_id')
+
+        # Deleting field 'Image.build_number_attribute'
+        db.delete_column('dashboard_app_image', 'build_number_attribute')
+
+        # Adding field 'Image.filter'
+        db.add_column('dashboard_app_image', 'filter',
+                      self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['dashboard_app.TestRunFilter']),
+                      keep_default=False)
+
+        # Removing M2M table for field bundle_streams on 'Image'
+        db.delete_table('dashboard_app_image_bundle_streams')
+
+        # Removing M2M table for field filters on 'ImageSet'
+        db.delete_table('dashboard_app_imageset_filters')
+
+        # Deleting field 'TestRunFilter.enable_as_image'
+        db.delete_column('dashboard_app_testrunfilter', 'enable_as_image')
+
+
+    def backwards(self, orm):
+        # Adding model 'ImageAttribute'
+        db.create_table('dashboard_app_imageattribute', (
+            ('image', self.gf('django.db.models.fields.related.ForeignKey')(related_name='required_attributes', to=orm['dashboard_app.Image'])),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('value', self.gf('django.db.models.fields.CharField')(max_length=1024)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=1024)),
+        ))
+        db.send_create_signal('dashboard_app', ['ImageAttribute'])
+
+        # Adding field 'Image.uploaded_by'
+        db.add_column('dashboard_app_image', 'uploaded_by',
+                      self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True),
+                      keep_default=False)
+
+
+        # User chose to not deal with backwards NULL issues for 'Image.build_number_attribute'
+        raise RuntimeError("Cannot reverse this migration. 'Image.build_number_attribute' and its values cannot be restored.")
+        # Deleting field 'Image.filter'
+        db.delete_column('dashboard_app_image', 'filter_id')
+
+        # 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 M2M table for field filters on 'ImageSet'
+        db.create_table('dashboard_app_imageset_filters', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('imageset', models.ForeignKey(orm['dashboard_app.imageset'], null=False)),
+            ('testrunfilter', models.ForeignKey(orm['dashboard_app.testrunfilter'], null=False))
+        ))
+        db.create_unique('dashboard_app_imageset_filters', ['imageset_id', 'testrunfilter_id'])
+
+        # Adding field 'TestRunFilter.enable_as_image'
+        db.add_column('dashboard_app_testrunfilter', 'enable_as_image',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'dashboard_app.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'public_url': ('django.db.models.fields.URLField', [], {'max_length': '512', 'blank': 'True'})
+        },
+        'dashboard_app.bundle': {
+            'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
+            '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
+            '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
+        },
+        'dashboard_app.bundledeserializationerror': {
+            'Meta': {'object_name': 'BundleDeserializationError'},
+            'bundle': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'deserialization_error'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.Bundle']"}),
+            'error_message': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'traceback': ('django.db.models.fields.TextField', [], {'max_length': '32768'})
+        },
+        'dashboard_app.bundlestream': {
+            'Meta': {'object_name': 'BundleStream'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'dashboard_app.hardwaredevice': {
+            'Meta': {'object_name': 'HardwareDevice'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'device_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'dashboard_app.image': {
+            'Meta': {'object_name': 'Image'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.imageset': {
+            'Meta': {'object_name': 'ImageSet'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'images': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.Image']", 'symmetrical': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.launchpadbug': {
+            'Meta': {'object_name': 'LaunchpadBug'},
+            'bug_id': ('django.db.models.fields.PositiveIntegerField', [], {'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'test_runs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'launchpad_bugs'", 'symmetrical': 'False', 'to': "orm['dashboard_app.TestRun']"})
+        },
+        'dashboard_app.namedattribute': {
+            'Meta': {'unique_together': "(('object_id', 'name'),)", 'object_name': 'NamedAttribute'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.TextField', [], {})
+        },
+        'dashboard_app.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']"})
+        },
+        'dashboard_app.testrunfilter': {
+            'Meta': {'unique_together': "(('owner', 'name'),)", 'object_name': 'TestRunFilter'},
+            'build_number_attribute': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'bundle_streams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.BundleStream']", 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'max_length': '1024'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"})
+        },
+        'dashboard_app.testrunfilterattribute': {
+            'Meta': {'object_name': 'TestRunFilterAttribute'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
+        },
+        'dashboard_app.testrunfiltersubscription': {
+            'Meta': {'unique_together': "(('user', 'filter'),)", 'object_name': 'TestRunFilterSubscription'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'dashboard_app.testrunfiltertest': {
+            'Meta': {'object_name': 'TestRunFilterTest'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tests'", 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.Test']"})
+        },
+        'dashboard_app.testrunfiltertestcase': {
+            'Meta': {'object_name': 'TestRunFilterTestCase'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cases'", 'to': "orm['dashboard_app.TestRunFilterTest']"}),
+            'test_case': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.TestCase']"})
+        },
+        'lava_projects.project': {
+            'Meta': {'object_name': 'Project'},
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'identifier': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}),
+            'is_aggregate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'registered_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}),
+            'registered_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['dashboard_app']
\ No newline at end of file

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2012-09-24 03:50:34 +0000
+++ dashboard_app/models.py	2012-09-25 23:31:26 +0000
@@ -1432,84 +1432,15 @@ 
             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.SlugField(max_length=1024, unique=True)
 
-    build_number_attribute = models.CharField(max_length=1024)
-
-    bundle_streams = models.ManyToManyField(BundleStream)
-
-    uploaded_by = models.ForeignKey(User, null=True, blank=True)
+    filter = models.ForeignKey("TestRunFilter", related_name='+', null=True)
 
     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())]
-        if self.uploaded_by:
-            args += [models.Q(uploaded_by=self.uploaded_by)]
-        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 <bundle.testrun.name is vexpress>
-        #    and <bundle.testrun.image_type = 'desktop';
-        #
-        # we generate this:
-        #
-        # select * from bundle
-        #  where <bundle.testrun.name is vexpress>
-        #    and bundle.id in
-        #      (select * from bundle
-        #       where <bundle.testrun.image_type = 'desktop');
-        #
-        # (additionally, we only consider the lava testrun to avoid returning
-        # bundles repeatedly).
-
-        for attr in self.required_attributes.all():
-            bundles = Bundle.objects.filter(
-                id__in=bundles.values_list('id'),
-                test_runs__test__test_id='lava',
-                test_runs__attributes__name=attr.name,
-                test_runs__attributes__value=attr.value)
-
-        return bundles
-
-    def get_bundles(self, user):
-        return Bundle.objects.filter(
-            id__in=self._get_bundles(user).values('id'),
-            test_runs__test__test_id='lava',
-            test_runs__attributes__name=self.build_number_attribute)
-
-    def get_latest_bundles(self, user, count):
-        return Bundle.objects.filter(
-            id__in=self._get_bundles(user).values('id'),
-            test_runs__test__test_id='lava',
-            test_runs__attributes__name=self.build_number_attribute).extra(
-            select={
-                'build_number': 'convert_to_integer("dashboard_app_namedattribute"."value")',
-                }).extra(
-            order_by=['-build_number'],
-            )[:count]
+        owner_name = getattr(self.filter, 'owner_name', '<NULL>')
+        return '%s, based on %s' % (self.name, owner_name)
 
     @models.permalink
     def get_absolute_url(self):
@@ -1520,9 +1451,7 @@ 
 
     name = models.CharField(max_length=1024, unique=True)
 
-    images = models.ManyToManyField(Image, help_text="This field is now obsolete")
-
-    filters = models.ManyToManyField("TestRunFilter")
+    images = models.ManyToManyField(Image)
 
     def __unicode__(self):
         return self.name
@@ -1621,14 +1550,15 @@ 
     """Wrap a QuerySet and construct FilterMatchs from what the wrapped query
     set returns.
 
-    Just enough of the QuerySet API to work with DataTable (i.e. ordering and
-    slicing)."""
+    Just enough of the QuerySet API to work with DataTable (i.e. pretend
+    ordering and real slicing)."""
 
     model = TestRun
 
-    def __init__(self, queryset, filter_data):
+    def __init__(self, queryset, filter_data, prefetch_related):
         self.queryset = queryset
         self.filter_data = filter_data
+        self.prefetch_related = prefetch_related
         if filter_data['build_number_attribute']:
             self.key = 'build_number'
             self.key_name = 'Build'
@@ -1642,7 +1572,8 @@ 
             test_run_ids.update(datum['id__arrayagg'])
         r = []
         trs = TestRun.objects.filter(id__in=test_run_ids).select_related(
-            'denormalization', 'bundle', 'bundle__bundle_stream', 'test')
+            'denormalization', 'bundle', 'bundle__bundle_stream', 'test').prefetch_related(
+            *self.prefetch_related)
         trs_by_id = {}
         for tr in trs:
             trs_by_id[tr.id] = tr
@@ -1690,7 +1621,7 @@ 
         return iter(r)
 
     def _wrap(self, queryset, **kw):
-        return self.__class__(queryset, self.filter_data, **kw)
+        return self.__class__(queryset, self.filter_data, self.prefetch_related, **kw)
 
     def order_by(self, *args):
         # the generic tables code calls this even when it shouldn't...
@@ -1771,6 +1702,11 @@ 
         help_text=("The <b>name</b> of a filter is used to refer to it in "
                    "the web UI and in email notifications triggered by this "
                    "filter."))
+
+    @property
+    def owner_name(self):
+        return '~%s/%s' % (self.owner.username, self.name)
+
     class Meta:
         unique_together = (('owner', 'name'))
 
@@ -1788,8 +1724,6 @@ 
         User, null=True, blank=True, related_name='+',
         help_text="Only consider bundles uploaded by this user")
 
-    enable_as_image = models.BooleanField(default=False)
-
     @property
     def summary_data(self):
         return {
@@ -1811,7 +1745,7 @@ 
     #    and testrun has attribute with key = keyN and value = valueN
     #    and testrun has any of the tests/testcases requested
 
-    def get_test_runs_impl(self, user, bundle_streams, attributes, tests):
+    def get_test_runs_impl(self, user, bundle_streams, attributes, tests, prefetch_related=[]):
         accessible_bundle_streams = BundleStream.objects.accessible_by_principal(
             user)
         bs_ids = [bs.id for bs in set(accessible_bundle_streams) & set(bundle_streams)]
@@ -1868,7 +1802,7 @@ 
             'build_number_attribute': self.build_number_attribute,
             }
 
-        return MatchMakingQuerySet(testruns, filter_data)
+        return MatchMakingQuerySet(testruns, filter_data, prefetch_related)
 
     # given bundle:
     # select from filter
@@ -1933,12 +1867,13 @@ 
             matches.append(match)
         return matches
 
-    def get_test_runs(self, user):
+    def get_test_runs(self, user, prefetch_related=[]):
         return self.get_test_runs_impl(
             user,
             self.bundle_streams.all(),
             self.attributes.values_list('name', 'value'),
-            self.tests.all())
+            self.tests.all(),
+            prefetch_related)
 
     @models.permalink
     def get_absolute_url(self):

=== added file 'dashboard_app/static/css/wider-filter-horizontal.css'
--- dashboard_app/static/css/wider-filter-horizontal.css	1970-01-01 00:00:00 +0000
+++ dashboard_app/static/css/wider-filter-horizontal.css	2012-09-25 22:38:13 +0000
@@ -0,0 +1,16 @@ 
+.selector {
+    width: 1000px;
+    float: left;
+}
+
+.selector select {
+    width: 480px;
+    height: 17.2em;
+}
+
+.selector-available, .selector-chosen {
+    float: left;
+    width: 480px;
+    text-align: center;
+    margin-bottom: 5px;
+}

=== modified file 'dashboard_app/templates/dashboard_app/image-report.html'
--- dashboard_app/templates/dashboard_app/image-report.html	2012-07-16 22:51:19 +0000
+++ dashboard_app/templates/dashboard_app/image-report.html	2012-09-25 23:31:26 +0000
@@ -41,18 +41,18 @@ 
         <table id="results-table" class="inner-table">
           <thead>
             <tr>
-              {% for bundle in bundles %}
+              {% for col in cols %}
               <th>
-                <a href="{{ bundle.link }}">{{ bundle.number }}</a>
+                <a href="{{ col.link }}">{{ col.number }}</a>
               </th>
               {% endfor %}
             </tr>
           </thead>
           <tbody>
             <tr>
-              {% for bundle in bundles %}
+              {% for col in cols %}
               <td>
-                {{ bundle.date|date }}
+                {{ col.date|date }}
               </td>
               {% endfor %}
             </tr>

=== modified file 'dashboard_app/views.py'
--- dashboard_app/views.py	2012-09-24 03:47:27 +0000
+++ dashboard_app/views.py	2012-09-25 23:49:47 +0000
@@ -1485,17 +1485,20 @@ 
 
 @BreadCrumb("Image Reports", parent=index)
 def image_report_list(request):
-    imagesets = ImageSet.objects.all()
+    imagesets = ImageSet.objects.filter()
     imagesets_data = []
     for imageset in imagesets:
         images_data = []
-        for filter in imageset.filters.all():
-            image_data = {
-                'name': filter.name,
-                'bundle_count': filter.get_test_runs(request.user).count(),
-                'link': filter.name,
-                }
-            images_data.append(image_data)
+        for image in imageset.images.all():
+            # Migration hack: Image.filter cannot be auto populated, so ignore
+            # images that have not been migrated to filters for now.
+            if image.filter:
+                image_data = {
+                    'name': image.name,
+                    'bundle_count': image.filter.get_test_runs(request.user).count(),
+                    'link': image.name,
+                    }
+                images_data.append(image_data)
         images_data.sort(key=lambda d:d['name'])
         imageset_data = {
             'name': imageset.name,
@@ -1513,83 +1516,55 @@ 
 @BreadCrumb("{name}", parent=image_report_list, needs=['name'])
 def image_report_detail(request, name):
 
-    filter = TestRunFilter.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).
-
-    bundle_id_to_data = {}
-
-    matches = filter.get_test_runs(request.user)[:50]
+    image = Image.objects.get(name=name)
+    matches = image.filter.get_test_runs(request.user, prefetch_related=['launchpad_bugs'])[:50]
+
+    build_number_to_cols = {}
+
+    test_run_names = set()
 
     for match in matches:
-        for tr in match.test_runs:
-            if tr.bundle_id not in bundle_id_to_data:
-                bundle = tr.bundle
-                bundle_id_to_data[bundle.id] = dict(
-                    number=match.tag,
-                    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').prefetch_related(
-        'launchpad_bugs')
-
-    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'
-        bug_ids = sorted([b.bug_id for b in test_run.launchpad_bugs.all()])
-        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(),
-            bug_ids=bug_ids,
-            )
-        bundle_id_to_data[test_run.bundle.id]['test_runs'][name] = test_run_data
-        if name != 'lava':
-            test_run_names.add(name)
+        for test_run in match.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'
+            bug_ids = sorted([b.bug_id for b in test_run.launchpad_bugs.all()])
+            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(),
+                bug_ids=bug_ids,
+                )
+            if match.tag not in build_number_to_cols:
+                # This assumes 1 bundle per match...
+                build_number_to_cols[match.tag] = {
+                    'test_runs': {},
+                    'number': match.tag,
+                    'date': test_run.bundle.uploaded_on,
+                    'link': test_run.bundle.get_absolute_url(),
+                    }
+            build_number_to_cols[match.tag]['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'])
+    cols = [c for n, c in sorted(build_number_to_cols.items())]
 
     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)
+        for col in cols:
+            test_run_data = col['test_runs'].get(test_run_name)
             if not test_run_data:
                 test_run_data = dict(
                     present=False,
@@ -1601,9 +1576,9 @@ 
     return render_to_response(
         "dashboard_app/image-report.html", {
             'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                image_report_detail, name=filter.name),
-            'image': filter,
-            'bundles': bundles,
+                image_report_detail, name=image.name),
+            'image': image,
+            'cols': cols,
             'table_data': table_data,
             'test_run_names': test_run_names,
         }, RequestContext(request))