From c7ddeed0f7b2cd65b4cd5e41e74f8ef5fc9a14b6 Mon Sep 17 00:00:00 2001 From: mdoglio Date: Wed, 9 Apr 2014 15:35:51 +0100 Subject: [PATCH 1/5] support exclusion profiles on the ui --- requirements/pure.txt | 2 + .../model/fixtures/visibility_profile.json | 18 ++ treeherder/model/migrations/0001_initial.py | 132 +++++---- ...le__add_exclusionprofile__add_jobfilter.py | 254 ++++++++++++++++++ treeherder/settings/base.py | 9 + treeherder/webapp/api/permissions.py | 25 +- treeherder/webapp/api/serializers.py | 50 ++++ treeherder/webapp/api/urls.py | 5 +- treeherder/webapp/api/views.py | 86 +++++- treeherder/webapp/templates/404.html | 5 + 10 files changed, 508 insertions(+), 78 deletions(-) create mode 100644 treeherder/model/fixtures/visibility_profile.json create mode 100644 treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py create mode 100644 treeherder/webapp/api/serializers.py create mode 100644 treeherder/webapp/templates/404.html diff --git a/requirements/pure.txt b/requirements/pure.txt index 9daf2639b..5083db1b7 100644 --- a/requirements/pure.txt +++ b/requirements/pure.txt @@ -25,3 +25,5 @@ httplib2==0.7.4 git+git://github.com/jeads/datasource@143ac08d11 git+git://github.com/mozilla/treeherder-client@1dc3644494 + +jsonfield==0.9.20 \ No newline at end of file diff --git a/treeherder/model/fixtures/visibility_profile.json b/treeherder/model/fixtures/visibility_profile.json new file mode 100644 index 000000000..ed1dfbb32 --- /dev/null +++ b/treeherder/model/fixtures/visibility_profile.json @@ -0,0 +1,18 @@ +[ + { + "pk": 1, + "model": "model.visibilityprofile", + "fields": { + "default": true, + + } + }, + { + "pk": 2, + "model": "djcelery.intervalschedule", + "fields": { + "every": 1, + "period": "minutes" + } + } +] \ No newline at end of file diff --git a/treeherder/model/migrations/0001_initial.py b/treeherder/model/migrations/0001_initial.py index f117709da..610029366 100644 --- a/treeherder/model/migrations/0001_initial.py +++ b/treeherder/model/migrations/0001_initial.py @@ -12,8 +12,8 @@ class Migration(SchemaMigration): db.create_table(u'product', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['Product']) @@ -23,7 +23,7 @@ class Migration(SchemaMigration): ('os_name', self.gf('django.db.models.fields.CharField')(max_length=25L)), ('platform', self.gf('django.db.models.fields.CharField')(max_length=25L)), ('architecture', self.gf('django.db.models.fields.CharField')(max_length=25L, blank=True)), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['BuildPlatform']) @@ -31,8 +31,8 @@ class Migration(SchemaMigration): db.create_table(u'option', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['Option']) @@ -40,8 +40,8 @@ class Migration(SchemaMigration): db.create_table(u'repository_group', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['RepositoryGroup']) @@ -53,8 +53,8 @@ class Migration(SchemaMigration): ('dvcs_type', self.gf('django.db.models.fields.CharField')(max_length=25L)), ('url', self.gf('django.db.models.fields.CharField')(max_length=255L)), ('codebase', self.gf('django.db.models.fields.CharField')(max_length=50L, blank=True)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['Repository']) @@ -64,7 +64,7 @@ class Migration(SchemaMigration): ('os_name', self.gf('django.db.models.fields.CharField')(max_length=25L)), ('platform', self.gf('django.db.models.fields.CharField')(max_length=25L)), ('architecture', self.gf('django.db.models.fields.CharField')(max_length=25L, blank=True)), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['MachinePlatform']) @@ -87,7 +87,7 @@ class Migration(SchemaMigration): ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), ('first_timestamp', self.gf('django.db.models.fields.IntegerField')()), ('last_timestamp', self.gf('django.db.models.fields.IntegerField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['Machine']) @@ -97,7 +97,7 @@ class Migration(SchemaMigration): ('machine', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['model.Machine'])), ('author', self.gf('django.db.models.fields.CharField')(max_length=50L)), ('machine_timestamp', self.gf('django.db.models.fields.IntegerField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), ('note', self.gf('django.db.models.fields.TextField')(blank=True)), )) db.send_create_signal(u'model', ['MachineNote']) @@ -112,20 +112,25 @@ class Migration(SchemaMigration): ('read_only_host', self.gf('django.db.models.fields.CharField')(max_length=128L, blank=True)), ('name', self.gf('django.db.models.fields.CharField')(max_length=128L)), ('type', self.gf('django.db.models.fields.CharField')(max_length=25L)), - ('oauth_consumer_key', self.gf('django.db.models.fields.CharField')(max_length=45L, blank=True)), - ('oauth_consumer_secret', self.gf('django.db.models.fields.CharField')(max_length=45L, blank=True)), - ('creation_date', self.gf('django.db.models.fields.DateTimeField')()), - ('cron_batch', self.gf('django.db.models.fields.CharField')(max_length=45L, blank=True)), + ('oauth_consumer_key', self.gf('django.db.models.fields.CharField')(max_length=45L, null=True, blank=True)), + ('oauth_consumer_secret', self.gf('django.db.models.fields.CharField')(max_length=45L, null=True, blank=True)), + ('creation_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), )) db.send_create_signal(u'model', ['Datasource']) + # Adding unique constraint on 'Datasource', fields ['project', 'dataset', 'contenttype'] + db.create_unique(u'datasource', ['project', 'dataset', 'contenttype']) + + # Adding unique constraint on 'Datasource', fields ['host', 'name'] + db.create_unique(u'datasource', ['host', 'name']) + # Adding model 'JobGroup' db.create_table(u'job_group', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('symbol', self.gf('django.db.models.fields.CharField')(max_length=10L)), + ('symbol', self.gf('django.db.models.fields.CharField')(default=u'?', max_length=10L)), ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['JobGroup']) @@ -135,26 +140,29 @@ class Migration(SchemaMigration): ('repository', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['model.Repository'])), ('version', self.gf('django.db.models.fields.CharField')(max_length=50L)), ('version_timestamp', self.gf('django.db.models.fields.IntegerField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['RepositoryVersion']) # Adding model 'OptionCollection' db.create_table(u'option_collection', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['model.Option'])), ('option_collection_hash', self.gf('django.db.models.fields.CharField')(max_length=40L)), + ('option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['model.Option'])), )) db.send_create_signal(u'model', ['OptionCollection']) + # Adding unique constraint on 'OptionCollection', fields ['option_collection_hash', 'option'] + db.create_unique(u'option_collection', ['option_collection_hash', 'option_id']) + # Adding model 'JobType' db.create_table(u'job_type', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('job_group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['model.JobGroup'], null=True, blank=True)), - ('symbol', self.gf('django.db.models.fields.CharField')(max_length=10L)), + ('symbol', self.gf('django.db.models.fields.CharField')(default=u'?', max_length=10L)), ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['JobType']) @@ -162,13 +170,22 @@ class Migration(SchemaMigration): db.create_table(u'failure_classification', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(max_length=50L)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('active_status', self.gf('django.db.models.fields.CharField')(max_length=7L, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default=u'fill me', blank=True)), + ('active_status', self.gf('django.db.models.fields.CharField')(default=u'active', max_length=7L, blank=True)), )) db.send_create_signal(u'model', ['FailureClassification']) def backwards(self, orm): + # Removing unique constraint on 'OptionCollection', fields ['option_collection_hash', 'option'] + db.delete_unique(u'option_collection', ['option_collection_hash', 'option_id']) + + # Removing unique constraint on 'Datasource', fields ['host', 'name'] + db.delete_unique(u'datasource', ['host', 'name']) + + # Removing unique constraint on 'Datasource', fields ['project', 'dataset', 'contenttype'] + db.delete_unique(u'datasource', ['project', 'dataset', 'contenttype']) + # Deleting model 'Product' db.delete_table(u'product') @@ -229,54 +246,53 @@ class Migration(SchemaMigration): }, u'model.buildplatform': { 'Meta': {'object_name': 'BuildPlatform', 'db_table': "u'build_platform'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '25L', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'os_name': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), 'platform': ('django.db.models.fields.CharField', [], {'max_length': '25L'}) }, u'model.datasource': { - 'Meta': {'object_name': 'Datasource', 'db_table': "u'datasource'"}, + 'Meta': {'unique_together': "[[u'project', u'dataset', u'contenttype'], [u'host', u'name']]", 'object_name': 'Datasource', 'db_table': "u'datasource'"}, 'contenttype': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), - 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), - 'cron_batch': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'blank': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'dataset': ('django.db.models.fields.IntegerField', [], {}), 'host': ('django.db.models.fields.CharField', [], {'max_length': '128L'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '128L'}), - 'oauth_consumer_key': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'blank': 'True'}), - 'oauth_consumer_secret': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'blank': 'True'}), + 'oauth_consumer_key': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'null': 'True', 'blank': 'True'}), + 'oauth_consumer_secret': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'null': 'True', 'blank': 'True'}), 'project': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), 'read_only_host': ('django.db.models.fields.CharField', [], {'max_length': '128L', 'blank': 'True'}), 'type': ('django.db.models.fields.CharField', [], {'max_length': '25L'}) }, u'model.failureclassification': { 'Meta': {'object_name': 'FailureClassification', 'db_table': "u'failure_classification'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) }, u'model.jobgroup': { 'Meta': {'object_name': 'JobGroup', 'db_table': "u'job_group'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), - 'symbol': ('django.db.models.fields.CharField', [], {'max_length': '10L'}) + 'symbol': ('django.db.models.fields.CharField', [], {'default': "u'?'", 'max_length': '10L'}) }, u'model.jobtype': { 'Meta': {'object_name': 'JobType', 'db_table': "u'job_type'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'job_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.JobGroup']", 'null': 'True', 'blank': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), - 'symbol': ('django.db.models.fields.CharField', [], {'max_length': '10L'}) + 'symbol': ('django.db.models.fields.CharField', [], {'default': "u'?'", 'max_length': '10L'}) }, u'model.machine': { 'Meta': {'object_name': 'Machine', 'db_table': "u'machine'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), 'first_timestamp': ('django.db.models.fields.IntegerField', [], {}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'last_timestamp': ('django.db.models.fields.IntegerField', [], {}), @@ -284,7 +300,7 @@ class Migration(SchemaMigration): }, u'model.machinenote': { 'Meta': {'object_name': 'MachineNote', 'db_table': "u'machine_note'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), 'author': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'machine': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.Machine']"}), @@ -293,7 +309,7 @@ class Migration(SchemaMigration): }, u'model.machineplatform': { 'Meta': {'object_name': 'MachinePlatform', 'db_table': "u'machine_platform'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '25L', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'os_name': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), @@ -301,50 +317,50 @@ class Migration(SchemaMigration): }, u'model.option': { 'Meta': {'object_name': 'Option', 'db_table': "u'option'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) }, u'model.optioncollection': { - 'Meta': {'object_name': 'OptionCollection', 'db_table': "u'option_collection'"}, + 'Meta': {'unique_together': "([u'option_collection_hash', u'option'],)", 'object_name': 'OptionCollection', 'db_table': "u'option_collection'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'option': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.Option']"}), - 'option_collection_hash': ('django.db.models.fields.CharField', [], {'max_length': '40L',}) + 'option_collection_hash': ('django.db.models.fields.CharField', [], {'max_length': '40L'}) }, u'model.product': { 'Meta': {'object_name': 'Product', 'db_table': "u'product'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) }, u'model.repository': { 'Meta': {'object_name': 'Repository', 'db_table': "u'repository'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), 'codebase': ('django.db.models.fields.CharField', [], {'max_length': '50L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'dvcs_type': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), 'repository_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.RepositoryGroup']"}), - 'dvcs_type': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), 'url': ('django.db.models.fields.CharField', [], {'max_length': '255L'}) }, u'model.repositorygroup': { 'Meta': {'object_name': 'RepositoryGroup', 'db_table': "u'repository_group'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) }, u'model.repositoryversion': { 'Meta': {'object_name': 'RepositoryVersion', 'db_table': "u'repository_version'"}, - 'active_status': ('django.db.models.fields.CharField', [], {'max_length': '7L', 'blank': 'True'}), + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'repository': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.Repository']"}), - 'version_timestamp': ('django.db.models.fields.IntegerField', [], {}), - 'version': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) + 'version': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), + 'version_timestamp': ('django.db.models.fields.IntegerField', [], {}) } } - complete_apps = ['model'] + complete_apps = ['model'] \ No newline at end of file diff --git a/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py b/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py new file mode 100644 index 000000000..560af66b2 --- /dev/null +++ b/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py @@ -0,0 +1,254 @@ +# -*- 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 'UserExclusionProfile' + db.create_table(u'model_userexclusionprofile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'exclusion_profiles', to=orm['auth.User'])), + ('exclusion_profile', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['model.ExclusionProfile'], null=True, blank=True)), + ('is_default', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal(u'model', ['UserExclusionProfile']) + + # Adding model 'ExclusionProfile' + db.create_table(u'model_exclusionprofile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('is_default', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('flat_exclusion', self.gf('jsonfield.fields.JSONField')(default={}, blank=True)), + ('author', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'exclusion_profiles_authored', to=orm['auth.User'])), + )) + db.send_create_signal(u'model', ['ExclusionProfile']) + + # Adding M2M table for field filters on 'ExclusionProfile' + db.create_table(u'model_exclusionprofile_filters', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('exclusionprofile', models.ForeignKey(orm[u'model.exclusionprofile'], null=False)), + ('jobfilter', models.ForeignKey(orm[u'model.jobfilter'], null=False)) + )) + db.create_unique(u'model_exclusionprofile_filters', ['exclusionprofile_id', 'jobfilter_id']) + + # Adding model 'JobFilter' + db.create_table(u'model_jobfilter', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + ('info', self.gf('jsonfield.fields.JSONField')()), + ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + )) + db.send_create_signal(u'model', ['JobFilter']) + + + def backwards(self, orm): + # Deleting model 'UserExclusionProfile' + db.delete_table(u'model_userexclusionprofile') + + # Deleting model 'ExclusionProfile' + db.delete_table(u'model_exclusionprofile') + + # Removing M2M table for field filters on 'ExclusionProfile' + db.delete_table('model_exclusionprofile_filters') + + # Deleting model 'JobFilter' + db.delete_table(u'model_jobfilter') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'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'}), + u'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'}) + }, + u'model.bugscache': { + 'Meta': {'object_name': 'Bugscache', 'db_table': "u'bugscache'"}, + 'crash_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'keywords': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'os': ('django.db.models.fields.CharField', [], {'max_length': '64L', 'blank': 'True'}), + 'resolution': ('django.db.models.fields.CharField', [], {'max_length': '64L', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64L', 'blank': 'True'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '255L'}) + }, + u'model.buildplatform': { + 'Meta': {'object_name': 'BuildPlatform', 'db_table': "u'build_platform'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '25L', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'os_name': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '25L'}) + }, + u'model.datasource': { + 'Meta': {'unique_together': "[[u'project', u'dataset', u'contenttype'], [u'host', u'name']]", 'object_name': 'Datasource', 'db_table': "u'datasource'"}, + 'contenttype': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'dataset': ('django.db.models.fields.IntegerField', [], {}), + 'host': ('django.db.models.fields.CharField', [], {'max_length': '128L'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128L'}), + 'oauth_consumer_key': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'null': 'True', 'blank': 'True'}), + 'oauth_consumer_secret': ('django.db.models.fields.CharField', [], {'max_length': '45L', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), + 'read_only_host': ('django.db.models.fields.CharField', [], {'max_length': '128L', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '25L'}) + }, + u'model.exclusionprofile': { + 'Meta': {'object_name': 'ExclusionProfile'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'exclusion_profiles_authored'", 'to': u"orm['auth.User']"}), + 'filters': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'profiles'", 'symmetrical': 'False', 'to': u"orm['model.JobFilter']"}), + 'flat_exclusion': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'model.failureclassification': { + 'Meta': {'object_name': 'FailureClassification', 'db_table': "u'failure_classification'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) + }, + u'model.jobfilter': { + 'Meta': {'object_name': 'JobFilter'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('jsonfield.fields.JSONField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'model.jobgroup': { + 'Meta': {'object_name': 'JobGroup', 'db_table': "u'job_group'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), + 'symbol': ('django.db.models.fields.CharField', [], {'default': "u'?'", 'max_length': '10L'}) + }, + u'model.jobtype': { + 'Meta': {'object_name': 'JobType', 'db_table': "u'job_type'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.JobGroup']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), + 'symbol': ('django.db.models.fields.CharField', [], {'default': "u'?'", 'max_length': '10L'}) + }, + u'model.machine': { + 'Meta': {'object_name': 'Machine', 'db_table': "u'machine'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'first_timestamp': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_timestamp': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) + }, + u'model.machinenote': { + 'Meta': {'object_name': 'MachineNote', 'db_table': "u'machine_note'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'author': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.Machine']"}), + 'machine_timestamp': ('django.db.models.fields.IntegerField', [], {}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + u'model.machineplatform': { + 'Meta': {'object_name': 'MachinePlatform', 'db_table': "u'machine_platform'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '25L', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'os_name': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '25L'}) + }, + u'model.option': { + 'Meta': {'object_name': 'Option', 'db_table': "u'option'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) + }, + u'model.optioncollection': { + 'Meta': {'unique_together': "([u'option_collection_hash', u'option'],)", 'object_name': 'OptionCollection', 'db_table': "u'option_collection'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'option': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.Option']"}), + 'option_collection_hash': ('django.db.models.fields.CharField', [], {'max_length': '40L'}) + }, + u'model.product': { + 'Meta': {'object_name': 'Product', 'db_table': "u'product'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) + }, + u'model.repository': { + 'Meta': {'object_name': 'Repository', 'db_table': "u'repository'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'codebase': ('django.db.models.fields.CharField', [], {'max_length': '50L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'dvcs_type': ('django.db.models.fields.CharField', [], {'max_length': '25L'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), + 'repository_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.RepositoryGroup']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255L'}) + }, + u'model.repositorygroup': { + 'Meta': {'object_name': 'RepositoryGroup', 'db_table': "u'repository_group'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u'fill me'", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) + }, + u'model.repositoryversion': { + 'Meta': {'object_name': 'RepositoryVersion', 'db_table': "u'repository_version'"}, + 'active_status': ('django.db.models.fields.CharField', [], {'default': "u'active'", 'max_length': '7L', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'repository': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.Repository']"}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '50L'}), + 'version_timestamp': ('django.db.models.fields.IntegerField', [], {}) + }, + u'model.userexclusionprofile': { + 'Meta': {'object_name': 'UserExclusionProfile'}, + 'exclusion_profile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['model.ExclusionProfile']", 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'exclusion_profiles'", 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['model'] \ No newline at end of file diff --git a/treeherder/settings/base.py b/treeherder/settings/base.py index b06d851bc..4ec7c938d 100644 --- a/treeherder/settings/base.py +++ b/treeherder/settings/base.py @@ -272,3 +272,12 @@ BROKER_URL = 'amqp://{0}:{1}@{2}:{3}/{4}'.format( API_HOSTNAME = SITE_URL BROWSERID_AUDIENCES = [SITE_URL] + + +def obtain_username(email): + if email.endswith("@mozilla.com"): + return email.rsplit('@', 1)[0] + else: + return email + +BROWSERID_USERNAME_ALGO = obtain_username diff --git a/treeherder/webapp/api/permissions.py b/treeherder/webapp/api/permissions.py index 85fe932c6..cf6ff2f5e 100644 --- a/treeherder/webapp/api/permissions.py +++ b/treeherder/webapp/api/permissions.py @@ -1,14 +1,29 @@ -from rest_framework.permissions import BasePermission -from rest_framework.permissions import SAFE_METHODS +from rest_framework import permissions -class IsStaffOrReadOnly(BasePermission): +class IsStaffOrReadOnly(permissions.BasePermission): """ The request is authenticated as an admin staff (eg. sheriffs), or is a read-only request. """ def has_permission(self, request, view): - return (request.method in SAFE_METHODS or + return (request.method in permissions.SAFE_METHODS or request.user and request.user.is_authenticated() and - request.user.is_staff) \ No newline at end of file + request.user.is_staff) + + +class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Object-level permission to only allow owners of an object to edit it. + Assumes the model instance has an `user` attribute. + """ + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + # Instance must have an attribute named `user`. + return obj.user == request.user diff --git a/treeherder/webapp/api/serializers.py b/treeherder/webapp/api/serializers.py new file mode 100644 index 000000000..df92f020d --- /dev/null +++ b/treeherder/webapp/api/serializers.py @@ -0,0 +1,50 @@ +from django.contrib.auth.models import User +from rest_framework import serializers + +from treeherder.model import models + + +class UserExclusionProfileSerializer(serializers.ModelSerializer): + exclusion_profile = serializers.PrimaryKeyRelatedField() + + class Meta: + model = models.UserExclusionProfile + fields = ["is_default", "exclusion_profile"] + + +class UserSerializer(serializers.ModelSerializer): + exclusion_profiles = UserExclusionProfileSerializer() + + class Meta: + model = User + fields = ["username", "is_superuser", "is_staff", "email", "exclusion_profiles"] + + +class JobFilterSerializer(serializers.ModelSerializer): + info = serializers.WritableField() + + class Meta: + model = models.JobFilter + + +class ExclusionProfileSerializer(serializers.ModelSerializer): + + flat_exclusion = serializers.WritableField(required=False) + filters = serializers.PrimaryKeyRelatedField(many=True) + + class Meta: + model = models.ExclusionProfile + exclude = ['users'] + + +class RepositoryGroupSerializer(serializers.ModelSerializer): + class Meta: + model = models.RepositoryGroup + fields = ('name', 'description') + + +class RepositorySerializer(serializers.ModelSerializer): + repository_group = RepositoryGroupSerializer() + + class Meta: + model = models.Repository \ No newline at end of file diff --git a/treeherder/webapp/api/urls.py b/treeherder/webapp/api/urls.py index e7132b624..6e7943b8a 100644 --- a/treeherder/webapp/api/urls.py +++ b/treeherder/webapp/api/urls.py @@ -46,7 +46,6 @@ project_bound_router.register( base_name='bug-job-map', ) - # this is the default router for plain restful endpoints # refdata endpoints: @@ -64,6 +63,10 @@ default_router.register(r'option', views.OptionViewSet) default_router.register(r'optioncollection', views.OptionCollectionViewSet) default_router.register(r'bugscache', views.BugscacheViewSet) default_router.register(r'failureclassification', views.FailureClassificationViewSet) +default_router.register(r'failureclassification', views.FailureClassificationViewSet) +default_router.register(r'user', views.UserViewSet) +default_router.register(r'exclusion-profile', views.ExclusionProfileViewSet) +default_router.register(r'job-filter', views.JobFilterViewSet) urlpatterns = patterns( diff --git a/treeherder/webapp/api/views.py b/treeherder/webapp/api/views.py index fe8bc014c..9251f3ba3 100644 --- a/treeherder/webapp/api/views.py +++ b/treeherder/webapp/api/views.py @@ -3,21 +3,25 @@ import itertools import oauth2 as oauth from django.conf import settings -from rest_framework import viewsets, serializers +from django.contrib.auth.models import User + +from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import action, link from rest_framework.reverse import reverse from rest_framework.exceptions import ParseError from rest_framework.authentication import SessionAuthentication -from treeherder.webapp.api.permissions import IsStaffOrReadOnly +from treeherder.webapp.api.permissions import (IsStaffOrReadOnly, + IsOwnerOrReadOnly) from treeherder.model import models from treeherder.model.derived import (JobsModel, DatasetNotFoundError, RefDataManager, ObjectNotFoundException) from treeherder.webapp.api.utils import UrlQueryFilter from treeherder.etl.oauth_utils import OAuthCredentials from treeherder.events.publisher import JobClassificationPublisher +from treeherder.webapp.api import serializers as th_serializers def oauth_required(func): @@ -713,21 +717,10 @@ class JobGroupViewSet(viewsets.ReadOnlyModelViewSet): model = models.JobGroup -class RepositoryGroupSerializer(serializers.ModelSerializer): - class Meta: - model = models.RepositoryGroup - fields = ('name', 'description') - -class RepositorySerializer(serializers.ModelSerializer): - repository_group = RepositoryGroupSerializer() - - class Meta: - model = models.Repository - class RepositoryViewSet(viewsets.ReadOnlyModelViewSet): """ViewSet for the refdata Repository model""" model = models.Repository - serializer_class = RepositorySerializer + serializer_class = th_serializers.RepositorySerializer class MachinePlatformViewSet(viewsets.ReadOnlyModelViewSet): @@ -787,6 +780,11 @@ class OptionCollectionViewSet(viewsets.ReadOnlyModelViewSet): model = models.OptionCollection +class OptionViewSet(viewsets.ReadOnlyModelViewSet): + """ViewSet for the refdata Option model""" + model = models.Option + + class JobTypeViewSet(viewsets.ReadOnlyModelViewSet): """ViewSet for the refdata JobType model""" model = models.JobType @@ -795,3 +793,63 @@ class JobTypeViewSet(viewsets.ReadOnlyModelViewSet): class FailureClassificationViewSet(viewsets.ReadOnlyModelViewSet): """ViewSet for the refdata FailureClassification model""" model = models.FailureClassification + + +############################# +# User and exclusion profiles +############################# + + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + """ + Info about a logged-in user. + Used by treeherder-ui to inspect user properties like the exclusion profile + """ + model = User + serializer_class = th_serializers.UserSerializer + authentication_classes = (SessionAuthentication,) + + def get_queryset(self): + return User.objects.filter(id=self.request.user.id) + + +class UserExclusionProfileViewSet(viewsets.ModelViewSet): + model = models.UserExclusionProfile + authentication_classes = (SessionAuthentication,) + permission_classes = (IsOwnerOrReadOnly,) + serializer_class = th_serializers.UserExclusionProfileSerializer + + +class JobFilterViewSet(viewsets.ModelViewSet): + model = models.JobFilter + authentication_classes = (SessionAuthentication,) + permission_classes = (IsStaffOrReadOnly,) + serializer_class = th_serializers.JobFilterSerializer + + def create(self, request, *args, **kwargs): + """ + Overrides the default Viewset to set the current user + as the author of this filter + """ + if "author" not in request.DATA: + request.DATA["author"] = request.user.id + return super(JobFilterViewSet, self).create(request, *args, **kwargs) + + +class ExclusionProfileViewSet(viewsets.ModelViewSet): + """ + + """ + model = models.ExclusionProfile + authentication_classes = (SessionAuthentication,) + permission_classes = (IsStaffOrReadOnly,) + serializer_class = th_serializers.ExclusionProfileSerializer + + def create(self, request, *args, **kwargs): + """ + Overrides the default Viewset to set the current user + as the author of this exclusion profile + """ + if "author" not in request.DATA: + request.DATA["author"] = request.user.id + return super(ExclusionProfileViewSet, self).create(request, *args, **kwargs) diff --git a/treeherder/webapp/templates/404.html b/treeherder/webapp/templates/404.html new file mode 100644 index 000000000..e1b0a3414 --- /dev/null +++ b/treeherder/webapp/templates/404.html @@ -0,0 +1,5 @@ + + +

