You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2018/01/09 14:49:36 UTC

[airavata-django-portal] 02/02: AIRAVATA-2598 Implement saving experiment

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 08ef4ae050a59ac8137c25f88ea3ad2d778dbf50
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Tue Jan 9 09:49:26 2018 -0500

    AIRAVATA-2598 Implement saving experiment
    
    thrift_utils updated to support
    * required fields
    * read_only fields
    * nested structs and lists of structs
---
 django_airavata/apps/api/serializers.py            | 32 ++++++-------
 .../api/static/django_airavata_api/js/index.js     |  2 +
 .../models/ComputationalResourceSchedulingModel.js | 30 ++++++++++--
 .../django_airavata_api/js/models/Experiment.js    | 37 ++++++++++-----
 .../js/models/InputDataObjectType.js               | 20 +++++++-
 .../django_airavata_api/js/models/ProcessModel.js  | 13 ++++++
 .../js/models/UserConfigurationData.js             | 54 ++++++++++++++++++----
 .../js/services/ExperimentService.js               | 30 ++++++++++++
 django_airavata/apps/api/thrift_utils.py           | 51 ++++++++++++++++----
 django_airavata/apps/api/urls.py                   |  1 +
 django_airavata/apps/api/views.py                  | 32 +++++++++----
 .../js/components/experiment/ExperimentEditor.vue  | 27 ++++++++---
 .../js/containers/CreateExperimentContainer.vue    |  1 +
 13 files changed, 261 insertions(+), 69 deletions(-)

diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 28b0124..511d080 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -103,23 +103,6 @@ class ProjectSerializer(serializers.Serializer):
         return instance
 
 
-class ExperimentSerializer(serializers.Serializer):
-
-    experimentId = serializers.CharField(read_only=True)
-    projectId = serializers.CharField(required=True)
-    project = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:project-detail', lookup_field='projectId', lookup_url_kwarg='project_id')
-    gatewayId = GatewayIdDefaultField()
-    experimentType = serializers.CharField(required=True)
-    userName = GatewayUsernameDefaultField()
-    experimentName = serializers.CharField(required=True)
-
-    def create(self, validated_data):
-        return ExperimentModel(**validated_data)
-
-    def update(self, instance, validated_data):
-        raise Exception("Not implemented")
-
-
 class ApplicationModuleSerializer(serializers.Serializer):
     url = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:application-detail', lookup_field='appModuleId', lookup_url_kwarg='app_module_id')
     appModuleId = serializers.CharField(required=True)
@@ -243,5 +226,20 @@ class ComputeResourceDescriptionSerializer(CustomSerializer):
     resourceDescription=serializers.CharField()
     enabled=serializers.BooleanField()
 
+
 class BatchQueueSerializer(thrift_utils.create_serializer_class(BatchQueue)):
     pass
+
+
+class ExperimentSerializer(
+        thrift_utils.create_serializer_class(ExperimentModel)):
+
+    class Meta:
+        required = ('projectId', 'experimentType', 'experimentName')
+        read_only = ('experimentId',)
+
+    url = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:experiment-detail', lookup_field='experimentId', lookup_url_kwarg='experiment_id')
+    project = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:project-detail', lookup_field='projectId', lookup_url_kwarg='project_id')
+    userName = GatewayUsernameDefaultField()
+    gatewayId = GatewayIdDefaultField()
+    creationTime = UTCPosixTimestampDateTimeField(allow_null=True)
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js
index fc3b6bc..8860efb 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/index.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js
@@ -9,6 +9,7 @@ import Project from './models/Project'
 import ApplicationDeploymentService from './services/ApplicationDeploymentService'
 import ApplicationInterfaceService from './services/ApplicationInterfaceService'
 import ApplicationModuleService from './services/ApplicationModuleService'
+import ExperimentService from './services/ExperimentService'
 import ProjectService from './services/ProjectService'
 
 import FetchUtils from './utils/FetchUtils'
