=== modified file 'dashboard_app/admin.py'
--- dashboard_app/admin.py	2013-01-11 16:25:54 +0000
+++ dashboard_app/admin.py	2013-04-01 08:50:05 +0000
@@ -47,6 +47,7 @@
     TestRunFilter,
     TestRunFilterAttribute,
     TestRunFilterSubscription,
+    TestDefinition,
 )
 
 
@@ -203,6 +204,9 @@
     save_as = True
 
 
+class TestDefinitionAdmin(admin.ModelAdmin):
+    list_display = ('name', 'version')
+
 admin.site.register(Attachment)
 admin.site.register(Bundle, BundleAdmin)
 admin.site.register(BundleDeserializationError, BundleDeserializationErrorAdmin)
@@ -221,3 +225,4 @@
 admin.site.register(TestRunFilter, TestRunFilterAdmin)
 admin.site.register(TestRunFilterSubscription)
 admin.site.register(Tag)
+admin.site.register(TestDefinition, TestDefinitionAdmin)

=== modified file 'dashboard_app/extension.py'
--- dashboard_app/extension.py	2013-01-31 02:52:51 +0000
+++ dashboard_app/extension.py	2013-03-25 19:39:46 +0000
@@ -49,6 +49,7 @@
             subm.append(Menu("Data Views", reverse("dashboard_app.views.data_view_list")))
         if not settings.DATAREPORTS_HIDE:
             subm.append(Menu("Reports", reverse("dashboard_app.views.report_list")))
+        subm.append(Menu("Test Definitions", reverse("dashboard_app.views.test_definition")))
 
         return menu
 

=== modified file 'dashboard_app/helpers.py'
--- dashboard_app/helpers.py	2012-11-16 01:01:22 +0000
+++ dashboard_app/helpers.py	2013-04-01 11:04:15 +0000
@@ -8,6 +8,7 @@
 import time
 
 from django.core.files.base import ContentFile
+from django.core.exceptions import ObjectDoesNotExist
 from django.db import connection, transaction, IntegrityError
 from linaro_dashboard_bundle.errors import DocumentFormatError
 from linaro_dashboard_bundle.evolution import DocumentEvolution
@@ -742,6 +743,50 @@
                 self._import_test_result_attachments(c_test_result, s_test_result)
 
 