Page Not found

+ + \ No newline at end of file From b40ad4c1a643ac2be6bc9523cea2c57a5d5deb37 Mon Sep 17 00:00:00 2001 From: mdoglio Date: Wed, 9 Apr 2014 15:41:21 +0100 Subject: [PATCH 2/5] fixup! support exclusion profiles on the ui --- treeherder/model/models.py | 75 +++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/treeherder/model/models.py b/treeherder/model/models.py index c204a3199..375091b26 100644 --- a/treeherder/model/models.py +++ b/treeherder/model/models.py @@ -3,7 +3,9 @@ from __future__ import unicode_literals import uuid import subprocess import os -import MySQLdb + +from collections import defaultdict +import itertools from datasource.bases.BaseHub import BaseHub from datasource.hubs.MySQL import MySQL @@ -11,8 +13,12 @@ from django.conf import settings from django.core.cache import cache from django.db import models from django.db.models import Max +from django.contrib.auth.models import User +from django.db.models.signals import post_save from warnings import filterwarnings, resetwarnings +from jsonfield import JSONField + from treeherder import path @@ -522,3 +528,70 @@ class FailureClassification(models.Model): def __unicode__(self): return self.name + + +# exclusion profiles models + +class JobFilter(models.Model): + """ + A filter represents a collection of properties + that you want to filter jobs on. These properties along with their values + are kept in the info field in json format + """ + name = models.CharField(max_length=255, unique=True) + description = models.TextField(blank=True) + info = JSONField() + author = models.ForeignKey(User) + + +class ExclusionProfile(models.Model): + """ + An exclusion profile represents a list of filters that can be associated with a user profile. + """ + name = models.CharField(max_length=255, unique=True) + is_default = models.BooleanField(default=False) + filters = models.ManyToManyField(JobFilter, related_name="profiles") + flat_exclusion = JSONField(blank=True, default={}) + author = models.ForeignKey(User, related_name="exclusion_profiles_authored") + + def save(self, *args, **kwargs): + super(ExclusionProfile, self).save(*args, **kwargs) + + # prepare the nested defaultdict structure for the flat filters + # options should be stored in a set but sets are not serializable. + # using a list instead + job_types_constructor = lambda: defaultdict(list) + platform_constructor = lambda: defaultdict(job_types_constructor) + flat_filters = defaultdict(platform_constructor) + + for filter in self.filters.all().select_related("info"): + # create a set of combinations for each property in the filter + combo = tuple(itertools.product(filter.info['repos'], filter.info['platforms'], + filter.info['job_types'], filter.info['options'])) + for repo, platform, job_type, option in combo: + # strip the job type symbol appended in the ui + job_type = job_type[:job_type.rfind(" (")] + options = flat_filters[repo][platform][job_type] + # using a list instead of a set and checking if the value already exists + if not options in options: + options.append(option) + + self.flat_exclusion = flat_filters + kwargs["force_insert"] = False + kwargs["force_update"] = True + super(ExclusionProfile, self).save(*args, **kwargs) + + # update the old default profile + if self.is_default: + ExclusionProfile.objects.filter(is_default=True).exclude(id=self.id).update(is_default=False) + + +class UserExclusionProfile(models.Model): + """ + An extension to the standard user model that keeps the exclusion + profile relationship. + """ + + user = models.ForeignKey(User, related_name="exclusion_profiles") + exclusion_profile = models.ForeignKey(ExclusionProfile, blank=True, null=True) + is_default = models.BooleanField(default=True) From 3c6c018232bafea15379868e2b3918232a8a28bc Mon Sep 17 00:00:00 2001 From: mdoglio Date: Thu, 10 Apr 2014 15:05:39 +0100 Subject: [PATCH 3/5] fix test setup to allow model migrations --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1a9ce2fc9..7c7d48fa1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ import os -import simplejson as json from os.path import dirname import sys from django.core.management import call_command @@ -80,6 +79,8 @@ def pytest_runtest_teardown(item): for ds in ds_list: ds.delete() + call_command("migrate", 'model', '0001_initial') + From a8ed7536d879859dc85e1ba20e9526fed0233887 Mon Sep 17 00:00:00 2001 From: mdoglio Date: Mon, 14 Apr 2014 14:42:20 +0100 Subject: [PATCH 4/5] Add jsonfield to vendor --- vendor/jsonfield/__init__.py | 1 + vendor/jsonfield/fields.py | 167 ++++++++++++++++++++++ vendor/jsonfield/models.py | 1 + vendor/jsonfield/subclassing.py | 60 ++++++++ vendor/jsonfield/tests.py | 239 ++++++++++++++++++++++++++++++++ 5 files changed, 468 insertions(+) create mode 100644 vendor/jsonfield/__init__.py create mode 100644 vendor/jsonfield/fields.py create mode 100644 vendor/jsonfield/models.py create mode 100644 vendor/jsonfield/subclassing.py create mode 100644 vendor/jsonfield/tests.py diff --git a/vendor/jsonfield/__init__.py b/vendor/jsonfield/__init__.py new file mode 100644 index 000000000..9c2a0edc5 --- /dev/null +++ b/vendor/jsonfield/__init__.py @@ -0,0 +1 @@ +from .fields import JSONField, JSONCharField diff --git a/vendor/jsonfield/fields.py b/vendor/jsonfield/fields.py new file mode 100644 index 000000000..8b0c1619c --- /dev/null +++ b/vendor/jsonfield/fields.py @@ -0,0 +1,167 @@ +import copy +from django.db import models +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.translation import ugettext_lazy as _ +try: + from django.utils import six +except ImportError: + import six + +try: + import json +except ImportError: + from django.utils import simplejson as json + +from django.forms import fields +from django.forms.util import ValidationError + +from .subclassing import SubfieldBase + + +class JSONFormFieldBase(object): + + def to_python(self, value): + if isinstance(value, six.string_types): + try: + return json.loads(value, **self.load_kwargs) + except ValueError: + raise ValidationError(_("Enter valid JSON")) + return value + + def clean(self, value): + + if not value and not self.required: + return None + + # Trap cleaning errors & bubble them up as JSON errors + try: + return super(JSONFormFieldBase, self).clean(value) + except TypeError: + raise ValidationError(_("Enter valid JSON")) + + +class JSONFormField(JSONFormFieldBase, fields.Field): + pass + +class JSONCharFormField(JSONFormFieldBase, fields.CharField): + pass + + +class JSONFieldBase(six.with_metaclass(SubfieldBase, models.Field)): + + def __init__(self, *args, **kwargs): + self.dump_kwargs = kwargs.pop('dump_kwargs', { + 'cls': DjangoJSONEncoder, + 'separators': (',', ':') + }) + self.load_kwargs = kwargs.pop('load_kwargs', {}) + + super(JSONFieldBase, self).__init__(*args, **kwargs) + + def pre_init(self, value, obj): + """Convert a string value to JSON only if it needs to be deserialized. + + SubfieldBase meteaclass has been modified to call this method instead of + to_python so that we can check the obj state and determine if it needs to be + deserialized""" + + if obj._state.adding: + # Make sure the primary key actually exists on the object before + # checking if it's empty. This is a special case for South datamigrations + # see: https://github.com/bradjasper/django-jsonfield/issues/52 + if not hasattr(obj, "pk") or obj.pk is not None: + if isinstance(value, six.string_types): + try: + return json.loads(value, **self.load_kwargs) + except ValueError: + raise ValidationError(_("Enter valid JSON")) + + return value + + def to_python(self, value): + """The SubfieldBase metaclass calls pre_init instead of to_python, however to_python + is still necessary for Django's deserializer""" + return value + + def get_db_prep_value(self, value, connection, prepared=False): + """Convert JSON object to a string""" + if self.null and value is None: + return None + return json.dumps(value, **self.dump_kwargs) + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return self.get_db_prep_value(value, None) + + def value_from_object(self, obj): + value = super(JSONFieldBase, self).value_from_object(obj) + if self.null and value is None: + return None + return self.dumps_for_display(value) + + def dumps_for_display(self, value): + return json.dumps(value, **self.dump_kwargs) + + def formfield(self, **kwargs): + + if "form_class" not in kwargs: + kwargs["form_class"] = self.form_class + + field = super(JSONFieldBase, self).formfield(**kwargs) + + if not field.help_text: + field.help_text = "Enter valid JSON" + + return field + + def get_default(self): + """ + Returns the default value for this field. + + The default implementation on models.Field calls force_unicode + on the default, which means you can't set arbitrary Python + objects as the default. To fix this, we just return the value + without calling force_unicode on it. Note that if you set a + callable as a default, the field will still call it. It will + *not* try to pickle and encode it. + + """ + if self.has_default(): + if callable(self.default): + return self.default() + return copy.deepcopy(self.default) + # If the field doesn't have a default, then we punt to models.Field. + return super(JSONFieldBase, self).get_default() + + def db_type(self, connection): + if connection.vendor == 'postgresql' and connection.pg_version >= 90300: + return 'json' + else: + return super(JSONFieldBase, self).db_type(connection) + +class JSONField(JSONFieldBase, models.TextField): + """JSONField is a generic textfield that serializes/unserializes JSON objects""" + form_class = JSONFormField + + def __init__(self, *args, **kwargs): + super(JSONField, self).__init__(*args, **kwargs) + self.form_class.load_kwargs = self.load_kwargs + + def dumps_for_display(self, value): + kwargs = { "indent": 2 } + kwargs.update(self.dump_kwargs) + return json.dumps(value, **kwargs) + + +class JSONCharField(JSONFieldBase, models.CharField): + """JSONCharField is a generic textfield that serializes/unserializes JSON objects, + stored in the database like a CharField, which enables it to be used + e.g. in unique keys""" + form_class = JSONCharFormField + + +try: + from south.modelsinspector import add_introspection_rules + add_introspection_rules([], ["^jsonfield\.fields\.(JSONField|JSONCharField)"]) +except ImportError: + pass diff --git a/vendor/jsonfield/models.py b/vendor/jsonfield/models.py new file mode 100644 index 000000000..e5faf1b16 --- /dev/null +++ b/vendor/jsonfield/models.py @@ -0,0 +1 @@ +# Django needs this to see it as a project diff --git a/vendor/jsonfield/subclassing.py b/vendor/jsonfield/subclassing.py new file mode 100644 index 000000000..21d0fc696 --- /dev/null +++ b/vendor/jsonfield/subclassing.py @@ -0,0 +1,60 @@ +## This file was copied from django.db.models.fields.subclassing so that we could +## change the Creator.__set__ behavior. Read the comment below for full details. + +""" +Convenience routines for creating non-trivial Field subclasses, as well as +backwards compatibility utilities. + +Add SubfieldBase as the __metaclass__ for your Field subclass, implement +to_python() and the other necessary methods and everything will work seamlessly. +""" + +class SubfieldBase(type): + """ + A metaclass for custom Field subclasses. This ensures the model's attribute + has the descriptor protocol attached to it. + """ + def __new__(cls, name, bases, attrs): + new_class = super(SubfieldBase, cls).__new__(cls, name, bases, attrs) + new_class.contribute_to_class = make_contrib( + new_class, attrs.get('contribute_to_class') + ) + return new_class + +class Creator(object): + """ + A placeholder class that provides a way to set the attribute on the model. + """ + def __init__(self, field): + self.field = field + + def __get__(self, obj, type=None): + if obj is None: + raise AttributeError('Can only be accessed via an instance.') + return obj.__dict__[self.field.name] + + def __set__(self, obj, value): + # Usually this would call to_python, but we've changed it to pre_init + # so that we can tell which state we're in. By passing an obj, + # we can definitively tell if a value has already been deserialized + # More: https://github.com/bradjasper/django-jsonfield/issues/33 + obj.__dict__[self.field.name] = self.field.pre_init(value, obj) + + +def make_contrib(superclass, func=None): + """ + Returns a suitable contribute_to_class() method for the Field subclass. + + If 'func' is passed in, it is the existing contribute_to_class() method on + the subclass and it is called before anything else. It is assumed in this + case that the existing contribute_to_class() calls all the necessary + superclass methods. + """ + def contribute_to_class(self, cls, name): + if func: + func(self, cls, name) + else: + super(superclass, self).contribute_to_class(cls, name) + setattr(cls, self.name, Creator(self)) + + return contribute_to_class diff --git a/vendor/jsonfield/tests.py b/vendor/jsonfield/tests.py new file mode 100644 index 000000000..e4538fada --- /dev/null +++ b/vendor/jsonfield/tests.py @@ -0,0 +1,239 @@ +from decimal import Decimal +from django.core.serializers import deserialize, serialize +from django.core.serializers.base import DeserializationError +from django.db import models +from django.test import TestCase +from django.utils import simplejson as json + +from .fields import JSONField, JSONCharField +from django.forms.util import ValidationError + +from collections import OrderedDict + +class JsonModel(models.Model): + json = JSONField() + default_json = JSONField(default={"check":12}) + complex_default_json = JSONField(default=[{"checkcheck": 1212}]) + +class JsonCharModel(models.Model): + json = JSONCharField(max_length=100) + default_json = JSONCharField(max_length=100, default={"check":34}) + +class ComplexEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, complex): + return { + '__complex__': True, + 'real': obj.real, + 'imag': obj.imag, + } + + return json.JSONEncoder.default(self, obj) + +def as_complex(dct): + if '__complex__' in dct: + return complex(dct['real'], dct['imag']) + return dct + +class JSONModelCustomEncoders(models.Model): + # A JSON field that can store complex numbers + json = JSONField( + dump_kwargs={'cls': ComplexEncoder, "indent": 4}, + load_kwargs={'object_hook': as_complex}, + ) + +class JSONFieldTest(TestCase): + """JSONField Wrapper Tests""" + + json_model = JsonModel + + def test_json_field_create(self): + """Test saving a JSON object in our JSONField""" + json_obj = { + "item_1": "this is a json blah", + "blergh": "hey, hey, hey"} + + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj) + + def test_string_in_json_field(self): + """Test saving an ordinary Python string in our JSONField""" + json_obj = 'blah blah' + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj) + + def test_float_in_json_field(self): + """Test saving a Python float in our JSONField""" + json_obj = 1.23 + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj) + + def test_int_in_json_field(self): + """Test saving a Python integer in our JSONField""" + json_obj = 1234567 + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj) + + def test_decimal_in_json_field(self): + """Test saving a Python Decimal in our JSONField""" + json_obj = Decimal(12.34) + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + # here we must know to convert the returned string back to Decimal, + # since json does not support that format + self.assertEqual(Decimal(new_obj.json), json_obj) + + def test_json_field_modify(self): + """Test modifying a JSON object in our JSONField""" + json_obj_1 = {'a': 1, 'b': 2} + json_obj_2 = {'a': 3, 'b': 4} + + obj = self.json_model.objects.create(json=json_obj_1) + self.assertEqual(obj.json, json_obj_1) + obj.json = json_obj_2 + + self.assertEqual(obj.json, json_obj_2) + obj.save() + self.assertEqual(obj.json, json_obj_2) + + self.assertTrue(obj) + + def test_json_field_load(self): + """Test loading a JSON object from the DB""" + json_obj_1 = {'a': 1, 'b': 2} + obj = self.json_model.objects.create(json=json_obj_1) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj_1) + + def test_json_list(self): + """Test storing a JSON list""" + json_obj = ["my", "list", "of", 1, "objs", {"hello": "there"}] + + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + self.assertEqual(new_obj.json, json_obj) + + def test_empty_objects(self): + """Test storing empty objects""" + for json_obj in [{}, [], 0, '', False]: + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + self.assertEqual(json_obj, obj.json) + self.assertEqual(json_obj, new_obj.json) + + def test_custom_encoder(self): + """Test encoder_cls and object_hook""" + value = 1 + 3j # A complex number + + obj = JSONModelCustomEncoders.objects.create(json=value) + new_obj = JSONModelCustomEncoders.objects.get(pk=obj.pk) + self.assertEqual(value, new_obj.json) + + def test_django_serializers(self): + """Test serializing/deserializing jsonfield data""" + for json_obj in [{}, [], 0, '', False, {'key': 'value', 'num': 42, + 'ary': list(range(5)), + 'dict': {'k': 'v'}}]: + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + queryset = self.json_model.objects.all() + ser = serialize('json', queryset) + for dobj in deserialize('json', ser): + obj = dobj.object + pulled = self.json_model.objects.get(id=obj.pk) + self.assertEqual(obj.json, pulled.json) + + def test_default_parameters(self): + """Test providing a default value to the model""" + model = JsonModel() + model.json = {"check": 12} + self.assertEqual(model.json, {"check": 12}) + self.assertEqual(type(model.json), dict) + + self.assertEqual(model.default_json, {"check": 12}) + self.assertEqual(type(model.default_json), dict) + + def test_invalid_json(self): + # invalid json data {] in the json and default_json fields + ser = '[{"pk": 1, "model": "jsonfield.jsoncharmodel", ' \ + '"fields": {"json": "{]", "default_json": "{]"}}]' + with self.assertRaises(DeserializationError) as cm: + next(deserialize('json', ser)) + inner = cm.exception.args[0] + self.assertTrue(isinstance(inner, ValidationError)) + self.assertEqual('Enter valid JSON', inner.messages[0]) + + def test_integer_in_string_in_json_field(self): + """Test saving the Python string '123' in our JSONField""" + json_obj = '123' + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj) + + def test_boolean_in_string_in_json_field(self): + """Test saving the Python string 'true' in our JSONField""" + json_obj = 'true' + obj = self.json_model.objects.create(json=json_obj) + new_obj = self.json_model.objects.get(id=obj.id) + + self.assertEqual(new_obj.json, json_obj) + + + def test_pass_by_reference_pollution(self): + """Make sure the default parameter is copied rather than passed by reference""" + model = JsonModel() + model.default_json["check"] = 144 + model.complex_default_json[0]["checkcheck"] = 144 + self.assertEqual(model.default_json["check"], 144) + self.assertEqual(model.complex_default_json[0]["checkcheck"], 144) + + # Make sure when we create a new model, it resets to the default value + # and not to what we just set it to (it would be if it were passed by reference) + model = JsonModel() + self.assertEqual(model.default_json["check"], 12) + self.assertEqual(model.complex_default_json[0]["checkcheck"], 1212) + +class JSONCharFieldTest(JSONFieldTest): + json_model = JsonCharModel + + +class OrderedJsonModel(models.Model): + json = JSONField(load_kwargs={'object_pairs_hook': OrderedDict}) + + +class OrderedDictSerializationTest(TestCase): + ordered_dict = OrderedDict([ + ('number', [1, 2, 3, 4]), + ('notes', True), + ]) + expected_key_order = ['number', 'notes'] + + def test_ordered_dict_differs_from_normal_dict(self): + self.assertEqual(list(self.ordered_dict.keys()), self.expected_key_order) + self.assertNotEqual(dict(self.ordered_dict).keys(), self.expected_key_order) + + def test_default_behaviour_loses_sort_order(self): + mod = JsonModel.objects.create(json=self.ordered_dict) + self.assertEqual(list(mod.json.keys()), self.expected_key_order) + mod_from_db = JsonModel.objects.get(id=mod.id) + + # mod_from_db lost ordering information during json.loads() + self.assertNotEqual(mod_from_db.json.keys(), self.expected_key_order) + + def test_load_kwargs_hook_does_not_lose_sort_order(self): + mod = OrderedJsonModel.objects.create(json=self.ordered_dict) + self.assertEqual(list(mod.json.keys()), self.expected_key_order) + mod_from_db = OrderedJsonModel.objects.get(id=mod.id) + self.assertEqual(list(mod_from_db.json.keys()), self.expected_key_order) From e4f92ee1854043a8fd5104a96d2646f1e155314f Mon Sep 17 00:00:00 2001 From: mdoglio Date: Mon, 14 Apr 2014 15:26:29 +0100 Subject: [PATCH 5/5] rename JobFilter to JobExclusion --- ...add_exclusionprofile__add_jobexclusion.py} | 28 +++++++++--------- treeherder/model/models.py | 29 ++++++++++++------- treeherder/webapp/api/refdata.py | 8 ++--- treeherder/webapp/api/serializers.py | 6 ++-- treeherder/webapp/api/urls.py | 2 +- 5 files changed, 40 insertions(+), 33 deletions(-) rename treeherder/model/migrations/{0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py => 0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobexclusion.py} (94%) diff --git a/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py b/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobexclusion.py similarity index 94% rename from treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py rename to treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobexclusion.py index 560af66b2..02c879a3b 100644 --- a/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobfilter.py +++ b/treeherder/model/migrations/0002_auto__add_userexclusionprofile__add_exclusionprofile__add_jobexclusion.py @@ -27,23 +27,23 @@ class Migration(SchemaMigration): )) db.send_create_signal(u'model', ['ExclusionProfile']) - # Adding M2M table for field filters on 'ExclusionProfile' - db.create_table(u'model_exclusionprofile_filters', ( + # Adding M2M table for field exclusions on 'ExclusionProfile' + db.create_table(u'model_exclusionprofile_exclusions', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), ('exclusionprofile', models.ForeignKey(orm[u'model.exclusionprofile'], null=False)), - ('jobfilter', models.ForeignKey(orm[u'model.jobfilter'], null=False)) + ('jobexclusion', models.ForeignKey(orm[u'model.jobexclusion'], null=False)) )) - db.create_unique(u'model_exclusionprofile_filters', ['exclusionprofile_id', 'jobfilter_id']) + db.create_unique(u'model_exclusionprofile_exclusions', ['exclusionprofile_id', 'jobexclusion_id']) - # Adding model 'JobFilter' - db.create_table(u'model_jobfilter', ( + # Adding model 'JobExclusion' + db.create_table(u'model_jobexclusion', ( (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), ('description', self.gf('django.db.models.fields.TextField')(blank=True)), ('info', self.gf('jsonfield.fields.JSONField')()), ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), )) - db.send_create_signal(u'model', ['JobFilter']) + db.send_create_signal(u'model', ['JobExclusion']) def backwards(self, orm): @@ -53,11 +53,11 @@ class Migration(SchemaMigration): # Deleting model 'ExclusionProfile' db.delete_table(u'model_exclusionprofile') - # Removing M2M table for field filters on 'ExclusionProfile' - db.delete_table('model_exclusionprofile_filters') + # Removing M2M table for field exclusion on 'ExclusionProfile' + db.delete_table('model_exclusionprofile_exclusions') - # Deleting model 'JobFilter' - db.delete_table(u'model_jobfilter') + # Deleting model 'JobExclusion' + db.delete_table(u'model_jobexclusion') models = { @@ -133,7 +133,7 @@ class Migration(SchemaMigration): u'model.exclusionprofile': { 'Meta': {'object_name': 'ExclusionProfile'}, 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'exclusion_profiles_authored'", 'to': u"orm['auth.User']"}), - 'filters': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'profiles'", 'symmetrical': 'False', 'to': u"orm['model.JobFilter']"}), + 'exclusions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'profiles'", 'symmetrical': 'False', 'to': u"orm['model.JobExclusion']"}), 'flat_exclusion': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), @@ -146,8 +146,8 @@ class Migration(SchemaMigration): 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50L'}) }, - u'model.jobfilter': { - 'Meta': {'object_name': 'JobFilter'}, + u'model.jobexclusion': { + 'Meta': {'object_name': 'JobExclusion'}, 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/treeherder/model/models.py b/treeherder/model/models.py index 3276b3614..9e3d50d67 100644 --- a/treeherder/model/models.py +++ b/treeherder/model/models.py @@ -534,7 +534,7 @@ class FailureClassification(models.Model): # exclusion profiles models -class JobFilter(models.Model): +class JobExclusion(models.Model): """ A filter represents a collection of properties that you want to filter jobs on. These properties along with their values @@ -545,40 +545,47 @@ class JobFilter(models.Model): info = JSONField() author = models.ForeignKey(User) + def save(self, *args, **kwargs): + super(JobExclusion, self).save(*args, **kwargs) + + # trigger the save method on all the profiles related to this exclusion + for profile in self.profiles.all(): + profile.save() + class ExclusionProfile(models.Model): """ - An exclusion profile represents a list of filters that can be associated with a user profile. + An exclusion profile represents a list of job exclusions that can be associated with a user profile. """ name = models.CharField(max_length=255, unique=True) is_default = models.BooleanField(default=False) - filters = models.ManyToManyField(JobFilter, related_name="profiles") + exclusions = models.ManyToManyField(JobExclusion, related_name="profiles") flat_exclusion = JSONField(blank=True, default={}) author = models.ForeignKey(User, related_name="exclusion_profiles_authored") def save(self, *args, **kwargs): super(ExclusionProfile, self).save(*args, **kwargs) - # prepare the nested defaultdict structure for the flat filters + # prepare the nested defaultdict structure for the flat exclusions # options should be stored in a set but sets are not serializable. # using a list instead job_types_constructor = lambda: defaultdict(list) platform_constructor = lambda: defaultdict(job_types_constructor) - flat_filters = defaultdict(platform_constructor) + flat_exclusions = defaultdict(platform_constructor) - for filter in self.filters.all().select_related("info"): - # create a set of combinations for each property in the filter - combo = tuple(itertools.product(filter.info['repos'], filter.info['platforms'], - filter.info['job_types'], filter.info['options'])) + for exclusion in self.exclusions.all().select_related("info"): + # create a set of combinations for each property in the exclusion + combo = tuple(itertools.product(exclusion.info['repos'], exclusion.info['platforms'], + exclusion.info['job_types'], exclusion.info['options'])) for repo, platform, job_type, option in combo: # strip the job type symbol appended in the ui job_type = job_type[:job_type.rfind(" (")] - options = flat_filters[repo][platform][job_type] + options = flat_exclusions[repo][platform][job_type] # using a list instead of a set and checking if the value already exists if not options in options: options.append(option) - self.flat_exclusion = flat_filters + self.flat_exclusion = flat_exclusions kwargs["force_insert"] = False kwargs["force_update"] = True super(ExclusionProfile, self).save(*args, **kwargs) diff --git a/treeherder/webapp/api/refdata.py b/treeherder/webapp/api/refdata.py index cb87cd4c6..7e5c370ff 100644 --- a/treeherder/webapp/api/refdata.py +++ b/treeherder/webapp/api/refdata.py @@ -129,11 +129,11 @@ class UserExclusionProfileViewSet(viewsets.ModelViewSet): serializer_class = th_serializers.UserExclusionProfileSerializer -class JobFilterViewSet(viewsets.ModelViewSet): - model = models.JobFilter +class JobExclusionViewSet(viewsets.ModelViewSet): + model = models.JobExclusion authentication_classes = (SessionAuthentication,) permission_classes = (IsStaffOrReadOnly,) - serializer_class = th_serializers.JobFilterSerializer + serializer_class = th_serializers.JobExclusionSerializer def create(self, request, *args, **kwargs): """ @@ -142,7 +142,7 @@ class JobFilterViewSet(viewsets.ModelViewSet): """ if "author" not in request.DATA: request.DATA["author"] = request.user.id - return super(JobFilterViewSet, self).create(request, *args, **kwargs) + return super(JobExclusionViewSet, self).create(request, *args, **kwargs) class ExclusionProfileViewSet(viewsets.ModelViewSet): diff --git a/treeherder/webapp/api/serializers.py b/treeherder/webapp/api/serializers.py index df92f020d..fe9e7fa85 100644 --- a/treeherder/webapp/api/serializers.py +++ b/treeherder/webapp/api/serializers.py @@ -20,17 +20,17 @@ class UserSerializer(serializers.ModelSerializer): fields = ["username", "is_superuser", "is_staff", "email", "exclusion_profiles"] -class JobFilterSerializer(serializers.ModelSerializer): +class JobExclusionSerializer(serializers.ModelSerializer): info = serializers.WritableField() class Meta: - model = models.JobFilter + model = models.JobExclusion class ExclusionProfileSerializer(serializers.ModelSerializer): flat_exclusion = serializers.WritableField(required=False) - filters = serializers.PrimaryKeyRelatedField(many=True) + exclusions = serializers.PrimaryKeyRelatedField(many=True) class Meta: model = models.ExclusionProfile diff --git a/treeherder/webapp/api/urls.py b/treeherder/webapp/api/urls.py index 2b518e6a8..57d8aa0ff 100644 --- a/treeherder/webapp/api/urls.py +++ b/treeherder/webapp/api/urls.py @@ -66,7 +66,7 @@ default_router.register(r'bugscache', refdata.BugscacheViewSet) default_router.register(r'failureclassification', refdata.FailureClassificationViewSet) default_router.register(r'user', refdata.UserViewSet) default_router.register(r'exclusion-profile', refdata.ExclusionProfileViewSet) -default_router.register(r'job-filter', refdata.JobFilterViewSet) +default_router.register(r'job-exclusion', refdata.JobExclusionViewSet) urlpatterns = patterns(