@@ -27,6 +28,7 @@ exports.services = {
     ApplicationDeploymentService,
     ApplicationInterfaceService,
     ApplicationModuleService,
+    ExperimentService,
     ProjectService,
 }
 
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputationalResourceSchedulingModel.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputationalResourceSchedulingModel.js
index f9cc61f..03c4782 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputationalResourceSchedulingModel.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputationalResourceSchedulingModel.js
@@ -8,11 +8,31 @@ const FIELDS = [
     'queueName',
     'wallTimeLimit',
     'totalPhysicalMemory',
-    'chessisNumber',
-    'staticWorkingDir',
-    'overrideLoginUserName',
-    'overrideScratchLocation',
-    'overrideAllocationProjectNumber',
+    {
+        name: 'chessisNumber',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'staticWorkingDir',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'overrideLoginUserName',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'overrideScratchLocation',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'overrideAllocationProjectNumber',
+        type: 'string',
+        default: '',
+    },
 ];
 
 export default class ComputationalResourceSchedulingModel extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
index 53aa4d2..89404c8 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
@@ -1,24 +1,38 @@
+
 import BaseModel from './BaseModel';
-import UserConfigurationData from './UserConfigurationData'
+import ErrorModel from './ErrorModel'
+import ExperimentStatus from './ExperimentStatus'
 import InputDataObjectType from './InputDataObjectType'
 import OutputDataObjectType from './OutputDataObjectType'
