=== modified file 'dashboard_app/admin.py'
@@ -44,6 +44,9 @@
TestCase,
TestResult,
TestRun,
+ TestRunFilter,
+ TestRunFilterAttribute,
+ TestRunFilterSubscription,
TestingEffort,
)
@@ -196,9 +199,20 @@
filter_horizontal = ['images']
save_as = True
+
class LaunchpadBugAdmin(admin.ModelAdmin):
raw_id_fields = ['test_runs']
+
+class TestRunFilterAdmin(admin.ModelAdmin):
+ filter_horizontal = ['bundle_streams']
+ class TestRunFilterAttributeInline(admin.TabularInline):
+ model = TestRunFilterAttribute
+ inlines = [TestRunFilterAttributeInline]
+ raw_id_fields = ['test_case']
+ save_as = True
+
+
admin.site.register(Attachment)
admin.site.register(Bundle, BundleAdmin)
admin.site.register(BundleDeserializationError, BundleDeserializationErrorAdmin)
@@ -213,5 +227,7 @@
admin.site.register(TestCase, TestCaseAdmin)
admin.site.register(TestResult, TestResultAdmin)
admin.site.register(TestRun, TestRunAdmin)
+admin.site.register(TestRunFilter, TestRunFilterAdmin)
+admin.site.register(TestRunFilterSubscription)
admin.site.register(Tag)
admin.site.register(TestingEffort, TestingEffortAdmin)
=== modified file 'dashboard_app/extension.py'
@@ -46,6 +46,7 @@
Menu("Data Views", reverse("dashboard_app.views.data_view_list")),
Menu("Reports", reverse("dashboard_app.views.report_list")),
Menu("Image Reports", reverse("dashboard_app.views.image_report_list")),
+ Menu("[BETA] Filters", reverse("dashboard_app.views.filters_list")),
]
return menu
=== added file 'dashboard_app/migrations/0017_auto__add_testrunfilterattribute__add_testrunfilter__add_unique_testru.py'
@@ -0,0 +1,290 @@
+# -*- 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 'TestRunFilterAttribute'
+ db.create_table('dashboard_app_testrunfilterattribute', (
+ ('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)),
+ ('filter', self.gf('django.db.models.fields.related.ForeignKey')(related_name='attributes', to=orm['dashboard_app.TestRunFilter'])),
+ ))
+ db.send_create_signal('dashboard_app', ['TestRunFilterAttribute'])
+
+ # Adding model 'TestRunFilter'
+ db.create_table('dashboard_app_testrunfilter', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('name', self.gf('django.db.models.fields.SlugField')(max_length=1024)),
+ ('test', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dashboard_app.Test'], null=True, blank=True)),
+ ('test_case', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dashboard_app.TestCase'], null=True, blank=True)),
+ ))
+ db.send_create_signal('dashboard_app', ['TestRunFilter'])
+
+ # Adding M2M table for field bundle_streams on 'TestRunFilter'
+ db.create_table('dashboard_app_testrunfilter_bundle_streams', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('testrunfilter', models.ForeignKey(orm['dashboard_app.testrunfilter'], null=False)),
+ ('bundlestream', models.ForeignKey(orm['dashboard_app.bundlestream'], null=False))
+ ))
+ db.create_unique('dashboard_app_testrunfilter_bundle_streams', ['testrunfilter_id', 'bundlestream_id'])
+
+ # Adding unique constraint on 'TestRunFilter', fields ['owner', 'name']
+ db.create_unique('dashboard_app_testrunfilter', ['owner_id', 'name'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'TestRunFilter', fields ['owner', 'name']
+ db.delete_unique('dashboard_app_testrunfilter', ['owner_id', 'name'])
+
+ # Deleting model 'TestRunFilterAttribute'
+ db.delete_table('dashboard_app_testrunfilterattribute')
+
+ # Deleting model 'TestRunFilter'
+ db.delete_table('dashboard_app_testrunfilter')
+
+ # Removing M2M table for field bundle_streams on 'TestRunFilter'
+ db.delete_table('dashboard_app_testrunfilter_bundle_streams')
+
+
+ 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.SlugField', [], {'unique': 'True', 'max_length': '1024'}),
+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ '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', [], {'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'},
+ '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']"}),
+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.Test']", 'null': 'True', 'blank': 'True'}),
+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestCase']", 'null': 'True', 'blank': 'True'})
+ },
+ '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'})
+ },
+ '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
=== added file 'dashboard_app/migrations/0018_auto__add_field_testrunfilter_public.py'
@@ -0,0 +1,257 @@
+# -*- 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 field 'TestRunFilter.public'
+ db.add_column('dashboard_app_testrunfilter', 'public',
+ self.gf('django.db.models.fields.BooleanField')(default=False),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'TestRunFilter.public'
+ db.delete_column('dashboard_app_testrunfilter', 'public')
+
+
+ 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.SlugField', [], {'unique': 'True', 'max_length': '1024'}),
+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ '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', [], {'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'},
+ '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'}),
+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.Test']", 'null': 'True', 'blank': 'True'}),
+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestCase']", 'null': 'True', 'blank': 'True'})
+ },
+ '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'})
+ },
+ '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
=== added file 'dashboard_app/migrations/0019_auto__add_testrunfiltersubscription__add_unique_testrunfiltersubscript.py'
@@ -0,0 +1,274 @@
+# -*- 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 'TestRunFilterSubscription'
+ db.create_table('dashboard_app_testrunfiltersubscription', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('filter', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dashboard_app.TestRunFilter'])),
+ ('level', self.gf('django.db.models.fields.IntegerField')(default=0)),
+ ))
+ db.send_create_signal('dashboard_app', ['TestRunFilterSubscription'])
+
+ # Adding unique constraint on 'TestRunFilterSubscription', fields ['user', 'filter']
+ db.create_unique('dashboard_app_testrunfiltersubscription', ['user_id', 'filter_id'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'TestRunFilterSubscription', fields ['user', 'filter']
+ db.delete_unique('dashboard_app_testrunfiltersubscription', ['user_id', 'filter_id'])
+
+ # Deleting model 'TestRunFilterSubscription'
+ db.delete_table('dashboard_app_testrunfiltersubscription')
+
+
+ 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.SlugField', [], {'unique': 'True', 'max_length': '1024'}),
+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ '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', [], {'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'},
+ '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'}),
+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.Test']", 'null': 'True', 'blank': 'True'}),
+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestCase']", 'null': 'True', 'blank': 'True'})
+ },
+ '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']"})
+ },
+ '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'
@@ -34,9 +34,11 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
+from django.contrib.sites.models import Site
+from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.files import locks, File
from django.core.files.storage import FileSystemStorage
+from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.fields import FieldDoesNotExist
@@ -44,6 +46,7 @@
from django.dispatch import receiver
from django.template import Template, Context
from django.template.defaultfilters import filesizeformat
+from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
@@ -491,7 +494,6 @@
return
try:
self._do_deserialize(prefer_evolution)
- bundle_was_deserialized.send(sender=self, bundle=self)
except Exception as ex:
import_error = BundleDeserializationError.objects.get_or_create(
bundle=self)[0]
@@ -505,6 +507,7 @@
pass
self.is_deserialized = True
self.save()
+ bundle_was_deserialized.send_robust(sender=self, bundle=self)
def _do_deserialize(self, prefer_evolution):
"""
@@ -1210,7 +1213,7 @@
"""
repository = DataViewRepository()
-
+
def __init__(self, name, backend_queries, arguments, documentation, summary):
self.name = name
self.backend_queries = backend_queries
@@ -1308,7 +1311,7 @@
Data reports are small snippets of xml that define
a limited django template.
"""
-
+
repository = DataReportRepository()
def __init__(self, **kwargs):
@@ -1544,7 +1547,7 @@
for field_name in meta.get_all_field_names():
# object that represents the metadata of the field
- try:
+ try:
field_meta = meta.get_field(field_name)
except FieldDoesNotExist:
continue
@@ -1559,3 +1562,422 @@
# the 'path' attribute contains the name of the file we need
if hasattr(field, 'path') and os.path.exists(field.path):
field.storage.delete(field.path)
+
+
+class FilterMatch(object):
+ """A non-database object that represents the way a filter matches a test_run.
+
+ Returned by TestRunFilter.matches_against_bundle and
+ TestRunFilter.get_test_runs.
+ """
+
+ bundle = None
+ specific_results = None
+ result_count = None
+ pass_count = None
+ test_run = None
+ filter = None
+
+ def format_for_mail(self):
+ r = [' ~%s/%s ' % (self.filter.owner.username, self.filter.name)]
+ if self.filter.test_case:
+ r.extend([
+ self.filter.test.test_id,
+ ':',
+ self.filter.test_case.test_case_id,
+ ])
+ for result in self.specific_results:
+ if self.filter.test_case.units:
+ result_desc = '%s%s' % (result.measurement, result.units)
+ else:
+ result_desc = result.RESULT_MAP[result.result]
+ r.extend([' ', result_desc])
+ elif self.filter.test:
+ r.append('%s %s pass/%s total' % (
+ self.filter.test.test_id, self.pass_count, self.result_count))
+ else:
+ r.append('%s pass/%s total' % (self.pass_count, self.result_count))
+ r.append('\n')
+ return ''.join(r)
+
+
+class MatchMakingQuerySet(object):
+ """Wrap a QuerySet and construct FilterMatchs from what the wrapped query
+ set returns.
+
+ Just enough of the QuerySet API to work with DataTable."""
+
+ model = TestRun
+
+ def __init__(self, queryset, filter):
+ self.queryset = queryset
+ self.filter = filter
+
+ def _makeMatches(self, data):
+ raise NotImplementedError(self._makeMatches)
+
+ def _wrap(self, queryset, **kw):
+ return self.__class__(queryset, self.filter, **kw)
+
+ def order_by(self, *args):
+ return self._wrap(self.queryset.order_by(*args))
+
+ def count(self):
+ return self.queryset.count()
+
+ def __getitem__(self, item):
+ return self._wrap(self.queryset[item])
+
+ def __iter__(self):
+ data = list(self.queryset)
+ return self._makeMatches(data)
+
+
+class SpecificTestCaseMatchMakingQuerySet(MatchMakingQuerySet):
+
+ def _makeMatches(self, runs):
+ results_by_run_id = {}
+ for run in runs:
+ results_by_run_id[run.id] = []
+ results = TestResult.objects.filter(
+ test_run_id__in=results_by_run_id.keys(),
+ test_case_id=self.filter.test_case.id)
+ for result in results:
+ results_by_run_id[result.test_run_id].append(result)
+ matches = []
+ for run in runs:
+ match = FilterMatch()
+ specific_results = results_by_run_id[result.test_run_id]
+ match.specific_results = specific_results
+ match.result_count = len(specific_results)
+ match.pass_count = len([r for r in specific_results if r.result == r.RESULT_PASS])
+ match.test_run = run
+ match.bundle = run.bundle
+ match.filter = self.filter
+ matches.append(match)
+ return iter(matches)
+
+
+
+class SpecificTestMatchMakingQuerySet(MatchMakingQuerySet):
+ def _makeMatches(self, runs):
+ matches = []
+ for run in runs:
+ match = FilterMatch()
+ match.specific_results = None
+ match.result_count = run.denormalization.count_all()
+ match.pass_count = run.denormalization.count_pass
+ match.test_run = run
+ match.bundle = run.bundle
+ match.filter = self.filter
+ matches.append(match)
+ return iter(matches)
+
+
+class BundleMatchMakingQuerySet(MatchMakingQuerySet):
+
+ model = Bundle
+
+ def __init__(self, queryset, filter, mis_ordered=False):
+ super(BundleMatchMakingQuerySet, self).__init__(queryset, filter)
+ self.mis_ordered = mis_ordered
+
+ def _makeMatches(self, bundles):
+ assert not self.mis_ordered, """
+ attempt to materialize BundleMatchMakingQuerySet when ordered on
+ non-bundle field"""
+ matches = []
+ counted_bundles = Bundle.objects.filter(
+ id__in=[b.id for b in bundles]).annotate(
+ pass_count=models.Sum('test_runs__denormalization__count_pass'),
+ unknown_count=models.Sum('test_runs__denormalization__count_unknown'),
+ skip_count=models.Sum('test_runs__denormalization__count_skip'),
+ fail_count=models.Sum('test_runs__denormalization__count_fail'))
+ bundles_by_id = {}
+ for bundle in counted_bundles:
+ bundles_by_id[bundle.id] = bundle
+ for bundle in bundles:
+ match = FilterMatch()
+ match.specific_results = None
+ cb = bundles_by_id[bundle.id]
+ match.result_count = cb.unknown_count + cb.skip_count + cb.pass_count + cb.fail_count
+ match.pass_count = cb.pass_count
+ match.test_run = None
+ match.bundle = bundle
+ match.filter = self.filter
+ matches.append(match)
+ return iter(matches)
+
+ def _wrap(self, queryset, **kw):
+ if 'mis_ordered' not in kw:
+ kw['mis_ordered'] = self.mis_ordered
+ return self.__class__(queryset, self.filter, **kw)
+
+ def order_by(self, field):
+ if field.startswith('bundle__') or field.startswith('-bundle__'):
+ if field.startswith('-'):
+ prefix = '-'
+ field = field[1:]
+ else:
+ prefix = ''
+ field = field[len('bundle__'):]
+ r = super(BundleMatchMakingQuerySet, self).order_by(
+ prefix+field)
+ r.mis_ordered = False
+ return r
+ else:
+ return self._wrap(self.queryset, mis_ordered=True)
+
+
+class TestRunFilterAttribute(models.Model):
+
+ name = models.CharField(max_length=1024)
+ value = models.CharField(max_length=1024)
+
+ filter = models.ForeignKey("TestRunFilter", related_name="attributes")
+
+ def __unicode__(self):
+ return '%s = %s' % (self.name, self.value)
+
+
+class TestRunFilter(models.Model):
+
+ owner = models.ForeignKey(User)
+
+ name = models.SlugField(
+ max_length=1024,
+ 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."))
+ class Meta:
+ unique_together = (('owner', 'name'))
+
+ bundle_streams = models.ManyToManyField(BundleStream)
+ bundle_streams.help_text = 'A filter only matches tests within the given <b>bundle streams</b>.'
+
+ test = models.ForeignKey(
+ Test, blank=True, null=True,
+ help_text=("A filter can optionally be restricted to a particular "
+ "<b>test</b>, or even a <b>test case</b> within a test."))
+
+ test_case = models.ForeignKey(TestCase, blank=True, null=True)
+
+ public = models.BooleanField(
+ default=False, help_text="Whether other users can see this filter.")
+
+ @property
+ def summary_data(self):
+ return {
+ 'bundle_streams': self.bundle_streams.all(),
+ 'attributes': self.attributes.all().values_list('name', 'value'),
+ 'test': self.test,
+ 'test_case': self.test_case,
+ }
+
+ def __unicode__(self):
+ test = self.test
+ if not test:
+ test = "<any>"
+ test_case = self.test_case
+ if not test_case:
+ test_case = "<any>"
+ attrs = []
+ for attr in self.attributes.all():
+ attrs.append(unicode(attr))
+ attrs = ', '.join(attrs)
+ if attrs:
+ attrs = ' ' + attrs + '; '
+ return "<TestRunFilter ~%s/%s %d streams;%s %s:%s>" % (
+ self.owner.username, self.name, self.bundle_streams.count(), attrs, test, test_case)
+
+
+ # given filter:
+ # select from testrun
+ # where testrun.bundle in filter.bundle_streams ^ accessible_bundles
+ # and testrun has attribute with key = key1 and value = value1
+ # and testrun has attribute with key = key2 and value = value2
+ # and ...
+ # and testrun has attribute with key = keyN and value = valueN
+ # and testrun has filter.test/testcase requested
+
+ def get_test_runs_impl(self, user, bundle_streams, attributes):
+ accessible_bundle_streams = BundleStream.objects.accessible_by_principal(
+ user)
+ testruns = TestRun.objects.filter(
+ models.Q(bundle__bundle_stream__in=accessible_bundle_streams),
+ models.Q(bundle__bundle_stream__in=bundle_streams),
+ )
+
+ for (name, value) in attributes:
+ testruns = TestRun.objects.filter(
+ id__in=testruns.values_list('id'),
+ attributes__name=name, attributes__value=value)
+
+ if self.test_case:
+ testruns = TestRun.objects.filter(
+ id__in=testruns.values_list('id'),
+ test_results__test_case=self.test_case,
+ test=self.test_case.test)
+ wrapper_cls = SpecificTestCaseMatchMakingQuerySet
+ elif self.test:
+ testruns = TestRun.objects.filter(
+ id__in=testruns.values_list('id'),
+ test=self.test)
+ wrapper_cls = SpecificTestMatchMakingQuerySet
+ else:
+ # if the filter doesn't specify a test, we still only return one
+ # test run per bundle. the display code knows to do different
+ # things in this case.
+ testruns = Bundle.objects.filter(
+ test_runs__in=testruns)
+ wrapper_cls = BundleMatchMakingQuerySet
+
+ return wrapper_cls(testruns, self)
+
+ # given bundle:
+ # select from filter
+ # where bundle.bundle_stream in filter.bundle_streams
+ # and filter.test in (select test from bundle.test_runs)
+ # and all the attributes on the filter are on a testrun in the bundle
+ # = the minimum over testrun (the number of attributes on the filter that are not on the testrun) is 0
+ # and (filter.test_case is null
+ # or filter.test_case in select test_case from bundle.test_runs.test_results.test_cases)
+
+ @classmethod
+ def matches_against_bundle(self, bundle):
+ filters = bundle.bundle_stream.testrunfilter_set.all()
+ filters = filters.filter(
+ models.Q(test__isnull=True)
+ |models.Q(test__in=bundle.test_runs.all().values('test')))
+ filters = filters.filter(
+ models.Q(test_case__isnull=True)
+ |models.Q(test_case__in=TestResult.objects.filter(
+ test_run__in=bundle.test_runs.all()).values('test_case')))
+ filters = filters.extra(
+ where=[
+ """(select min((select count(*)
+ from dashboard_app_testrunfilterattribute
+ where filter_id = dashboard_app_testrunfilter.id
+ and (name, value) not in (select name, value
+ from dashboard_app_namedattribute
+ where content_type_id = (
+ select django_content_type.id from django_content_type
+ where app_label = 'dashboard_app' and model='testrun')
+ and object_id = dashboard_app_testrun.id)))
+ from dashboard_app_testrun where dashboard_app_testrun.bundle_id = %s) = 0 """ % bundle.id],
+ )
+ filters = list(filters)
+ matches = []
+ for filter in filters:
+ if filter.test:
+ for test_run in bundle.test_runs.filter(test=filter.test):
+ match = FilterMatch()
+ match.filter = filter
+ match.test_run = test_run
+ if filter.test_case:
+ match.specific_results = list(
+ test_run.test_results.filter(test_case=filter.test_case))
+ match.result_count = len(match.specific_results)
+ match.pass_count = len(
+ [r for r in match.specific_results if r.result == r.RESULT_PASS])
+ else:
+ match.specific_results = None
+ match.result_count = test_run.denormalization.count_all()
+ match.pass_count = test_run.denormalization.count_pass
+ matches.append(match)
+ else:
+ match = FilterMatch()
+ match.filter = filter
+ match.test_run = None
+ bundle_with_counts = Bundle.objects.annotate(
+ pass_count=models.Sum('test_runs__denormalization__count_pass'),
+ unknown_count=models.Sum('test_runs__denormalization__count_unknown'),
+ skip_count=models.Sum('test_runs__denormalization__count_skip'),
+ fail_count=models.Sum('test_runs__denormalization__count_fail')).get(
+ id=bundle.id)
+ match.specific_results = None
+ b = bundle_with_counts
+ match.result_count = b.unknown_count + b.skip_count + b.pass_count + b.fail_count
+ match.pass_count = bundle_with_counts.pass_count
+ matches.append(match)
+ return matches
+
+ def get_test_runs(self, user):
+ return self.get_test_runs_impl(
+ user,
+ self.bundle_streams.all(),
+ self.attributes.values_list('name', 'value'))
+
+ @models.permalink
+ def get_absolute_url(self):
+ return (
+ "dashboard_app.views.filter_detail",
+ [self.owner.username, self.name])
+
+
+class TestRunFilterSubscription(models.Model):
+
+ user = models.ForeignKey(User)
+
+ filter = models.ForeignKey(TestRunFilter)
+
+ class Meta:
+ unique_together = (('user', 'filter'))
+
+ NOTIFICATION_FAILURE, NOTIFICATION_ALWAYS = range(2)
+
+ NOTIFICATION_CHOICES = (
+ (NOTIFICATION_FAILURE, "Only when failed"),
+ (NOTIFICATION_ALWAYS, "Always"))
+
+ level = models.IntegerField(
+ default=NOTIFICATION_FAILURE, choices=NOTIFICATION_CHOICES,
+ help_text=("You can choose to be <b>notified by email</b>:<ul><li>whenever a test "
+ "that matches the criteria of this filter is executed"
+ "</li><li>only when a test that matches the criteria of this filter fails</ul>"))
+
+ @classmethod
+ def recipients_for_bundle(cls, bundle):
+ matches = TestRunFilter.matches_against_bundle(bundle)
+ matches_by_filter_id = {}
+ for match in matches:
+ matches_by_filter_id[match.filter.id] = match
+ args = [models.Q(filter_id__in=list(matches_by_filter_id))]
+ bs = bundle.bundle_stream
+ if not bs.is_public:
+ if bs.group:
+ args.append(models.Q(user__in=bs.group.user_set.all()))
+ else:
+ args.append(models.Q(user=bs.user))
+ subscriptions = TestRunFilterSubscription.objects.filter(*args)
+ recipients = {}
+ for sub in subscriptions:
+ match = matches_by_filter_id[sub.filter.id]
+ if sub.level == cls.NOTIFICATION_FAILURE and match.pass_count == match.result_count:
+ continue
+ recipients.setdefault(sub.user, []).append(match)
+ return recipients
+
+
+def send_bundle_notifications(sender, bundle, **kwargs):
+ recipients = TestRunFilterSubscription.recipients_for_bundle(bundle)
+ domain = '???'
+ try:
+ site = Site.objects.get_current()
+ except (Site.DoesNotExist, ImproperlyConfigured):
+ pass
+ else:
+ domain = site.domain
+ url_prefix = 'http://%s' % domain
+ for user, matches in recipients.items():
+ data = {'bundle': bundle, 'user': user, 'matches': matches, 'url_prefix': url_prefix}
+ mail = render_to_string(
+ 'dashboard_app/filter_subscription_mail.txt',
+ data)
+ filter_names = ', '.join(match.filter.name for match in matches)
+ send_mail(
+ "LAVA result notification: " + filter_names, mail,
+ settings.SERVER_EMAIL, [user.email])
+
+
+bundle_was_deserialized.connect(send_bundle_notifications)
=== added file 'dashboard_app/static/css/filter-edit.css'
@@ -0,0 +1,10 @@
+@import url("../../admin/css/widgets.css");
+div.selector span.helptext { display: none; }
+div.selector h2 { margin: 0; font-size: 11pt; }
+div.selector a { text-decoration: none; }
+div.selector select { height: 10em; }
+div.selector ul.selector-chooser { margin-top: 5.5em; }
+div.selector .selector-chosen select {
+ border: 1px solid rgb(204, 204, 204);
+ border-top: none;
+}
=== added file 'dashboard_app/static/js/filter-edit.js'
@@ -0,0 +1,67 @@
+var row_number;
+$(function () {
+function updateTestCasesFromTest() {
+ var test_id=$("#id_test option:selected").html();
+ var select = $("#id_test_case");
+ select.empty();
+ select.append(Option("<any>", ""));
+ if (test_id != '<any>') {
+ $.ajax(
+ {
+ url: test_case_url + test_id,
+ dataType: 'json',
+ success: function (data) {
+ $(data).each(
+ function (index, val) {
+ select.append(Option(val.test_case_id, val.id));
+ });
+ select.removeAttr("disabled");
+ }
+ });
+ } else {
+ select.attr('disabled', 'disabled');
+ }
+};
+$("#id_test").change(updateTestCasesFromTest);
+row_number = $("#attribute-table tbody tr").size();
+$("#add-attribute").click(
+ function (e) {
+ e.preventDefault();
+ var body = $("#attribute-table tbody");
+ var row = $("#template-row").clone(true, true);
+ row.show();
+ row.find('.key').attr('id', 'id_attribute_key_' + row_number);
+ row.find('.value').attr('id', 'id_attribute_value_' + row_number);
+ row.find('.key').attr('name', 'attribute_key_' + row_number);
+ row.find('.value').attr('name', 'attribute_value_' + row_number);
+ row_number += 1;
+ body.append(row);
+ row.find(".key").autocomplete(keyAutocompleteConfig);
+ row.find(".value").autocomplete(valueAutocompleteConfig);
+ });
+$("a.delete-row").click(
+ function (e) {
+ e.preventDefault();
+ $(this).closest('tr').remove();
+ });
+var keyAutocompleteConfig = {
+ source: attr_name_completion_url
+ };
+var valueAutocompleteConfig = {
+ source: function (request, response) {
+ var attrName = this.element.closest('tr').find('input.key').val();
+ $.getJSON(
+ attr_value_completion_url,
+ {
+ 'name': attrName,
+ 'term': request.term
+ },
+ function (data) {
+ response(data);
+ }
+ );
+ }
+ };
+$("tbody .key").autocomplete(keyAutocompleteConfig);
+$("tbody .value").autocomplete(valueAutocompleteConfig);
+});
\ No newline at end of file
=== added file 'dashboard_app/templates/dashboard_app/filter_add.html'
@@ -0,0 +1,26 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+
+{% block extrahead %}
+{{ block.super }}
+{{ form.media }}
+{% endblock %}
+
+{% block content %}
+{% if form.instance.pk %}
+<h1>[BETA] Edit filter “{{ form.instance.name }}”…</h1>
+{% else %}
+<h1>[BETA] Add new filter…</h1>
+{% endif %}
+
+<form action="" method="post">
+ {% csrf_token %}
+ {% include "dashboard_app/filter_form.html" %}
+{% if form.instance.pk %}
+ <input type="submit" value="Preview changes">
+{% else %}
+ <input type="submit" value="Preview">
+{% endif %}
+</form>
+
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/filter_delete.html'
@@ -0,0 +1,14 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+
+{% block content %}
+<h1>[BETA] Delete filter {{ filter.name }}</h1>
+
+<form action="" method="POST">
+ {% csrf_token %}
+ Do you really want to delete the filter {{ filter.name }}?
+ <input type="submit" name="yes" value="Yes">
+ <input type="submit" name="no" value="No">
+</form>
+
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/filter_detail.html'
@@ -0,0 +1,30 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+{% load django_tables2 %}
+
+{% block content %}
+
+<h1>[BETA] Filter {{ filter.name }}</h1>
+
+{% include "dashboard_app/filter_summary.html" with summary_data=filter.summary_data %}
+
+{% if filter.owner == request.user %}
+<p>
+ You can <a href="{{ filter.get_absolute_url }}/+edit">edit</a>
+ or <a href="{{ filter.get_absolute_url }}/+delete">delete</a> this filter.
+</p>
+{% endif %}
+
+{% if subscription %}
+<p>
+ <a href="{% url dashboard_app.views.filter_subscribe username=filter.owner.username name=filter.name %}">Manage</a> your subscription to this filter.
+</p>
+{% else %}
+<p>
+ <a href="{% url dashboard_app.views.filter_subscribe username=filter.owner.username name=filter.name %}">Subscribe</a> to this filter.
+</p>
+{% endif %}
+
+{% render_table filter_table %}
+
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/filter_form.html'
@@ -0,0 +1,98 @@
+ <p>
+ A filter matches test runs by a number of criteria.
+ </p>
+ {{ form.non_field_errors }}
+ <dl>
+ <dt>
+ Metadata:
+ </dt>
+ <dd>
+ {{ form.name.errors }}
+ {{ form.name.label_tag }}: {{ form.name }}
+ <br /><span class="helptext">{{ form.name.help_text|safe }}</span>
+ <br />{{ form.public.errors }}
+ {{ form.public }}{{ form.public.label_tag }}
+ <br /><span class="helptext">{{ form.public.help_text|safe }}</span>
+ </dd>
+ <dt>
+ {{ form.bundle_streams.label_tag }}:
+ </dt>
+ <dd>
+ {{ form.bundle_streams.errors }}
+ <div>{{ form.bundle_streams }}</div>
+ <div style="clear:left">{{ form.bundle_streams.help_text|safe }}</div>
+ </dd>
+ <dt>
+ Attributes:
+ </dt>
+ <dd>
+ <table id="attribute-table">
+ <thead>
+ <tr>
+ <th>
+ Name
+ </th>
+ <th>
+ Value
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for attr in form.attributes %}
+ <tr>
+ <td>
+ <input class="key"
+ id="id_attribute_key_{{ forloop.counter0 }}"
+ name="attribute_key_{{ forloop.counter0 }}"
+ value="{{attr.0}}" />
+ </td>
+ <td>
+ <input class="value"
+ id="id_attribute_value_{{ forloop.counter0 }}"
+ name="attribute_value_{{ forloop.counter0 }}"
+ value="{{attr.1}}" />
+ </td>
+ <td>
+ <a href="#" class="delete-row">remove</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr style="display:none" id="template-row">
+ <td>
+ <input class="key" />
+ </td>
+ <td>
+ <input class="value" />
+ </td>
+ <td>
+ <a href="#" class="delete-row">remove</a>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <a id="add-attribute" href="#">Add a required attribute</a>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ <br /><span class="helptext">
+ A filter can be limited to test runs with particular values for particular <b>attributes</b>.
+ </span>
+ </dd>
+ <dt>
+ Test and test case:
+ </dt>
+ <dd>
+ {{ form.test.errors }}
+ {{ form.test_case.errors }}
+ {{ form.test }}
+ {{ form.test_case }}
+ <br />
+ <span class="helptext">
+ A filter can optionally be restricted to a particular <b>test</b>, or even a <b>test case</b>
+ within a test.
+ </span>
+ </dd>
+ </dl>
=== added file 'dashboard_app/templates/dashboard_app/filter_preview.html'
@@ -0,0 +1,53 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+{% load django_tables2 %}
+
+{% block extrahead %}
+{{ block.super }}
+{{ form.media }}
+{% endblock %}
+
+{% block content %}
+{% if form.instance.pk %}
+<h1>[BETA] Previewing changes to filter “{{ form.instance.name }}”</h1>
+{% else %}
+<h1>[BETA] Previewing new filter “{{ form.name.value }}”</h1>
+{% endif %}
+
+{% include "dashboard_app/filter_summary.html" with summary_data=form.summary_data %}
+
+<p>
+ These are the results matched by your filter.
+</p>
+
+{% render_table table %}
+
+<p>
+
+<form action="" method="post">
+ <p>
+ If this is what you expected, you can
+ {% if form.instance.pk %}
+ <input type="submit" name="save" value="save changes"> to the filter.
+ {% else %}
+ <input type="submit" name="save" value="save"> the filter.
+ {% endif %}
+ </p>
+ <p>
+ Otherwise, you can <a href="#" id="edit-link">edit</a> it.
+ </p>
+ {% csrf_token %}
+ <div id="filter-edit" style="display: none">
+ {% include "dashboard_app/filter_form.html" %}
+ <input type="submit" name="preview" value="Preview again">
+ </div>
+</form>
+
+
+<script type="text/javascript">
+$("#edit-link").click(function (e) {
+e.preventDefault();
+$("#filter-edit").show();
+});
+</script>
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/filter_subscribe.html'
@@ -0,0 +1,26 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+{% load django_tables2 %}
+
+{% block content %}
+
+<h1>[BETA] Subscribe to filter {{ filter.name }}</h1>
+
+<form action="" method="POST">
+ {% csrf_token %}
+ <p>
+ {{ form.level.help_text|safe }}
+ {{ form.level.label_tag }}:
+ {{ form.level }}
+ </p>
+ <p>
+ {% if form.instance.pk %}
+ <input type="submit" name="update" value="Update"/>
+ <input type="submit" name="unsubscribe" value="Unsubscribe"/>
+ {% else %}
+ <input type="submit" name="subscribe" value="Subscribe"/>
+ {% endif %}
+ </p>
+</form>
+
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/filter_subscription_mail.txt'
@@ -0,0 +1,13 @@
+Dear {{ user.first_name }} {{ user.last_name }},
+
+The bundle {{ bundle.content_filename }} was uploaded at {{ bundle.uploaded_on|date:"Y-m-d H:i:s" }} by {% if bundle.uploaded_by %}{{ bundle.uploaded_by }}{% else %}an anonymous user{% endif %}.
+
+It matched the following filters that you are subscribed to:
+
+{% for match in matches %}{{ match.format_for_mail }}{% endfor %}
+You can see more details at:
+
+ {{ url_prefix }}{{ bundle.get_absolute_url }}
+
+LAVA
+Linaro Automated Validation Architecture
=== added file 'dashboard_app/templates/dashboard_app/filter_summary.html'
@@ -0,0 +1,38 @@
+<table>
+ <tr>
+ <th>
+ Bundle streams
+ </th>
+ <td>
+ {% for stream in summary_data.bundle_streams.all %}
+ {{stream.pathname}}{% if not forloop.last %}, {% endif %}
+ {% endfor %}
+ </td>
+ </tr>
+{% if summary_data.attributes %}
+ <tr>
+ <th>
+ Attributes
+ </th>
+ <td>
+ {% for a in summary_data.attributes %}
+ {{ a.0 }} == {{ a.1 }} <br />
+ {% endfor %}
+ </td>
+ </tr>
+{% endif %}
+ <tr>
+ <th>
+ Test case
+ </th>
+ <td>
+ {% if summary_data.test_case %}
+ {{ summary_data.test }}:{{ summary_data.test_case }}
+ {% elif summary_data.test %}
+ {{ summary_data.test }}:<any>
+ {% else %}
+ <any>:<any>
+ {% endif %}
+ </td>
+ </tr>
+</table>
=== added file 'dashboard_app/templates/dashboard_app/filters_list.html'
@@ -0,0 +1,35 @@
+{% extends "dashboard_app/_content.html" %}
+{% load i18n %}
+{% load django_tables2 %}
+
+{% block content %}
+<h1>[BETA] Filters</h1>
+
+<p>
+ A filter matches test runs by a number of criteria.
+</p>
+
+{% if user_filters_table %}
+
+<h2>Your Filters</h2>
+
+{% render_table user_filters_table %}
+
+<p>
+ <a href="{% url dashboard_app.views.filter_add %}">Add new filter…</a>
+</p>
+
+{% else %}
+
+<p>
+ Please log in to see and manage your filters.
+</p>
+
+{% endif %}
+
+<h2>Public Filters</h2>
+
+{% render_table public_filters_table %}
+
+
+{% endblock %}
=== modified file 'dashboard_app/urls.py'
@@ -35,6 +35,17 @@
url(r'^reports/(?P<name>[a-zA-Z0-9-_]+)/$', 'report_detail'),
url(r'^tests/$', 'test_list'),
url(r'^tests/(?P<test_id>[^/]+)/$', 'test_detail'),
+ url(r'^filters/$', 'filters_list'),
+ url(r'^filters/\+add$', 'filter_add'),
+ url(r'^filters/\+add-preview-json$', 'filter_preview_json'),
+ url(r'^filters/\+add-cases-for-test-json$', 'filter_add_cases_for_test_json'),
+ url(r'^filters/\+attribute-name-completion-json$', 'filter_attr_name_completion_json'),
+ url(r'^filters/\+attribute-value-completion-json$', 'filter_attr_value_completion_json'),
+ url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)$', 'filter_detail'),
+ url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/json$', 'filter_json'),
+ url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filter_edit'),
+ url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filter_subscribe'),
+ url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filter_delete'),
url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler,
name='dashboard_app.views.dashboard_xml_rpc_handler',
kwargs={
=== modified file 'dashboard_app/views.py'
@@ -23,14 +23,20 @@
import re
import json
+from django.conf import settings
+from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.decorators import login_required
from django.contrib.sites.models import Site
+from django.core.exceptions import PermissionDenied, ValidationError
from django.core.urlresolvers import reverse
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
+from django import forms
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response, redirect, get_object_or_404
from django.template import RequestContext, loader
+from django.template import Template, Context
+from django.utils.html import escape
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
@@ -53,10 +59,14 @@
Image,
ImageSet,
LaunchpadBug,
+ NamedAttribute,
Tag,
Test,
+ TestCase,
TestResult,
TestRun,
+ TestRunFilter,
+ TestRunFilterSubscription,
TestingEffort,
)
@@ -434,6 +444,439 @@
searchable_columns = ['test_case__test_case_id']
+class UserFiltersTable(DataTablesTable):
+
+ name = TemplateColumn('''
+ <a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
+ ''')
+
+ bundle_streams = TemplateColumn('''
+ {% for r in record.bundle_streams.all %}
+ {{r.pathname}} <br />
+ {% endfor %}
+ ''')
+
+ attributes = TemplateColumn('''
+ {% for a in record.attributes.all %}
+ {{ a }} <br />
+ {% endfor %}
+ ''')
+
+ test = TemplateColumn('''
+ {% if record.test_case %}
+ {{ record.test }}:{{ record.test_case }}
+ {% elif record.test %}
+ {{ record.test }}:<any>
+ {% else %}
+ <any>:<any>
+ {% endif %}
+ ''')
+
+ subscription = Column()
+ def render_subscription(self, record):
+ try:
+ sub = TestRunFilterSubscription.objects.get(
+ user=self.user, filter=record)
+ except TestRunFilterSubscription.DoesNotExist:
+ return "None"
+ else:
+ return sub.get_level_display()
+
+ public = Column()
+
+ def get_queryset(self, user):
+ return TestRunFilter.objects.filter(owner=user)
+
+
+class PublicFiltersTable(UserFiltersTable):
+
+ name = TemplateColumn('''
+ <a href="{{ record.get_absolute_url }}">~{{ record.owner.username }}/{{ record.name }}</a>
+ ''')
+
+ def __init__(self, *args, **kw):
+ super(PublicFiltersTable, self).__init__(*args, **kw)
+ del self.base_columns['public']
+
+ def get_queryset(self):
+ return TestRunFilter.objects.filter(public=True)
+
+
+@BreadCrumb("Filters and Subscriptions", parent=index)
+def filters_list(request):
+ public_filters_table = PublicFiltersTable("public-filters", None)
+ if request.user.is_authenticated():
+ public_filters_table.user = request.user
+ user_filters_table = UserFiltersTable("user-filters", None, params=(request.user,))
+ user_filters_table.user = request.user
+ else:
+ user_filters_table = None
+ del public_filters_table.base_columns['subscription']
+
+ return render_to_response(
+ 'dashboard_app/filters_list.html', {
+ 'user_filters_table': user_filters_table,
+ 'public_filters_table': public_filters_table,
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ filters_list),
+ }, RequestContext(request)
+ )
+
+
+class SpecificCaseColumn(Column):
+ def render(self, value, record):
+ r = []
+ for result in value:
+ if result.result == result.RESULT_PASS and result.units:
+ s = '%s %s' % (result.measurement, result.units)
+ else:
+ s = result.RESULT_MAP[result.result]
+ r.append('<a href="' + result.get_absolute_url() + '">'+s+'</a>')
+ return mark_safe(', '.join(r))
+
+
+class BundleColumn(Column):
+ def render(self, record):
+ return mark_safe('<a href="' + record.bundle.get_absolute_url() + '">' + escape(record.bundle.content_filename) + '</a>')
+
+
+class FilterTable(DataTablesTable):
+ def __init__(self, *args, **kwargs):
+ filter = kwargs['params'][1]
+ data = filter.summary_data
+ super(FilterTable, self).__init__(*args, **kwargs)
+ if len(data['bundle_streams']) == 1:
+ del self.base_columns['bundle_stream']
+ if data['test_case']:
+ del self.base_columns['bundle']
+ del self.base_columns['passes']
+ del self.base_columns['total']
+ self.base_columns['specific_results'].verbose_name = mark_safe(
+ data['test_case'].test_case_id)
+ elif data['test']:
+ del self.base_columns['bundle']
+ del self.base_columns['specific_results']
+ else:
+ del self.base_columns['test_run']
+ self.base_columns['passes']
+ self.base_columns['total']
+ del self.base_columns['specific_results']
+ uploaded_col_index = self.base_columns.keys().index('uploaded_on')
+ self.datatable_opts = self.datatable_opts.copy()
+ self.datatable_opts['aaSorting'] = [[uploaded_col_index, 'desc']]
+ self._compute_queryset(kwargs['params'])
+
+ bundle_stream = Column(accessor='bundle.bundle_stream')
+
+ bundle = BundleColumn(accessor='bundle', sortable=False)
+
+ test_run = TemplateColumn(
+ '<a href="{{ record.test_run.get_absolute_url }}">'
+ '<code>{{ record.test_run.test }} results<code/></a>',
+ accessor="test__test_id",
+ )
+
+ uploaded_on = TemplateColumn(
+ '{{ record.bundle.uploaded_on|date:"Y-m-d H:i:s" }}',
+ accessor='bundle__uploaded_on')
+
+ passes = Column(accessor='pass_count', sortable=False)
+ total = Column(accessor='result_count', sortable=False)
+ specific_results = SpecificCaseColumn(accessor='specific_results', sortable=False)
+ def get_queryset(self, user, filter):
+ return filter.get_test_runs(user)
+
+ datatable_opts = {
+ "sPaginationType": "full_numbers",
+ "iDisplayLength": 25,
+ }
+
+
+def filter_json(request, username, name):
+ filter = TestRunFilter.objects.get(owner__username=username, name=name)
+ return FilterTable.json(request, params=(request.user, filter))
+
+
+class FilterPreviewTable(FilterTable):
+ def get_queryset(self, user, form):
+ return form.get_test_runs(user)
+
+ datatable_opts = FilterTable.datatable_opts.copy()
+ datatable_opts.update({
+ "iDisplayLength": 10,
+ })
+
+
+def filter_preview_json(request):
+ try:
+ filter = TestRunFilter.objects.get(owner=request.user, name=request.GET['name'])
+ except TestRunFilter.DoesNotExist:
+ filter = None
+ form = TestRunFilterForm(request.user, request.GET, instance=filter)
+ if not form.is_valid():
+ raise ValidationError(str(form.errors))
+ return FilterPreviewTable.json(request, params=(request.user, form))
+
+
+@BreadCrumb("Filter ~{username}/{name}", parent=filters_list, needs=['username', 'name'])
+def filter_detail(request, username, name):
+ filter = TestRunFilter.objects.get(owner__username=username, name=name)
+ if not filter.public and filter.owner != request.user:
+ raise PermissionDenied()
+ if not request.user.is_authenticated():
+ subscription = None
+ else:
+ try:
+ subscription = TestRunFilterSubscription.objects.get(
+ user=request.user, filter=filter)
+ except TestRunFilterSubscription.DoesNotExist:
+ subscription = None
+ return render_to_response(
+ 'dashboard_app/filter_detail.html', {
+ 'filter': filter,
+ 'subscription': subscription,
+ 'filter_table': FilterTable(
+ "filter-table",
+ reverse(filter_json, kwargs=dict(username=username, name=name)),
+ params=(request.user, filter)),
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ filter_detail, name=name, username=username),
+ }, RequestContext(request)
+ )
+
+
+class TestRunFilterSubscriptionForm(forms.ModelForm):
+ class Meta:
+ model = TestRunFilterSubscription
+ fields = ('level',)
+ def __init__(self, filter, user, *args, **kwargs):
+ super(TestRunFilterSubscriptionForm, self).__init__(*args, **kwargs)
+ self.instance.filter = filter
+ self.instance.user = user
+
+
+@BreadCrumb("Manage Subscription", parent=filter_detail, needs=['name', 'username'])
+@login_required
+def filter_subscribe(request, username, name):
+ filter = TestRunFilter.objects.get(owner__username=username, name=name)
+ if not filter.public and filter.owner != request.user:
+ raise PermissionDenied()
+ try:
+ subscription = TestRunFilterSubscription.objects.get(
+ user=request.user, filter=filter)
+ except TestRunFilterSubscription.DoesNotExist:
+ subscription = None
+ if request.method == "POST":
+ form = TestRunFilterSubscriptionForm(
+ filter, request.user, request.POST, instance=subscription)
+ if form.is_valid():
+ if 'unsubscribe' in request.POST:
+ subscription.delete()
+ else:
+ form.save()
+ return HttpResponseRedirect(filter.get_absolute_url())
+ else:
+ form = TestRunFilterSubscriptionForm(
+ filter, request.user, instance=subscription)
+ return render_to_response(
+ 'dashboard_app/filter_subscribe.html', {
+ 'filter': filter,
+ 'form': form,
+ 'subscription': subscription,
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ filter_subscribe, name=name, username=username),
+ }, RequestContext(request)
+ )
+
+
+test_run_filter_head = '''
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-edit.css" />
+<script type="text/javascript" src="{% url admin:jsi18n %}"></script>
+<script type="text/javascript">
+var django = {};
+django.jQuery = $;
+var test_case_url = "{% url dashboard_app.views.filter_add_cases_for_test_json %}?test=";
+var attr_name_completion_url = "{% url dashboard_app.views.filter_attr_name_completion_json %}";
+var attr_value_completion_url = "{% url dashboard_app.views.filter_attr_value_completion_json %}";
+</script>
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-edit.js"></script>
+'''
+
+
+class TestRunFilterForm(forms.ModelForm):
+ class Meta:
+ model = TestRunFilter
+ exclude = ('owner',)
+ widgets = {
+ 'bundle_streams': FilteredSelectMultiple("Bundle Streams", False),
+ }
+
+ @property
+ def media(self):
+ super_media = str(super(TestRunFilterForm, self).media)
+ return mark_safe(Template(test_run_filter_head).render(
+ Context({'STATIC_URL': settings.STATIC_URL})
+ )) + super_media
+
+ test = forms.ModelChoiceField(
+ queryset=Test.objects.order_by('test_id'), empty_label="<any>", required=False)
+
+ test_case = forms.ModelChoiceField(
+ queryset=TestCase.objects.none(), empty_label="<any>", required=False)
+
+ def validate_name(self, value):
+ self.instance.name = value
+ try:
+ self.instance.validate_unique()
+ except ValidationError, e:
+ if e.message_dict.values() == [[
+ u'Test run filter with this Owner and Name already exists.']]:
+ raise ValidationError("You already have a filter with this name")
+ else:
+ raise
+
+ def save(self, commit=True, **kwargs):
+ instance = super(TestRunFilterForm, self).save(commit=commit, **kwargs)
+ if commit:
+ instance.attributes.all().delete()
+ for (name, value) in self.attributes:
+ instance.attributes.create(name=name, value=value)
+ return instance
+
+ @property
+ def summary_data(self):
+ data = self.cleaned_data.copy()
+ data['attributes'] = self.attributes
+ return data
+
+ def __init__(self, user, *args, **kwargs):
+ super(TestRunFilterForm, self).__init__(*args, **kwargs)
+ self.instance.owner = user
+ self.fields['bundle_streams'].queryset = BundleStream.objects.accessible_by_principal(user)
+ self.fields['name'].validators.append(self.validate_name)
+ test = self['test'].value()
+ if test:
+ if not isinstance(test, int):
+ test = int(repr(test)[2:-1])
+ test = Test.objects.get(pk=test)
+ self.fields['test_case'].queryset = TestCase.objects.filter(test=test).order_by('test_case_id')
+
+ @property
+ def attributes(self):
+ if not self.is_bound and self.instance.pk:
+ return self.instance.attributes.values_list('name', 'value')
+ else:
+ attributes = []
+ for (var, value) in self.data.iteritems():
+ if var.startswith('attribute_key_'):
+ index = int(var[len('attribute_key_'):])
+ attr_value = self.data['attribute_value_' + str(index)]
+ attributes.append((index, value, attr_value))
+
+ attributes.sort()
+ attributes = [a[1:] for a in attributes]
+ return attributes
+
+ def get_test_runs(self, user):
+ assert self.is_valid(), self.errors
+ filter = self.save(commit=False)
+ return filter.get_test_runs_impl(
+ user, self.cleaned_data['bundle_streams'], self.attributes)
+
+
+def filter_form(request, bread_crumb_trail, instance=None):
+ if request.method == 'POST':
+ form = TestRunFilterForm(request.user, request.POST, instance=instance)
+
+ if form.is_valid():
+ if 'save' in request.POST:
+ filter = form.save()
+ return HttpResponseRedirect(filter.get_absolute_url())
+ else:
+ c = request.POST.copy()
+ c.pop('csrfmiddlewaretoken', None)
+ return render_to_response(
+ 'dashboard_app/filter_preview.html', {
+ 'bread_crumb_trail': bread_crumb_trail,
+ 'form': form,
+ 'table': FilterPreviewTable(
+ 'filter-preview',
+ reverse(filter_preview_json) + '?' + c.urlencode(),
+ params=(request.user, form)),
+ }, RequestContext(request))
+ else:
+ form = TestRunFilterForm(request.user, instance=instance)
+ return render_to_response(
+ 'dashboard_app/filter_add.html', {
+ 'bread_crumb_trail': bread_crumb_trail,
+ 'form': form,
+ }, RequestContext(request))
+
+
+@BreadCrumb("Add new filter", parent=filters_list)
+def filter_add(request):
+ return filter_form(
+ request,
+ BreadCrumbTrail.leading_to(filter_add))
+
+
+@BreadCrumb("Edit", parent=filter_detail, needs=['name', 'username'])
+def filter_edit(request, username, name):
+ if request.user.username != username:
+ raise PermissionDenied()
+ filter = TestRunFilter.objects.get(owner=request.user, name=name)
+ return filter_form(
+ request,
+ BreadCrumbTrail.leading_to(filter_edit, name=name, username=username),
+ instance=filter)
+
+
+@BreadCrumb("Delete", parent=filter_detail, needs=['name', 'username'])
+def filter_delete(request, username, name):
+ if request.user.username != username:
+ raise PermissionDenied()
+ filter = TestRunFilter.objects.get(owner=request.user, name=name)
+ if request.method == "POST":
+ if 'yes' in request.POST:
+ filter.delete()
+ return HttpResponseRedirect(reverse(filters_list))
+ else:
+ return HttpResponseRedirect(filter.get_absolute_url())
+ return render_to_response(
+ 'dashboard_app/filter_delete.html', {
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(filter_delete, name=name, username=username),
+ 'filter': filter,
+ }, RequestContext(request))
+
+
+def filter_add_cases_for_test_json(request):
+ test = Test.objects.get(test_id=request.GET['test'])
+ result = TestCase.objects.filter(test=test).order_by('test_case_id').values('test_case_id', 'id')
+ return HttpResponse(
+ json.dumps(list(result)),
+ mimetype='application/json')
+
+
+def filter_attr_name_completion_json(request):
+ term = request.GET['term']
+ result = NamedAttribute.objects.filter(
+ name__startswith=term).distinct('name').order_by('name').values_list('name', flat=True)
+ return HttpResponse(
+ json.dumps(list(result)),
+ mimetype='application/json')
+
+
+def filter_attr_value_completion_json(request):
+ name = request.GET['name']
+ term = request.GET['term']
+ result = NamedAttribute.objects.filter(
+ name=name,
+ value__startswith=term).distinct('value').order_by('value').values_list('value', flat=True)
+ return HttpResponse(
+ json.dumps(list(result)),
+ mimetype='application/json')
+
+
def test_run_detail_test_json(request, pathname, content_sha1, analyzer_assigned_uuid):
test_run = get_restricted_object_or_404(
TestRun, lambda test_run: test_run.bundle.bundle_stream,
=== modified file 'doc/changes.rst'
@@ -6,6 +6,7 @@
Version 0.21
============
* Unreleased
+* Add the concept of a test run filter.
.. _version_0_20:
@@ -21,7 +22,6 @@
Version 0.19
============
-
* Add image status views and models for use by the QA services team.
* Allow linking test runs to launchpad bugs from the image status view.