+class BundleFormatImporter_1_6(BundleFormatImporter_1_5):
+    """
+    IFormatImporter subclass capable of loading "Dashboard Bundle Format 1.6"
+    """
+
+    def _import_testdef(self, c_test_id, c_testdef_metadata):
+        """
+        Import dashboard_app.models.TestDefinition into the database
+        based on a client-side description of a TestRun metadata.
+        """
+        from dashboard_app.models import TestDefinition
+
+        testdef_meta = {
+            'name': c_test_id,
+            'version': c_testdef_metadata.get("version"),
+            'description': c_testdef_metadata.get("description"),
+            'format': c_testdef_metadata.get("format"),
+            'location': c_testdef_metadata.get("location"),
+            'url': c_testdef_metadata.get("url"),
+            'environment': c_testdef_metadata.get("environment"),
+            'target_os': c_testdef_metadata.get("os"),
+            'target_dev_types': c_testdef_metadata.get("devices"),
+            }
+
+        try:
+            s_testdef = TestDefinition.objects.get(name=c_test_id)
+            # Do not try to update name since it is unique, hence
+            # pop it from the dictionary.
+            testdef_meta.pop('name', None)
+            TestDefinition.objects.filter(name=c_test_id).update(
+                **testdef_meta)
+        except ObjectDoesNotExist:
+            s_testdef = TestDefinition.objects.create(**testdef_meta)
+            s_testdef.save()
+
+    def _import_test_results(self, c_test_run, s_test_run):
+        from dashboard_app.models import TestResult
+        super(BundleFormatImporter_1_6, self)._import_test_results(c_test_run,
+                                                                   s_test_run)
+        if c_test_run.get("testdef_metadata"):
+            self._import_testdef(c_test_run["test_id"],
+                                 c_test_run["testdef_metadata"])
+
+
 class BundleDeserializer(object):
     """
     Helper class for de-serializing JSON bundle content into database models
@@ -755,6 +800,7 @@
         "Dashboard Bundle Format 1.3": BundleFormatImporter_1_3,
         "Dashboard Bundle Format 1.4": BundleFormatImporter_1_4,
         "Dashboard Bundle Format 1.5": BundleFormatImporter_1_5,
+        "Dashboard Bundle Format 1.6": BundleFormatImporter_1_6,
     }
 
     def deserialize(self, s_bundle, prefer_evolution):

=== added file 'dashboard_app/migrations/0029_auto__add_testdefinition.py'
--- dashboard_app/migrations/0029_auto__add_testdefinition.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/migrations/0029_auto__add_testdefinition.py	2013-04-01 10:16:02 +0000
@@ -0,0 +1,281 @@
+# -*- 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 'TestDefinition'
+        db.create_table('dashboard_app_testdefinition', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=512)),
+            ('version', self.gf('django.db.models.fields.CharField')(max_length=256)),
+            ('description', self.gf('django.db.models.fields.TextField')()),
+            ('format', self.gf('django.db.models.fields.CharField')(max_length=128)),
+            ('location', self.gf('django.db.models.fields.CharField')(default='LOCAL', max_length=64)),
+            ('url', self.gf('django.db.models.fields.CharField')(max_length=1024)),
+            ('environment', self.gf('django.db.models.fields.CharField')(max_length=256)),
+            ('target_os', self.gf('django.db.models.fields.CharField')(max_length=512)),
+            ('target_dev_types', self.gf('django.db.models.fields.CharField')(max_length=512)),
+            ('content', self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True, blank=True)),
+            ('mime_type', self.gf('django.db.models.fields.CharField')(default='text/plain', max_length=64)),
+        ))
+        db.send_create_signal('dashboard_app', ['TestDefinition'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'TestDefinition'
+        db.delete_table('dashboard_app_testdefinition')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'dashboard_app.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'public_url': ('django.db.models.fields.URLField', [], {'max_length': '512', 'blank': 'True'})
+        },
+        'dashboard_app.bundle': {
+            'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
+            '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
+            '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
+        },
+        'dashboard_app.bundledeserializationerror': {
+            'Meta': {'object_name': 'BundleDeserializationError'},
+            'bundle': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'deserialization_error'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.Bundle']"}),
+            'error_message': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'traceback': ('django.db.models.fields.TextField', [], {'max_length': '32768'})
+        },
+        'dashboard_app.bundlestream': {
+            'Meta': {'object_name': 'BundleStream'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'dashboard_app.hardwaredevice': {
+            'Meta': {'object_name': 'HardwareDevice'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'device_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'dashboard_app.image': {
+            'Meta': {'object_name': 'Image'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.imageset': {
+            'Meta': {'object_name': 'ImageSet'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'images': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.Image']", 'symmetrical': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        'dashboard_app.launchpadbug': {
+            'Meta': {'object_name': 'LaunchpadBug'},
+            'bug_id': ('django.db.models.fields.PositiveIntegerField', [], {'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'test_runs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'launchpad_bugs'", 'symmetrical': 'False', 'to': "orm['dashboard_app.TestRun']"})
+        },
+        'dashboard_app.namedattribute': {
+            'Meta': {'unique_together': "(('object_id', 'name'),)", 'object_name': 'NamedAttribute'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'value': ('django.db.models.fields.TextField', [], {})
+        },
+        'dashboard_app.pmqabundlestream': {
+            'Meta': {'object_name': 'PMQABundleStream'},
+            'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.BundleStream']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        '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': '1024', 'blank': 'True'}),
+            'test_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
+        },
+        '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.testdefinition': {
+            'Meta': {'object_name': 'TestDefinition'},
+            'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'environment': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'format': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'location': ('django.db.models.fields.CharField', [], {'default': "'LOCAL'", 'max_length': '64'}),
+            'mime_type': ('django.db.models.fields.CharField', [], {'default': "'text/plain'", 'max_length': '64'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+            'target_dev_types': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'target_os': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        },
+        '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'}),
+            'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'packages': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwarePackage']"}),
+            'sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwareSource']"}),
+            'sw_image_desc': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.Tag']"}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Test']"}),
+            'time_check_performed': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'dashboard_app.testrundenormalization': {
+            'Meta': {'object_name': 'TestRunDenormalization'},
+            'count_fail': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'count_pass': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'count_skip': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'count_unknown': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test_run': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'denormalization'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.TestRun']"})
+        },
+        'dashboard_app.testrunfilter': {
+            'Meta': {'unique_together': "(('owner', 'name'),)", 'object_name': 'TestRunFilter'},
+            'build_number_attribute': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'bundle_streams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.BundleStream']", 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'max_length': '1024'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"})
+        },
+        'dashboard_app.testrunfilterattribute': {
+            'Meta': {'object_name': 'TestRunFilterAttribute'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
+        },
+        'dashboard_app.testrunfiltersubscription': {
+            'Meta': {'unique_together': "(('user', 'filter'),)", 'object_name': 'TestRunFilterSubscription'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'dashboard_app.testrunfiltertest': {
+            'Meta': {'object_name': 'TestRunFilterTest'},
+            'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tests'", 'to': "orm['dashboard_app.TestRunFilter']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.Test']"})
+        },
+        'dashboard_app.testrunfiltertestcase': {
+            'Meta': {'object_name': 'TestRunFilterTestCase'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cases'", 'to': "orm['dashboard_app.TestRunFilterTest']"}),
+            'test_case': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.TestCase']"})
+        }
+    }
+
+    complete_apps = ['dashboard_app']
\ No newline at end of file

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2013-02-07 22:30:46 +0000
+++ dashboard_app/models.py	2013-04-01 08:50:05 +0000
@@ -701,6 +701,82 @@
         return self.test_results.filter(result=TestResult.RESULT_FAIL).count()
 
 
+class TestDefinition(models.Model):
+    """
+    Model for representing test definitions.
+
+    Test Definition are in YAML format.
+    """
+    LOCATION_CHOICES = (
+        ('LOCAL', 'Local'),
+        ('URL', 'URL'),
+        ('GIT', 'GIT Repo'),
+        ('BZR', 'BZR Repo'),
+        )
+
+    name = models.CharField(
+        max_length = 512,
+        verbose_name = _("Name"),
+        unique = True,
+        help_text = _help_max_length(512))
+
+    version = models.CharField(
+        max_length=256,
+        verbose_name = _("Version"),
+        help_text = _help_max_length(256))
+
+    description = models.TextField(
+        verbose_name = _("Description"))
+
+    format = models.CharField(
+        max_length = 128,
+        verbose_name = _("Format"),
+        help_text = _help_max_length(128))
+
+    location = models.CharField(
+        max_length = 64,
+        verbose_name = _("Location"),
+        choices = LOCATION_CHOICES,
+        default = 'LOCAL')
+
+    url = models.CharField(
+        verbose_name = _(u"URL"),
+        max_length = 1024,
+        blank = False,
+        help_text = _help_max_length(1024))
+
+    environment = models.CharField(
+        max_length = 256,
+        verbose_name = _("Environment"),
+        help_text = _help_max_length(256))
+
+    target_os = models.CharField(
+        max_length = 512,
+        verbose_name = _("Operating Systems"),
+        help_text = _help_max_length(512))
+
+    target_dev_types = models.CharField(
+        max_length = 512,
+        verbose_name = _("Device types"),
+        help_text = _help_max_length(512))
+
+    content = models.FileField(
+        verbose_name = _(u"Upload Test Definition"),
+        help_text = _(u"Test definition file"),
+        upload_to = 'testdef',
+        blank = True,
+        null = True)
+
+    mime_type = models.CharField(
+        verbose_name = _(u"MIME type"),
+        default = 'text/plain',
+        max_length = 64,
+        help_text = _help_max_length(64))
+
+    def __unicode__(self):
+        return self.name
+
+
 class SoftwareSource(models.Model):
     """
     Model for representing source reference of a particular project