-import ExperimentStatus from './ExperimentStatus'
-import ErrorModel from './ErrorModel'
+import ProcessModel from './ProcessModel'
+import UserConfigurationData from './UserConfigurationData'
 
 const FIELDS = [
     'experimentId',
     'projectId',
     'gatewayId',
-    'experimentType',
+    {
+        name: 'experimentType',
+        type: 'number',
+        default: 0,
+    },
     'userName',
     'experimentName',
     {
         name: 'creationTime',
         type: 'date'
     },
-    'description',
+    {
+        name: 'description',
+        type: 'string',
+        default: '',
+    },
     'executionId',
-    'enableEmailNotification',
+    {
+        name: 'enableEmailNotification',
+        type: 'boolean',
+        default: false,
+    },
     {
         name: 'emailAddresses',
         type: 'string',
@@ -49,12 +63,11 @@ const FIELDS = [
         type: ErrorModel,
         list: true,
     },
-    // TODO: map the ProcessModel
-    // {
-    //     name: 'processes',
-    //     type: ProcessModel,
-    //     list: true,
-    // },
+    {
+        name: 'processes',
+        type: ProcessModel,
+        list: true,
+    },
 ];
 
 export default class Experiment extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js b/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
index 601fdd0..cf3e349 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
@@ -8,17 +8,33 @@ const FIELDS = [
     'applicationArgument',
     'standardInput',
     'userFriendlyDescription',
-    'metaData',
+    {
+        name: 'metaData',
+        type: 'string',
+        default: '',
+    },
     'inputOrder',
     'isRequired',
     'requiredToAddedToCommandLine',
     'dataStaged',
-    'storageResourceId',
+    {
+        name: 'storageResourceId',
+        type: 'string',
+        default: '',
+    },
     'isReadOnly',
 ];
 
 export default class InputDataObjectType extends BaseModel {
     constructor(data = {}) {
         super(FIELDS, data);
+        // TODO: move into BaseModel
+        // Convert null strings into empty strings
+        if ('metaData' in this && this.metaData === null) {
+            this.metaData = '';
+        }
+        if ('storageResourceId' in this && this.storageResourceId === null) {
+            this.storageResourceId = '';
+        }
     }
 }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js
new file mode 100644
index 0000000..ee37636
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js
@@ -0,0 +1,13 @@
+import BaseModel from './BaseModel';
+
+const FIELDS = [
+    'processId',
+    'experimentId',
+    // TODO: finish mapping fields
+];
+
+export default class ProcessModel extends BaseModel {
+    constructor(data = {}) {
+        super(FIELDS, data);
+    }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserConfigurationData.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserConfigurationData.js
index e1a5a47..f70ffdf 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserConfigurationData.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserConfigurationData.js
@@ -2,20 +2,56 @@ import BaseModel from './BaseModel';
 import ComputationalResourceSchedulingModel from './ComputationalResourceSchedulingModel'
 
 const FIELDS = [
-    'airavataAutoSchedule',
-    'overrideManualScheduledParams',
-    'shareExperimentPublicly',
+    {
+        name: 'airavataAutoSchedule',
+        type: 'boolean',
+        default: false,
+    },
+    {
+        name: 'overrideManualScheduledParams',
+        type: 'boolean',
+        default: false,
+    },
+    {
+        name: 'shareExperimentPublicly',
+        type: 'boolean',
+        default: false,
+    },
     {
         name: 'computationalResourceScheduling',
         type: ComputationalResourceSchedulingModel,
         default: BaseModel.defaultNewInstance(ComputationalResourceSchedulingModel),
     },
-    'throttleResources',
-    'userDN',
-    'generateCert',
-    'storageId',
-    'experimentDataDir',
-    'useUserCRPref',
+    {
+        name: 'throttleResources',
+        type: 'boolean',
+        default: false,
+    },
+    {
+        name: 'userDN',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'generateCert',
+        type: 'boolean',
+        default: false,
+    },
+    {
+        name: 'storageId',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'experimentDataDir',
+        type: 'string',
+        default: '',
+    },
+    {
+        name: 'useUserCRPref',
+        type: 'boolean',
+        default: false,
+    }
 ];
 
 export default class UserConfigurationData extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/services/ExperimentService.js b/django_airavata/apps/api/static/django_airavata_api/js/services/ExperimentService.js
new file mode 100644
index 0000000..b840b86
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/services/ExperimentService.js
@@ -0,0 +1,30 @@
+
+import Experiment from '../models/Experiment'
+import FetchUtils from '../utils/FetchUtils'
+
+class ExperimentService {
+    list(data = null) {
+        if (data) {
+            return Promise.resolve(data.map(result => new Experiment(result)));
+        } else {
+            return FetchUtils.get('/api/experiments/')
+                .then(results => results.map(result => new Experiment(result)));
+        }
+    }
+
+    create(experiment) {
+        return FetchUtils.post('/api/experiments/', JSON.stringify(experiment))
+            .then(result => new Experiment(result));
+    }
+
+    update() {
+        // TODO
+    }
+
+    get() {
+        // TODO
+    }
+}
+
+// Export as a singleton
+export default new ExperimentService();
\ No newline at end of file
diff --git a/django_airavata/apps/api/thrift_utils.py b/django_airavata/apps/api/thrift_utils.py
index 5bccf0e..476cfbf 100644
--- a/django_airavata/apps/api/thrift_utils.py
+++ b/django_airavata/apps/api/thrift_utils.py
@@ -33,11 +33,17 @@ def create_serializer_class(thrift_data_type):
     class CustomSerializerMeta(SerializerMetaclass):
 
         def __new__(cls, name, bases, attrs):
+            meta = attrs.get('Meta', None)
             thrift_spec = thrift_data_type.thrift_spec
             for field in thrift_spec:
                 # Don't replace existing attrs to allow subclasses to override
                 if field and field[2] not in attrs:
-                    field_serializer = process_field(field)
+                    required = field[2] in meta.required if meta else False
+                    read_only = field[2] in meta.read_only if meta else False
+                    allow_null = not required
+                    field_serializer = process_field(
+                        field, required=required, read_only=read_only,
+                        allow_null=allow_null)
                     attrs[field[2]] = field_serializer
             return super().__new__(cls, name, bases, attrs)
 
@@ -47,16 +53,22 @@ def create_serializer_class(thrift_data_type):
         Custom Serializer which handle the list fields which holds custom class objects
         """
 
-        def process_list_fields(self, validated_data):
+        def process_nested_fields(self, validated_data):
             fields = self.fields
             params = copy.deepcopy(validated_data)
             for field_name, serializer in fields.items():
                 if isinstance(serializer, ListField):
-                    params[field_name] = serializer.to_representation(params[field_name])
+                    if (params[field_name] is not None or not serializer.allow_null):
+                        if isinstance(serializer.child, Serializer):
+                            params[field_name] = [serializer.child.create(item) for item in params[field_name]]
+                        else:
+                            params[field_name] = serializer.to_representation(params[field_name])
+                elif isinstance(serializer, Serializer):
+                    params[field_name] = serializer.create(params[field_name])
             return params
 
         def create(self, validated_data):
-            params = self.process_list_fields(validated_data)
+            params = self.process_nested_fields(validated_data)
             return thrift_data_type(**params)
 
         def update(self, instance, validated_data):
@@ -65,22 +77,40 @@ def create_serializer_class(thrift_data_type):
     return CustomSerializer
 
 
-def process_field(field):
+def process_field(field, required=False, read_only=False, allow_null=False):
     """
     Used to process a thrift data type field
     :param field:
+    :param required:
+    :param read_only:
+    :param allow_null:
     :return:
     """
     if field[1] in mapping:
-        # handling scenarios when the thrift field type is present in the mapping
-        return mapping[field[1]](required=False)
+        # handling scenarios when the thrift field type is present in the
+        # mapping
+        field_class = mapping[field[1]]
+        kwargs = dict(required=required, read_only=read_only)
+        # allow_null isn't allowed for BooleanField and we'll use allow_blank
+        # for CharField
+        if field_class not in (BooleanField, CharField):
+            kwargs['allow_null'] = allow_null
+        if field_class == CharField:
+            kwargs['allow_blank'] = allow_null
+        return mapping[field[1]](**kwargs)
     elif field[1] == TType.LIST:
         # handling scenario when the thrift field type is list
         list_field_serializer = process_list_field(field)
-        return ListField(child=list_field_serializer, required=False)
+        return ListField(child=list_field_serializer,
+                         required=required,
+                         read_only=read_only,
+                         allow_null=allow_null)
     elif field[1] == TType.STRUCT:
         # handling scenario when the thrift field type is struct
-        return create_serializer(field[3][0])
+        return create_serializer(field[3][0],
+                                 required=required,
+                                 read_only=read_only,
+                                 allow_null=allow_null)
 
 
 def process_list_field(field):
@@ -91,7 +121,8 @@ def process_list_field(field):
     """
     list_details = field[3]
     if list_details[0] in mapping:
-        # handling scenario when the data type hold by the list is in the mapping
+        # handling scenario when the data type hold by the list is in the
+        # mapping
         return mapping[list_details[0]]()
     elif list_details[0] == TType.STRUCT:
         # handling scenario when the data type hold by the list is a struct
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index f9c6e4d..a9c059a 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -10,6 +10,7 @@ logger = logging.getLogger(__name__)
 
 router = routers.DefaultRouter()
 router.register(r'projects', views.ProjectViewSet, base_name='project')
+router.register(r'experiments', views.ExperimentViewSet, base_name='experiment')
 router.register(r'new/application/module', views.RegisterApplicationModule, base_name='register_app_module')
 router.register(r'application-interfaces', views.ApplicationInterfaceViewSet, base_name='application-interface')
 router.register(r'applications', views.ApplicationModuleViewSet, base_name='application')
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 9751a1b..dcada13 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -225,15 +225,31 @@ class ProjectViewSet(APIBackedViewSet):
         serializer = serializers.ExperimentSerializer(experiments, many=True, context={'request': request})
         return Response(serializer.data)
 
-# TODO: convert to ViewSet
-class ExperimentList(APIView):
-    def get(self, request, format=None):
-        gateway_id = settings.GATEWAY_ID
-        username = request.user.username
 
-        experiments = request.airavata_client.getUserExperiments(request.authz_token, gateway_id, username, -1, 0)
-        serializer = serializers.ExperimentSerializer(experiments, many=True, context={'request': request})
-        return Response(serializer.data)
+class ExperimentViewSet(APIBackedViewSet):
+
+    serializer_class = serializers.ExperimentSerializer
+    lookup_field = 'experiment_id'
+
+    def get_list(self):
+        return self.request.airavata_client.getUserExperiments(self.authz_token, self.gateway_id, self.username, 1, 0)
+
+    def get_instance(self, lookup_value):
+        return self.request.airavata_client.getExperiment(self.authz_token, lookup_value)
+
+    def perform_create(self, serializer):
+        experiment = serializer.save()
+        experiment_id = self.request.airavata_client.createExperiment(self.authz_token, self.gateway_id, experiment)
+        experiment.experimentId = experiment_id
+
+    def perform_update(self, serializer):
+        experiment = serializer.save()
+        self.request.airavata_client.updateExperiment(self.authz_token, experiment.experimentId, experiment)
+
+    @detail_route(methods=['post'])
+    def launch(self, request, experiment_id=None):
+        request.airavata_client.launchExperiment(request.authz_token, experiment_id, self.gateway_id)
+        return Response({'success': True})
 
 
 class ApplicationModuleViewSet(APIBackedViewSet):
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
index 5e18b38..8ec6543 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
@@ -87,7 +87,7 @@ export default {
     // TODO: clone experiment instead of editing it directly
     props: {
         experiment: {
-            type: models.ExperimentModel,
+            type: models.Experiment,
             required: true
         },
         appModule: {
@@ -125,9 +125,21 @@ export default {
         saveExperiment: function() {
             console.log(JSON.stringify(this.localExperiment));
             // TODO: validate experiment
-            // TODO: save experiment
-            // TODO: set the experiment ID on the new experiment
-            // TODO: dispatch save event with updated experiment
+            // save experiment
+            if (this.localExperiment.experimentId) {
+                services.ExperimentService.update(this.localExperiment)
+                    .then(experiment => {
+                        console.log(experiment);
+                        this.$emit('saved', experiment);
+                    });
+            } else {
+                services.ExperimentService.create(this.localExperiment)
+                    .then(experiment => {
+                        this.localExperiment.experimentId = experiment.experimentId;
+                        console.log(experiment);
+                        this.$emit('saved', experiment);
+                    });
+            }
         },
         saveAndLaunchExperiment: function() {
             console.log(JSON.stringify(this.localExperiment));
@@ -135,9 +147,12 @@ export default {
             // TODO: save experiment
             // TODO: set the experiment ID on the new experiment
             // TODO: dispatch save event with updated experiment
-        }
+        },
     },
     watch: {
+        experiment: function(newValue) {
+            this.localExperiment = newValue.clone();
+        },
     }
 }
 </script>
@@ -149,4 +164,4 @@ export default {
 #col-exp-buttons {
     text-align: right;
 }
-</style>
\ No newline at end of file
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/CreateExperimentContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/CreateExperimentContainer.vue
index 9ffdb94..e78cc8b 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/CreateExperimentContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/CreateExperimentContainer.vue
@@ -41,6 +41,7 @@ export default {
             .then(appInterface => {
                 this.experiment.experimentInputs = appInterface.getOrderedApplicationInputs().map(input => input.clone());
                 this.appInterface = appInterface;
+                this.experiment.executionId = this.appInterface.applicationInterfaceId;
             });
     }
 }

-- 
To stop receiving notification emails like this one, please contact
"commits@airavata.apache.org" <co...@airavata.apache.org>.