Merge branch 'AIRAVATA-3126--Implement-compute-resource-reservation-addition'
diff --git a/.travis.yml b/.travis.yml
index 9b65b65..0029b02 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,7 @@
- cp django_airavata/settings_local.py.sample django_airavata/settings_local.py
- python manage.py migrate
- python manage.py check
+ - python manage.py test
# For now ignore long line endings
- flake8 --ignore=E501 .
- ./lint_js.sh
diff --git a/airavata/model/application/io/ttypes.py b/airavata/model/application/io/ttypes.py
index 8c9017a..17f1760 100644
--- a/airavata/model/application/io/ttypes.py
+++ b/airavata/model/application/io/ttypes.py
@@ -74,6 +74,8 @@
metaData:
Any metadat. This is typically ignore by Airavata and is used by gateways for application configuration.
+ overrideFilename:
+ Rename input file to given value when staging to compute resource.
Attributes:
- name
@@ -89,6 +91,7 @@
- dataStaged
- storageResourceId
- isReadOnly
+ - overrideFilename
"""
thrift_spec = (
@@ -106,9 +109,10 @@
(11, TType.BOOL, 'dataStaged', None, None, ), # 11
(12, TType.STRING, 'storageResourceId', 'UTF8', None, ), # 12
(13, TType.BOOL, 'isReadOnly', None, None, ), # 13
+ (14, TType.STRING, 'overrideFilename', 'UTF8', None, ), # 14
)
- def __init__(self, name=None, value=None, type=None, applicationArgument=None, standardInput=None, userFriendlyDescription=None, metaData=None, inputOrder=None, isRequired=None, requiredToAddedToCommandLine=None, dataStaged=None, storageResourceId=None, isReadOnly=None,):
+ def __init__(self, name=None, value=None, type=None, applicationArgument=None, standardInput=None, userFriendlyDescription=None, metaData=None, inputOrder=None, isRequired=None, requiredToAddedToCommandLine=None, dataStaged=None, storageResourceId=None, isReadOnly=None, overrideFilename=None,):
self.name = name
self.value = value
self.type = type
@@ -122,6 +126,7 @@
self.dataStaged = dataStaged
self.storageResourceId = storageResourceId
self.isReadOnly = isReadOnly
+ self.overrideFilename = overrideFilename
def read(self, iprot):
if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
@@ -197,6 +202,11 @@
self.isReadOnly = iprot.readBool()
else:
iprot.skip(ftype)
+ elif fid == 14:
+ if ftype == TType.STRING:
+ self.overrideFilename = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString()
+ else:
+ iprot.skip(ftype)
else:
iprot.skip(ftype)
iprot.readFieldEnd()
@@ -259,6 +269,10 @@
oprot.writeFieldBegin('isReadOnly', TType.BOOL, 13)
oprot.writeBool(self.isReadOnly)
oprot.writeFieldEnd()
+ if self.overrideFilename is not None:
+ oprot.writeFieldBegin('overrideFilename', TType.STRING, 14)
+ oprot.writeString(self.overrideFilename.encode('utf-8') if sys.version_info[0] == 2 else self.overrideFilename)
+ oprot.writeFieldEnd()
oprot.writeFieldStop()
oprot.writeStructEnd()
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue
index 2f6f472..1d50b2d 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue
@@ -11,16 +11,19 @@
<span class="sr-only">Delete</span>
</b-link>
</div>
- <b-collapse :visible="!collapse">
+ <b-collapse :id="id+'-collapse'" :visible="!collapse">
<b-form-group label="Name" :label-for="id+'-name'">
<b-form-input :id="id+'-name'" type="text" v-model="data.name" ref="nameInput" required :disabled="readonly"></b-form-input>
</b-form-group>
- <b-form-group label="Initial Value" :label-for="id+'-value'">
- <b-form-input :id="id+'-value'" type="text" v-model="data.value" :disabled="readonly"></b-form-input>
- </b-form-group>
<b-form-group label="Type" :label-for="id+'-type'">
<b-form-select :id="id+'-type'" v-model="data.type" :options="inputTypeOptions" :disabled="readonly" />
</b-form-group>
+ <b-form-group label="Initial Value" :label-for="id+'-value'" v-if="showValueField">
+ <b-form-input :id="id+'-value'" type="text" v-model="data.value" :disabled="readonly"></b-form-input>
+ </b-form-group>
+ <b-form-group label="Override Filename" :label-for="id+'-value'" v-if="showOverrideFilenameField">
+ <b-form-input :id="id+'-override-filename'" type="text" v-model="data.overrideFilename" :disabled="readonly"></b-form-input>
+ </b-form-group>
<b-form-group label="Application Argument" :label-for="id+'-argument'">
<b-form-input :id="id+'-argument'" type="text" v-model="data.applicationArgument" :disabled="readonly"></b-form-input>
</b-form-group>
@@ -102,6 +105,12 @@
},
id() {
return "id-" + this.data.key;
+ },
+ showValueField() {
+ return this.data.type.isSimpleValueType
+ },
+ showOverrideFilenameField() {
+ return this.data.type === models.DataType.URI;
}
},
methods: {
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
index 99faa51..ad6e17a 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
@@ -91,7 +91,7 @@
},
data() {
return {
- focusApplicationInputIndex: null,
+ focusApplicationInputKey: null,
focusApplicationOutputKey: null,
dragOptions: {
handle: ".drag-handle"
@@ -113,8 +113,9 @@
Object.assign(input, newValue);
},
addApplicationInput() {
- this.data.applicationInputs.push(new models.InputDataObjectType());
- this.focusApplicationInputIndex = this.data.applicationInputs.length - 1;
+ const appInput = new models.InputDataObjectType();
+ this.data.applicationInputs.push(appInput);
+ this.focusApplicationInputKey = appInput.key;
},
deleteInput(input) {
const inputIndex = this.data.applicationInputs.findIndex(
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/JSONEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/JSONEditor.vue
index 54f8941..88b240c 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/JSONEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/JSONEditor.vue
@@ -8,7 +8,6 @@
props: {
value: {
type: Object,
- required: true
},
id: String,
rows: Number,
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
index 9bd2eba..77fab42 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
@@ -1,14 +1,20 @@
<template>
<div>
- <user-group-membership-editor
- v-if="iamUserProfile.airavataUserProfileExists"
- v-model="localIAMUserProfile.groups"
- :editable-groups="editableGroups"
- :airavata-internal-user-id="iamUserProfile.airavataInternalUserId"
- @input="groupsUpdated"
- />
+ <b-card header="Edit Groups">
+ <user-group-membership-editor
+ v-if="iamUserProfile.airavataUserProfileExists"
+ v-model="localIAMUserProfile.groups"
+ :editable-groups="editableGroups"
+ :airavata-internal-user-id="iamUserProfile.airavataInternalUserId"
+ />
+ <b-button @click="groupsUpdated" variant="primary" :disabled="!areGroupsUpdated">Save</b-button>
+ </b-card>
<activate-user-panel
- v-if="iamUserProfile.enabled && iamUserProfile.emailVerified && !iamUserProfile.airavataUserProfileExists"
+ v-if="
+ iamUserProfile.enabled &&
+ iamUserProfile.emailVerified &&
+ !iamUserProfile.airavataUserProfileExists
+ "
:username="iamUserProfile.userId"
@activate-user="$emit('enable-user', $event)"
/>
@@ -64,7 +70,33 @@
groupsUpdated() {
this.$emit("groups-updated", this.localIAMUserProfile);
}
+ },
+ computed: {
+ currentGroupIds() {
+ const groupIds = this.iamUserProfile.groups.map(g => g.id);
+ groupIds.sort();
+ return groupIds;
+ },
+ updatedGroupIds() {
+ const groupIds = this.localIAMUserProfile.groups.map(g => g.id);
+ groupIds.sort();
+ return groupIds;
+ },
+ areGroupsUpdated() {
+ for (const groupId of this.currentGroupIds) {
+ // Check if a group was removed
+ if (this.updatedGroupIds.indexOf(groupId) < 0) {
+ return true;
+ }
+ }
+ for (const groupId of this.updatedGroupIds) {
+ // Check if a group was added
+ if (this.currentGroupIds.indexOf(groupId) < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
}
};
</script>
-
diff --git a/django_airavata/apps/api/data_products_helper.py b/django_airavata/apps/api/data_products_helper.py
index 30ab128..6dcdd84 100644
--- a/django_airavata/apps/api/data_products_helper.py
+++ b/django_airavata/apps/api/data_products_helper.py
@@ -1,4 +1,6 @@
+import copy
import logging
+import mimetypes
import os
from urllib.parse import urlparse
@@ -19,30 +21,34 @@
TMP_INPUT_FILE_UPLOAD_DIR = "tmp"
-def save(request, path, file, name=None):
+def save(request, path, file, name=None, content_type=None):
"Save file in path in the user's storage."
username = request.user.username
full_path = datastore.save(username, path, file, name=name)
- data_product = _save_data_product(request, full_path, name=name)
+ data_product = _save_data_product(request, full_path, name=name,
+ content_type=content_type)
return data_product
-def move_from_filepath(request, source_path, target_path, name=None):
+def move_from_filepath(request, source_path, target_path, name=None,
+ content_type=None):
"Move a file from filesystem into user's storage."
username = request.user.username
file_name = name if name is not None else os.path.basename(source_path)
full_path = datastore.move_external(
source_path, username, target_path, file_name)
- data_product = _save_data_product(request, full_path, name=file_name)
+ data_product = _save_data_product(request, full_path, name=file_name,
+ content_type=content_type)
return data_product
-def save_input_file_upload(request, file, name=None):
+def save_input_file_upload(request, file, name=None, content_type=None):
"""Save input file in staging area for input file uploads."""
username = request.user.username
file_name = name if name is not None else os.path.basename(file.name)
full_path = datastore.save(username, TMP_INPUT_FILE_UPLOAD_DIR, file)
- data_product = _save_data_product(request, full_path, name=file_name)
+ data_product = _save_data_product(request, full_path, name=file_name,
+ content_type=content_type)
return data_product
@@ -54,7 +60,7 @@
request.user.username,
TMP_INPUT_FILE_UPLOAD_DIR,
name=name)
- return _save_data_product(request, full_path, name=name)
+ return _save_copy_of_data_product(request, full_path, data_product)
def is_input_file_upload(request, data_product):
@@ -77,21 +83,23 @@
path,
file_name)
_delete_data_product(data_product.ownerName, source_path)
- data_product = _save_data_product(request, full_path, name=file_name)
+ data_product = _save_copy_of_data_product(request, full_path, data_product)
return data_product
-def move_input_file_upload_from_filepath(request, source_path, name=None):
+def move_input_file_upload_from_filepath(request, source_path, name=None,
+ content_type=None):
"Move a file from filesystem into user's input file staging area."
username = request.user.username
file_name = name if name is not None else os.path.basename(source_path)
full_path = datastore.move_external(
source_path, username, TMP_INPUT_FILE_UPLOAD_DIR, file_name)
- data_product = _save_data_product(request, full_path, name=file_name)
+ data_product = _save_data_product(request, full_path, name=file_name,
+ content_type=content_type)
return data_product
-def open(request, data_product):
+def open_file(request, data_product):
"Return file object for replica if it exists in user storage."
path = _get_replica_filepath(data_product)
return datastore.open(data_product.ownerName, path)
@@ -196,19 +204,43 @@
return product_uri
-def _save_data_product(request, full_path, name=None):
+def _save_data_product(request, full_path, name=None, content_type=None):
"Create, register and record in DB a data product for full_path."
data_product = _create_data_product(
- request.user.username, full_path, name=name)
+ request.user.username, full_path, name=name, content_type=content_type)
+ product_uri = _register_data_product(request, full_path, data_product)
+ data_product.productUri = product_uri
+ return data_product
+
+
+def _register_data_product(request, full_path, data_product):
product_uri = request.airavata_client.registerDataProduct(
request.authz_token, data_product)
- data_product.productUri = product_uri
user_file_instance = models.User_Files(
username=request.user.username,
file_path=full_path,
file_dpu=product_uri)
user_file_instance.save()
- return data_product
+ return product_uri
+
+
+def _save_copy_of_data_product(request, full_path, data_product):
+ """Save copy of a data product with a different path."""
+ data_product_copy = _copy_data_product(request, data_product, full_path)
+ product_uri = _register_data_product(request, full_path, data_product_copy)
+ data_product_copy.productUri = product_uri
+ return data_product_copy
+
+
+def _copy_data_product(request, data_product, full_path):
+ """Create an unsaved copy of a data product with different path."""
+ data_product_copy = copy.copy(data_product)
+ data_product_copy.productUri = None
+ data_product_copy.ownerName = request.user.username
+ data_replica_location = _create_replica_location(
+ full_path, data_product_copy.productName)
+ data_product_copy.replicaLocations = [data_replica_location]
+ return data_product_copy
def _delete_data_product(username, full_path):
@@ -220,7 +252,8 @@
user_file.delete()
-def _create_data_product(username, full_path, name=None):
+def _create_data_product(username, full_path, name=None,
+ content_type=None):
data_product = DataProductModel()
data_product.gatewayId = settings.GATEWAY_ID
data_product.ownerName = username
@@ -230,6 +263,31 @@
file_name = os.path.basename(full_path)
data_product.productName = file_name
data_product.dataProductType = DataProductType.FILE
+ final_content_type = _determine_content_type(full_path, content_type)
+ if final_content_type is not None:
+ data_product.productMetadata = {'mime-type': final_content_type}
+ data_replica_location = _create_replica_location(full_path, file_name)
+ data_product.replicaLocations = [data_replica_location]
+ return data_product
+
+
+def _determine_content_type(full_path, content_type=None):
+ result = content_type
+ if result is None:
+ # Try to guess the content-type from file extension
+ guessed_type, encoding = mimetypes.guess_type(full_path)
+ result = guessed_type
+ if result is None or result == 'application/octet-stream':
+ # Check if file is Unicode text by trying to read some of it
+ try:
+ open(full_path, 'r').read(1024)
+ result = 'text/plain'
+ except UnicodeDecodeError:
+ logger.debug(f"Failed to read as Unicode text: {full_path}")
+ return result
+
+
+def _create_replica_location(full_path, file_name):
data_replica_location = DataReplicaLocationModel()
data_replica_location.storageResourceId = \
settings.GATEWAY_DATA_STORE_RESOURCE_ID
@@ -242,8 +300,7 @@
data_replica_location.filePath = \
"file://{}:{}".format(settings.GATEWAY_DATA_STORE_HOSTNAME,
full_path)
- data_product.replicaLocations = [data_replica_location]
- return data_product
+ return data_replica_location
def _get_replica_filepath(data_product):
diff --git a/django_airavata/apps/api/output_views.py b/django_airavata/apps/api/output_views.py
index 2375bd2..0614e56 100644
--- a/django_airavata/apps/api/output_views.py
+++ b/django_airavata/apps/api/output_views.py
@@ -193,7 +193,7 @@
data_product = request.airavata_client.getDataProduct(
request.authz_token, experiment_output.value)
if data_products_helper.exists(request, data_product):
- output_file = data_products_helper.open(request, data_product)
+ output_file = data_products_helper.open_file(request, data_product)
elif settings.DEBUG and test_output_file is not None:
output_file = open(test_output_file, 'rb')
# TODO: change interface to provide output_file as a path
diff --git a/django_airavata/apps/api/signals.py b/django_airavata/apps/api/signals.py
index 3967e20..f01df92 100644
--- a/django_airavata/apps/api/signals.py
+++ b/django_airavata/apps/api/signals.py
@@ -1,15 +1,20 @@
-"""Signal receivers for the api app."""
+"""Signal and receivers for the api app."""
import logging
from django.contrib.auth.signals import user_logged_in
-from django.dispatch import receiver
+from django.dispatch import Signal, receiver
from . import data_products_helper
log = logging.getLogger(__name__)
+# Signals
+user_added_to_group = Signal(providing_args=["user", "groups", "request"])
+
+
+# Receivers
@receiver(user_logged_in)
def create_user_storage_dir(sender, request, user, **kwargs):
"""Create user's home direct in gateway storage."""
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js b/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js
index 2634807..da6f6d9 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/DataProduct.js
@@ -32,6 +32,7 @@
];
const FILENAME_REGEX = /[^/]+$/;
+const TEXT_MIME_TYPE_REGEX = /^text\/.+/;
export default class DataProduct extends BaseModel {
constructor(data = {}) {
@@ -49,4 +50,12 @@
}
return null;
}
+
+ get isText() {
+ return this.mimeType && TEXT_MIME_TYPE_REGEX.test(this.mimeType);
+ }
+
+ get mimeType() {
+ return this.productMetadata && this.productMetadata['mime-type'] ? this.productMetadata['mime-type'] : null;
+ }
}
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 f3a25fb..872bc99 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
@@ -41,7 +41,8 @@
name: "isReadOnly",
type: "boolean",
default: false
- }
+ },
+ "overrideFilename"
];
const IS_REQUIRED_DEFAULT = "This field is required.";
@@ -241,10 +242,11 @@
this.editorDependencies.show
);
if ("showOptions" in this.editorDependencies) {
- if ("toggle" in this.editorDependencies.showOptions) {
- this.editorDependencies.showOptions.toggle.forEach(prop => {
- this[prop] = this.show;
- });
+ if (
+ "isRequired" in this.editorDependencies.showOptions &&
+ this.editorDependencies.showOptions.isRequired
+ ) {
+ this.isRequired = this.show;
}
}
}
diff --git a/django_airavata/apps/api/tests.py b/django_airavata/apps/api/tests.py
deleted file mode 100644
index 4929020..0000000
--- a/django_airavata/apps/api/tests.py
+++ /dev/null
@@ -1,2 +0,0 @@
-
-# Create your tests here.
diff --git a/django_airavata/apps/api/tests/__init__.py b/django_airavata/apps/api/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/django_airavata/apps/api/tests/__init__.py
diff --git a/django_airavata/apps/api/tests/test_data_products_helper.py b/django_airavata/apps/api/tests/test_data_products_helper.py
new file mode 100644
index 0000000..839cb28
--- /dev/null
+++ b/django_airavata/apps/api/tests/test_data_products_helper.py
@@ -0,0 +1,180 @@
+import io
+import os
+import tempfile
+import uuid
+from unittest.mock import MagicMock
+from urllib.parse import urlparse
+
+from django.contrib.auth.models import User
+from django.test import RequestFactory, TestCase, override_settings
+
+from airavata.model.data.replica.ttypes import (
+ DataProductModel,
+ DataProductType,
+ DataReplicaLocationModel,
+ ReplicaLocationCategory
+)
+from django_airavata.apps.api import data_products_helper
+
+GATEWAY_ID = 'test-gateway'
+
+
+@override_settings(GATEWAY_ID=GATEWAY_ID)
+class BaseTestCase(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user('testuser')
+ self.factory = RequestFactory()
+ # Dummy POST request
+ self.request = self.factory.post('/upload', {})
+ self.request.user = self.user
+ self.request.airavata_client = MagicMock(name="airavata_client")
+ self.product_uri = f"airavata-dp://{uuid.uuid4()}"
+ self.request.airavata_client.registerDataProduct.return_value = \
+ self.product_uri
+ self.request.authz_token = "dummy"
+
+
+class SaveTests(BaseTestCase):
+
+ def test_save_with_defaults(self):
+ "Test save with default name and content type"
+ with tempfile.TemporaryDirectory() as tmpdirname, \
+ self.settings(GATEWAY_DATA_STORE_DIR=tmpdirname,
+ GATEWAY_DATA_STORE_HOSTNAME="gateway.com"):
+ # path is just the user directory in gateway storage
+ path = os.path.join(tmpdirname, self.user.username)
+ file = io.StringIO("Foo file")
+ file.name = "foo.txt"
+ data_product = data_products_helper.save(self.request, path, file)
+
+ self.assertEqual(data_product.productUri, self.product_uri)
+ self.request.airavata_client.registerDataProduct.\
+ assert_called_once()
+ args, kws = self.request.airavata_client.registerDataProduct.\
+ call_args
+ dp = args[1]
+ self.assertEqual(self.user.username, dp.ownerName)
+ self.assertEqual("foo.txt", dp.productName)
+ self.assertEqual(DataProductType.FILE, dp.dataProductType)
+ self.assertDictEqual({'mime-type': 'text/plain'},
+ dp.productMetadata)
+ self.assertEqual(1, len(dp.replicaLocations))
+ self.assertEqual(f"file://gateway.com:{path}/{file.name}",
+ dp.replicaLocations[0].filePath)
+
+ def test_save_with_name_and_content_type(self):
+ "Test save with specified name and content type"
+ with tempfile.TemporaryDirectory() as tmpdirname, \
+ self.settings(GATEWAY_DATA_STORE_DIR=tmpdirname,
+ GATEWAY_DATA_STORE_HOSTNAME="gateway.com"):
+ # path is just the user directory in gateway storage
+ path = os.path.join(tmpdirname, self.user.username)
+ file = io.StringIO("Foo file")
+ file.name = "foo.txt"
+ data_product = data_products_helper.save(
+ self.request, path, file, name="bar.txt",
+ content_type="application/some-app")
+
+ self.assertEqual(data_product.productUri, self.product_uri)
+ self.request.airavata_client.registerDataProduct.\
+ assert_called_once()
+ args, kws = self.request.airavata_client.registerDataProduct.\
+ call_args
+ dp = args[1]
+ self.assertEqual(self.user.username, dp.ownerName)
+ self.assertEqual("bar.txt", dp.productName)
+ self.assertEqual(DataProductType.FILE, dp.dataProductType)
+ self.assertDictEqual({'mime-type': 'application/some-app'},
+ dp.productMetadata)
+ self.assertEqual(1, len(dp.replicaLocations))
+ self.assertEqual(f"file://gateway.com:{path}/bar.txt",
+ dp.replicaLocations[0].filePath)
+
+ def test_save_with_unknown_text_file_type(self):
+ "Test save with unknown file ext for text file"
+ with tempfile.TemporaryDirectory() as tmpdirname, \
+ self.settings(GATEWAY_DATA_STORE_DIR=tmpdirname,
+ GATEWAY_DATA_STORE_HOSTNAME="gateway.com"):
+ path = os.path.join(
+ tmpdirname, "foo.someext")
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, 'w') as f:
+ f.write("Some Unicode text")
+ with open(path, 'r') as f:
+ dp = data_products_helper.save(
+ self.request, "some/path", f,
+ content_type="application/octet-stream")
+ # Make sure that the file contents are tested to see if text
+ self.assertDictEqual({'mime-type': 'text/plain'},
+ dp.productMetadata)
+
+ def test_save_with_unknown_binary_file_type(self):
+ "Test save with unknown file ext for binary file"
+ with tempfile.TemporaryDirectory() as tmpdirname, \
+ self.settings(GATEWAY_DATA_STORE_DIR=tmpdirname,
+ GATEWAY_DATA_STORE_HOSTNAME="gateway.com"):
+ path = os.path.join(
+ tmpdirname, "foo.someext")
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, 'wb') as f:
+ f.write(bytes(range(256)))
+ with open(path, 'rb') as f:
+ dp = data_products_helper.save(
+ self.request, "some/path", f,
+ content_type="application/octet-stream")
+ # Make sure that DID NOT determine file contents are text
+ self.assertDictEqual({'mime-type': 'application/octet-stream'},
+ dp.productMetadata)
+
+
+class CopyInputFileUploadTests(BaseTestCase):
+ def test_copy_input_file_upload(self):
+ "Test copy input file upload copies data product"
+ with tempfile.TemporaryDirectory() as tmpdirname, \
+ self.settings(GATEWAY_DATA_STORE_DIR=tmpdirname,
+ GATEWAY_DATA_STORE_HOSTNAME="gateway.com"):
+ # path is just the user directory in gateway storage
+ source_path = os.path.join(
+ tmpdirname, self.user.username, "foo.ext")
+ os.makedirs(os.path.dirname(source_path))
+ with open(source_path, 'wb') as f:
+ f.write(b"123")
+
+ data_product = DataProductModel()
+ data_product.productUri = f"airavata-dp://{uuid.uuid4()}"
+ data_product.gatewayId = GATEWAY_ID
+ data_product.ownerName = self.user.username
+ data_product.productName = "foo.ext"
+ data_product.dataProductType = DataProductType.FILE
+ data_product.productMetadata = {
+ 'mime-type': 'application/some-app'
+ }
+ replica_category = ReplicaLocationCategory.GATEWAY_DATA_STORE
+ replica_path = f"file://gateway.com:{source_path}"
+ data_product.replicaLocations = [
+ DataReplicaLocationModel(
+ filePath=replica_path,
+ replicaLocationCategory=replica_category)]
+
+ data_product_copy = data_products_helper.copy_input_file_upload(
+ self.request, data_product)
+
+ self.request.airavata_client.registerDataProduct.\
+ assert_called_once()
+ self.assertIsNot(data_product_copy, data_product)
+ self.assertNotEqual(data_product_copy.productUri,
+ data_product.productUri)
+ self.assertDictEqual(data_product_copy.productMetadata,
+ data_product.productMetadata)
+ self.assertEqual(data_product_copy.productName,
+ data_product.productName)
+ self.assertEqual(data_product_copy.dataProductType,
+ data_product.dataProductType)
+ replica_copy_path = data_product_copy.replicaLocations[0].filePath
+ self.assertNotEqual(replica_copy_path, replica_path)
+ replica_copy_filepath = urlparse(replica_copy_path).path
+ self.assertEqual(
+ os.path.dirname(replica_copy_filepath),
+ os.path.join(tmpdirname, self.user.username, "tmp"),
+ msg="Verify input file copied to user's tmp dir")
diff --git a/django_airavata/apps/api/tests/test_views.py b/django_airavata/apps/api/tests/test_views.py
new file mode 100644
index 0000000..a70703f
--- /dev/null
+++ b/django_airavata/apps/api/tests/test_views.py
@@ -0,0 +1,470 @@
+from unittest.mock import MagicMock, call, patch
+
+from django.contrib.auth.models import User
+from django.test import TestCase, override_settings
+from django.urls import reverse
+# from rest_framework import status
+from rest_framework.test import APIRequestFactory, force_authenticate
+
+from airavata.model.appcatalog.gatewaygroups.ttypes import GatewayGroups
+from airavata.model.group.ttypes import GroupModel
+from airavata.model.user.ttypes import UserProfile
+from django_airavata.apps.api import signals, views
+
+GATEWAY_ID = "test-gateway"
+PORTAL_ADMINS = [('Admin Name', 'admin@example.com')]
+
+
+@override_settings(
+ GATEWAY_ID=GATEWAY_ID,
+ PORTAL_ADMINS=PORTAL_ADMINS
+)
+class GroupViewSetTests(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user('testuser')
+ self.factory = APIRequestFactory()
+
+ def test_create_group_sends_user_added_to_group_signal(self):
+
+ url = reverse('django_airavata_api:group-list')
+ data = {
+ "id": None,
+ "name": "test",
+ "description": None,
+ "members": [
+ f"{self.user.username}@{GATEWAY_ID}", # owner
+ f"testuser1@{GATEWAY_ID}"],
+ "admins": []
+ }
+ request = self.factory.post(url, data)
+ force_authenticate(request, self.user)
+
+ # Mock api clients
+ group_manager_mock = MagicMock(name='group_manager')
+ user_profile_mock = MagicMock(name='user_profile')
+ request.profile_service = {
+ 'group_manager': group_manager_mock,
+ 'user_profile': user_profile_mock,
+ }
+ request.airavata_client = MagicMock(name="airavata_client")
+ request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+ gatewayId=GATEWAY_ID,
+ adminsGroupId="adminsGroupId",
+ readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+ defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+ )
+ request.authz_token = "dummy"
+ request.session = {}
+ group_manager_mock.createGroup.return_value = "abc123"
+ user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ user_profile_mock.getUserProfileById.return_value = user_profile
+
+ # Mock signal handler to verify 'user_added_to_group' signal is sent
+ user_added_to_group_handler = MagicMock()
+ signals.user_added_to_group.connect(
+ user_added_to_group_handler,
+ sender=views.GroupViewSet)
+ group_create = views.GroupViewSet.as_view({'post': 'create'})
+ response = group_create(request)
+ self.assertEquals(201, response.status_code)
+ self.assertEquals("abc123", response.data['id'])
+ user_added_to_group_handler.assert_called_once()
+ args, kwargs = user_added_to_group_handler.call_args
+ self.assertEquals("abc123", kwargs["groups"][0].id)
+ self.assertIs(user_profile, kwargs["user"])
+
+ def test_update_group_sends_user_added_to_group_signal(self):
+ url = reverse('django_airavata_api:group-detail',
+ kwargs={'group_id': 'abc123'})
+ data = {
+ "id": "abc123",
+ "name": "test",
+ "description": None,
+ "members": [
+ f"{self.user.username}@{GATEWAY_ID}", # owner
+ f"testuser1@{GATEWAY_ID}", # existing member
+ f"testuser3@{GATEWAY_ID}"], # new member
+ "admins": []
+ }
+ request = self.factory.put(url, data)
+ force_authenticate(request, self.user)
+
+ # Mock api clients
+ group_manager_mock = MagicMock(name='group_manager')
+ user_profile_mock = MagicMock(name='user_profile')
+ request.profile_service = {
+ 'group_manager': group_manager_mock,
+ 'user_profile': user_profile_mock,
+ }
+ request.airavata_client = MagicMock(name="airavata_client")
+ request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+ gatewayId=GATEWAY_ID,
+ adminsGroupId="adminsGroupId",
+ readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+ defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+ )
+ request.authz_token = "dummy"
+ request.session = {}
+
+ # mock getGroup
+ group = GroupModel(id="abc123", name="My Group",
+ ownerId=f"{self.user.username}@{GATEWAY_ID}",
+ members=[
+ f"{self.user.username}@{GATEWAY_ID}", # owner
+ f"testuser1@{GATEWAY_ID}", # existing member
+ f"testuser2@{GATEWAY_ID}", # new member
+ ],
+ admins=[])
+ group_manager_mock.getGroup.return_value = group
+
+ # Only user added is testuser3, so getUserProfileById will be called
+ # for that user
+ user_profile = UserProfile(
+ airavataInternalUserId=f"testuser3@{GATEWAY_ID}",
+ userId="testuser3",
+ firstName="Test",
+ lastName="User3",
+ emails=["testuser3@example.com"]
+ )
+ user_profile_mock.getUserProfileById.return_value = user_profile
+
+ # Mock signal handler to verify 'user_added_to_group' signal is sent
+ user_added_to_group_handler = MagicMock()
+ signals.user_added_to_group.connect(
+ user_added_to_group_handler,
+ sender=views.GroupViewSet)
+ group_update = views.GroupViewSet.as_view({'put': 'update'})
+ response = group_update(request, group_id="abc123")
+ self.assertEquals(200, response.status_code)
+ self.assertEquals("abc123", response.data['id'])
+
+ # verify addUsersToGroup
+ group_manager_mock.addUsersToGroup.assert_called_once()
+ args, kwargs = group_manager_mock.addUsersToGroup.call_args
+ self.assertEqual(args[1], [f"testuser3@{GATEWAY_ID}"])
+
+ # verify removeUsersFromGroup
+ group_manager_mock.removeUsersFromGroup.assert_called_once()
+ args, kwargs = group_manager_mock.removeUsersFromGroup.call_args
+ self.assertEqual(args[1], [f"testuser2@{GATEWAY_ID}"])
+
+ # verify updateGroup
+ group_manager_mock.updateGroup.assert_called_once()
+
+ user_added_to_group_handler.assert_called_once()
+ args, kwargs = user_added_to_group_handler.call_args
+ self.assertEquals("abc123", kwargs["groups"][0].id)
+ self.assertIs(user_profile, kwargs["user"])
+
+
+@override_settings(
+ GATEWAY_ID=GATEWAY_ID,
+ PORTAL_ADMINS=PORTAL_ADMINS
+)
+class IAMUserViewSetTests(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user('testuser')
+ self.factory = APIRequestFactory()
+
+ @patch("django_airavata.apps.api.views.iam_admin_client")
+ def test_update_that_adds_user_to_group_sends_user_added_to_group_signal(
+ self, iam_admin_client):
+
+ username = "testuser1"
+ url = reverse(
+ 'django_airavata_api:iam-user-profile-detail',
+ kwargs={'user_id': username})
+ data = {
+ "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
+ "userId": username,
+ "gatewayId": GATEWAY_ID,
+ "email": "testuser1@example.com",
+ "firstName": "Test",
+ "lastName": "User1",
+ "airavataUserProfileExists": True,
+ "enabled": True,
+ "emailVerified": True,
+ "groups": [
+ {"id": "group1", "name": "Group 1"},
+ {"id": "group2", "name": "Group 2"}
+ ]
+ }
+ request = self.factory.put(url, data)
+ force_authenticate(request, self.user)
+ request.is_gateway_admin = True
+
+ # Mock api clients
+ iam_user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ iam_admin_client.get_user.return_value = iam_user_profile
+ group_manager_mock = MagicMock(name='group_manager')
+ user_profile_mock = MagicMock(name='user_profile')
+ request.profile_service = {
+ 'group_manager': group_manager_mock,
+ 'user_profile': user_profile_mock,
+ }
+ request.authz_token = "dummy"
+ user_profile_mock.doesUserExist.return_value = True
+ user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ user_profile_mock.getUserProfileById.return_value = user_profile
+ group_manager_mock.getAllGroupsUserBelongs.return_value = [
+ GroupModel(id="group1")]
+ group = GroupModel(
+ id="group2", name="Group 2"
+ )
+ group_manager_mock.getGroup.return_value = group
+ request.airavata_client = MagicMock(name="airavata_client")
+ request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+ gatewayId=GATEWAY_ID,
+ adminsGroupId="adminsGroupId",
+ readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+ defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+ )
+ request.session = {}
+
+ # Mock signal handler to verify 'user_added_to_group' signal is sent
+ user_added_to_group_handler = MagicMock(
+ name="user_added_to_group_handler")
+ signals.user_added_to_group.connect(
+ user_added_to_group_handler,
+ sender=views.IAMUserViewSet)
+ iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+ response = iam_user_update(request, user_id=username)
+ self.assertEquals(200, response.status_code)
+
+ user_profile_mock.doesUserExist.assert_called_once()
+ group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
+
+ user_profile_mock.getUserProfileById.assert_called_once()
+ args, kwargs = user_profile_mock.getUserProfileById.call_args
+ self.assertSequenceEqual(
+ args, [request.authz_token, "testuser1", GATEWAY_ID])
+
+ group_manager_mock.getGroup.assert_called_once()
+ args, kwargs = group_manager_mock.getGroup.call_args
+ self.assertSequenceEqual(args, [request.authz_token, "group2"])
+
+ group_manager_mock.addUsersToGroup.assert_called_once()
+ args, kwargs = group_manager_mock.addUsersToGroup.call_args
+ self.assertSequenceEqual(
+ args, [request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group2"]
+ )
+
+ user_added_to_group_handler.assert_called_once()
+ args, kwargs = user_added_to_group_handler.call_args
+ self.assertEqual(kwargs["sender"], views.IAMUserViewSet)
+ self.assertEqual(kwargs["user"], user_profile)
+ self.assertEqual(kwargs["groups"][0], group)
+
+ @patch("django_airavata.apps.api.views.iam_admin_client")
+ def test_update_that_adds_user_to_multiple_groups(
+ self, iam_admin_client):
+
+ username = "testuser1"
+ url = reverse(
+ 'django_airavata_api:iam-user-profile-detail',
+ kwargs={'user_id': username})
+ data = {
+ "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
+ "userId": username,
+ "gatewayId": GATEWAY_ID,
+ "email": "testuser1@example.com",
+ "firstName": "Test",
+ "lastName": "User1",
+ "airavataUserProfileExists": True,
+ "enabled": True,
+ "emailVerified": True,
+ "groups": [
+ {"id": "group1", "name": "Group 1"},
+ {"id": "group2", "name": "Group 2"},
+ {"id": "group3", "name": "Group 3"},
+ ]
+ }
+ request = self.factory.put(url, data)
+ force_authenticate(request, self.user)
+ request.is_gateway_admin = True
+
+ # Mock api clients
+ iam_user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ iam_admin_client.get_user.return_value = iam_user_profile
+ group_manager_mock = MagicMock(name='group_manager')
+ user_profile_mock = MagicMock(name='user_profile')
+ request.profile_service = {
+ 'group_manager': group_manager_mock,
+ 'user_profile': user_profile_mock,
+ }
+ request.authz_token = "dummy"
+ user_profile_mock.doesUserExist.return_value = True
+ user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ user_profile_mock.getUserProfileById.return_value = user_profile
+ group_manager_mock.getAllGroupsUserBelongs.return_value = [
+ GroupModel(id="group1")]
+
+ def side_effect(authz_token, group_id):
+ if group_id == "group2":
+ return GroupModel(id="group2", name="Group 2")
+ elif group_id == "group3":
+ return GroupModel(id="group3", name="Group 3")
+ else:
+ raise Exception("Unexpected group id: " + group_id)
+
+ group_manager_mock.getGroup.side_effect = side_effect
+ request.airavata_client = MagicMock(name="airavata_client")
+ request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+ gatewayId=GATEWAY_ID,
+ adminsGroupId="adminsGroupId",
+ readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+ defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+ )
+ request.session = {}
+
+ # Mock signal handler to verify 'user_added_to_group' signal is sent
+ user_added_to_group_handler = MagicMock(
+ name="user_added_to_group_handler")
+ signals.user_added_to_group.connect(
+ user_added_to_group_handler,
+ sender=views.IAMUserViewSet)
+ iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+ response = iam_user_update(request, user_id=username)
+ self.assertEquals(200, response.status_code)
+
+ user_profile_mock.doesUserExist.assert_called_once()
+ group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
+
+ user_profile_mock.getUserProfileById.assert_called_once()
+ args, kwargs = user_profile_mock.getUserProfileById.call_args
+ self.assertSequenceEqual(
+ args, [request.authz_token, "testuser1", GATEWAY_ID])
+
+ group_manager_mock.getGroup.assert_has_calls([
+ call(request.authz_token, "group2"),
+ call(request.authz_token, "group3")
+ ], any_order=True)
+
+ group_manager_mock.addUsersToGroup.assert_has_calls([
+ call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group2"),
+ call(request.authz_token, [f"testuser1@{GATEWAY_ID}"], "group3"),
+ ], any_order=True)
+
+ # user_added_to_group signal should only be called once, with both
+ # groups passed to it
+ user_added_to_group_handler.assert_called_once()
+ args, kwargs = user_added_to_group_handler.call_args
+ self.assertEqual(kwargs["sender"], views.IAMUserViewSet)
+ self.assertEqual(kwargs["user"], user_profile)
+ self.assertSetEqual({"group2", "group3"},
+ {g.id for g in kwargs["groups"]})
+
+ @patch("django_airavata.apps.api.views.iam_admin_client")
+ def test_update_that_does_not_add_user_to_groups(
+ self, iam_admin_client):
+
+ username = "testuser1"
+ url = reverse(
+ 'django_airavata_api:iam-user-profile-detail',
+ kwargs={'user_id': username})
+ data = {
+ "airavataInternalUserId": f"{username}@{GATEWAY_ID}",
+ "userId": username,
+ "gatewayId": GATEWAY_ID,
+ "email": "testuser1@example.com",
+ "firstName": "Test",
+ "lastName": "User1",
+ "airavataUserProfileExists": True,
+ "enabled": True,
+ "emailVerified": True,
+ "groups": [
+ {"id": "group1", "name": "Group 1"},
+ ]
+ }
+ request = self.factory.put(url, data)
+ force_authenticate(request, self.user)
+ request.is_gateway_admin = True
+
+ # Mock api clients
+ iam_user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ iam_admin_client.get_user.return_value = iam_user_profile
+ group_manager_mock = MagicMock(name='group_manager')
+ user_profile_mock = MagicMock(name='user_profile')
+ request.profile_service = {
+ 'group_manager': group_manager_mock,
+ 'user_profile': user_profile_mock,
+ }
+ request.authz_token = "dummy"
+ user_profile_mock.doesUserExist.return_value = True
+ user_profile = UserProfile(
+ airavataInternalUserId=f"testuser1@{GATEWAY_ID}",
+ userId="testuser1",
+ firstName="Test",
+ lastName="User1",
+ emails=["testuser1@example.com"]
+ )
+ user_profile_mock.getUserProfileById.return_value = user_profile
+ group_manager_mock.getAllGroupsUserBelongs.return_value = [
+ GroupModel(id="group1")]
+
+ request.airavata_client = MagicMock(name="airavata_client")
+ request.airavata_client.getGatewayGroups.return_value = GatewayGroups(
+ gatewayId=GATEWAY_ID,
+ adminsGroupId="adminsGroupId",
+ readOnlyAdminsGroupId="readOnlyAdminsGroupId",
+ defaultGatewayUsersGroupId="defaultGatewayUsersGroupId"
+ )
+ request.session = {}
+
+ # Mock signal handler to verify 'user_added_to_group' signal is sent
+ user_added_to_group_handler = MagicMock(
+ name="user_added_to_group_handler")
+ signals.user_added_to_group.connect(
+ user_added_to_group_handler,
+ sender=views.IAMUserViewSet)
+ iam_user_update = views.IAMUserViewSet.as_view({'put': 'update'})
+ response = iam_user_update(request, user_id=username)
+ self.assertEquals(200, response.status_code)
+
+ user_profile_mock.doesUserExist.assert_called_once()
+ group_manager_mock.getAllGroupsUserBelongs.assert_called_once()
+
+ # Since user wasn't added to a group, these all should not have been
+ # called
+ user_profile_mock.getUserProfileById.assert_not_called()
+ group_manager_mock.getGroup.assert_not_called()
+ group_manager_mock.addUsersToGroup.assert_not_called()
+ user_added_to_group_handler.assert_not_called()
diff --git a/django_airavata/apps/api/tus.py b/django_airavata/apps/api/tus.py
index 41d7640..8f6fea1 100644
--- a/django_airavata/apps/api/tus.py
+++ b/django_airavata/apps/api/tus.py
@@ -12,7 +12,7 @@
"""
Move upload identified by upload_url using the provided move_function.
- move_function signature should be (file_path, file_name). It's
+ move_function signature should be (file_path, file_name, file_type). It's
return value will be returned.
"""
# file UUID is last path component in URL. For example:
@@ -34,6 +34,7 @@
with open(upload_info_path) as upload_info_file:
upload_info = json.load(upload_info_file)
filename = upload_info['MetaData']['filename']
- result = move_function(upload_bin_path, filename)
+ filetype = upload_info['MetaData']['filetype']
+ result = move_function(upload_bin_path, filename, filetype)
os.remove(upload_info_path)
return result
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index c5d22cb..174d3b1 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -50,6 +50,7 @@
models,
output_views,
serializers,
+ signals,
thrift_utils,
tus,
view_utils
@@ -87,6 +88,8 @@
group_id = self.request.profile_service['group_manager'].createGroup(
self.authz_token, group)
group.id = group_id
+ users_added_to_group = set(group.members) - {group.ownerId}
+ self._send_users_added_to_group(users_added_to_group, group)
def perform_update(self, serializer):
group = serializer.save()
@@ -94,6 +97,7 @@
if len(group._added_members) > 0:
group_manager_client.addUsersToGroup(
self.authz_token, group._added_members, group.id)
+ self._send_users_added_to_group(group._added_members, group)
if len(group._removed_members) > 0:
group_manager_client.removeUsersFromGroup(
self.authz_token, group._removed_members, group.id)
@@ -110,6 +114,17 @@
group_manager_client.deleteGroup(
self.authz_token, group.id, group.ownerId)
+ def _send_users_added_to_group(self, internal_user_ids, group):
+ for internal_user_id in internal_user_ids:
+ user_id, gateway_id = internal_user_id.rsplit("@", maxsplit=1)
+ user_profile = self.request.profile_service['user_profile'].getUserProfileById(
+ self.authz_token, user_id, gateway_id)
+ signals.user_added_to_group.send(
+ sender=self.__class__,
+ user=user_profile,
+ groups=[group],
+ request=self.request)
+
class ProjectViewSet(APIBackedViewSet):
serializer_class = serializers.ProjectSerializer
@@ -649,6 +664,7 @@
def perform_create(self, serializer):
application_interface = serializer.save()
+ self._update_input_metadata(application_interface)
log.debug("application_interface: {}".format(application_interface))
app_interface_id = self.request.airavata_client.registerApplicationInterface(
self.authz_token, self.gateway_id, application_interface)
@@ -656,6 +672,7 @@
def perform_update(self, serializer):
application_interface = serializer.save()
+ self._update_input_metadata(application_interface)
self.request.airavata_client.updateApplicationInterface(
self.authz_token,
application_interface.applicationInterfaceId,
@@ -665,6 +682,21 @@
self.request.airavata_client.deleteApplicationInterface(
self.authz_token, instance.applicationInterfaceId)
+ def _update_input_metadata(self, app_interface):
+ for app_input in app_interface.applicationInputs:
+ if app_input.metaData:
+ metadata = json.loads(app_input.metaData)
+ # Automatically add {showOptions: {isRequired: true/false}} to
+ # toggle isRequired on hidden/shown inputs
+ if ("editor" in metadata and
+ "dependencies" in metadata["editor"] and
+ "show" in metadata["editor"]["dependencies"]):
+ if "showOptions" not in metadata["editor"]["dependencies"]:
+ metadata["editor"]["dependencies"]["showOptions"] = {}
+ o = metadata["editor"]["dependencies"]["showOptions"]
+ o["isRequired"] = app_input.isRequired
+ app_input.metaData = json.dumps(metadata)
+
@detail_route()
def compute_resources(self, request, app_interface_id):
compute_resources = request.airavata_client.getAvailableAppInterfaceComputeResources(
@@ -911,7 +943,7 @@
try:
input_file = request.FILES['file']
data_product = data_products_helper.save_input_file_upload(
- request, input_file)
+ request, input_file, content_type=input_file.content_type)
serializer = serializers.DataProductSerializer(
data_product, context={'request': request})
return JsonResponse({'uploaded': True,
@@ -927,15 +959,15 @@
def tus_upload_finish(request):
uploadURL = request.POST['uploadURL']
- def move_input_file(file_path, file_name):
+ def move_input_file(file_path, file_name, file_type):
return data_products_helper.move_input_file_upload_from_filepath(
- request, file_path, name=file_name)
+ request, file_path, name=file_name, content_type=file_type)
try:
data_product = tus.move_tus_upload(uploadURL, move_input_file)
serializer = serializers.DataProductSerializer(
data_product, context={'request': request})
return JsonResponse({'uploaded': True,
- 'data-product': serializer.data})
+ 'data-product': serializer.data})
except Exception as e:
return exceptions.generic_json_exception_response(e, status=400)
@@ -960,12 +992,14 @@
.format(data_product_uri), exc_info=True)
raise Http404("data product does not exist") from e
try:
- data_file = data_products_helper.open(request, data_product)
+ data_file = data_products_helper.open_file(request, data_product)
response = FileResponse(data_file, content_type=mime_type)
file_name = os.path.basename(data_file.name)
if mime_type == 'application/octet-stream' or force_download:
response['Content-Disposition'] = ('attachment; filename="{}"'
.format(file_name))
+ else:
+ response['Content-Disposition'] = f'filename="{file_name}"'
return response
except ObjectDoesNotExist as e:
raise Http404(str(e)) from e
@@ -1474,14 +1508,15 @@
if 'file' in request.FILES:
user_file = request.FILES['file']
data_product = data_products_helper.save(
- request, path, user_file)
+ request, path, user_file, content_type=user_file.content_type)
# Handle a tus upload
elif 'uploadURL' in request.POST:
uploadURL = request.POST['uploadURL']
- def move_file(file_path, file_name):
+ def move_file(file_path, file_name, file_type):
return data_products_helper.move_from_filepath(
- request, file_path, path, name=file_name)
+ request, file_path, path, name=file_name,
+ content_type=file_type)
data_product = tus.move_tus_upload(uploadURL, move_file)
return self._create_response(request, path, uploaded=data_product)
@@ -1596,10 +1631,24 @@
def perform_update(self, serializer):
managed_user_profile = serializer.save()
group_manager_client = self.request.profile_service['group_manager']
+ user_profile_client = self.request.profile_service['user_profile']
user_id = managed_user_profile['airavataInternalUserId']
+ added_groups = []
for group_id in managed_user_profile['_added_group_ids']:
+ group = group_manager_client.getGroup(self.authz_token, group_id)
group_manager_client.addUsersToGroup(
self.authz_token, [user_id], group_id)
+ added_groups.append(group)
+ if len(added_groups) > 0:
+ user_profile = user_profile_client.getUserProfileById(
+ self.authz_token,
+ managed_user_profile['userId'],
+ settings.GATEWAY_ID)
+ signals.user_added_to_group.send(
+ sender=self.__class__,
+ user=user_profile,
+ groups=added_groups,
+ request=self.request)
for group_id in managed_user_profile['_removed_group_ids']:
group_manager_client.removeUsersFromGroup(
self.authz_token, [user_id], group_id)
diff --git a/django_airavata/apps/auth/apps.py b/django_airavata/apps/auth/apps.py
index 1e103a4..cca02f9 100644
--- a/django_airavata/apps/auth/apps.py
+++ b/django_airavata/apps/auth/apps.py
@@ -4,3 +4,6 @@
class AuthConfig(AppConfig):
name = 'django_airavata.apps.auth'
label = 'django_airavata_auth'
+
+ def ready(self):
+ from . import signals # noqa
diff --git a/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
new file mode 100644
index 0000000..eeb4b99
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0005_auto_20191211_2011.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.21 on 2019-12-11 20:11
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+from django_airavata.apps.auth.models import USER_ADDED_TO_GROUP_TEMPLATE
+
+
+def default_templates(apps, schema_editor):
+
+ EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+ user_added_to_group_template = EmailTemplate(
+ template_type=USER_ADDED_TO_GROUP_TEMPLATE,
+ subject="You've been added to group{{ group_names|length|pluralize }} [{{group_names|join:'] and ['}}] in {{portal_title}}",
+ body="""
+ <p>
+ Dear {{first_name}} {{last_name}},
+ </p>
+
+ <p>
+ Your user account (username {{username}}) has been added to the
+ group{{ group_names|length|pluralize }} {{group_names|join:' and '}}.
+ {{portal_title}} uses groups to share applications and experiments.
+ </p>
+
+ <p>
+ You may have access to additional applications now that you are a
+ member of {{group_names|join:' and '}}. To check what applications you
+ have access to, please check: <a href="{{dashboard_url}}">{{dashboard_url}}</a>.
+ </p>
+
+ <p>
+ You may also have access to additional experiments. To check what
+ experiments you have access to, please check: <a
+ href="{{experiments_url}}">{{experiments_url}}</a>.
+ </p>
+
+ <p>
+ Please let us know if you have any questions. Thanks.
+ </p>
+ """.strip())
+ user_added_to_group_template.save()
+
+
+def delete_default_templates(apps, schema_editor):
+ EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+ EmailTemplate.objects.filter(
+ template_type=USER_ADDED_TO_GROUP_TEMPLATE).delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_auth', '0004_password_reset_request'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='emailtemplate',
+ name='template_type',
+ field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template')], primary_key=True, serialize=False),
+ ),
+ migrations.RunPython(default_templates,
+ reverse_code=delete_default_templates)
+ ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 5e4d109..cfea8c1 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -5,6 +5,7 @@
VERIFY_EMAIL_TEMPLATE = 1
NEW_USER_EMAIL_TEMPLATE = 2
PASSWORD_RESET_EMAIL_TEMPLATE = 3
+USER_ADDED_TO_GROUP_TEMPLATE = 4
class EmailVerification(models.Model):
@@ -20,6 +21,7 @@
(VERIFY_EMAIL_TEMPLATE, 'Verify Email Template'),
(NEW_USER_EMAIL_TEMPLATE, 'New User Email Template'),
(PASSWORD_RESET_EMAIL_TEMPLATE, 'Password Reset Email Template'),
+ (USER_ADDED_TO_GROUP_TEMPLATE, 'User Added to Group Template'),
)
template_type = models.IntegerField(
primary_key=True, choices=TEMPLATE_TYPE_CHOICES)
diff --git a/django_airavata/apps/auth/signals.py b/django_airavata/apps/auth/signals.py
new file mode 100644
index 0000000..32f3cb0
--- /dev/null
+++ b/django_airavata/apps/auth/signals.py
@@ -0,0 +1,25 @@
+from django.conf import settings
+from django.dispatch import receiver
+from django.shortcuts import reverse
+from django.template import Context
+
+from django_airavata.apps.api.signals import user_added_to_group
+
+from . import models, utils
+
+
+@receiver(user_added_to_group, dispatch_uid="auth_email_user_added_to_group")
+def email_user_added_to_group(sender, user, groups, request, **kwargs):
+ context = Context({
+ "email": user.emails[0],
+ "first_name": user.firstName,
+ "last_name": user.lastName,
+ "username": user.userId,
+ "portal_title": settings.PORTAL_TITLE,
+ "dashboard_url": request.build_absolute_uri(
+ reverse("django_airavata_workspace:dashboard")),
+ "experiments_url": request.build_absolute_uri(
+ reverse("django_airavata_workspace:experiments")),
+ "group_names": [g.name for g in groups]
+ })
+ utils.send_email_to_user(models.USER_ADDED_TO_GROUP_TEMPLATE, context)
diff --git a/django_airavata/apps/auth/tests.py b/django_airavata/apps/auth/tests.py
index 4929020..2dbcc6a 100644
--- a/django_airavata/apps/auth/tests.py
+++ b/django_airavata/apps/auth/tests.py
@@ -1,2 +1,82 @@
+from django.core import mail
+from django.shortcuts import reverse
+from django.test import RequestFactory, TestCase, override_settings
-# Create your tests here.
+from airavata.model.group.ttypes import GroupModel
+from airavata.model.user.ttypes import UserProfile
+from django_airavata.apps.api.signals import user_added_to_group
+
+from . import signals # noqa
+
+GATEWAY_ID = "test-gateway"
+SERVER_EMAIL = "admin@test-gateway.com"
+PORTAL_TITLE = "Test Gateway"
+PORTAL_ADMINS = [('Portal Admin', 'admin@test-gateway.com')]
+
+
+@override_settings(
+ GATEWAY_ID=GATEWAY_ID,
+ SERVER_EMAIL=SERVER_EMAIL,
+ PORTAL_TITLE=PORTAL_TITLE,
+ PORTAL_ADMINS=PORTAL_ADMINS
+)
+class EmailUserAddedToGroupSignalReceiverTests(TestCase):
+
+ def setUp(self):
+ factory = RequestFactory()
+ self.request = factory.get("/")
+ self.user = UserProfile(
+ airavataInternalUserId=f"testuser@{GATEWAY_ID}",
+ userId="testuser",
+ gatewayId=GATEWAY_ID,
+ emails=["testuser@example.com"],
+ firstName="Test",
+ lastName="User")
+
+ def test(self):
+ group = GroupModel(id="abc123", name="Test Group")
+ user_added_to_group.send(None,
+ user=self.user,
+ groups=[group],
+ request=self.request)
+ self.assertEqual(len(mail.outbox), 1)
+ msg = mail.outbox[0]
+ self._assert_common_email_attributes(msg)
+ self.assertEqual(msg.subject,
+ f"You've been added to group "
+ f"[{group.name}] in {PORTAL_TITLE}")
+
+ def test_multiple_groups(self):
+ group1 = GroupModel(id="abc123", name="Test Group")
+ group2 = GroupModel(id="group2", name="Group 2")
+ user_added_to_group.send(None,
+ user=self.user,
+ groups=[group1, group2],
+ request=self.request)
+ self.assertEqual(len(mail.outbox), 1)
+ msg = mail.outbox[0]
+ self._assert_common_email_attributes(msg)
+ self.assertEqual(msg.subject,
+ f"You've been added to groups "
+ f"[{group1.name}] and [{group2.name}] "
+ f"in {PORTAL_TITLE}")
+ self.assertIn("groups Test Group and Group 2", msg.body)
+
+ def _assert_common_email_attributes(self, msg):
+ self.assertEqual(msg.from_email,
+ f"\"{PORTAL_TITLE}\" <{SERVER_EMAIL}>")
+ self.assertEqual(
+ msg.reply_to,
+ [f"\"{PORTAL_ADMINS[0][0]}\" <{PORTAL_ADMINS[0][1]}>"])
+ self.assertSequenceEqual(
+ msg.to, [f"\"{self.user.firstName} {self.user.lastName}\" "
+ f"<{self.user.emails[0]}>"])
+ self.assertIn(
+ self.request.build_absolute_uri(
+ reverse("django_airavata_workspace:dashboard")),
+ msg.body)
+ self.assertIn(
+ self.request.build_absolute_uri(
+ reverse("django_airavata_workspace:experiments")),
+ msg.body)
+ self.assertIn(self.user.userId, msg.body)
diff --git a/django_airavata/apps/auth/utils.py b/django_airavata/apps/auth/utils.py
index e8ce6d0..0c35a3b 100644
--- a/django_airavata/apps/auth/utils.py
+++ b/django_airavata/apps/auth/utils.py
@@ -100,3 +100,21 @@
to=[a[1] for a in settings.PORTAL_ADMINS])
msg.content_subtype = 'html'
msg.send()
+
+
+def send_email_to_user(template_id, context):
+ email_template = models.EmailTemplate.objects.get(pk=template_id)
+ subject = Template(email_template.subject).render(context)
+ body = Template(email_template.body).render(context)
+ msg = EmailMessage(
+ subject=subject,
+ body=body,
+ from_email="\"{}\" <{}>".format(settings.PORTAL_TITLE,
+ settings.SERVER_EMAIL),
+ to=["\"{} {}\" <{}>".format(context['first_name'],
+ context['last_name'],
+ context['email'])],
+ reply_to=[f"\"{a[0]}\" <{a[1]}>" for a in settings.PORTAL_ADMINS]
+ )
+ msg.content_subtype = 'html'
+ msg.send()
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index 47fddb9..5da2a2e 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -7,12 +7,12 @@
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.core.exceptions import ObjectDoesNotExist
-from django.core.mail import EmailMessage
from django.forms import ValidationError
from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import redirect, render, resolve_url
-from django.template import Context, Template
+from django.template import Context
from django.urls import reverse
+from django.views.decorators.debug import sensitive_variables
from requests_oauthlib import OAuth2Session
from . import forms, iam_admin_client, models, utils
@@ -69,6 +69,7 @@
raise Exception("idp_alias is not valid")
+@sensitive_variables('password')
def handle_login(request):
username = request.POST['username']
password = request.POST['password']
@@ -151,6 +152,7 @@
})
+@sensitive_variables('password')
def create_account(request):
if request.method == 'POST':
form = forms.CreateAccountForm(request.POST)
@@ -305,7 +307,7 @@
"portal_title": settings.PORTAL_TITLE,
"url": verification_uri,
})
- _send_email_to_user(models.VERIFY_EMAIL_TEMPLATE, context)
+ utils.send_email_to_user(models.VERIFY_EMAIL_TEMPLATE, context)
def forgot_password(request):
@@ -371,9 +373,10 @@
"portal_title": settings.PORTAL_TITLE,
"url": verification_uri,
})
- _send_email_to_user(models.PASSWORD_RESET_EMAIL_TEMPLATE, context)
+ utils.send_email_to_user(models.PASSWORD_RESET_EMAIL_TEMPLATE, context)
+@sensitive_variables('password')
def reset_password(request, code):
try:
password_reset_request = models.PasswordResetRequest.objects.get(
@@ -423,23 +426,6 @@
})
-def _send_email_to_user(template_id, context):
- email_template = models.EmailTemplate.objects.get(
- pk=template_id)
- subject = Template(email_template.subject).render(context)
- body = Template(email_template.body).render(context)
- msg = EmailMessage(
- subject=subject,
- body=body,
- from_email="{} <{}>".format(settings.PORTAL_TITLE,
- settings.SERVER_EMAIL),
- to=["{} {} <{}>".format(context['first_name'],
- context['last_name'],
- context['email'])])
- msg.content_subtype = 'html'
- msg.send()
-
-
def login_desktop(request):
context = {
'options': settings.AUTHENTICATION_OPTIONS,
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue
index b8af3e9..84087f6 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue
@@ -1,29 +1,35 @@
<template>
-
<div>
<div class="row">
<div class="col">
- <div
- class="card border-default"
- :class="{ 'border-danger': !valid }"
- >
+ <div class="card border-default" :class="{ 'border-danger': !valid }">
<b-link
@click="showConfiguration = !showConfiguration"
class="card-link text-dark"
>
<div class="card-body">
- <h5 class="card-title mb-4">Settings for queue {{ localComputationalResourceScheduling.queueName }}</h5>
+ <h5 class="card-title mb-4">
+ Settings for queue
+ {{ localComputationalResourceScheduling.queueName }}
+ </h5>
<div class="row">
<div class="col">
- <h3 class="h5 mb-0">{{ localComputationalResourceScheduling.nodeCount }}</h3>
+ <h3 class="h5 mb-0">
+ {{ localComputationalResourceScheduling.nodeCount }}
+ </h3>
<span class="text-muted text-uppercase">NODE COUNT</span>
</div>
<div class="col">
- <h3 class="h5 mb-0">{{ localComputationalResourceScheduling.totalCPUCount }}</h3>
+ <h3 class="h5 mb-0">
+ {{ localComputationalResourceScheduling.totalCPUCount }}
+ </h3>
<span class="text-muted text-uppercase">CORE COUNT</span>
</div>
<div class="col">
- <h3 class="h5 mb-0">{{ localComputationalResourceScheduling.wallTimeLimit }} minutes</h3>
+ <h3 class="h5 mb-0">
+ {{ localComputationalResourceScheduling.wallTimeLimit }}
+ minutes
+ </h3>
<span class="text-muted text-uppercase">TIME LIMIT</span>
</div>
</div>
@@ -51,7 +57,7 @@
>
</b-form-select>
<div slot="description">
- {{ selectedQueueDefault.queueDescription }}
+ {{ queueDescription }}
</div>
</b-form-group>
<b-form-group
@@ -71,10 +77,7 @@
>
</b-form-input>
<div slot="description">
- <i
- class="fa fa-info-circle"
- aria-hidden="true"
- ></i>
+ <i class="fa fa-info-circle" aria-hidden="true"></i>
Max Allowed Nodes = {{ maxNodes }}
</div>
</b-form-group>
@@ -95,10 +98,7 @@
>
</b-form-input>
<div slot="description">
- <i
- class="fa fa-info-circle"
- aria-hidden="true"
- ></i>
+ <i class="fa fa-info-circle" aria-hidden="true"></i>
Max Allowed Cores = {{ maxCPUCount }}
</div>
</b-form-group>
@@ -121,10 +121,7 @@
</b-form-input>
</b-input-group>
<div slot="description">
- <i
- class="fa fa-info-circle"
- aria-hidden="true"
- ></i>
+ <i class="fa fa-info-circle" aria-hidden="true"></i>
Max Allowed Wall Time = {{ maxWalltime }} minutes
</div>
</b-form-group>
@@ -134,11 +131,9 @@
href="#"
@click.prevent="showConfiguration = false"
>
- <i
- class="fa fa-times text-secondary"
- aria-hidden="true"
- ></i>
- Hide Settings</a>
+ <i class="fa fa-times text-secondary" aria-hidden="true"></i>
+ Hide Settings</a
+ >
</div>
</div>
</div>
@@ -172,8 +167,8 @@
},
data() {
return {
- queueDefaults: [],
- showConfiguration: false
+ showConfiguration: false,
+ appDeploymentQueues: null
};
},
computed: {
@@ -197,6 +192,9 @@
);
},
maxCPUCount: function() {
+ if (!this.selectedQueueDefault) {
+ return 0;
+ }
const batchQueueResourcePolicy = this.getBatchQueueResourcePolicy(
this.selectedQueueDefault.queueName
);
@@ -209,6 +207,9 @@
return this.selectedQueueDefault.maxProcessors;
},
maxNodes: function() {
+ if (!this.selectedQueueDefault) {
+ return 0;
+ }
const batchQueueResourcePolicy = this.getBatchQueueResourcePolicy(
this.selectedQueueDefault.queueName
);
@@ -221,6 +222,9 @@
return this.selectedQueueDefault.maxNodes;
},
maxWalltime: function() {
+ if (!this.selectedQueueDefault) {
+ return 0;
+ }
const batchQueueResourcePolicy = this.getBatchQueueResourcePolicy(
this.selectedQueueDefault.queueName
);
@@ -232,6 +236,27 @@
}
return this.selectedQueueDefault.maxRunTime;
},
+ queueDefaults() {
+ return this.appDeploymentQueues
+ ? this.appDeploymentQueues
+ .filter(q => this.isQueueInComputeResourcePolicy(q.queueName))
+ .sort((a, b) => {
+ // Sort default first, then by alphabetically by name
+ if (a.isDefaultQueue) {
+ return -1;
+ } else if (b.isDefaultQueue) {
+ return 1;
+ } else {
+ return a.queueName.localeCompare(b.queueName);
+ }
+ })
+ : [];
+ },
+ queueDescription() {
+ return this.selectedQueueDefault
+ ? this.selectedQueueDefault.queueDescription
+ : null;
+ },
validation() {
// Don't run validation if we don't have selectedQueueDefault
if (!this.selectedQueueDefault) {
@@ -262,41 +287,28 @@
this.$emit("valid");
}
},
- loadQueueDefaults: function(updateQueueSettings) {
+ loadAppDeploymentQueues() {
return services.ApplicationDeploymentService.getQueues({
lookup: this.appDeploymentId
- }).then(queueDefaults => {
- // Sort queue defaults
- this.queueDefaults = queueDefaults
- .filter(q => this.isQueueInComputeResourcePolicy(q.queueName))
- .sort((a, b) => {
- // Sort default first, then by alphabetically by name
- if (a.isDefaultQueue) {
- return -1;
- } else if (b.isDefaultQueue) {
- return 1;
- } else {
- return a.queueName.localeCompare(b.queueName);
- }
- });
+ }).then(queueDefaults => (this.appDeploymentQueues = queueDefaults));
+ },
+ setDefaultQueue() {
+ if (this.queueDefaults.length === 0) {
+ return;
+ }
+ const defaultQueue = this.queueDefaults[0];
- if (updateQueueSettings) {
- // Find the default queue and apply it's settings
- const defaultQueue = this.queueDefaults[0];
-
- this.localComputationalResourceScheduling.queueName =
- defaultQueue.queueName;
- this.localComputationalResourceScheduling.totalCPUCount = this.getDefaultCPUCount(
- defaultQueue
- );
- this.localComputationalResourceScheduling.nodeCount = this.getDefaultNodeCount(
- defaultQueue
- );
- this.localComputationalResourceScheduling.wallTimeLimit = this.getDefaultWalltime(
- defaultQueue
- );
- }
- });
+ this.localComputationalResourceScheduling.queueName =
+ defaultQueue.queueName;
+ this.localComputationalResourceScheduling.totalCPUCount = this.getDefaultCPUCount(
+ defaultQueue
+ );
+ this.localComputationalResourceScheduling.nodeCount = this.getDefaultNodeCount(
+ defaultQueue
+ );
+ this.localComputationalResourceScheduling.wallTimeLimit = this.getDefaultWalltime(
+ defaultQueue
+ );
},
isQueueInComputeResourcePolicy: function(queueName) {
if (!this.computeResourcePolicy) {
@@ -363,19 +375,27 @@
}
},
watch: {
- appDeploymentId: function() {
- this.loadQueueDefaults(true);
+ appDeploymentId() {
+ this.loadAppDeploymentQueues().then(() => this.setDefaultQueue());
+ },
+ computeResourcePolicy() {
+ this.setDefaultQueue();
+ },
+ batchQueueResourcePolicies() {
+ this.setDefaultQueue();
}
},
mounted: function() {
- // For brand new queue settings (no queueName specified) load the default
- // queue and its default values and apply them
- const updateQueueSettings = !this.value.queueName;
- this.loadQueueDefaults(updateQueueSettings).then(() => this.validate());
+ this.loadAppDeploymentQueues().then(() => {
+ // For brand new queue settings (no queueName specified) load the default
+ // queue and its default values and apply them
+ if (!this.value.queueName) {
+ this.setDefaultQueue();
+ }
+ });
this.$on("input", () => this.validate());
}
};
</script>
-<style>
-</style>
+<style></style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
index 52c0c39..3dcf631 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
@@ -8,8 +8,9 @@
class="mr-auto"
:data-product="dataProduct"
:input-file="true"
+ :open-in-new-window="true"
/>
- <b-link @click="viewFile">
+ <b-link @click="viewFile" v-if="isViewable">
View File <i class="fa fa-eye"></i>
<span class="sr-only">View file</span>
</b-link>
@@ -85,6 +86,9 @@
} else {
return [];
}
+ },
+ isViewable() {
+ return this.dataProduct.isText;
}
},
data() {
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
index 3351fd1..627935f 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
@@ -22,7 +22,7 @@
v-else
:href="data.item.downloadURL"
:target="downloadTarget"
- > <i class="fa fa-download"></i> {{ data.item.name }}</b-link>
+ > {{ data.item.name }}</b-link>
</template>
<template
slot="createdTimestamp"
diff --git a/django_airavata/context_processors.py b/django_airavata/context_processors.py
index e98666f..866bd63 100644
--- a/django_airavata/context_processors.py
+++ b/django_airavata/context_processors.py
@@ -158,3 +158,9 @@
def resolver_match(request):
"""Put resolver_match (ResolverMatch instance) into the context."""
return {'resolver_match': request.resolver_match}
+
+
+def google_analytics_tracking_id(request):
+ """Put the Google Analytics tracking id into context."""
+ return {'ga_tracking_id':
+ getattr(settings, 'GOOGLE_ANALYTICS_TRACKING_ID', None)}
diff --git a/django_airavata/settings.py b/django_airavata/settings.py
index 9efe750..2b023a4 100644
--- a/django_airavata/settings.py
+++ b/django_airavata/settings.py
@@ -144,6 +144,7 @@
'django_airavata.context_processors.custom_app_registry',
'django_airavata.context_processors.get_notifications',
'django_airavata.context_processors.user_session_data',
+ 'django_airavata.context_processors.google_analytics_tracking_id',
# 'django_airavata.context_processors.resolver_match',
],
},
@@ -235,6 +236,7 @@
),
'EXCEPTION_HANDLER':
'django_airavata.apps.api.exceptions.custom_exception_handler',
+ 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
AUTHENTICATION_BACKENDS = [
diff --git a/django_airavata/settings_local.py.sample b/django_airavata/settings_local.py.sample
index 65358f4..da98351 100644
--- a/django_airavata/settings_local.py.sample
+++ b/django_airavata/settings_local.py.sample
@@ -106,6 +106,10 @@
# Legacy (PGA) Portal link - uncomment to provide a link to the legacy portal
# PGA_URL = '...'
+# Google Analytics Tracking ID ("UA-XXXXXXXX-X"). If this setting is set, then
+# Google Analytics tracking will be added to all pages.
+# GOOGLE_ANALYTICS_TRACKING_ID = '...'
+
# Logging configuration. Uncomment following to override default log configuration
# LOGGING = {
# 'version': 1,
diff --git a/django_airavata/static/common/js/components/DataProductViewer.vue b/django_airavata/static/common/js/components/DataProductViewer.vue
index 52f35f7..788ca79 100644
--- a/django_airavata/static/common/js/components/DataProductViewer.vue
+++ b/django_airavata/static/common/js/components/DataProductViewer.vue
@@ -1,8 +1,7 @@
<template>
<span v-if="downloadURL">
- <a :href="downloadURL" class="action-link">
- <i class="fa fa-download"></i>
+ <a :href="downloadURL" class="action-link" :target="linkTarget">
{{ filename }}
</a>
</span>
@@ -24,6 +23,10 @@
},
mimeType: {
type: String
+ },
+ openInNewWindow: {
+ type: Boolean,
+ default: false
}
},
computed: {
@@ -45,6 +48,9 @@
} else {
return this.dataProduct.downloadURL;
}
+ },
+ linkTarget() {
+ return this.openInNewWindow ? "_blank": "_self";
}
}
};
diff --git a/django_airavata/templates/base.html b/django_airavata/templates/base.html
index a3e3266..09778e1 100644
--- a/django_airavata/templates/base.html
+++ b/django_airavata/templates/base.html
@@ -4,6 +4,8 @@
<!DOCTYPE html>
<head>
+ {% include "./django_airavata/google_analytics.html" %}
+
{% render_bundle 'chunk-vendors' 'css' 'COMMON' %}
{% render_bundle 'app' 'css' 'COMMON' %}
{% block css %}
diff --git a/django_airavata/templates/django_airavata/google_analytics.html b/django_airavata/templates/django_airavata/google_analytics.html
new file mode 100644
index 0000000..ee990ca
--- /dev/null
+++ b/django_airavata/templates/django_airavata/google_analytics.html
@@ -0,0 +1,11 @@
+{% if ga_tracking_id %}
+<!-- Global site tag (gtag.js) - Google Analytics -->
+<script async src="https://www.googletagmanager.com/gtag/js?id={{ ga_tracking_id }}"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+
+ gtag('config', '{{ ga_tracking_id }}');
+</script>
+{% endif %}
diff --git a/django_airavata/templates/includes/head.html b/django_airavata/templates/includes/head.html
index 2e3febe..2b605c6 100644
--- a/django_airavata/templates/includes/head.html
+++ b/django_airavata/templates/includes/head.html
@@ -18,6 +18,8 @@
<meta name="description" content="{% if self.search_description %}{{ self.search_description }}{% endif %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
+ {% include "../django_airavata/google_analytics.html" %}
+
<!-- Bootstrap Core + Font awesome CSS (see common/js/cms.js) -->
{% render_bundle 'chunk-vendors' 'css' 'COMMON' %}