=== added file 'dashboard_app/templates/dashboard_app/add_test_definition.html'
--- dashboard_app/templates/dashboard_app/add_test_definition.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/add_test_definition.html	2013-03-25 21:49:16 +0000
@@ -0,0 +1,25 @@
+{% extends "dashboard_app/_content_with_sidebar.html" %}
+{% load i18n %}
+{% load stylize %}
+
+{% block extrahead %}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava-server/css/demo_table_jui.css"/>
+{% endblock %}
+
+{% block sidebar %}
+<h3>Actions</h3>
+<ul>
+  <li><a href="{% url dashboard_app.views.test_definition %}">
+      List test definitions</a></li>
+  <li><a href="{% url dashboard_app.views.add_test_definition %}">
+      Add test definition</a></li>
+</ul>
+{% endblock %}
+
+{% block content %}
+<form method="post" action="">
+  {% csrf_token %}
+  <table>{{ form.as_table }}</table>
+  <input type="submit" value="Save"/>
+</form>
+{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/test_definition.html'
--- dashboard_app/templates/dashboard_app/test_definition.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/test_definition.html	2013-03-25 21:39:58 +0000
@@ -0,0 +1,23 @@
+{% extends "dashboard_app/_content_with_sidebar.html" %}
+{% load i18n %}
+{% load stylize %}
+{% load django_tables2 %}
+
+{% block extrahead %}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}lava-server/css/demo_table_jui.css"/>
+<script type="text/javascript" src="{{ STATIC_URL }}lava-server/js/jquery.dataTables.min.js"></script> 
+{% endblock %}
+
+{% block sidebar %}
+<h3>Actions</h3>
+<ul>
+  <li><a href="{% url dashboard_app.views.test_definition %}">
+      List test definitions</a></li>
+  <li><a href="{% url dashboard_app.views.add_test_definition %}">
+      Add test definition</a></li>
+</ul>
+{% endblock %}
+
+{% block content %}
+    {% render_table testdefinition_table %}
+{% endblock %}

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2013-01-11 17:07:32 +0000
+++ dashboard_app/urls.py	2013-01-15 14:09:48 +0000
@@ -70,4 +70,7 @@
     url(r'^image-reports/(?P<name>[A-Za-z0-9_-]+)$', 'images.image_report_detail'),
     url(r'^api/link-bug-to-testrun', 'images.link_bug_to_testrun'),
     url(r'^api/unlink-bug-and-testrun', 'images.unlink_bug_and_testrun'),
+    url(r'^test-definition/add_test_definition', 'add_test_definition'),
+    url(r'^test-definition/$', 'test_definition'),
+    url(r'^testdefinition_table_json$', 'testdefinition_table_json'),
 )

=== modified file 'dashboard_app/views/__init__.py'
--- dashboard_app/views/__init__.py	2013-01-11 16:25:54 +0000
+++ dashboard_app/views/__init__.py	2013-04-01 08:50:05 +0000
@@ -40,6 +40,8 @@
 from django.template import RequestContext, loader
 from django.utils.safestring import mark_safe
 from django.views.generic.list_detail import object_list, object_detail
+from django.forms import ModelForm
+from django import forms
 
 from django_tables2 import Attrs, Column, TemplateColumn
 
@@ -60,6 +62,7 @@
     Test,
     TestResult,
     TestRun,
+    TestDefinition,
 )
 
 
@@ -673,3 +676,51 @@
         request.user,
         content_sha1=content_sha1)
     return redirect_to(request, bundle, trailing)
+
+
+class TestDefinitionTable(DataTablesTable):
+    name = Column()
+    version = Column()
+    location = Column()
+    description = Column()
+    def get_queryset(self):
+        return TestDefinition.objects.all()
+
+
+def testdefinition_table_json(request):
+    return TestDefinitionTable.json(request)
+
+
+@BreadCrumb("Test Definitions", parent=index)
+def test_definition(request):
+    return render_to_response(
+        "dashboard_app/test_definition.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(test_definition),
+            "testdefinition_table": TestDefinitionTable(
+                'testdeflist',
+                reverse(testdefinition_table_json))
+        }, RequestContext(request))
+
+
+class AddTestDefForm(ModelForm):
+    class Meta:
+        model = TestDefinition
+        fields = ('name', 'version', 'description', 'format', 'location',
+                  'url', 'environment', 'target_os', 'target_dev_types',
+                  'content', 'mime_type')
+
+@BreadCrumb("Add Test Definition", parent=index)
+def add_test_definition(request):
+    if request.method == 'POST':
+        form = AddTestDefForm(request.POST)
+        if form.is_valid():
+            form.save()
+            return HttpResponseRedirect('/dashboard/test-definition/')
+    else:
+        form = AddTestDefForm()
+    return render_to_response(
+        "dashboard_app/add_test_definition.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                add_test_definition),
+            "form": form,
+            }, RequestContext(request))

=== modified file 'dashboard_app/xmlrpc.py'
--- dashboard_app/xmlrpc.py	2013-01-15 00:30:25 +0000
+++ dashboard_app/xmlrpc.py	2013-04-04 03:33:50 +0000
@@ -43,6 +43,7 @@
     DataView,
     Test,
     TestRunFilter,
+    TestDefinition,
 )
 
 
@@ -882,6 +883,51 @@
         matches = matches[:100]
         return [match.serializable() for match in matches]
 
+    @xml_rpc_signature('str')
+    def get_test_definitions(self, os=None, device=None, environment=None):
+        """
+        Name
+        ----
+        `get_test_definitions` ([`os`[, `device`[, `environment`]]])
+
+        Description
+        -----------
+        Get the name and url of all the test definitions.
+
+        Arguments
+        ---------
+        `os`: string
+            The type of operating system the retrieved test definitions should
+            apply to.
+
+        `device`: string
+            The type of device the retrieved test definitions should apply to.
+
+        `environment`: string
+            The type of test environment the retrieved test definitions should
+            apply to.
+
+        Return value
+        ------------
+        This function returns an XML-RPC structure of test definition name and
+        URL where the test definition exists.
+        """
+        testdefs = {}
+        tds = TestDefinition.objects.all()
+
+        if os:
+            tds = tds.filter(target_os__contains=os)
+
+        if device:
+            tds = tds.filter(target_dev_types__contains=device)
+
+        if environment:
+            tds = tds.filter(environment__contains=environment)
+
+        for testdef in tds:
+                testdefs[testdef.name] = testdef.url
+        return testdefs
+
 # Mapper used by the legacy URL
 legacy_mapper = Mapper()
 legacy_mapper.register_introspection_methods()

