Merge branch 'AIRAVATA-3654' into develop
diff --git a/README.md b/README.md
index d0623ed..5f776a2 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,9 @@
[the most recent LTS version of Node.js](https://nodejs.org/en/download/). You
can also use [nvm](https://github.com/nvm-sh/nvm) to manage the Node.js install.
If you have nvm installed you can run `nvm install && nvm use` before running
-any yarn commands. See [the Yarn package manager](https://yarnpkg.com) for
-information on how to install yarn.
+any yarn commands. See
+[the Yarn package manager](https://classic.yarnpkg.com/lang/en/) for information
+on how to install Yarn 1 (Classic).
1. Checkout this project and create a virtual environment.
diff --git a/django_airavata/apps/admin/apps.py b/django_airavata/apps/admin/apps.py
index 82876b9..a9a6642 100644
--- a/django_airavata/apps/admin/apps.py
+++ b/django_airavata/apps/admin/apps.py
@@ -25,7 +25,7 @@
'label': 'Manage Users',
'icon': 'fa fa-users',
'url': 'django_airavata_admin:users',
- 'active_prefixes': ['users'],
+ 'active_prefixes': ['users', 'extended-user-profile'],
'enabled': lambda req: (req.is_gateway_admin or
req.is_read_only_gateway_admin),
},
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 417bc44..e87e4f1 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
@@ -66,15 +66,29 @@
:disabled="readonly"
></b-form-input>
</b-form-group>
+ <b-form-group
+ class="flex-fill"
+ label="Required on Command Line"
+ :label-for="id + '-required-command-line'"
+ description="Add this input's value to the command line in the generated job script."
+ >
+ <b-form-radio-group
+ :id="id + '-required-command-line'"
+ v-model="data.requiredToAddedToCommandLine"
+ :options="trueFalseOptions"
+ :disabled="readonly"
+ >
+ </b-form-radio-group>
+ </b-form-group>
<div class="d-flex">
<b-form-group
class="flex-fill"
- label="Standard Input"
- :label-for="id + '-standard-input'"
+ label="Required"
+ :label-for="id + '-required'"
>
<b-form-radio-group
- :id="id + '-standard-input'"
- v-model="data.standardInput"
+ :id="id + '-required'"
+ v-model="data.isRequired"
:options="trueFalseOptions"
:disabled="readonly"
>
@@ -105,47 +119,6 @@
:disabled="readonly"
/>
</b-form-group>
- <div class="d-flex">
- <b-form-group
- class="flex-fill"
- label="Data is staged"
- :label-for="id + '-data-staged'"
- >
- <b-form-radio-group
- :id="id + '-data-staged'"
- v-model="data.dataStaged"
- :options="trueFalseOptions"
- :disabled="readonly"
- >
- </b-form-radio-group>
- </b-form-group>
- <b-form-group
- class="flex-fill"
- label="Required"
- :label-for="id + '-required'"
- >
- <b-form-radio-group
- :id="id + '-required'"
- v-model="data.isRequired"
- :options="trueFalseOptions"
- :disabled="readonly"
- >
- </b-form-radio-group>
- </b-form-group>
- </div>
- <b-form-group
- class="flex-fill"
- label="Required on Command Line"
- :label-for="id + '-required-command-line'"
- >
- <b-form-radio-group
- :id="id + '-required-command-line'"
- v-model="data.requiredToAddedToCommandLine"
- :options="trueFalseOptions"
- :disabled="readonly"
- >
- </b-form-radio-group>
- </b-form-group>
<b-form-group
label="Advanced Input Field Modification Metadata"
:label-for="id + '-metadata'"
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 c33c8c4..3114370 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
@@ -37,6 +37,22 @@
cores, walltime limit).
</div>
</b-form-group>
+ <b-form-group
+ label="Queue Settings Calculator"
+ description="Select function to automatically compute queue settings."
+ >
+ <b-form-select
+ v-model="data.queueSettingsCalculatorId"
+ :options="queueSettingsCalculatorOptions"
+ :disabled="queueSettingsCalculatorOptions.length === 0"
+ >
+ <template slot="first">
+ <option :value="null">
+ If applicable, select a queue settings calculator
+ </option>
+ </template>
+ </b-form-select>
+ </b-form-group>
</div>
</div>
<div class="w-100">
@@ -45,9 +61,13 @@
label-for="application-description"
>
<b-form-textarea
- id="application-description" :rows="5"
+ id="application-description"
+ :rows="5"
v-model="data.applicationDescription"
- :state="!data.applicationDescription || data.applicationDescription.length < 500"
+ :state="
+ !data.applicationDescription ||
+ data.applicationDescription.length < 500
+ "
>
</b-form-textarea>
<b-form-valid-feedback v-if="!!data.applicationDescription">
@@ -120,8 +140,8 @@
</template>
<script>
-import {models} from "django-airavata-api";
-import {mixins} from "django-airavata-common-ui";
+import { models, services } from "django-airavata-api";
+import { mixins } from "django-airavata-common-ui";
import ApplicationInputFieldEditor from "./ApplicationInputFieldEditor.vue";
import ApplicationOutputFieldEditor from "./ApplicationOutputFieldEditor.vue";
@@ -144,13 +164,28 @@
ApplicationOutputFieldEditor,
draggable,
},
+ created() {
+ this.loadQueueSettingsCalculators();
+ },
computed: {
trueFalseOptions() {
return [
- {text: "True", value: true},
- {text: "False", value: false},
+ { text: "True", value: true },
+ { text: "False", value: false },
];
},
+ queueSettingsCalculatorOptions() {
+ if (this.queueSettingsCalculators) {
+ return this.queueSettingsCalculators.map((qsc) => {
+ return {
+ text: qsc.name,
+ value: qsc.id,
+ };
+ });
+ } else {
+ return [];
+ }
+ },
},
data() {
return {
@@ -160,6 +195,7 @@
handle: ".drag-handle",
},
collapseApplicationInputs: false,
+ queueSettingsCalculators: null,
};
},
methods: {
@@ -209,6 +245,9 @@
onDragEnd() {
this.collapseApplicationInputs = false;
},
+ async loadQueueSettingsCalculators() {
+ this.queueSettingsCalculators = await services.QueueSettingsCalculatorService.list();
+ },
},
};
</script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfileContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfileContainer.vue
new file mode 100644
index 0000000..8a38d1b
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfileContainer.vue
@@ -0,0 +1,120 @@
+<template>
+ <div class="has-fixed-footer">
+ <div class="row mb-2">
+ <div class="col-auto mr-auto">
+ <h1 class="h4">Extended User Profile Editor</h1>
+ <p class="text-muted small">
+ Add and edit additional user profile fields for gateway users to
+ complete.
+ </p>
+ </div>
+ </div>
+ <transition-group name="fade">
+ <div
+ v-for="field in extendedUserProfileFields"
+ class="row"
+ :key="field.key"
+ >
+ <div class="col">
+ <extended-user-profile-field-editor
+ :extendedUserProfileField="field"
+ @valid="recordValidChildComponent(field)"
+ @invalid="recordInvalidChildComponent(field)"
+ />
+ </div>
+ </div>
+ </transition-group>
+ <div ref="bottom" />
+ <div class="fixed-footer">
+ <b-dropdown text="Add Field">
+ <b-dropdown-item @click="addField('text')">Text</b-dropdown-item>
+ <b-dropdown-item @click="addField('single_choice')"
+ >Single Choice</b-dropdown-item
+ >
+ <b-dropdown-item @click="addField('multi_choice')"
+ >Multi Choice</b-dropdown-item
+ >
+ <b-dropdown-item @click="addField('user_agreement')"
+ >User Agreement</b-dropdown-item
+ >
+ </b-dropdown>
+ <b-button variant="primary" @click="save" :disabled="!valid" class="ml-2"
+ >Save</b-button
+ >
+ </div>
+ </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from "vuex";
+import ExtendedUserProfileFieldEditor from "./field-editors/ExtendedUserProfileFieldEditor.vue";
+import { mixins } from "django-airavata-common-ui";
+export default {
+ mixins: [mixins.ValidationParent],
+ components: { ExtendedUserProfileFieldEditor },
+ data() {
+ return {};
+ },
+ created() {
+ this.loadExtendedUserProfileFields();
+ },
+ methods: {
+ ...mapActions("extendedUserProfile", [
+ "loadExtendedUserProfileFields",
+ "saveExtendedUserProfileFields",
+ "addExtendedUserProfileField",
+ ]),
+ addField(field_type) {
+ this.addExtendedUserProfileField({ field_type });
+ this.$nextTick(() => {
+ this.$refs.bottom.scrollIntoView();
+ });
+ },
+ addOption(field) {
+ if (!field.options) {
+ field.options = [];
+ }
+ field.options.push({ id: null, name: "" });
+ },
+ deleteOption(field, option) {
+ const i = field.options.indexOf(option);
+ field.options.splice(i, 1);
+ },
+ addLink(field) {
+ if (!field.links) {
+ field.links = [];
+ }
+ field.links.push({
+ id: null,
+ url: "",
+ title: "",
+ display_link: true,
+ display_inline: false,
+ });
+ },
+ addConditional(field) {
+ if (!field.conditional) {
+ field.conditional = {
+ id: null,
+ conditions: [],
+ require_when: true,
+ show_when: true,
+ };
+ }
+ },
+ deleteLink(field, link) {
+ const i = field.links.indexOf(link);
+ field.links.splice(i, 1);
+ },
+ save() {
+ this.saveExtendedUserProfileFields();
+ },
+ },
+ computed: {
+ ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+ valid() {
+ return this.childComponentsAreValid;
+ },
+ },
+};
+</script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
new file mode 100644
index 0000000..c3e376c
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
@@ -0,0 +1,82 @@
+<template>
+ <b-card header="Extended User Profile">
+ <b-table :items="items" :fields="fields" small borderless>
+ <template #cell(value)="{ value, item }">
+ <!-- only show a valid checkmark when there is a user provided value -->
+ <i v-if="value && item.valid" class="fas fa-check text-success"></i>
+ <i v-if="!item.valid" class="fas fa-times text-danger"></i>
+ <template v-if="Array.isArray(value)">
+ <ul>
+ <li v-for="result in value" :key="result">
+ {{ result }}
+ </li>
+ </ul>
+ </template>
+ <template v-else> {{ value }} </template>
+ </template>
+ </b-table>
+ </b-card>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+import { mapActions, mapGetters } from "vuex";
+export default {
+ props: {
+ iamUserProfile: {
+ type: models.IAMUserProfile,
+ required: true,
+ },
+ },
+ created() {
+ this.loadExtendedUserProfileFields();
+ this.loadExtendedUserProfileValues({
+ username: this.iamUserProfile.userId,
+ });
+ },
+ computed: {
+ ...mapGetters("extendedUserProfile", [
+ "extendedUserProfileFields",
+ "extendedUserProfileValues",
+ ]),
+ fields() {
+ return ["name", "value"];
+ },
+ items() {
+ if (this.extendedUserProfileFields && this.extendedUserProfileValues) {
+ const items = [];
+ for (const field of this.extendedUserProfileFields) {
+ const value = this.getValue(field);
+ items.push({
+ name: field.name,
+ value: value ? value.value_display : null,
+ // if no value, consider it invalid only if it is required
+ valid: value ? value.valid : !field.required,
+ });
+ }
+ return items;
+ } else {
+ return [];
+ }
+ },
+ },
+ methods: {
+ ...mapActions("extendedUserProfile", [
+ "loadExtendedUserProfileFields",
+ "loadExtendedUserProfileValues",
+ ]),
+ getValue(field) {
+ return this.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === field.id
+ );
+ },
+ },
+};
+</script>
+
+<style scoped>
+ul {
+ display: inline-block;
+ padding-left: 20px;
+}
+</style>
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 bc27702..ed0f762 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
@@ -25,6 +25,7 @@
@save="groupsUpdated"
/>
<user-profile-panel :iamUserProfile="iamUserProfile" />
+ <extended-user-profile-panel :iamUserProfile="iamUserProfile" />
<external-idp-user-info-panel
v-if="hasExternalIDPUserInfo"
:externalIDPUserInfo="localIAMUserProfile.externalIDPUserInfo"
@@ -78,6 +79,7 @@
import EditGroupsPanel from "./EditGroupsPanel.vue";
import ExternalIDPUserInfoPanel from "./ExternalIDPUserInfoPanel.vue";
import UserProfilePanel from "./UserProfilePanel.vue";
+import ExtendedUserProfilePanel from "./ExtendedUserProfilePanel.vue";
export default {
name: "user-details-container",
@@ -100,6 +102,7 @@
EditGroupsPanel,
"external-idp-user-info-panel": ExternalIDPUserInfoPanel,
UserProfilePanel,
+ ExtendedUserProfilePanel,
},
data() {
return {
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
index 7eeeff4..c03f7eb 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
@@ -5,6 +5,11 @@
<h1 class="h4 mb-4">Manage Users</h1>
</div>
<div class="col-auto">
+ <b-button :to="{ name: 'extended-user-profile' }"
+ >Extended User Profile</b-button
+ >
+ </div>
+ <div class="col-auto">
<b-dropdown :text="menuText">
<b-dropdown-item
:to="{ name: 'identity-service-users' }"
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/field-editors/ExtendedUserProfileFieldEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/field-editors/ExtendedUserProfileFieldEditor.vue
new file mode 100644
index 0000000..7f1371b
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/field-editors/ExtendedUserProfileFieldEditor.vue
@@ -0,0 +1,366 @@
+<template>
+ <b-card :title="title">
+ <b-form-group label="Name" label-cols="3">
+ <b-form-input v-model="name" :state="validateState($v.name)" />
+ <b-form-invalid-feedback :state="validateState($v.name)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </b-form-group>
+ <b-form-group label="Help text" label-cols="3">
+ <b-form-input v-model="help_text" />
+ </b-form-group>
+ <b-form-group>
+ <b-form-checkbox v-model="required" switch> Required </b-form-checkbox>
+ </b-form-group>
+ <b-card title="Options" v-if="extendedUserProfileField.supportsChoices">
+ <transition-group name="fade">
+ <template
+ v-for="({ $model: choice, display_text: $v_display_text },
+ index) in $v.choices.$each.$iter"
+ >
+ <b-form-group :key="choice.key">
+ <b-input-group>
+ <b-form-input
+ :value="choice.display_text"
+ @input="
+ handleChoiceDisplayTextChanged(
+ choice,
+ $event,
+ $v_display_text
+ )
+ "
+ :state="validateState($v_display_text)"
+ />
+ <b-input-group-append>
+ <b-button
+ @click="handleChoiceMoveUp(choice)"
+ :disabled="index === 0"
+ v-b-tooltip.hover.left
+ title="Move Up"
+ >
+ <i class="fa fa-arrow-up" aria-hidden="true"></i>
+ </b-button>
+ <b-button
+ @click="handleChoiceMoveDown(choice)"
+ :disabled="
+ index === extendedUserProfileField.choices.length - 1
+ "
+ v-b-tooltip.hover.left
+ title="Move Down"
+ >
+ <i class="fa fa-arrow-down" aria-hidden="true"></i>
+ </b-button>
+ <b-button
+ @click="handleChoiceDeleted(choice)"
+ variant="danger"
+ v-b-tooltip.hover.left
+ title="Delete Option"
+ >
+ <i class="fa fa-trash" aria-hidden="true"></i>
+ </b-button>
+ </b-input-group-append>
+ </b-input-group>
+ <b-form-invalid-feedback :state="validateState($v_display_text)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </b-form-group>
+ </template>
+ <b-form-group :key="'other'" v-if="extendedUserProfileField.other">
+ <b-input-group>
+ <b-form-input
+ placeholder="User will see: Other (please specify)"
+ disabled
+ />
+ <b-input-group-append>
+ <b-button disabled>
+ <i class="fa fa-arrow-up" aria-hidden="true"></i>
+ </b-button>
+ <b-button disabled>
+ <i class="fa fa-arrow-down" aria-hidden="true"></i>
+ </b-button>
+ <b-button
+ @click="other = false"
+ variant="danger"
+ v-b-tooltip.hover.left
+ title="Remove Other option"
+ >
+ <i class="fa fa-trash" aria-hidden="true"></i>
+ </b-button>
+ </b-input-group-append>
+ </b-input-group>
+ </b-form-group>
+ </transition-group>
+ <b-form-group>
+ <b-button
+ @click="addChoice({ field: extendedUserProfileField })"
+ size="sm"
+ >Add Option</b-button
+ >
+ </b-form-group>
+ <b-form-group>
+ <b-form-checkbox v-model="other" switch>
+ Allow user to type in an "Other" option
+ </b-form-checkbox>
+ </b-form-group>
+ </b-card>
+
+ <template v-if="links && links.length > 0">
+ <transition-group name="fade">
+ <b-card
+ :title="`Link: ${link.label}`"
+ v-for="{ $model: link, label: $v_label, url: $v_url } in $v.links
+ .$each.$iter"
+ :key="link.key"
+ >
+ <b-form-group label="Label" label-cols="3">
+ <b-form-input
+ :value="link.label"
+ @input="handleLinkLabelChanged(link, $event, $v_label)"
+ :state="validateState($v_label)"
+ />
+ <b-form-invalid-feedback :state="validateState($v_label)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </b-form-group>
+ <b-form-group label="URL" label-cols="3">
+ <b-form-input
+ :value="link.url"
+ @input="handleLinkURLChanged(link, $event, $v_url)"
+ :state="validateState($v_url)"
+ />
+ <b-form-invalid-feedback :state="validateState($v_url)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </b-form-group>
+ <b-row>
+ <b-col>
+ <b-form-group>
+ <b-form-checkbox
+ :checked="link.display_link"
+ @input="handleLinkDisplayLinkChanged(link, $event)"
+ switch
+ >
+ Show as link?
+ </b-form-checkbox>
+ </b-form-group>
+ </b-col>
+ <b-col>
+ <b-form-group>
+ <b-form-checkbox
+ :checked="link.display_inline"
+ @input="handleLinkDisplayInlineChanged(link, $event)"
+ switch
+ >
+ Show inline?
+ </b-form-checkbox>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-button @click="handleLinkDeleted(link)" variant="danger" size="sm">
+ Delete Link
+ </b-button>
+ </b-card>
+ </transition-group>
+ </template>
+ <b-button @click="addLink({ field: extendedUserProfileField })" size="sm"
+ >Add Link</b-button
+ >
+ <b-button
+ @click="handleMoveUp({ field: extendedUserProfileField })"
+ :disabled="
+ extendedUserProfileFields.indexOf(extendedUserProfileField) === 0
+ "
+ size="sm"
+ >Move Up</b-button
+ >
+ <b-button
+ @click="handleMoveDown({ field: extendedUserProfileField })"
+ :disabled="
+ extendedUserProfileFields.indexOf(extendedUserProfileField) ===
+ extendedUserProfileFields.length - 1
+ "
+ size="sm"
+ >Move Down</b-button
+ >
+ <b-button @click="handleDelete" variant="danger" size="sm">Delete</b-button>
+ </b-card>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+export default {
+ mixins: [validationMixin],
+ props: ["extendedUserProfileField"],
+ computed: {
+ ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+ name: {
+ get() {
+ return this.extendedUserProfileField.name;
+ },
+ set(value) {
+ this.setName({ value, field: this.extendedUserProfileField });
+ this.$v.name.$touch();
+ },
+ },
+ help_text: {
+ get() {
+ return this.extendedUserProfileField.help_text;
+ },
+ set(value) {
+ this.setHelpText({ value, field: this.extendedUserProfileField });
+ },
+ },
+ required: {
+ get() {
+ return this.extendedUserProfileField.required;
+ },
+ set(value) {
+ this.setRequired({ value, field: this.extendedUserProfileField });
+ },
+ },
+ other: {
+ get() {
+ return this.extendedUserProfileField.other;
+ },
+ set(value) {
+ this.setOther({ value, field: this.extendedUserProfileField });
+ },
+ },
+ title() {
+ const fieldTypes = {
+ text: "Text",
+ single_choice: "Single Choice",
+ multi_choice: "Multi Choice",
+ user_agreement: "User Agreement",
+ };
+ return `${fieldTypes[this.extendedUserProfileField.field_type]}: ${
+ this.name
+ }`;
+ },
+ choices() {
+ return this.extendedUserProfileField.choices;
+ },
+ links() {
+ return this.extendedUserProfileField.links;
+ },
+ valid() {
+ return !this.$v.$invalid;
+ },
+ },
+ validations() {
+ return {
+ name: {
+ required,
+ },
+ choices: {
+ $each: {
+ display_text: {
+ required,
+ },
+ },
+ },
+ links: {
+ $each: {
+ label: {
+ required,
+ },
+ url: {
+ required,
+ },
+ },
+ },
+ };
+ },
+ methods: {
+ ...mapMutations("extendedUserProfile", [
+ "setName",
+ "setHelpText",
+ "setRequired",
+ "setOther",
+ "addChoice",
+ "updateChoiceDisplayText",
+ "deleteChoice",
+ "updateChoiceIndex",
+ "addLink",
+ "updateLinkLabel",
+ "updateLinkURL",
+ "updateLinkDisplayLink",
+ "updateLinkDisplayInline",
+ "deleteLink",
+ "updateFieldIndex",
+ "deleteField",
+ ]),
+ handleChoiceDisplayTextChanged(choice, display_text, $v) {
+ this.updateChoiceDisplayText({ choice, display_text });
+ $v.$touch();
+ },
+ handleChoiceDeleted(choice) {
+ this.deleteChoice({ field: this.extendedUserProfileField, choice });
+ },
+ handleChoiceMoveUp(choice) {
+ let index = this.extendedUserProfileField.choices.indexOf(choice);
+ index--;
+ this.updateChoiceIndex({
+ field: this.extendedUserProfileField,
+ choice,
+ index,
+ });
+ },
+ handleChoiceMoveDown(choice) {
+ let index = this.extendedUserProfileField.choices.indexOf(choice);
+ index++;
+ this.updateChoiceIndex({
+ field: this.extendedUserProfileField,
+ choice,
+ index,
+ });
+ },
+ handleLinkLabelChanged(link, label, $v) {
+ this.updateLinkLabel({ link, label });
+ $v.$touch();
+ },
+ handleLinkURLChanged(link, url, $v) {
+ this.updateLinkURL({ link, url });
+ $v.$touch();
+ },
+ handleLinkDisplayLinkChanged(link, display_link) {
+ this.updateLinkDisplayLink({ link, display_link });
+ },
+ handleLinkDisplayInlineChanged(link, display_inline) {
+ this.updateLinkDisplayInline({ link, display_inline });
+ },
+ handleLinkDeleted(link) {
+ this.deleteLink({ field: this.extendedUserProfileField, link });
+ },
+ handleMoveUp({ field }) {
+ let index = this.extendedUserProfileFields.indexOf(field);
+ index--;
+ this.updateFieldIndex({ field, index });
+ },
+ handleMoveDown({ field }) {
+ let index = this.extendedUserProfileFields.indexOf(field);
+ index++;
+ this.updateFieldIndex({ field, index });
+ },
+ handleDelete() {
+ this.deleteField({
+ field: this.extendedUserProfileField,
+ });
+ },
+ validateState: errors.vuelidateHelpers.validateState,
+ },
+ watch: {
+ valid: {
+ handler(valid) {
+ this.$emit(valid ? "valid" : "invalid");
+ },
+ immediate: true,
+ },
+ },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/main.js b/django_airavata/apps/admin/static/django_airavata_admin/src/main.js
index 8feb8d9..8d0fb8f 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/main.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/main.js
@@ -6,6 +6,7 @@
import router from "./router";
import "flatpickr/dist/flatpickr.css";
+import createStore from "./store";
entry((Vue) => {
Vue.config.productionTip = false;
@@ -14,7 +15,10 @@
Vue.use(VueRouter);
Vue.use(VueFlatPickr);
+ const store = createStore(Vue);
+
new Vue({
+ store,
render: (h) => h(components.MainLayout, [h(App)]),
router,
}).$mount("#app");
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
index de0c0d4..b5e722e 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
@@ -9,6 +9,7 @@
import CredentialStoreDashboard from "./components/dashboards/CredentialStoreDashboard";
import DevelopersContainer from "./components/developers//DevelopersContainer.vue";
import ExperimentStatisticsContainer from "./components/statistics/ExperimentStatisticsContainer";
+import ExtendedUserProfileContainer from "./components/users/ExtendedUserProfileContainer";
import GatewayResourceProfileEditorContainer from "./components/gatewayprofile/GatewayResourceProfileEditorContainer.vue";
import GroupComputeResourcePreference from "./components/admin/group_resource_preferences/GroupComputeResourcePreference";
import IdentityServiceUserManagementContainer from "./components/users/IdentityServiceUserManagementContainer.vue";
@@ -141,6 +142,11 @@
],
},
{
+ path: "/extended-user-profile",
+ component: ExtendedUserProfileContainer,
+ name: "extended-user-profile",
+ },
+ {
path: "/notices",
component: NoticesManagementContainer,
name: "notices",
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/index.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/index.js
new file mode 100644
index 0000000..0ee4817
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/index.js
@@ -0,0 +1,16 @@
+import Vuex from "vuex";
+import extendedUserProfile from "./modules/extendedUserProfile";
+
+const debug = process.env.NODE_ENV !== "production";
+
+function createStore(Vue) {
+ Vue.use(Vuex);
+ return new Vuex.Store({
+ modules: {
+ extendedUserProfile,
+ },
+ strict: debug,
+ });
+}
+
+export default createStore;
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
new file mode 100644
index 0000000..a058bee
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
@@ -0,0 +1,190 @@
+import { models, services } from "django-airavata-api";
+
+const state = () => ({
+ extendedUserProfileFields: null,
+ extendedUserProfileValues: null,
+ deletedExtendedUserProfileFields: [],
+});
+
+const getters = {
+ extendedUserProfileFields: (state) => state.extendedUserProfileFields,
+ extendedUserProfileValues: (state) => state.extendedUserProfileValues,
+};
+
+const actions = {
+ async loadExtendedUserProfileFields({ commit }) {
+ const extendedUserProfileFields = await services.ExtendedUserProfileFieldService.list();
+ commit("setExtendedUserProfileFields", { extendedUserProfileFields });
+ },
+ async loadExtendedUserProfileValues({ commit }, { username }) {
+ const extendedUserProfileValues = await services.ExtendedUserProfileValueService.list(
+ { username }
+ );
+ commit("setExtendedUserProfileValues", { extendedUserProfileValues });
+ },
+ async saveExtendedUserProfileFields({ commit, dispatch, state }) {
+ let order = 1;
+ for (const field of state.extendedUserProfileFields) {
+ commit("setOrder", { field, order: order++ });
+ if (field.supportsChoices) {
+ for (let index = 0; index < field.choices.length; index++) {
+ const choice = field.choices[index];
+ commit("setChoiceOrder", { choice, order: index });
+ }
+ }
+ for (let index = 0; index < field.links.length; index++) {
+ const link = field.links[index];
+ commit("setLinkOrder", { link, order: index });
+ }
+ // Create or update each field
+ if (field.id) {
+ await services.ExtendedUserProfileFieldService.update({
+ lookup: field.id,
+ data: field,
+ });
+ } else {
+ await services.ExtendedUserProfileFieldService.create({ data: field });
+ }
+ }
+ if (state.deletedExtendedUserProfileFields.length > 0) {
+ for (const field of state.deletedExtendedUserProfileFields) {
+ await services.ExtendedUserProfileFieldService.delete({
+ lookup: field.id,
+ });
+ }
+ commit("resetDeletedExtendedUserProfileFields");
+ }
+ // Reload the fields
+ dispatch("loadExtendedUserProfileFields");
+ },
+ async addExtendedUserProfileField({ state, commit }, { field_type }) {
+ const field = new models.ExtendedUserProfileField({
+ field_type,
+ name: `New Field ${state.extendedUserProfileFields.length + 1}`,
+ description: "",
+ help_text: "",
+ required: true,
+ links: [],
+ other: false,
+ choices: [],
+ checkbox_label: "",
+ });
+ commit("addExtendedUserProfileField", { field });
+ },
+};
+
+function getField(state, field) {
+ const extendedUserProfileField = state.extendedUserProfileFields.find(
+ (f) => f === field
+ );
+ return extendedUserProfileField;
+}
+function setFieldProp(state, field, prop, value) {
+ const extendedUserProfileField = getField(state, field);
+ extendedUserProfileField[prop] = value;
+}
+
+const mutations = {
+ setExtendedUserProfileFields(state, { extendedUserProfileFields }) {
+ state.extendedUserProfileFields = extendedUserProfileFields;
+ },
+ setExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+ state.extendedUserProfileValues = extendedUserProfileValues;
+ },
+ setName(state, { value, field }) {
+ setFieldProp(state, field, "name", value);
+ },
+ setHelpText(state, { value, field }) {
+ setFieldProp(state, field, "help_text", value);
+ },
+ setRequired(state, { value, field }) {
+ setFieldProp(state, field, "required", value);
+ },
+ setOrder(state, { order, field }) {
+ setFieldProp(state, field, "order", order);
+ },
+ setOther(state, { value, field }) {
+ setFieldProp(state, field, "other", value);
+ },
+ addExtendedUserProfileField(state, { field }) {
+ if (!state.extendedUserProfileFields) {
+ state.extendedUserProfileFields = [];
+ }
+ state.extendedUserProfileFields.push(field);
+ },
+ addChoice(state, { field }) {
+ field.choices.push(
+ new models.ExtendedUserProfileFieldChoice({
+ display_text: "",
+ })
+ );
+ },
+ setChoiceOrder(state, { choice, order }) {
+ choice.order = order;
+ },
+ updateChoiceDisplayText(state, { choice, display_text }) {
+ choice.display_text = display_text;
+ },
+ updateChoiceIndex(state, { field, choice, index }) {
+ const currentIndex = field.choices.indexOf(choice);
+ field.choices.splice(currentIndex, 1);
+ field.choices.splice(index, 0, choice);
+ },
+ deleteChoice(state, { field, choice }) {
+ const index = field.choices.indexOf(choice);
+ field.choices.splice(index, 1);
+ },
+ addLink(state, { field }) {
+ field.links.push(
+ new models.ExtendedUserProfileFieldLink({
+ label: "",
+ url: "",
+ display_link: true,
+ display_inline: false,
+ })
+ );
+ },
+ updateLinkLabel(state, { link, label }) {
+ link.label = label;
+ },
+ updateLinkURL(state, { link, url }) {
+ link.url = url;
+ },
+ updateLinkDisplayLink(state, { link, display_link }) {
+ link.display_link = display_link;
+ },
+ updateLinkDisplayInline(state, { link, display_inline }) {
+ link.display_inline = display_inline;
+ },
+ setLinkOrder(state, { link, order }) {
+ link.order = order;
+ },
+ deleteLink(state, { field, link }) {
+ const index = field.links.indexOf(link);
+ field.links.splice(index, 1);
+ },
+ updateFieldIndex(state, { field, index }) {
+ const currentIndex = state.extendedUserProfileFields.indexOf(field);
+ state.extendedUserProfileFields.splice(currentIndex, 1);
+ state.extendedUserProfileFields.splice(index, 0, field);
+ },
+ deleteField(state, { field }) {
+ const index = state.extendedUserProfileFields.indexOf(field);
+ state.extendedUserProfileFields.splice(index, 1);
+ // later when we save we'll need to sync this delete with the server
+ if (field.id) {
+ state.deletedExtendedUserProfileFields.push(field);
+ }
+ },
+ resetDeletedExtendedUserProfileFields(state) {
+ state.deletedExtendedUserProfileFields = [];
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/django_airavata/apps/admin/urls.py b/django_airavata/apps/admin/urls.py
index 24a4b90..f526727 100644
--- a/django_airavata/apps/admin/urls.py
+++ b/django_airavata/apps/admin/urls.py
@@ -15,5 +15,6 @@
name='gateway_resource_profile'),
re_path(r'^notices/', views.notices, name='notices'),
re_path(r'^users/', views.users, name='users'),
+ path('extended-user-profile/', views.extended_user_profile, name="extended_user_profile"),
path('developers/', views.developers, name='developers'),
]
diff --git a/django_airavata/apps/admin/views.py b/django_airavata/apps/admin/views.py
index caca63a..e8be104 100644
--- a/django_airavata/apps/admin/views.py
+++ b/django_airavata/apps/admin/views.py
@@ -54,6 +54,12 @@
@login_required
+def extended_user_profile(request):
+ request.active_nav_item = 'users'
+ return render(request, 'admin/admin_base.html')
+
+
+@login_required
def experiment_statistics(request):
request.active_nav_item = 'experiment-statistics'
return render(request, 'admin/admin_base.html')
diff --git a/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py b/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py
new file mode 100644
index 0000000..bc7bf8f
--- /dev/null
+++ b/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.15 on 2022-08-25 13:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_api', '0008_merge_20220601_1951'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='applicationsettings',
+ name='queue_settings_calculator_id',
+ field=models.CharField(max_length=255, null=True),
+ ),
+ ]
diff --git a/django_airavata/apps/api/models.py b/django_airavata/apps/api/models.py
index 829c88f..45c8816 100644
--- a/django_airavata/apps/api/models.py
+++ b/django_airavata/apps/api/models.py
@@ -72,3 +72,4 @@
class ApplicationSettings(models.Model):
application_module_id = models.CharField(max_length=255, unique=True)
show_queue_settings = models.BooleanField(default=True)
+ queue_settings_calculator_id = models.CharField(max_length=255, null=True)
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 84d32b5..d429ae0 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -60,7 +60,11 @@
NotificationPriority,
Project
)
-from airavata_django_portal_sdk import experiment_util, user_storage
+from airavata_django_portal_sdk import (
+ experiment_util,
+ queue_settings_calculators,
+ user_storage
+)
from django.conf import settings
from django.contrib.auth import get_user_model
from django.urls import reverse
@@ -336,6 +340,7 @@
applicationOutputs = OutputDataObjectTypeSerializer(many=True)
userHasWriteAccess = serializers.SerializerMethodField()
showQueueSettings = serializers.BooleanField(required=False)
+ queueSettingsCalculatorId = serializers.CharField(allow_null=True, required=False)
def to_representation(self, instance):
representation = super().to_representation(instance)
@@ -343,28 +348,34 @@
application_settings, created = models.ApplicationSettings.objects.get_or_create(
application_module_id=application_module_id)
representation["showQueueSettings"] = application_settings.show_queue_settings
+ # check that queue_settings_calculator_id exists
+ if queue_settings_calculators.exists(application_settings.queue_settings_calculator_id):
+ representation["queueSettingsCalculatorId"] = application_settings.queue_settings_calculator_id
return representation
def create(self, validated_data):
- showQueueSettings = validated_data.pop("showQueueSettings", None)
+ showQueueSettings = validated_data.pop("showQueueSettings", True)
+ queueSettingsCalculatorId = validated_data.pop("queueSettingsCalculatorId", None)
application_interface = super().create(validated_data)
application_module_id = application_interface.applicationModules[0]
- if showQueueSettings is not None:
- models.ApplicationSettings.objects.update_or_create(
- application_module_id=application_module_id,
- defaults={"show_queue_settings": showQueueSettings}
- )
+ models.ApplicationSettings.objects.update_or_create(
+ application_module_id=application_module_id,
+ defaults={"show_queue_settings": showQueueSettings,
+ "queue_settings_calculator_id": queueSettingsCalculatorId}
+ )
return application_interface
def update(self, instance, validated_data):
- showQueueSettings = validated_data.pop("showQueueSettings", None)
+ defaults = {}
+ if "showQueueSettings" in validated_data:
+ defaults["show_queue_settings"] = validated_data.pop("showQueueSettings")
+ if "queueSettingsCalculatorId" in validated_data:
+ defaults["queue_settings_calculator_id"] = validated_data.pop("queueSettingsCalculatorId")
application_interface = super().update(instance, validated_data)
application_module_id = application_interface.applicationModules[0]
- if showQueueSettings is not None:
- models.ApplicationSettings.objects.update_or_create(
- application_module_id=application_module_id,
- defaults={"show_queue_settings": showQueueSettings}
- )
+ models.ApplicationSettings.objects.update_or_create(
+ application_module_id=application_module_id, defaults=defaults
+ )
return application_interface
def get_userHasWriteAccess(self, appDeployment):
@@ -531,6 +542,7 @@
class DataProductSerializer(
thrift_utils.create_serializer_class(DataProductModel)):
creationTime = UTCPosixTimestampDateTimeField()
+ modifiedTime = UTCPosixTimestampDateTimeField()
lastModifiedTime = UTCPosixTimestampDateTimeField()
replicaLocations = DataReplicaLocationSerializer(many=True)
downloadURL = serializers.SerializerMethodField()
@@ -932,6 +944,7 @@
downloadURL = serializers.SerializerMethodField()
dataProductURI = serializers.CharField(source='data-product-uri')
createdTime = serializers.DateTimeField(source='created_time')
+ modifiedTime = serializers.DateTimeField(source='modified_time')
mimeType = serializers.CharField(source='mime_type')
size = serializers.IntegerField()
hidden = serializers.BooleanField()
@@ -946,6 +959,7 @@
name = serializers.CharField()
path = serializers.CharField()
createdTime = serializers.DateTimeField(source='created_time')
+ modifiedTime = serializers.DateTimeField(source='modified_time')
size = serializers.IntegerField()
hidden = serializers.BooleanField()
url = FullyEncodedHyperlinkedIdentityField(
@@ -972,6 +986,7 @@
name = serializers.CharField()
path = serializers.CharField()
createdTime = serializers.DateTimeField(source='created_time')
+ modifiedTime = serializers.DateTimeField(source='modified_time')
size = serializers.IntegerField()
url = serializers.SerializerMethodField()
@@ -1162,3 +1177,8 @@
fileUploadMaxFileSize = serializers.IntegerField()
tusEndpoint = serializers.CharField()
pgaUrl = serializers.CharField()
+
+
+class QueueSettingsCalculatorSerializer(serializers.Serializer):
+ id = serializers.CharField()
+ name = serializers.CharField()
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 ca08f1d..008335c 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
@@ -18,6 +18,9 @@
import Experiment from "./models/Experiment";
import ExperimentSearchFields from "./models/ExperimentSearchFields";
import ExperimentState from "./models/ExperimentState";
+import ExtendedUserProfileField from "./models/ExtendedUserProfileField";
+import ExtendedUserProfileFieldChoice from "./models/ExtendedUserProfileFieldChoice";
+import ExtendedUserProfileFieldLink from "./models/ExtendedUserProfileFieldLink";
import FullExperiment from "./models/FullExperiment";
import Group from "./models/Group";
import GroupComputeResourcePreference from "./models/GroupComputeResourcePreference";
@@ -80,6 +83,9 @@
Experiment,
ExperimentSearchFields,
ExperimentState,
+ ExtendedUserProfileField,
+ ExtendedUserProfileFieldChoice,
+ ExtendedUserProfileFieldLink,
FullExperiment,
Group,
GroupComputeResourcePreference,
@@ -120,6 +126,12 @@
ExperimentStoragePathService: ServiceFactory.service(
"ExperimentStoragePaths"
),
+ ExtendedUserProfileFieldService: ServiceFactory.service(
+ "ExtendedUserProfileFields"
+ ),
+ ExtendedUserProfileValueService: ServiceFactory.service(
+ "ExtendedUserProfileValues"
+ ),
FullExperimentService: ServiceFactory.service("FullExperiments"),
GatewayResourceProfileService: ServiceFactory.service(
"GatewayResourceProfile"
@@ -135,6 +147,9 @@
ParserService: ServiceFactory.service("Parsers"),
ProjectService: ServiceFactory.service("Projects"),
+ QueueSettingsCalculatorService: ServiceFactory.service(
+ "QueueSettingsCalculators"
+ ),
SCPDataMovementService,
ServiceFactory,
SettingsService: ServiceFactory.service("Settings"),
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
index 008f3a0..d75164c 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js
@@ -44,6 +44,11 @@
type: "boolean",
default: true,
},
+ {
+ name: "queueSettingsCalculatorId",
+ type: "string",
+ default: null,
+ },
];
export default class ApplicationInterfaceDefinition extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js
new file mode 100644
index 0000000..04938ca
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js
@@ -0,0 +1,76 @@
+import BaseModel from "./BaseModel";
+import ExtendedUserProfileFieldChoice from "./ExtendedUserProfileFieldChoice";
+import ExtendedUserProfileFieldLink from "./ExtendedUserProfileFieldLink";
+import uuidv4 from "uuid/v4";
+
+const FIELDS = [
+ "id",
+ "name",
+ "help_text",
+ "order",
+ {
+ name: "created_date",
+ type: "date",
+ },
+ {
+ name: "updated_date",
+ type: "date",
+ },
+ "field_type",
+ {
+ name: "links",
+ list: true,
+ type: ExtendedUserProfileFieldLink,
+ },
+ // For user_agreement type
+ "checkbox_label",
+ // For single_choice and multi_choice types
+ {
+ name: "choices",
+ list: true,
+ type: ExtendedUserProfileFieldChoice,
+ },
+ "other",
+ "required",
+];
+
+export default class ExtendedUserProfileField extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ this._key = data.key ? data.key : uuidv4();
+ }
+
+ get key() {
+ return this._key;
+ }
+
+ toJSON() {
+ const copy = Object.assign({}, this);
+ // Remove unnecessary properties
+ switch (this.field_type) {
+ case "text":
+ delete copy["other"];
+ delete copy["choices"];
+ delete copy["checkbox_label"];
+ break;
+ case "single_choice":
+ case "multi_choice":
+ delete copy["checkbox_label"];
+ break;
+ case "user_agreement":
+ delete copy["other"];
+ delete copy["choices"];
+ break;
+ default:
+ // eslint-disable-next-line no-console
+ console.error("Unrecognized field type", this.field_type);
+ break;
+ }
+ return copy;
+ }
+ get supportsChoices() {
+ return (
+ this.field_type === "single_choice" || this.field_type === "multi_choice"
+ );
+ }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js
new file mode 100644
index 0000000..3ea1ea5
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js
@@ -0,0 +1,24 @@
+import BaseModel from "./BaseModel";
+import uuidv4 from "uuid/v4";
+
+const FIELDS = ["id", "display_text", "order"];
+
+export default class ExtendedUserProfileFieldChoice extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ this._key = data.key ? data.key : uuidv4();
+ }
+
+ get key() {
+ return this._key;
+ }
+
+ toJSON() {
+ const copy = Object.assign({}, this);
+ // id must either have a value or be missing, it can't be null
+ if (!copy.id) {
+ delete copy.id;
+ }
+ return copy;
+ }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js
new file mode 100644
index 0000000..31e007e
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js
@@ -0,0 +1,31 @@
+import BaseModel from "./BaseModel";
+import uuidv4 from "uuid/v4";
+
+const FIELDS = [
+ "id",
+ "label",
+ "url",
+ "order",
+ "display_link",
+ "display_inline",
+];
+
+export default class ExtendedUserProfileFieldLink extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ this._key = data.key ? data.key : uuidv4();
+ }
+
+ get key() {
+ return this._key;
+ }
+
+ toJSON() {
+ const copy = Object.assign({}, this);
+ // id must either have a value or be missing, it can't be null
+ if (!copy.id) {
+ delete copy.id;
+ }
+ return copy;
+ }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
new file mode 100644
index 0000000..02618cb
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
@@ -0,0 +1,46 @@
+import BaseModel from "./BaseModel";
+
+const FIELDS = [
+ "id",
+ "value_type",
+ "ext_user_profile_field",
+ "text_value",
+ "choices",
+ "other_value",
+ "agreement_value",
+ "valid",
+ "value_display",
+];
+
+export default class ExtendedUserProfileValue extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ }
+
+ toJSON() {
+ const copy = Object.assign({}, this);
+ // Remove unnecessary properties
+ switch (this.value_type) {
+ case "text":
+ delete copy["other_value"];
+ delete copy["choices"];
+ delete copy["agreement_value"];
+ break;
+ case "single_choice":
+ case "multi_choice":
+ delete copy["text_value"];
+ delete copy["agreement_value"];
+ break;
+ case "user_agreement":
+ delete copy["text_value"];
+ delete copy["other_value"];
+ delete copy["choices"];
+ break;
+ default:
+ // eslint-disable-next-line no-console
+ console.error("Unrecognized value type", this.value_type);
+ break;
+ }
+ return copy;
+ }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js
index 52e6f11..07fd669 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js
@@ -23,7 +23,11 @@
type: NotificationPriority,
},
"userHasWriteAccess",
- "showInDashboard"
+ {
+ name: "showInDashboard",
+ type: "boolean",
+ default: false,
+ },
];
export default class Notification extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/QueueSettingsCalculator.js b/django_airavata/apps/api/static/django_airavata_api/js/models/QueueSettingsCalculator.js
new file mode 100644
index 0000000..4396d74
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/QueueSettingsCalculator.js
@@ -0,0 +1,9 @@
+import BaseModel from "./BaseModel";
+
+const FIELDS = ["id", "name"];
+
+export default class QueueSettingsCalculator extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/User.js b/django_airavata/apps/api/static/django_airavata_api/js/models/User.js
index e414444..198a927 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/User.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/User.js
@@ -8,7 +8,8 @@
"email",
"pending_email_change",
"complete",
- "username_valid"
+ "username_valid",
+ "ext_user_profile_valid",
];
export default class User extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
index f1c0c3a..2ff65df 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
@@ -4,6 +4,7 @@
"name",
"path",
{ name: "createdTime", type: "date" },
+ { name: "modifiedTime", type: "date" },
"size",
"hidden",
];
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
index 85d070a..ee6ac6a 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
@@ -5,6 +5,7 @@
"downloadURL",
"dataProductURI",
{ name: "createdTime", type: "date" },
+ { name: "modifiedTime", type: "date" },
"size",
"mimeType",
];
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index 95ee7ac..c02058d 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -10,6 +10,8 @@
import ExperimentStatistics from "./models/ExperimentStatistics";
import ExperimentStoragePath from "./models/ExperimentStoragePath";
import ExperimentSummary from "./models/ExperimentSummary";
+import ExtendedUserProfileField from "./models/ExtendedUserProfileField";
+import ExtendedUserProfileValue from "./models/ExtendedUserProfileValue";
import FullExperiment from "./models/FullExperiment";
import GatewayResourceProfile from "./models/GatewayResourceProfile";
import Group from "./models/Group";
@@ -19,6 +21,7 @@
import Notification from "./models/Notification";
import Parser from "./models/Parser";
import Project from "./models/Project";
+import QueueSettingsCalculator from "./models/QueueSettingsCalculator";
import Settings from "./models/Settings";
import SharedEntity from "./models/SharedEntity";
import StoragePreference from "./models/StoragePreference";
@@ -245,6 +248,27 @@
},
},
},
+ ExtendedUserProfileFields: {
+ url: "/auth/extended-user-profile-fields",
+ viewSet: true,
+ modelClass: ExtendedUserProfileField,
+ },
+ ExtendedUserProfileValues: {
+ url: "/auth/extended-user-profile-values",
+ viewSet: true,
+ modelClass: ExtendedUserProfileValue,
+ queryParams: ["username"],
+ methods: {
+ saveAll: {
+ url: "/auth/extended-user-profile-values/save-all/",
+ requestType: "post",
+ modelClass: ExtendedUserProfileValue,
+ bodyParams: {
+ name: "data",
+ },
+ },
+ },
+ },
FullExperiments: {
url: "/api/full-experiments",
viewSet: [
@@ -341,6 +365,20 @@
queryParams: ["limit", "offset"],
modelClass: Project,
},
+ QueueSettingsCalculators: {
+ url: "/api/queue-settings-calculators",
+ viewSet: ["retrieve", "list"],
+ methods: {
+ calculate: {
+ url: "/api/queue-settings-calculators/<lookup>/calculate/",
+ requestType: "post",
+ bodyParams: {
+ name: "data",
+ },
+ },
+ },
+ modelClass: QueueSettingsCalculator,
+ },
Settings: {
url: "/api/settings/",
methods: {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js b/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
index 5aa1e61..29bf84a 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
@@ -274,6 +274,9 @@
}
};
let resultHandler = (data) => {
+ if (Array.isArray(data)) {
+ return data.map((item) => resultHandler(item));
+ }
return config.modelClass ? new config.modelClass(data) : data;
};
switch (config.requestType.toLowerCase()) {
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index 61b2717..f655d64 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -45,6 +45,8 @@
basename='iam-user-profile')
router.register(r'unverified-email-users', views.UnverifiedEmailUserViewSet,
basename='unverified-email-user-profile')
+router.register(r'queue-settings-calculators', views.QueueSettingsCalculatorViewSet,
+ basename='queue-settings-calculator')
app_name = 'django_airavata_api'
urlpatterns = [
diff --git a/django_airavata/apps/api/view_utils.py b/django_airavata/apps/api/view_utils.py
index ddef7e3..3f06ad0 100644
--- a/django_airavata/apps/api/view_utils.py
+++ b/django_airavata/apps/api/view_utils.py
@@ -221,3 +221,8 @@
request.is_read_only_gateway_admin)
else:
return request.is_gateway_admin
+
+
+class ReadOnly(permissions.BasePermission):
+ def has_permission(self, request, view):
+ return request.method in permissions.SAFE_METHODS
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 79816b3..75fe425 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -23,7 +23,11 @@
from airavata.model.experiment.ttypes import ExperimentSearchFields
from airavata.model.group.ttypes import ResourcePermissionType
from airavata.model.user.ttypes import Status
-from airavata_django_portal_sdk import experiment_util, user_storage
+from airavata_django_portal_sdk import (
+ experiment_util,
+ queue_settings_calculators,
+ user_storage
+)
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
@@ -1862,3 +1866,28 @@
experiment_id,
test_mode=test_mode,
**params.dict())
+
+
+class QueueSettingsCalculatorViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericAPIBackedViewSet):
+ serializer_class = serializers.QueueSettingsCalculatorSerializer
+
+ def get_list(self):
+ return queue_settings_calculators.get_all()
+
+ def get_instance(self, lookup_value):
+ calcs = queue_settings_calculators.get_all()
+ calc = [calc for calc in calcs if calc.id == lookup_value]
+ if len(calc) == 0:
+ return None
+ return calc[0]
+
+ @action(methods=['post'], detail=True, serializer_class=serializers.ExperimentSerializer)
+ def calculate(self, request, pk=None):
+
+ serializer = self.get_serializer(data=request.data)
+ result = {}
+ # Just ignore invalid experiment model since likely caused by late initialization
+ if serializer.is_valid():
+ experiment_model = serializer.save()
+ result = queue_settings_calculators.calculate_queue_settings(pk, request, experiment_model)
+ return Response(result)
diff --git a/django_airavata/apps/auth/middleware.py b/django_airavata/apps/auth/middleware.py
index 0226956..8e37405 100644
--- a/django_airavata/apps/auth/middleware.py
+++ b/django_airavata/apps/auth/middleware.py
@@ -90,8 +90,14 @@
reverse('django_airavata_auth:user_profile'),
reverse('django_airavata_auth:logout'),
]
- if (hasattr(request.user, "user_profile") and
- not request.user.user_profile.is_complete and
+ incomplete_user_profile = (hasattr(request.user, "user_profile") and
+ not request.user.user_profile.is_complete)
+ # Exclude admin's from the ext user profile check since they will be
+ # creating/editing the ext user profile fields
+ invalid_ext_user_profile = (not getattr(request, "is_gateway_admin", False) and
+ hasattr(request.user, "user_profile") and
+ not request.user.user_profile.is_ext_user_profile_valid)
+ if ((incomplete_user_profile or invalid_ext_user_profile) and
request.path not in allowed_paths and
'text/html' in request.META['HTTP_ACCEPT']):
return redirect('django_airavata_auth:user_profile')
diff --git a/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py b/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
new file mode 100644
index 0000000..2b5601c
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
@@ -0,0 +1,107 @@
+# Generated by Django 3.2.11 on 2022-03-11 14:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_auth', '0013_auto_20220118_1650'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ExtendedUserProfileField',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=64)),
+ ('help_text', models.TextField(blank=True)),
+ ('order', models.IntegerField()),
+ ('created_date', models.DateTimeField(auto_now_add=True)),
+ ('updated_date', models.DateTimeField(auto_now=True)),
+ ('deleted', models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileAgreementField',
+ fields=[
+ ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='user_agreement', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+ ('checkbox_label', models.TextField(blank=True)),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilefield',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileMultiChoiceField',
+ fields=[
+ ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='multi_choice', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+ ('other', models.BooleanField(default=False)),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilefield',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileSingleChoiceField',
+ fields=[
+ ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='single_choice', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+ ('other', models.BooleanField(default=False)),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilefield',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileTextField',
+ fields=[
+ ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='text', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilefield',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileInfo',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('id_value', models.BigIntegerField(null=True)),
+ ('text_value', models.CharField(blank=True, max_length=255)),
+ ('created_date', models.DateTimeField(auto_now_add=True)),
+ ('updated_date', models.DateTimeField(auto_now=True)),
+ ('ext_user_profile_field', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_airavata_auth.extendeduserprofilefield')),
+ ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extended_profile', to='django_airavata_auth.userprofile')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileFieldLink',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('label', models.TextField()),
+ ('url', models.URLField()),
+ ('order', models.IntegerField()),
+ ('display_link', models.BooleanField(default=True)),
+ ('display_inline', models.BooleanField(default=False)),
+ ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='django_airavata_auth.extendeduserprofilefield')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileSingleChoiceFieldChoice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('display_text', models.CharField(max_length=255)),
+ ('order', models.IntegerField()),
+ ('deleted', models.BooleanField(default=False)),
+ ('single_choice_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilesinglechoicefield')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileMultiChoiceFieldChoice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('display_text', models.CharField(max_length=255)),
+ ('order', models.IntegerField()),
+ ('deleted', models.BooleanField(default=False)),
+ ('multi_choice_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilemultichoicefield')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py b/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
new file mode 100644
index 0000000..49da5c1
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
@@ -0,0 +1,72 @@
+# Generated by Django 3.2.11 on 2022-03-29 17:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_auth', '0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ExtendedUserProfileMultiChoiceValueChoice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('value', models.BigIntegerField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileValue',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_date', models.DateTimeField(auto_now_add=True)),
+ ('updated_date', models.DateTimeField(auto_now=True)),
+ ('ext_user_profile_field', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_airavata_auth.extendeduserprofilefield')),
+ ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extended_profile', to='django_airavata_auth.userprofile')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileAgreementValue',
+ fields=[
+ ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='user_agreement', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+ ('agreement_value', models.BooleanField()),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilevalue',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileMultiChoiceValue',
+ fields=[
+ ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='multi_choice', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+ ('other_value', models.TextField(blank=True)),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilevalue',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileSingleChoiceValue',
+ fields=[
+ ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='single_choice', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+ ('choice', models.BigIntegerField(null=True)),
+ ('other_value', models.TextField(blank=True)),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilevalue',),
+ ),
+ migrations.CreateModel(
+ name='ExtendedUserProfileTextValue',
+ fields=[
+ ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='text', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+ ('text_value', models.TextField()),
+ ],
+ bases=('django_airavata_auth.extendeduserprofilevalue',),
+ ),
+ migrations.DeleteModel(
+ name='ExtendedUserProfileInfo',
+ ),
+ migrations.AddField(
+ model_name='extendeduserprofilemultichoicevaluechoice',
+ name='multi_choice_value',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilemultichoicevalue'),
+ ),
+ ]
diff --git a/django_airavata/apps/auth/migrations/0016_extendeduserprofilefield_required.py b/django_airavata/apps/auth/migrations/0016_extendeduserprofilefield_required.py
new file mode 100644
index 0000000..4c6dd6f
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0016_extendeduserprofilefield_required.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.11 on 2022-05-10 20:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_auth', '0015_auto_20220329_1708'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='extendeduserprofilefield',
+ name='required',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/django_airavata/apps/auth/migrations/0017_auto_20220616_1831.py b/django_airavata/apps/auth/migrations/0017_auto_20220616_1831.py
new file mode 100644
index 0000000..47ed795
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0017_auto_20220616_1831.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.2.11 on 2022-06-16 18:31
+
+from django.db import migrations
+
+from django_airavata.apps.auth.models import (
+ USER_PROFILE_COMPLETED_TEMPLATE,
+)
+
+
+def default_templates(apps, schema_editor):
+
+ EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+ user_profile_completed_template = EmailTemplate(
+ template_type=USER_PROFILE_COMPLETED_TEMPLATE,
+ subject="User {{first_name}} {{last_name}} ({{username}}) has completed their profile",
+ body="""
+ <p>Gateway Portal: {{http_host}}</p>
+ <p>Tenant: {{gateway_id}}</p>
+ <h3>User Profile</h3>
+ <p>Username: {{username}}</p>
+ <p>Name: {{first_name}} {{last_name}}</p>
+ <p>Email: {{email}}</p>
+ {% if extended_profile_values %}
+ <h3>Extended User Profile</h3>
+ <table><tr><th>Name</th><th>Value</th></tr>
+ {% for value in extended_profile_values %}
+ <tr><td>{{ value.ext_user_profile_field.name }}</td>
+ {% if value.value_display_list and value.value_display_list|length > 1 %}
+ <td><ul>
+ {% for display_item in value.value_display_list %}
+ <li>{{ display_item }}</li>
+ {% endfor %}
+ </ul></td>
+ {% elif value.value_display_list and value.value_display_list|length == 1 %}
+ <td>{{ value.value_display_list|first }}</td>
+ {% else %}
+ <td>{{ value.value_display_list }}</td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+ """.strip())
+ user_profile_completed_template.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_auth', '0016_extendeduserprofilefield_required'),
+ ]
+
+ operations = [
+ migrations.RunPython(default_templates)
+ ]
diff --git a/django_airavata/apps/auth/migrations/0018_merge_0014_auto_20220217_2255_0017_auto_20220616_1831.py b/django_airavata/apps/auth/migrations/0018_merge_0014_auto_20220217_2255_0017_auto_20220616_1831.py
new file mode 100644
index 0000000..ff67fdb
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0018_merge_0014_auto_20220217_2255_0017_auto_20220616_1831.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.2.14 on 2022-07-08 19:43
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_airavata_auth', '0014_auto_20220217_2255'),
+ ('django_airavata_auth', '0017_auto_20220616_1831'),
+ ]
+
+ operations = [
+ ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 54395e0..6cc658e 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -11,6 +11,7 @@
PASSWORD_RESET_EMAIL_TEMPLATE = 3
USER_ADDED_TO_GROUP_TEMPLATE = 4
VERIFY_EMAIL_CHANGE_TEMPLATE = 5
+USER_PROFILE_COMPLETED_TEMPLATE = 6
class EmailVerification(models.Model):
@@ -29,6 +30,7 @@
(PASSWORD_RESET_EMAIL_TEMPLATE, 'Password Reset Email Template'),
(USER_ADDED_TO_GROUP_TEMPLATE, 'User Added to Group Template'),
(VERIFY_EMAIL_CHANGE_TEMPLATE, 'Verify Email Change Template'),
+ (USER_PROFILE_COMPLETED_TEMPLATE, 'User Profile Completed Template'),
)
template_type = models.IntegerField(
primary_key=True, choices=TEMPLATE_TYPE_CHOICES)
@@ -109,6 +111,19 @@
result.append('last_name')
return result
+ @property
+ def is_ext_user_profile_valid(self):
+ fields = ExtendedUserProfileField.objects.filter(deleted=False)
+ for field in fields:
+ try:
+ value = self.extended_profile_values.filter(ext_user_profile_field=field).get()
+ if not value.valid:
+ return False
+ except ExtendedUserProfileValue.DoesNotExist:
+ if field.required:
+ return False
+ return True
+
def is_non_empty(self, value: str):
return value is not None and value.strip() != ""
@@ -149,3 +164,230 @@
max_length=36, unique=True, default=uuid.uuid4)
created_date = models.DateTimeField(auto_now_add=True)
verified = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileField(models.Model):
+ name = models.CharField(max_length=64)
+ help_text = models.TextField(blank=True)
+ order = models.IntegerField()
+ created_date = models.DateTimeField(auto_now_add=True)
+ updated_date = models.DateTimeField(auto_now=True)
+ deleted = models.BooleanField(default=False)
+ required = models.BooleanField(default=True)
+
+ def __str__(self) -> str:
+ return f"{self.name} ({self.id})"
+
+ @property
+ def field_type(self):
+ if hasattr(self, 'text'):
+ return 'text'
+ elif hasattr(self, 'single_choice'):
+ return 'single_choice'
+ elif hasattr(self, 'multi_choice'):
+ return 'multi_choice'
+ elif hasattr(self, 'user_agreement'):
+ return 'user_agreement'
+ else:
+ raise Exception("Could not determine field_type")
+
+
+class ExtendedUserProfileTextField(ExtendedUserProfileField):
+ field_ptr = models.OneToOneField(ExtendedUserProfileField,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="text")
+
+
+class ExtendedUserProfileSingleChoiceField(ExtendedUserProfileField):
+ field_ptr = models.OneToOneField(ExtendedUserProfileField,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="single_choice")
+ other = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileFieldChoice(models.Model):
+ display_text = models.CharField(max_length=255)
+ order = models.IntegerField()
+ deleted = models.BooleanField(default=False)
+
+ class Meta:
+ abstract = True
+
+ def __str__(self) -> str:
+ return f"{self.display_text} ({self.id})"
+
+
+class ExtendedUserProfileSingleChoiceFieldChoice(ExtendedUserProfileFieldChoice):
+ single_choice_field = models.ForeignKey(ExtendedUserProfileSingleChoiceField, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileMultiChoiceField(ExtendedUserProfileField):
+ field_ptr = models.OneToOneField(ExtendedUserProfileField,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="multi_choice")
+ other = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileMultiChoiceFieldChoice(ExtendedUserProfileFieldChoice):
+ multi_choice_field = models.ForeignKey(ExtendedUserProfileMultiChoiceField, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileAgreementField(ExtendedUserProfileField):
+ field_ptr = models.OneToOneField(ExtendedUserProfileField,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="user_agreement")
+ # if no checkbox label, then some default text will be used
+ checkbox_label = models.TextField(blank=True)
+
+
+class ExtendedUserProfileFieldLink(models.Model):
+ label = models.TextField()
+ url = models.URLField()
+ order = models.IntegerField()
+ display_link = models.BooleanField(default=True)
+ display_inline = models.BooleanField(default=False)
+ # Technically any field can have links
+ field = models.ForeignKey(ExtendedUserProfileField, on_delete=models.CASCADE, related_name="links")
+
+ def __str__(self) -> str:
+ return f"{self.label} {self.url}"
+
+
+class ExtendedUserProfileValue(models.Model):
+ ext_user_profile_field = models.ForeignKey(ExtendedUserProfileField, on_delete=models.SET_NULL, null=True)
+ user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name="extended_profile_values")
+ created_date = models.DateTimeField(auto_now_add=True)
+ updated_date = models.DateTimeField(auto_now=True)
+
+ @property
+ def value_type(self):
+ if hasattr(self, 'text'):
+ return 'text'
+ elif hasattr(self, 'single_choice'):
+ return 'single_choice'
+ elif hasattr(self, 'multi_choice'):
+ return 'multi_choice'
+ elif hasattr(self, 'user_agreement'):
+ return 'user_agreement'
+ else:
+ raise Exception("Could not determine value_type")
+
+ @property
+ def value_display(self):
+ if self.value_type == 'text':
+ return self.text.text_value
+ elif self.value_type == 'single_choice':
+ if self.single_choice.choice:
+ try:
+ choice = self.ext_user_profile_field.single_choice.choices.get(id=self.single_choice.choice)
+ return choice.display_text
+ except ExtendedUserProfileSingleChoiceFieldChoice.DoesNotExist:
+ return None
+ elif self.single_choice.other_value:
+ return f"Other: {self.single_choice.other_value}"
+ elif self.value_type == 'multi_choice':
+ result = []
+ if self.multi_choice.choices:
+ mc_field = self.ext_user_profile_field.multi_choice
+ for choice_value in self.multi_choice.choices.all():
+ try:
+ choice = mc_field.choices.get(id=choice_value.value)
+ result.append(choice.display_text)
+ except ExtendedUserProfileMultiChoiceFieldChoice.DoesNotExist:
+ continue
+ if self.multi_choice.other_value:
+ result.append(f"Other: {self.multi_choice.other_value}")
+ return result
+ elif self.value_type == 'user_agreement':
+ if self.user_agreement.agreement_value:
+ return "Yes"
+ else:
+ return "No"
+ return None
+
+ @property
+ def value_display_list(self):
+ """Same as value_display except coerced always to a list."""
+ value_display = self.value_display
+ if value_display is not None and not isinstance(value_display, list):
+ return [value_display]
+ else:
+ return value_display
+
+ @property
+ def valid(self):
+ # if the field is deleted, whatever the value, consider it valid
+ if self.ext_user_profile_field.deleted:
+ return True
+ if self.ext_user_profile_field.required:
+ if self.value_type == 'text':
+ return self.text.text_value and len(self.text.text_value.strip()) > 0
+ if self.value_type == 'single_choice':
+ choice_exists = (self.single_choice.choice and
+ self.ext_user_profile_field.single_choice.choices
+ .filter(id=self.single_choice.choice).exists())
+ has_other = (self.ext_user_profile_field.single_choice.other and
+ self.single_choice.other_value and
+ len(self.single_choice.other_value.strip()) > 0)
+ return choice_exists or has_other
+ if self.value_type == 'multi_choice':
+ choice_ids = list(map(lambda c: c.value, self.multi_choice.choices.all()))
+ choice_exists = self.ext_user_profile_field.multi_choice.choices.filter(id__in=choice_ids).exists()
+ has_other = (self.ext_user_profile_field.multi_choice.other and
+ self.multi_choice.other_value and
+ len(self.multi_choice.other_value.strip()) > 0)
+ return choice_exists or has_other
+ if self.value_type == 'user_agreement':
+ return self.user_agreement.agreement_value is True
+ return True
+
+
+class ExtendedUserProfileTextValue(ExtendedUserProfileValue):
+ value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="text")
+ text_value = models.TextField()
+
+
+class ExtendedUserProfileSingleChoiceValue(ExtendedUserProfileValue):
+ value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="single_choice")
+ # Only one of value or other_value should be populated, not both
+ choice = models.BigIntegerField(null=True)
+ other_value = models.TextField(blank=True)
+
+
+class ExtendedUserProfileMultiChoiceValue(ExtendedUserProfileValue):
+ value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="multi_choice")
+ other_value = models.TextField(blank=True)
+
+
+class ExtendedUserProfileMultiChoiceValueChoice(models.Model):
+ value = models.BigIntegerField()
+ multi_choice_value = models.ForeignKey(ExtendedUserProfileMultiChoiceValue, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileAgreementValue(ExtendedUserProfileValue):
+ value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+ on_delete=models.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ related_name="user_agreement")
+ agreement_value = models.BooleanField()
diff --git a/django_airavata/apps/auth/package.json b/django_airavata/apps/auth/package.json
index a136c92..bcc0445 100644
--- a/django_airavata/apps/auth/package.json
+++ b/django_airavata/apps/auth/package.json
@@ -17,7 +17,8 @@
"django-airavata-api": "link:../api/",
"django-airavata-common-ui": "link:../../static/common/",
"vue": "^2.5.21",
- "vuelidate": "^0.7.6"
+ "vuelidate": "^0.7.6",
+ "vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.1.1",
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index c10b918..06d8def 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -27,11 +27,13 @@
pending_email_change = serializers.SerializerMethodField()
complete = serializers.SerializerMethodField()
username_valid = serializers.SerializerMethodField()
+ ext_user_profile_valid = serializers.SerializerMethodField()
class Meta:
model = get_user_model()
fields = ['id', 'username', 'first_name', 'last_name', 'email',
- 'pending_email_change', 'complete', 'username_valid']
+ 'pending_email_change', 'complete', 'username_valid',
+ 'ext_user_profile_valid']
read_only_fields = ('username',)
def get_pending_email_change(self, instance):
@@ -49,6 +51,9 @@
def get_username_valid(self, instance):
return instance.user_profile.is_username_valid
+ def get_ext_user_profile_valid(self, instance):
+ return instance.user_profile.is_ext_user_profile_valid
+
@atomic
def update(self, instance, validated_data):
request = self.context['request']
@@ -98,3 +103,267 @@
"url": verification_uri,
})
utils.send_email_to_user(models.VERIFY_EMAIL_CHANGE_TEMPLATE, context)
+
+
+class ExtendedUserProfileFieldChoiceSerializer(serializers.Serializer):
+ id = serializers.IntegerField(required=False)
+ display_text = serializers.CharField()
+ order = serializers.IntegerField()
+
+
+class ExtendedUserProfileFieldLinkSerializer(serializers.Serializer):
+ id = serializers.IntegerField(required=False)
+ label = serializers.CharField()
+ url = serializers.URLField()
+ order = serializers.IntegerField()
+ display_link = serializers.BooleanField(default=True)
+ display_inline = serializers.BooleanField(default=False)
+
+
+class ExtendedUserProfileFieldSerializer(serializers.ModelSerializer):
+ field_type = serializers.ChoiceField(choices=["text", "single_choice", "multi_choice", "user_agreement"])
+ other = serializers.BooleanField(required=False)
+ choices = ExtendedUserProfileFieldChoiceSerializer(required=False, many=True)
+ checkbox_label = serializers.CharField(allow_blank=True, required=False)
+ links = ExtendedUserProfileFieldLinkSerializer(required=False, many=True)
+
+ class Meta:
+ model = models.ExtendedUserProfileField
+ fields = ['id', 'name', 'help_text', 'order', 'created_date',
+ 'updated_date', 'field_type', 'other', 'choices', 'checkbox_label', 'links', 'required']
+ read_only_fields = ('created_date', 'updated_date')
+
+ def to_representation(self, instance):
+ result = super().to_representation(instance)
+ if instance.field_type == 'single_choice':
+ result['other'] = instance.single_choice.other
+ result['choices'] = ExtendedUserProfileFieldChoiceSerializer(instance.single_choice.choices.filter(deleted=False).order_by('order'), many=True).data
+ if instance.field_type == 'multi_choice':
+ result['other'] = instance.multi_choice.other
+ result['choices'] = ExtendedUserProfileFieldChoiceSerializer(instance.multi_choice.choices.filter(deleted=False).order_by('order'), many=True).data
+ if instance.field_type == 'user_agreement':
+ result['checkbox_label'] = instance.user_agreement.checkbox_label
+ result['links'] = ExtendedUserProfileFieldLinkSerializer(instance.links.order_by('order'), many=True).data
+ return result
+
+ def create(self, validated_data):
+ field_type = validated_data.pop('field_type')
+ other = validated_data.pop('other', False)
+ choices = validated_data.pop('choices', [])
+ checkbox_label = validated_data.pop('checkbox_label', '')
+ links = validated_data.pop('links', [])
+ if field_type == 'text':
+ instance = models.ExtendedUserProfileTextField.objects.create(**validated_data)
+ elif field_type == 'single_choice':
+ instance = models.ExtendedUserProfileSingleChoiceField.objects.create(**validated_data, other=other)
+ # add choices
+ for choice in choices:
+ choice.pop('id', None)
+ instance.choices.create(**choice)
+ elif field_type == 'multi_choice':
+ instance = models.ExtendedUserProfileMultiChoiceField.objects.create(**validated_data, other=other)
+ # add choices
+ for choice in choices:
+ choice.pop('id', None)
+ instance.choices.create(**choice)
+ elif field_type == 'user_agreement':
+ instance = models.ExtendedUserProfileAgreementField.objects.create(**validated_data, checkbox_label=checkbox_label)
+ else:
+ raise Exception(f"Unrecognized field type: {field_type}")
+ # create links
+ for link in links:
+ link.pop('id', None)
+ instance.links.create(**link)
+ return instance
+
+ def update(self, instance, validated_data):
+ instance.name = validated_data['name']
+ instance.help_text = validated_data['help_text']
+ instance.order = validated_data['order']
+ instance.required = validated_data.get('required', instance.required)
+ # logger.debug(f"instance.field_type={instance.field_type}, validated_data={validated_data}")
+ if instance.field_type == 'single_choice':
+ instance.single_choice.other = validated_data.get('other', instance.single_choice.other)
+ choices = validated_data.pop('choices', None)
+ if choices:
+ choice_ids = [choice['id'] for choice in choices if 'id' in choice]
+ # Soft delete any choices that are not in the list
+ instance.single_choice.choices.exclude(id__in=choice_ids).update(deleted=True)
+ for choice in choices:
+ choice_id = choice.pop('id', None)
+ models.ExtendedUserProfileSingleChoiceFieldChoice.objects.update_or_create(
+ id=choice_id,
+ defaults={**choice, "single_choice_field": instance.single_choice},
+ )
+ instance.single_choice.save()
+ elif instance.field_type == 'multi_choice':
+ instance.multi_choice.other = validated_data.get('other', instance.multi_choice.other)
+ choices = validated_data.pop('choices', None)
+ if choices:
+ choice_ids = [choice['id'] for choice in choices if 'id' in choice]
+ # Soft delete any choices that are not in the list
+ instance.multi_choice.choices.exclude(id__in=choice_ids).update(deleted=True)
+ for choice in choices:
+ choice_id = choice.pop('id', None)
+ models.ExtendedUserProfileMultiChoiceFieldChoice.objects.update_or_create(
+ id=choice_id,
+ defaults={**choice, "multi_choice_field": instance.multi_choice},
+ )
+ instance.multi_choice.save()
+ elif instance.field_type == 'user_agreement':
+ instance.user_agreement.checkbox_label = validated_data.pop('checkbox_label', instance.user_agreement.checkbox_label)
+ instance.user_agreement.save()
+
+ # update links
+ links = validated_data.pop('links', [])
+ link_ids = [link['id'] for link in links if 'id' in link]
+ instance.links.exclude(id__in=link_ids).delete()
+ for link in links:
+ link_id = link.pop('id', None)
+ link['field'] = instance
+ models.ExtendedUserProfileFieldLink.objects.update_or_create(
+ id=link_id,
+ defaults=link,
+ )
+
+ instance.save()
+ return instance
+
+
+class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
+ id = serializers.IntegerField(label='ID', required=False)
+ text_value = serializers.CharField(required=False, allow_blank=True)
+ # choices must be write_only so that DRF ignores trying to deserialized this related field
+ # deserialization is handled explicitly in to_representation, see below
+ choices = serializers.ListField(child=serializers.IntegerField(), required=False, write_only=True)
+ other_value = serializers.CharField(required=False, allow_blank=True)
+ agreement_value = serializers.BooleanField(required=False)
+
+ class Meta:
+ model = models.ExtendedUserProfileValue
+ fields = ['id', 'value_type', 'ext_user_profile_field', 'text_value',
+ 'choices', 'other_value', 'agreement_value', 'valid', 'value_display']
+ read_only_fields = ['value_type', 'value_display']
+
+ def to_representation(self, instance):
+ result = super().to_representation(instance)
+ if instance.value_type == 'text':
+ result['text_value'] = instance.text.text_value
+ elif instance.value_type == 'single_choice':
+ choices = []
+ if instance.single_choice.choice is not None:
+ choices.append(instance.single_choice.choice)
+ result['choices'] = choices
+ result['other_value'] = instance.single_choice.other_value
+ elif instance.value_type == 'multi_choice':
+ result['choices'] = list(map(lambda c: c.value, instance.multi_choice.choices.all()))
+ result['other_value'] = instance.multi_choice.other_value
+ elif instance.value_type == 'user_agreement':
+ result['agreement_value'] = instance.user_agreement.agreement_value
+ return result
+
+ def create(self, validated_data):
+ request = self.context['request']
+ user = request.user
+ user_profile = user.user_profile
+
+ # Support create/update in the many=True situation. When many=True and
+ # .save() is called, .create() will be called on each value. Here we
+ # need to see if there is an id and if so call .update() instead.
+ if "id" in validated_data:
+ instance = models.ExtendedUserProfileValue.objects.get(id=validated_data["id"])
+ return self.update(instance, validated_data)
+
+ ext_user_profile_field = validated_data.pop('ext_user_profile_field')
+ if ext_user_profile_field.field_type == 'text':
+ text_value = validated_data.pop('text_value')
+ return models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=ext_user_profile_field,
+ user_profile=user_profile,
+ text_value=text_value)
+ elif ext_user_profile_field.field_type == 'single_choice':
+ choices = validated_data.pop('choices', [])
+ choice = choices[0] if len(choices) > 0 else None
+ other_value = validated_data.pop('other_value', '')
+ return models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=ext_user_profile_field,
+ user_profile=user_profile,
+ choice=choice,
+ other_value=other_value,
+ )
+ elif ext_user_profile_field.field_type == 'multi_choice':
+ choices = validated_data.pop('choices', [])
+ other_value = validated_data.pop('other_value', '')
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=ext_user_profile_field,
+ user_profile=user_profile,
+ other_value=other_value,
+ )
+ for choice in choices:
+ models.ExtendedUserProfileMultiChoiceValueChoice.objects.create(
+ value=choice,
+ multi_choice_value=value
+ )
+ return value
+ elif ext_user_profile_field.field_type == 'user_agreement':
+ agreement_value = validated_data.get('agreement_value')
+ return models.ExtendedUserProfileAgreementValue.objects.create(
+ ext_user_profile_field=ext_user_profile_field,
+ user_profile=user_profile,
+ agreement_value=agreement_value
+ )
+
+ def update(self, instance, validated_data):
+ if instance.value_type == 'text':
+ text_value = validated_data.pop('text_value')
+ instance.text.text_value = text_value
+ instance.text.save()
+ elif instance.value_type == 'single_choice':
+ choices = validated_data.pop('choices', [])
+ choice = choices[0] if len(choices) > 0 else None
+ other_value = validated_data.pop('other_value', '')
+ instance.single_choice.choice = choice
+ instance.single_choice.other_value = other_value
+ instance.single_choice.save()
+ elif instance.value_type == 'multi_choice':
+ choices = validated_data.pop('choices', [])
+ other_value = validated_data.pop('other_value', '')
+ # Delete any that are no longer in the set
+ instance.multi_choice.choices.exclude(value__in=choices).delete()
+ # Create records as needed for new entries
+ for choice in choices:
+ models.ExtendedUserProfileMultiChoiceValueChoice.objects.update_or_create(
+ value=choice, multi_choice_value=instance.multi_choice)
+ instance.multi_choice.other_value = other_value
+ instance.multi_choice.save()
+ elif instance.value_type == 'user_agreement':
+ agreement_value = validated_data.pop('agreement_value')
+ instance.user_agreement.agreement_value = agreement_value
+ instance.user_agreement.save()
+ instance.save()
+ return instance
+
+ def validate(self, attrs):
+ ext_user_profile_field = attrs['ext_user_profile_field']
+ # validate that id_value is only provided for choice fields, and 'text_value' only for the others
+ if ext_user_profile_field.field_type == 'single_choice':
+ choices = attrs.get('choices', [])
+ other_value = attrs.get('other_value', '')
+ # Check that choices are valid
+ for choice in choices:
+ if not ext_user_profile_field.single_choice.choices.filter(id=choice, deleted=False).exists():
+ raise serializers.ValidationError({'choices': 'Invalid choice.'})
+ if len(choices) > 1:
+ raise serializers.ValidationError({'choices': "Must specify only a single choice."})
+ if len(choices) == 1 and other_value != '':
+ raise serializers.ValidationError("Must specify only a single choice or the other choice, but not both.")
+ if len(choices) == 0 and other_value == '':
+ raise serializers.ValidationError("Must specify one of a single choice or the other choice (but not both).")
+ elif ext_user_profile_field.field_type == 'multi_choice':
+ choices = attrs.get('choices', [])
+ other_value = attrs.get('other_value', '')
+ # Check that choices are valid
+ for choice in choices:
+ if not ext_user_profile_field.multi_choice.choices.filter(id=choice, deleted=False).exists():
+ raise serializers.ValidationError({'choices': 'Invalid choice.'})
+ return attrs
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
new file mode 100644
index 0000000..aaf9fc7
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
@@ -0,0 +1,57 @@
+<template>
+ <div>
+ <template v-for="extendedUserProfileField in extendedUserProfileFields">
+ <component
+ ref="extendedUserProfileFieldComponents"
+ :key="extendedUserProfileField.id"
+ :is="getEditor(extendedUserProfileField)"
+ :extended-user-profile-field="extendedUserProfileField"
+ @valid="recordValidChildComponent(extendedUserProfileField.id)"
+ @invalid="recordInvalidChildComponent(extendedUserProfileField.id)"
+ />
+ </template>
+ </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import ExtendedUserProfileMultiChoiceValueEditor from "./ExtendedUserProfileMultiChoiceValueEditor.vue";
+import ExtendedUserProfileSingleChoiceValueEditor from "./ExtendedUserProfileSingleChoiceValueEditor.vue";
+import ExtendedUserProfileTextValueEditor from "./ExtendedUserProfileTextValueEditor.vue";
+import ExtendedUserProfileUserAgreementValueEditor from "./ExtendedUserProfileUserAgreementValueEditor.vue";
+import { mixins } from "django-airavata-common-ui";
+export default {
+ mixins: [mixins.ValidationParent],
+ computed: {
+ ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+ valid() {
+ return this.childComponentsAreValid;
+ },
+ },
+ methods: {
+ getEditor(extendedUserProfileField) {
+ const fieldTypeEditors = {
+ text: ExtendedUserProfileTextValueEditor,
+ single_choice: ExtendedUserProfileSingleChoiceValueEditor,
+ multi_choice: ExtendedUserProfileMultiChoiceValueEditor,
+ user_agreement: ExtendedUserProfileUserAgreementValueEditor,
+ };
+
+ if (extendedUserProfileField.field_type in fieldTypeEditors) {
+ return fieldTypeEditors[extendedUserProfileField.field_type];
+ } else {
+ // eslint-disable-next-line no-console
+ console.error(
+ "Unexpected field_type",
+ extendedUserProfileField.field_type
+ );
+ }
+ },
+ touch() {
+ this.$refs.extendedUserProfileFieldComponents.forEach((c) => c.touch());
+ },
+ },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceValueEditor.vue
new file mode 100644
index 0000000..9905c89
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceValueEditor.vue
@@ -0,0 +1,157 @@
+<template>
+ <extended-user-profile-value-editor v-bind="$props">
+ <b-form-checkbox-group
+ v-model="value"
+ :options="options"
+ stacked
+ @change="onChange"
+ :state="validateStateErrorOnly($v.value)"
+ >
+ <b-form-checkbox
+ :value="otherOptionValue"
+ v-if="extendedUserProfileField.other"
+ >Other (please specify)</b-form-checkbox
+ >
+
+ <b-form-invalid-feedback :state="validateState($v.value)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </b-form-checkbox-group>
+ <template v-if="showOther">
+ <b-form-input
+ class="mt-2"
+ v-model="other"
+ placeholder="Please specify"
+ :state="validateState($v.other)"
+ @input="onInput"
+ />
+ <b-form-invalid-feedback :state="validateState($v.other)"
+ >Please specify a value for 'Other'.</b-form-invalid-feedback
+ >
+ </template>
+ </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required, requiredIf } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+const OTHER_OPTION = new Object(); // sentinel value
+export default {
+ mixins: [validationMixin],
+ components: { ExtendedUserProfileValueEditor },
+ props: ["extendedUserProfileField"],
+ data() {
+ return {
+ otherOptionSelected: false,
+ };
+ },
+ computed: {
+ ...mapGetters("extendedUserProfile", [
+ "getMultiChoiceValue",
+ "getMultiChoiceOther",
+ ]),
+ value: {
+ get() {
+ const copy = this.getMultiChoiceValue(
+ this.extendedUserProfileField.id
+ ).slice();
+ if (this.showOther) {
+ copy.push(this.otherOptionValue);
+ }
+ return copy;
+ },
+ set(value) {
+ const values = value.filter((v) => v !== this.otherOptionValue);
+ this.setMultiChoiceValue({
+ value: values,
+ id: this.extendedUserProfileField.id,
+ });
+ this.$v.value.$touch();
+ },
+ },
+ other: {
+ get() {
+ return this.getMultiChoiceOther(this.extendedUserProfileField.id);
+ },
+ set(value) {
+ this.setMultiChoiceOther({
+ value,
+ id: this.extendedUserProfileField.id,
+ });
+ this.$v.other.$touch();
+ },
+ },
+ showOther() {
+ return this.other || this.otherOptionSelected;
+ },
+ options() {
+ return this.extendedUserProfileField &&
+ this.extendedUserProfileField.choices
+ ? this.extendedUserProfileField.choices.map((choice) => {
+ return {
+ value: choice.id,
+ text: choice.display_text,
+ };
+ })
+ : [];
+ },
+ otherOptionValue() {
+ return OTHER_OPTION;
+ },
+ valid() {
+ return !this.$v.$invalid;
+ },
+ required() {
+ return this.extendedUserProfileField.required;
+ },
+ },
+ validations() {
+ const validations = {
+ value: {
+ required: requiredIf("required"),
+ },
+ other: {},
+ };
+ if (this.showOther) {
+ validations.other = { required };
+ }
+ return validations;
+ },
+ methods: {
+ ...mapMutations("extendedUserProfile", [
+ "setMultiChoiceValue",
+ "setMultiChoiceOther",
+ ]),
+ onChange(value) {
+ this.otherOptionSelected = value.includes(this.otherOptionValue);
+ if (!this.otherOptionSelected) {
+ this.other = "";
+ }
+ },
+ onInput() {
+ // Handle case where initially there is an other value. If the user
+ // deletes the other value, then we still want to keep the other text box
+ // until the user unchecks the other option.
+ this.otherOptionSelected = true;
+ },
+ validateState: errors.vuelidateHelpers.validateState,
+ validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
+ touch() {
+ this.$v.$touch();
+ },
+ },
+ watch: {
+ valid: {
+ handler(valid) {
+ this.$emit(valid ? "valid" : "invalid");
+ },
+ immediate: true,
+ },
+ },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceValueEditor.vue
new file mode 100644
index 0000000..1ca3ef4
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceValueEditor.vue
@@ -0,0 +1,159 @@
+<template>
+ <extended-user-profile-value-editor v-bind="$props">
+ <b-form-select
+ v-model="value"
+ :options="options"
+ @change="onChange"
+ :state="validateStateErrorOnly($v.value)"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled
+ >-- Please select an option --</b-form-select-option
+ >
+ </template>
+
+ <b-form-select-option
+ :value="otherOptionValue"
+ v-if="extendedUserProfileField.other"
+ >Other (please specify)</b-form-select-option
+ >
+ </b-form-select>
+ <b-form-invalid-feedback :state="validateState($v.value)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ <template v-if="showOther">
+ <b-form-input
+ class="mt-2"
+ v-model="other"
+ placeholder="Please specify"
+ :state="validateState($v.other)"
+ @input="onInput"
+ />
+ <b-form-invalid-feedback :state="validateState($v.other)"
+ >Please specify a value for 'Other'.</b-form-invalid-feedback
+ >
+ </template>
+ </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required, requiredIf } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+const OTHER_OPTION = new Object(); // sentinel value
+
+export default {
+ mixins: [validationMixin],
+ components: { ExtendedUserProfileValueEditor },
+ props: ["extendedUserProfileField"],
+ data() {
+ return {
+ otherOptionSelected: false,
+ };
+ },
+ computed: {
+ ...mapGetters("extendedUserProfile", [
+ "getSingleChoiceValue",
+ "getSingleChoiceOther",
+ ]),
+ value: {
+ get() {
+ if (this.showOther) {
+ return this.otherOptionValue;
+ } else {
+ return this.getSingleChoiceValue(this.extendedUserProfileField.id);
+ }
+ },
+ set(value) {
+ if (value !== this.otherOptionValue) {
+ this.setSingleChoiceValue({
+ value,
+ id: this.extendedUserProfileField.id,
+ });
+ this.$v.value.$touch();
+ }
+ },
+ },
+ other: {
+ get() {
+ return this.getSingleChoiceOther(this.extendedUserProfileField.id);
+ },
+ set(value) {
+ this.setSingleChoiceOther({
+ value,
+ id: this.extendedUserProfileField.id,
+ });
+ this.$v.other.$touch();
+ },
+ },
+ showOther() {
+ const value = this.getSingleChoiceValue(this.extendedUserProfileField.id);
+ return (value === null && this.other) || this.otherOptionSelected;
+ },
+ options() {
+ return this.extendedUserProfileField &&
+ this.extendedUserProfileField.choices
+ ? this.extendedUserProfileField.choices.map((choice) => {
+ return {
+ value: choice.id,
+ text: choice.display_text,
+ };
+ })
+ : [];
+ },
+ otherOptionValue() {
+ return OTHER_OPTION;
+ },
+ valid() {
+ return !this.$v.$invalid;
+ },
+ required() {
+ return this.extendedUserProfileField.required;
+ },
+ },
+ validations() {
+ const validations = {
+ value: {},
+ other: {},
+ };
+ if (this.showOther) {
+ validations.other = { required };
+ } else {
+ validations.value = { required: requiredIf("required") };
+ }
+ return validations;
+ },
+ methods: {
+ ...mapMutations("extendedUserProfile", [
+ "setSingleChoiceValue",
+ "setSingleChoiceOther",
+ ]),
+ onChange(value) {
+ this.otherOptionSelected = value === this.otherOptionValue;
+ },
+ onInput() {
+ // Handle case where initially there is an other value. If the user
+ // deletes the other value, then we still want to keep the other text box
+ // until the user unchecks the other option.
+ this.otherOptionSelected = true;
+ },
+ validateState: errors.vuelidateHelpers.validateState,
+ validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
+ touch() {
+ this.$v.$touch();
+ },
+ },
+ watch: {
+ valid: {
+ handler(valid) {
+ this.$emit(valid ? "valid" : "invalid");
+ },
+ immediate: true,
+ },
+ },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextValueEditor.vue
new file mode 100644
index 0000000..644ed82
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextValueEditor.vue
@@ -0,0 +1,63 @@
+<template>
+ <extended-user-profile-value-editor v-bind="$props">
+ <b-form-input v-model="value" :state="validateState($v.value)" />
+ <b-form-invalid-feedback :state="validateState($v.value)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { requiredIf } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+export default {
+ mixins: [validationMixin],
+ components: { ExtendedUserProfileValueEditor },
+ props: ["extendedUserProfileField"],
+ computed: {
+ ...mapGetters("extendedUserProfile", ["getTextValue"]),
+ value: {
+ get() {
+ return this.getTextValue(this.extendedUserProfileField.id);
+ },
+ set(value) {
+ this.setTextValue({ value, id: this.extendedUserProfileField.id });
+ this.$v.$touch();
+ },
+ },
+ valid() {
+ return !this.$v.$invalid;
+ },
+ required() {
+ return this.extendedUserProfileField.required;
+ },
+ },
+ validations() {
+ return {
+ value: {
+ required: requiredIf("required"),
+ },
+ };
+ },
+ methods: {
+ ...mapMutations("extendedUserProfile", ["setTextValue"]),
+ validateState: errors.vuelidateHelpers.validateState,
+ touch() {
+ this.$v.$touch();
+ },
+ },
+ watch: {
+ valid: {
+ handler(valid) {
+ this.$emit(valid ? "valid" : "invalid");
+ },
+ immediate: true,
+ },
+ },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementValueEditor.vue
new file mode 100644
index 0000000..035044d
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementValueEditor.vue
@@ -0,0 +1,81 @@
+<template>
+ <extended-user-profile-value-editor v-bind="$props">
+ <b-form-checkbox
+ v-model="value"
+ :unchecked-value="false"
+ :value="true"
+ :state="validateStateErrorOnly($v.value)"
+ >
+ {{ extendedUserProfileField.checkbox_label }}
+ </b-form-checkbox>
+ <b-form-invalid-feedback :state="validateState($v.value)"
+ >This field is required.</b-form-invalid-feedback
+ >
+ </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+
+export default {
+ mixins: [validationMixin],
+ components: { ExtendedUserProfileValueEditor },
+ props: ["extendedUserProfileField"],
+ computed: {
+ ...mapGetters("extendedUserProfile", ["getUserAgreementValue"]),
+ value: {
+ get() {
+ return this.getUserAgreementValue(this.extendedUserProfileField.id);
+ },
+ set(value) {
+ this.setUserAgreementValue({
+ value,
+ id: this.extendedUserProfileField.id,
+ });
+ this.$v.value.$touch();
+ },
+ },
+ valid() {
+ return !this.$v.$invalid;
+ },
+ required() {
+ return this.extendedUserProfileField.required;
+ },
+ },
+ validations() {
+ const validations = {
+ value: {
+ mustBeTrue: this.mustBeTrue,
+ },
+ };
+ return validations;
+ },
+ methods: {
+ ...mapMutations("extendedUserProfile", ["setUserAgreementValue"]),
+ mustBeTrue(value) {
+ if (this.required) {
+ return value === true;
+ } else {
+ // If not required, always valid
+ return true;
+ }
+ },
+ validateState: errors.vuelidateHelpers.validateState,
+ validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
+ touch() {
+ this.$v.$touch();
+ },
+ },
+ watch: {
+ valid: {
+ handler(valid) {
+ this.$emit(valid ? "valid" : "invalid");
+ },
+ immediate: true,
+ },
+ },
+};
+</script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileValueEditor.vue
new file mode 100644
index 0000000..d9e3ca4
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileValueEditor.vue
@@ -0,0 +1,47 @@
+<template>
+ <b-form-group
+ :label="extendedUserProfileField.name"
+ :description="extendedUserProfileField.help_text"
+ >
+ <template #label>
+ {{ extendedUserProfileField.name }}
+ <small
+ v-if="!extendedUserProfileField.required"
+ class="text-muted text-small"
+ >(Optional)</small
+ >
+ </template>
+ <b-card
+ v-for="link in extendedUserProfileField.links"
+ :key="link.id"
+ :header="link.label"
+ class="ml-3 mb-3"
+ >
+ <b-card-text v-if="link.display_inline">
+ <iframe :src="link.url" />
+ </b-card-text>
+ <a
+ v-if="link.display_link"
+ :href="link.url"
+ target="_blank"
+ class="card-link"
+ >Open '{{ link.label }}' in separate tab.</a
+ >
+ </b-card>
+ <slot />
+ </b-form-group>
+</template>
+
+<script>
+export default {
+ props: ["extendedUserProfileField"],
+};
+</script>
+
+<style scoped>
+iframe {
+ border: none;
+ width: 100%;
+ height: 50vh;
+}
+</style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
index 669d356..bbac76a 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
@@ -1,30 +1,34 @@
<template>
- <b-card>
- <b-form-group label="Username" :disabled="true" description="Only administrators can update a username.">
+ <div v-if="user">
+ <b-form-group
+ label="Username"
+ :disabled="true"
+ description="Only administrators can update a username."
+ >
<b-form-input v-model="user.username" />
</b-form-group>
<b-form-group label="First Name" :disabled="disabled">
<b-form-input
- v-model="$v.user.first_name.$model"
+ v-model="$v.first_name.$model"
@keydown.native.enter="save"
- :state="validateState($v.user.first_name)"
+ :state="validateState($v.first_name)"
/>
</b-form-group>
<b-form-group label="Last Name" :disabled="disabled">
<b-form-input
- v-model="$v.user.last_name.$model"
+ v-model="$v.last_name.$model"
@keydown.native.enter="save"
- :state="validateState($v.user.last_name)"
+ :state="validateState($v.last_name)"
/>
</b-form-group>
<b-form-group label="Email" :disabled="disabled">
<b-form-input
- v-model="$v.user.email.$model"
+ v-model="$v.email.$model"
@keydown.native.enter="save"
- :state="validateState($v.user.email)"
+ :state="validateState($v.email)"
/>
- <b-form-invalid-feedback v-if="!$v.user.email.email">
- {{ user.email }} is not a valid email address.
+ <b-form-invalid-feedback v-if="!$v.email.email">
+ {{ email }} is not a valid email address.
</b-form-invalid-feedback>
<b-alert class="mt-1" show v-if="user.pending_email_change"
>Once you verify your email address at
@@ -36,26 +40,19 @@
></b-alert
>
</b-form-group>
- <b-button variant="primary" @click="save" :disabled="$v.$invalid || disabled"
- >Save</b-button
- >
- </b-card>
+ </div>
</template>
<script>
-import { models } from "django-airavata-api";
import { errors } from "django-airavata-common-ui";
import { validationMixin } from "vuelidate";
import { email, required } from "vuelidate/lib/validators";
+import { mapGetters, mapMutations } from "vuex";
export default {
name: "user-profile-editor",
mixins: [validationMixin],
props: {
- value: {
- type: models.User,
- required: true,
- },
disabled: {
type: Boolean,
default: false,
@@ -63,46 +60,63 @@
},
created() {
if (!this.disabled) {
- this.$v.user.$touch();
+ this.$v.$touch();
}
},
data() {
- return {
- user: this.cloneValue(),
- };
+ return {};
+ },
+ computed: {
+ ...mapGetters("userProfile", ["user"]),
+ first_name: {
+ get() {
+ return this.user.first_name;
+ },
+ set(first_name) {
+ this.setFirstName({ first_name });
+ },
+ },
+ last_name: {
+ get() {
+ return this.user.last_name;
+ },
+ set(last_name) {
+ this.setLastName({ last_name });
+ },
+ },
+ email: {
+ get() {
+ return this.user.email;
+ },
+ set(email) {
+ this.setEmail({ email });
+ },
+ },
+ valid() {
+ return !this.$v.$invalid;
+ },
},
validations() {
return {
- user: {
- first_name: {
- required,
- },
- last_name: {
- required,
- },
- email: {
- required,
- email,
- },
+ first_name: {
+ required,
+ },
+ last_name: {
+ required,
+ },
+ email: {
+ required,
+ email,
},
};
},
methods: {
- cloneValue() {
- return JSON.parse(JSON.stringify(this.value));
- },
+ ...mapMutations("userProfile", ["setFirstName", "setLastName", "setEmail"]),
save() {
- if (!this.$v.$invalid) {
- this.$emit("save", this.user);
- }
+ this.$emit("save");
},
validateState: errors.vuelidateHelpers.validateState,
},
- watch: {
- value() {
- this.user = this.cloneValue();
- },
- },
};
</script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
index 647a60a..c70772c 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
@@ -13,17 +13,27 @@
In the meantime, please complete as much of your profile as possible.
</p>
</b-alert>
- <b-alert v-else-if="user && !user.complete" show>
+ <b-alert v-else-if="mustComplete" show
>Please complete your user profile before continuing.</b-alert
>
- <user-profile-editor
- v-if="user"
- v-model="user"
- @save="onSave"
- @resend-email-verification="resendEmailVerification"
- />
+ <b-card>
+ <user-profile-editor
+ ref="userProfileEditor"
+ @save="onSave"
+ @resend-email-verification="handleResendEmailVerification"
+ />
+ <!-- include extended-user-profile-editor if there are extendedUserProfileFields -->
+ <template
+ v-if="extendedUserProfileFields && extendedUserProfileFields.length > 0"
+ >
+ <hr />
+ <extended-user-profile-editor ref="extendedUserProfileEditor" />
+ </template>
+
+ <b-button variant="primary" @click="onSave">Save</b-button>
+ </b-card>
<b-link
- v-if="user && user.complete"
+ v-if="!mustComplete"
class="text-muted small"
href="/workspace/dashboard"
>Return to Dashboard</b-link
@@ -32,36 +42,66 @@
</template>
<script>
-import { services } from "django-airavata-api";
import UserProfileEditor from "../components/UserProfileEditor.vue";
import { notifications } from "django-airavata-common-ui";
+import { mapActions, mapGetters } from "vuex";
+import ExtendedUserProfileEditor from "../components/ExtendedUserProfileEditor.vue";
export default {
- components: { UserProfileEditor },
+ components: { UserProfileEditor, ExtendedUserProfileEditor },
name: "user-profile-container",
- created() {
- services.UserService.current()
- .then((user) => {
- this.user = user;
- })
- .then(() => {
- const queryParams = new URLSearchParams(window.location.search);
- if (queryParams.has("code")) {
- this.verifyEmailChange(queryParams.get("code"));
- }
- });
+ async created() {
+ await this.loadCurrentUser();
+ await this.loadExtendedUserProfileFields();
+ await this.loadExtendedUserProfileValues();
+
+ const queryParams = new URLSearchParams(window.location.search);
+ if (queryParams.has("code")) {
+ await this.verifyEmailChange({ code: queryParams.get("code") });
+ notifications.NotificationList.add(
+ new notifications.Notification({
+ type: "SUCCESS",
+ message: "Email address verified and updated",
+ duration: 5,
+ })
+ );
+ // Update URL, removing the code from the query string
+ window.history.replaceState({}, "", "/auth/user-profile/");
+ }
},
data() {
return {
- user: null,
+ invalidForm: false,
};
},
+ computed: {
+ ...mapGetters("userProfile", ["user"]),
+ ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+ mustComplete() {
+ return (
+ this.user && (!this.user.complete || !this.user.ext_user_profile_valid)
+ );
+ },
+ },
methods: {
- onSave(value) {
- services.UserService.update({
- lookup: value.id,
- data: value,
- }).then((user) => {
+ ...mapActions("userProfile", [
+ "loadCurrentUser",
+ "verifyEmailChange",
+ "updateUser",
+ "resendEmailVerification",
+ ]),
+ ...mapActions("extendedUserProfile", [
+ "loadExtendedUserProfileFields",
+ "loadExtendedUserProfileValues",
+ "saveExtendedUserProfileValues",
+ ]),
+ async onSave() {
+ if (
+ this.$refs.userProfileEditor.valid &&
+ this.$refs.extendedUserProfileEditor.valid
+ ) {
+ await this.updateUser();
+ await this.saveExtendedUserProfileValues();
notifications.NotificationList.add(
new notifications.Notification({
type: "SUCCESS",
@@ -69,39 +109,19 @@
duration: 5,
})
);
- this.user = user;
- });
+ } else {
+ this.$refs.extendedUserProfileEditor.touch();
+ }
},
- resendEmailVerification() {
- services.UserService.resendEmailVerification({
- lookup: this.user.id,
- }).then(() => {
- notifications.NotificationList.add(
- new notifications.Notification({
- type: "SUCCESS",
- message: "Verification link sent",
- duration: 5,
- })
- );
- });
- },
- verifyEmailChange(code) {
- services.UserService.verifyEmailChange({
- lookup: this.user.id,
- data: { code: code },
- }).then((user) => {
- // User now updated with email change
- this.user = user;
- notifications.NotificationList.add(
- new notifications.Notification({
- type: "SUCCESS",
- message: "Email address verified and updated",
- duration: 5,
- })
- );
- // Update URL, removing the code from the query string
- window.history.replaceState({}, "", "/auth/user-profile/");
- });
+ async handleResendEmailVerification() {
+ await this.resendEmailVerification();
+ notifications.NotificationList.add(
+ new notifications.Notification({
+ type: "SUCCESS",
+ message: "Verification link sent",
+ duration: 5,
+ })
+ );
},
},
};
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js
index 1052e61..382bfcd 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js
@@ -1,8 +1,11 @@
import { components, entry } from "django-airavata-common-ui";
import UserProfileContainer from "./containers/UserProfileContainer.vue";
+import createStore from "./store";
-entry(Vue => {
- new Vue({
- render: h => h(components.MainLayout, [h(UserProfileContainer)])
- }).$mount("#user-profile");
+entry((Vue) => {
+ const store = createStore(Vue);
+ new Vue({
+ store,
+ render: (h) => h(components.MainLayout, [h(UserProfileContainer)]),
+ }).$mount("#user-profile");
});
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js
new file mode 100644
index 0000000..8dd262a
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js
@@ -0,0 +1,18 @@
+import Vuex from "vuex";
+import userProfile from "./modules/userProfile";
+import extendedUserProfile from "./modules/extendedUserProfile";
+
+const debug = process.env.NODE_ENV !== "production";
+
+function createStore(Vue) {
+ Vue.use(Vuex);
+ return new Vuex.Store({
+ modules: {
+ userProfile,
+ extendedUserProfile,
+ },
+ strict: debug,
+ });
+}
+
+export default createStore;
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
new file mode 100644
index 0000000..084731b
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
@@ -0,0 +1,188 @@
+import { services } from "django-airavata-api";
+
+const state = () => ({
+ extendedUserProfileFields: null,
+ extendedUserProfileValues: [],
+});
+
+const getters = {
+ extendedUserProfileFields: (state) => state.extendedUserProfileFields,
+ extendedUserProfileValues: (state) => state.extendedUserProfileValues,
+ getTextValue: (state) => (id) => {
+ const value = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ return value ? value.text_value : null;
+ },
+ getSingleChoiceValue: (state) => (id) => {
+ const value = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (value && value.choices && value.choices.length === 1) {
+ return value.choices[0];
+ } else {
+ return null;
+ }
+ },
+ getSingleChoiceOther: (state) => (id) => {
+ const value = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ return value ? value.other_value : null;
+ },
+ getMultiChoiceValue: (state) => (id) => {
+ const value = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (value && value.choices) {
+ return value.choices;
+ } else {
+ return [];
+ }
+ },
+ getMultiChoiceOther: (state) => (id) => {
+ const value = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ return value ? value.other_value : null;
+ },
+ getUserAgreementValue: (state) => (id) => {
+ const value = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ return value ? value.agreement_value : false;
+ },
+};
+
+const actions = {
+ async loadExtendedUserProfileFields({ commit }) {
+ const extendedUserProfileFields = await services.ExtendedUserProfileFieldService.list();
+ commit("setExtendedUserProfileFields", { extendedUserProfileFields });
+ },
+ async loadExtendedUserProfileValues({ commit }) {
+ const extendedUserProfileValues = await services.ExtendedUserProfileValueService.list();
+ commit("setExtendedUserProfileValues", { extendedUserProfileValues });
+ },
+ async saveExtendedUserProfileValues({ state, commit }) {
+ const extendedUserProfileValues = await services.ExtendedUserProfileValueService.saveAll(
+ { data: state.extendedUserProfileValues }
+ );
+ commit("updateExtendedUserProfileValues", { extendedUserProfileValues });
+ },
+};
+
+const mutations = {
+ setExtendedUserProfileFields(state, { extendedUserProfileFields }) {
+ state.extendedUserProfileFields = extendedUserProfileFields;
+ },
+ setExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+ state.extendedUserProfileValues = extendedUserProfileValues;
+ },
+ setTextValue(state, { value, id }) {
+ const profileValue = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (profileValue) {
+ profileValue.text_value = value;
+ } else {
+ state.extendedUserProfileValues.push({
+ value_type: "text",
+ ext_user_profile_field: id,
+ text_value: value,
+ });
+ }
+ },
+ setSingleChoiceValue(state, { value, id }) {
+ const profileValue = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (profileValue) {
+ profileValue.choices = [value];
+ profileValue.other_value = "";
+ } else {
+ state.extendedUserProfileValues.push({
+ value_type: "single_choice",
+ ext_user_profile_field: id,
+ choices: [value],
+ });
+ }
+ },
+ setSingleChoiceOther(state, { value, id }) {
+ const profileValue = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (profileValue) {
+ profileValue.choices = [];
+ profileValue.other_value = value;
+ } else {
+ state.extendedUserProfileValues.push({
+ value_type: "single_choice",
+ ext_user_profile_field: id,
+ choices: [],
+ other_value: value,
+ });
+ }
+ },
+ setMultiChoiceValue(state, { value, id }) {
+ const profileValue = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (profileValue) {
+ profileValue.choices = value;
+ } else {
+ state.extendedUserProfileValues.push({
+ value_type: "multi_choice",
+ ext_user_profile_field: id,
+ choices: value,
+ });
+ }
+ },
+ setMultiChoiceOther(state, { value, id }) {
+ const profileValue = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (profileValue) {
+ profileValue.other_value = value;
+ } else {
+ state.extendedUserProfileValues.push({
+ value_type: "multi_choice",
+ ext_user_profile_field: id,
+ choices: [],
+ other_value: value,
+ });
+ }
+ },
+ setUserAgreementValue(state, { value, id }) {
+ const profileValue = state.extendedUserProfileValues.find(
+ (v) => v.ext_user_profile_field === id
+ );
+ if (profileValue) {
+ profileValue.agreement_value = value;
+ } else {
+ state.extendedUserProfileValues.push({
+ value_type: "user_agreement",
+ ext_user_profile_field: id,
+ agreement_value: value,
+ });
+ }
+ },
+ updateExtendedUserProfileValue(state, { extendedUserProfileValue }) {
+ const index = state.extendedUserProfileValues.findIndex(
+ (v) =>
+ v.ext_user_profile_field ===
+ extendedUserProfileValue.ext_user_profile_field
+ );
+ state.extendedUserProfileValues.splice(index, 1, extendedUserProfileValue);
+ },
+ updateExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+ state.extendedUserProfileValues = extendedUserProfileValues;
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/userProfile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/userProfile.js
new file mode 100644
index 0000000..9b629ef
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/userProfile.js
@@ -0,0 +1,64 @@
+import { services } from "django-airavata-api";
+
+const state = () => ({
+ user: null,
+});
+
+const getters = {
+ user: (state) => state.user,
+};
+
+const actions = {
+ async loadCurrentUser({ commit }) {
+ const user = await services.UserService.current();
+ commit("setUser", { user });
+ },
+
+ async verifyEmailChange({ commit, state }, { code }) {
+ const user = await services.UserService.verifyEmailChange({
+ lookup: state.user.id,
+ data: { code },
+ });
+ commit("setUser", { user });
+ },
+
+ async updateUser({ commit, state }) {
+ const user = await services.UserService.update({
+ lookup: state.user.id,
+ data: state.user,
+ });
+ commit("setUser", { user });
+ },
+
+ async resendEmailVerification({ state }) {
+ await services.UserService.resendEmailVerification({
+ lookup: state.user.id,
+ });
+ },
+};
+
+const mutations = {
+ setUser(state, { user }) {
+ state.user = user;
+ },
+
+ setFirstName(state, { first_name }) {
+ state.user.first_name = first_name;
+ },
+
+ setLastName(state, { last_name }) {
+ state.user.last_name = last_name;
+ },
+
+ setEmail(state, { email }) {
+ state.user.email = email;
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/django_airavata/apps/auth/tests/test_middleware.py b/django_airavata/apps/auth/tests/test_middleware.py
new file mode 100644
index 0000000..81091af
--- /dev/null
+++ b/django_airavata/apps/auth/tests/test_middleware.py
@@ -0,0 +1,123 @@
+
+from unittest.mock import MagicMock, sentinel
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser
+from django.http import HttpResponseRedirect
+from django.test import RequestFactory, TestCase
+from django.urls import reverse
+
+from django_airavata.apps.auth import models
+from django_airavata.apps.auth.middleware import (
+ user_profile_completeness_check
+)
+
+
+class UserProfileCompletenessCheckTestCase(TestCase):
+
+ def setUp(self):
+ User = get_user_model()
+ self.user: User = User.objects.create_user("testuser")
+ self.user_profile: models.UserProfile = models.UserProfile.objects.create(user=self.user)
+ self.factory = RequestFactory()
+
+ def _middleware_passes_through(self, request):
+ get_response = MagicMock(return_value=sentinel.response)
+ response = user_profile_completeness_check(get_response)(request)
+ get_response.assert_called()
+ self.assertIs(response, sentinel.response)
+
+ def _middleware_redirects_to_user_profile(self, request):
+ get_response = MagicMock(return_value=sentinel.response)
+ response = user_profile_completeness_check(get_response)(request)
+ get_response.assert_not_called()
+ self.assertIsInstance(response, HttpResponseRedirect)
+ self.assertEqual(response.url, reverse('django_airavata_auth:user_profile'))
+
+ def test_not_authenticated(self):
+ """Test that completeness check is skipped when not authenticated."""
+ request = self.factory.get(reverse('django_airavata_workspace:dashboard'))
+ request.user = AnonymousUser()
+ self.assertFalse(request.user.is_authenticated)
+ self._middleware_passes_through(request)
+
+ def test_user_profile_is_incomplete(self):
+ """Test user profile incomplete, should redirect to user_profile view."""
+ request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ self.assertTrue(request.user.is_authenticated)
+ self.assertFalse(self.user_profile.is_complete)
+ self._middleware_redirects_to_user_profile(request)
+
+ def test_user_profile_is_incomplete_but_allowed(self):
+ """Test user profile incomplete, but should be able to access user_profile."""
+ request = self.factory.get(reverse('django_airavata_auth:user_profile'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ self.assertTrue(request.user.is_authenticated)
+ self.assertFalse(self.user_profile.is_complete)
+ self._middleware_passes_through(request)
+
+ def test_user_profile_is_complete(self):
+ """Test user profile is complete, should pass through."""
+ request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ self.user.first_name = "Test"
+ self.user.last_name = "User"
+ self.user.email = "testuser@gateway.edu"
+ self.assertTrue(request.user.is_authenticated)
+ self.assertTrue(self.user_profile.is_complete)
+ self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+ self._middleware_passes_through(request)
+
+ def test_user_profile_is_complete_but_ext_up_is_invalid(self):
+ """Test user profile is complete, but ext user prof is invalid."""
+ request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ self.user.first_name = "Test"
+ self.user.last_name = "User"
+ self.user.email = "testuser@gateway.edu"
+ models.ExtendedUserProfileTextField.objects.create(name="test1", order=1, required=True)
+ self.assertTrue(request.user.is_authenticated)
+ self.assertTrue(self.user_profile.is_complete)
+ self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+ self._middleware_redirects_to_user_profile(request)
+
+ def test_user_profile_is_complete_and_ext_up_is_valid(self):
+ """Test user profile is complete and ext user prof is valid."""
+ request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ self.user.first_name = "Test"
+ self.user.last_name = "User"
+ self.user.email = "testuser@gateway.edu"
+ field1 = models.ExtendedUserProfileTextField.objects.create(name="test1", order=1, required=True)
+ models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field1, user_profile=self.user_profile,
+ text_value="Answer #1"
+ )
+ self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+ self.assertTrue(request.user.is_authenticated)
+ self.assertTrue(self.user_profile.is_complete)
+ self.assertEqual(1, len(self.user_profile.extended_profile_values.all()))
+ self._middleware_passes_through(request)
+
+ def test_user_profile_is_complete_ext_up_is_invalid_but_user_is_admin(self):
+ """Test user profile is complete, ext user prof is invalid, but user is gateway admin."""
+ request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ request.is_gateway_admin = True
+ self.user.first_name = "Admin"
+ self.user.last_name = "User"
+ self.user.email = "admin@gateway.edu"
+ models.ExtendedUserProfileTextField.objects.create(name="test1", order=1, required=True)
+ self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+ self.assertTrue(request.user.is_authenticated)
+ self.assertTrue(self.user_profile.is_complete)
+ self._middleware_passes_through(request)
+
+ def test_user_profile_is_incomplete_but_logout_allowed(self):
+ """Test user profile incomplete, but should be able to access logout."""
+ request = self.factory.get(reverse('django_airavata_auth:logout'), HTTP_ACCEPT=['text/html'])
+ request.user = self.user
+ self.assertTrue(request.user.is_authenticated)
+ self.assertFalse(self.user_profile.is_complete)
+ self._middleware_passes_through(request)
diff --git a/django_airavata/apps/auth/tests/test_models.py b/django_airavata/apps/auth/tests/test_models.py
new file mode 100644
index 0000000..677fc8f
--- /dev/null
+++ b/django_airavata/apps/auth/tests/test_models.py
@@ -0,0 +1,383 @@
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+
+from django_airavata.apps.auth import models
+
+
+class ExtendedUserProfileValueTestCase(TestCase):
+
+ def setUp(self) -> None:
+ User = get_user_model()
+ user = User.objects.create_user("testuser")
+ self.user_profile = models.UserProfile.objects.create(user=user)
+
+ def test_value_display_of_text_value(self):
+ field = models.ExtendedUserProfileTextField.objects.create(
+ name="test", order=1)
+ value = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ text_value="Some random answer.")
+ self.assertEqual(value.value_display, value.text_value)
+
+ def test_value_display_of_single_choice_with_choice(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ choice_two = field.choices.get(display_text="Choice #2")
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ choice=choice_two.id)
+ self.assertEqual(value.value_display, choice_two.display_text)
+
+ def test_value_display_of_single_choice_with_non_existent_choice(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ choice=-1)
+ self.assertEqual(value.value_display, None)
+
+ def test_value_display_of_single_choice_with_other(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ other_value="Write-in value")
+ self.assertEqual(value.value_display, "Other: Write-in value")
+
+ def test_value_display_of_multi_choice_with_choices(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1)
+ choice_one = field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ choice_three = field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile)
+ value.choices.create(value=choice_one.id)
+ value.choices.create(value=choice_three.id)
+ self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text])
+
+ def test_value_display_of_multi_choice_with_other(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ other_value="Some write-in value.")
+ self.assertListEqual(value.value_display, ["Other: Some write-in value."])
+
+ def test_value_display_of_multi_choice_with_choices_and_other(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1)
+ choice_one = field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ choice_three = field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ other_value="Some write-in value.")
+ value.choices.create(value=choice_one.id)
+ value.choices.create(value=choice_three.id)
+ self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text, "Other: Some write-in value."])
+
+ def test_value_display_of_multi_choice_with_non_existent_choices(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1)
+ choice_one = field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ choice_three = field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile)
+ value.choices.create(value=choice_one.id)
+ value.choices.create(value=choice_three.id)
+ value.choices.create(value=-1)
+ self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text])
+
+ def test_value_display_of_user_agreement_with_no(self):
+ field = models.ExtendedUserProfileAgreementField.objects.create(
+ name="test", order=1)
+ value = models.ExtendedUserProfileAgreementValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ agreement_value=False)
+ self.assertEqual(value.value_display, "No")
+
+ def test_value_display_of_user_agreement_with_yes(self):
+ field = models.ExtendedUserProfileAgreementField.objects.create(
+ name="test", order=1)
+ value = models.ExtendedUserProfileAgreementValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ agreement_value=True)
+ self.assertEqual(value.value_display, "Yes")
+
+ def test_valid_of_text(self):
+ field = models.ExtendedUserProfileTextField.objects.create(
+ name="test", order=1, required=True)
+ value = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ text_value="Some value")
+ self.assertTrue(value.valid)
+
+ def test_valid_of_text_empty(self):
+ field = models.ExtendedUserProfileTextField.objects.create(
+ name="test", order=1, required=True)
+ value = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ text_value="")
+ self.assertFalse(value.valid)
+
+ def test_valid_of_text_empty_deleted(self):
+ """Invalid value but field is deleted so valid should be true."""
+ field = models.ExtendedUserProfileTextField.objects.create(
+ name="test", order=1, required=True, deleted=True)
+ value = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ text_value="")
+ self.assertTrue(value.valid, "Although value is empty but required, since the field is deleted, consider it not invalid")
+
+ def test_valid_of_text_empty_no_required(self):
+ field = models.ExtendedUserProfileTextField.objects.create(
+ name="test", order=1, required=False)
+ value = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ text_value="")
+ self.assertTrue(value.valid)
+
+ def test_valid_of_single_choice_none(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1, required=True)
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile)
+ self.assertFalse(value.valid)
+
+ def test_valid_of_single_choice_with_choice(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1, required=True)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ choice_two = field.choices.get(display_text="Choice #2")
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ choice=choice_two.id)
+ self.assertTrue(value.valid)
+
+ def test_valid_of_single_choice_with_non_existent_choice(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1, required=True)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ choice=-1)
+ self.assertFalse(value.valid)
+
+ def test_valid_of_single_choice_with_other(self):
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1, required=True, other=True)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ other_value="Some write-in value.")
+ self.assertTrue(value.valid)
+
+ def test_valid_of_single_choice_with_other_but_not_allowed(self):
+ # Configure field so that 'Other' isn't an option
+ field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+ name="test", order=1, required=True, other=False)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ other_value="Some write-in value.")
+ self.assertFalse(value.valid)
+
+ def test_valid_of_multi_choice_with_none(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1, required=True)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile)
+ self.assertFalse(value.valid)
+
+ def test_valid_of_multi_choice_with_some(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1, required=True)
+ choice_one = field.choices.create(display_text="Choice #1", order=1)
+ choice_two = field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile)
+ value.choices.create(value=choice_one.id)
+ value.choices.create(value=choice_two.id)
+ self.assertTrue(value.valid)
+
+ def test_valid_of_multi_choice_with_non_existent_choice(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1, required=True)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile)
+ value.choices.create(value=-1)
+ self.assertFalse(value.valid)
+
+ def test_valid_of_multi_choice_with_other(self):
+ field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+ name="test", order=1, required=True, other=True)
+ field.choices.create(display_text="Choice #1", order=1)
+ field.choices.create(display_text="Choice #2", order=2)
+ field.choices.create(display_text="Choice #3", order=3)
+ value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+ ext_user_profile_field=field, user_profile=self.user_profile,
+ other_value="Some write-in value.")
+ self.assertTrue(value.valid)
+
+
+class UserProfileTestCase(TestCase):
+
+ def setUp(self) -> None:
+ User = get_user_model()
+ user = User.objects.create_user("testuser")
+ self.user_profile: models.UserProfile = models.UserProfile.objects.create(user=user)
+
+ def test_is_ext_user_profile_valid_no_fields(self):
+ self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+
+ def test_is_ext_user_profile_valid_some_fields_some_values_valid(self):
+ """Values for all fields, but only some are valid"""
+ field1 = models.ExtendedUserProfileTextField.objects.create(
+ name="test1", order=1, required=True)
+ field2 = models.ExtendedUserProfileTextField.objects.create(
+ name="test2", order=2, required=True)
+ field3 = models.ExtendedUserProfileTextField.objects.create(
+ name="test3", order=3, required=True)
+ value1 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field1, user_profile=self.user_profile,
+ text_value="Answer #1"
+ )
+ value2 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field2, user_profile=self.user_profile,
+ text_value="Answer #2"
+ )
+ value3 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field3, user_profile=self.user_profile,
+ text_value="" # intentionally blank
+ )
+ self.assertTrue(value1.valid)
+ self.assertTrue(value2.valid)
+ self.assertFalse(value3.valid)
+ self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+
+ def test_is_ext_user_profile_valid_some_fields_all_values_valid(self):
+ """Values for all fields, and all values are valid."""
+ field1 = models.ExtendedUserProfileTextField.objects.create(
+ name="test1", order=1, required=True)
+ field2 = models.ExtendedUserProfileTextField.objects.create(
+ name="test2", order=2, required=True)
+ field3 = models.ExtendedUserProfileTextField.objects.create(
+ name="test3", order=3, required=True)
+ value1 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field1, user_profile=self.user_profile,
+ text_value="Answer #1"
+ )
+ value2 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field2, user_profile=self.user_profile,
+ text_value="Answer #2"
+ )
+ value3 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field3, user_profile=self.user_profile,
+ text_value="Answer #3"
+ )
+ self.assertTrue(value1.valid)
+ self.assertTrue(value2.valid)
+ self.assertTrue(value3.valid)
+ self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+
+ def test_is_ext_user_profile_valid_some_fields_some_not_required_values_missing(self):
+ """Some values are missing but they are optional."""
+ field1 = models.ExtendedUserProfileTextField.objects.create(
+ name="test1", order=1, required=True)
+ field2 = models.ExtendedUserProfileTextField.objects.create(
+ name="test2", order=2, required=False)
+ field3 = models.ExtendedUserProfileTextField.objects.create(
+ name="test3", order=3, required=False)
+ value1 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field1, user_profile=self.user_profile,
+ text_value="Answer #1"
+ )
+ self.assertTrue(value1.valid)
+ self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+ user_profile=self.user_profile, ext_user_profile_field=field2).exists(),
+ "No value for field2")
+ self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+ user_profile=self.user_profile, ext_user_profile_field=field3).exists(),
+ "No value for field3")
+ self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+
+ def test_is_ext_user_profile_valid_some_fields_some_required_values_missing(self):
+ """Some required values are missing."""
+ field1 = models.ExtendedUserProfileTextField.objects.create(
+ name="test1", order=1, required=True)
+ field2 = models.ExtendedUserProfileTextField.objects.create(
+ name="test2", order=2, required=True)
+ field3 = models.ExtendedUserProfileTextField.objects.create(
+ name="test3", order=3, required=False)
+ value1 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field1, user_profile=self.user_profile,
+ text_value="Answer #1"
+ )
+ self.assertTrue(value1.valid)
+ self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+ user_profile=self.user_profile, ext_user_profile_field=field2).exists(),
+ "No value for field2, but field2 is required")
+ self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+ user_profile=self.user_profile, ext_user_profile_field=field3).exists(),
+ "No value for field3")
+ self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+
+ def test_is_ext_user_profile_valid_some_fields_invalid_value_but_field_deleted(self):
+ """Value is invalid but field is deleted so it shouldn't count."""
+ field1 = models.ExtendedUserProfileTextField.objects.create(
+ name="test1", order=1, required=True)
+ field2 = models.ExtendedUserProfileTextField.objects.create(
+ name="test2", order=2, required=True)
+ field3 = models.ExtendedUserProfileTextField.objects.create(
+ name="test3", order=3, required=True)
+
+ value1 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field1, user_profile=self.user_profile,
+ text_value="Answer #1"
+ )
+ value2 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field2, user_profile=self.user_profile,
+ text_value="Answer #2"
+ )
+ value3 = models.ExtendedUserProfileTextValue.objects.create(
+ ext_user_profile_field=field3, user_profile=self.user_profile,
+ text_value=""
+ )
+ self.assertTrue(value1.valid)
+ self.assertTrue(value2.valid)
+ self.assertFalse(value3.valid)
+ self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+
+ field3.deleted = True
+ field3.save()
+ self.assertTrue(value3.valid)
+ self.assertTrue(self.user_profile.is_ext_user_profile_valid)
diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py
index 89909a5..a762271 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -7,6 +7,8 @@
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet, basename='user')
+router.register(r'extended-user-profile-fields', views.ExtendedUserProfileFieldViewset, basename='extended-user-profile-field')
+router.register(r'extended-user-profile-values', views.ExtendedUserProfileValueViewset, basename='extended-user-profile-value')
app_name = 'django_airavata_auth'
urlpatterns = [
re_path(r'^', include(router.urls)),
diff --git a/django_airavata/apps/auth/utils.py b/django_airavata/apps/auth/utils.py
index d7ebda8..cfa53c0 100644
--- a/django_airavata/apps/auth/utils.py
+++ b/django_airavata/apps/auth/utils.py
@@ -129,14 +129,7 @@
})
subject = Template(new_user_email_template.subject).render(context)
body = Template(new_user_email_template.body).render(context)
- msg = EmailMessage(subject=subject,
- body=body,
- from_email=f'"{settings.PORTAL_TITLE}" <{settings.SERVER_EMAIL}>',
- to=[f'"{a[0]}" <{a[1]}>' for a in getattr(settings,
- 'PORTAL_ADMINS',
- settings.ADMINS)])
- msg.content_subtype = 'html'
- msg.send()
+ send_email_to_admins(subject, body)
def send_admin_alert_about_uninitialized_username(request, username, email, first_name, last_name):
@@ -174,6 +167,33 @@
portal.
</p>
""".strip()).render(context)
+ send_email_to_admins(subject, body)
+
+
+def send_admin_user_completed_profile(request, user_profile):
+ domain, port = split_domain_port(request.get_host())
+ user = user_profile.user
+ extended_profile_values = user_profile.extended_profile_values.filter(
+ ext_user_profile_field__deleted=False).order_by("ext_user_profile_field__order").all()
+ context = Context({
+ "username": user.username,
+ "email": user.email,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "portal_title": settings.PORTAL_TITLE,
+ "gateway_id": settings.GATEWAY_ID,
+ "http_host": domain,
+ "extended_profile_values": extended_profile_values
+ })
+
+ user_profile_completed_template = models.EmailTemplate.objects.get(
+ pk=models.USER_PROFILE_COMPLETED_TEMPLATE)
+ subject = Template(user_profile_completed_template.subject).render(context)
+ body = Template(user_profile_completed_template.body).render(context)
+ send_email_to_admins(subject, body)
+
+
+def send_email_to_admins(subject, body):
msg = EmailMessage(subject=subject,
body=body,
from_email=f'"{settings.PORTAL_TITLE}" <{settings.SERVER_EMAIL}>',
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index edf8861..0b4fdd9 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -24,10 +24,14 @@
from django.urls import reverse
from django.views.decorators.debug import sensitive_variables
from requests_oauthlib import OAuth2Session
-from rest_framework import permissions, viewsets
+from rest_framework import mixins, permissions, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
+from django_airavata.apps.api.view_utils import (
+ IsInAdminsGroupPermission,
+ ReadOnly
+)
from django_airavata.apps.auth import serializers
from . import forms, iam_admin_client, models, utils
@@ -701,3 +705,67 @@
r = requests.get(client_endpoint + "/client-secret", headers=headers)
r.raise_for_status()
return r.json()['value']
+
+
+class ExtendedUserProfileFieldViewset(viewsets.ModelViewSet):
+ serializer_class = serializers.ExtendedUserProfileFieldSerializer
+ queryset = models.ExtendedUserProfileField.objects.all().order_by('order')
+ permission_classes = [permissions.IsAuthenticated, IsInAdminsGroupPermission | ReadOnly]
+
+ def get_queryset(self):
+ queryset = super().get_queryset()
+ if self.action == 'list':
+ queryset = queryset.filter(deleted=False)
+ return queryset
+
+ def perform_destroy(self, instance):
+ instance.deleted = True
+ instance.save()
+
+
+class IsExtendedUserProfileOwnerOrReadOnlyForAdmins(permissions.BasePermission):
+ def has_permission(self, request, view):
+ return request.user.is_authenticated
+
+ def has_object_permission(self, request, view, obj):
+ if (request.method in permissions.SAFE_METHODS and
+ request.is_gateway_admin):
+ return True
+ return obj.user_profile.user == request.user
+
+
+class ExtendedUserProfileValueViewset(mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.ListModelMixin,
+ viewsets.GenericViewSet):
+ serializer_class = serializers.ExtendedUserProfileValueSerializer
+ permission_classes = [IsExtendedUserProfileOwnerOrReadOnlyForAdmins]
+
+ def get_queryset(self):
+ user = self.request.user
+ if self.request.is_gateway_admin:
+ queryset = models.ExtendedUserProfileValue.objects.all()
+ username = self.request.query_params.get('username')
+ if username is not None:
+ queryset = queryset.filter(user_profile__user__username=username)
+ else:
+ queryset = user.user_profile.extended_profile_values.all()
+ return queryset
+
+ @action(methods=['POST'], detail=False, url_path="save-all")
+ @atomic
+ def save_all(self, request, format=None):
+ user = request.user
+ user_profile: models.UserProfile = user.user_profile
+ old_valid = user_profile.is_ext_user_profile_valid
+ serializer: serializers.ExtendedUserProfileValueSerializer = self.get_serializer(data=request.data, many=True)
+ serializer.is_valid(raise_exception=True)
+ values = serializer.save()
+
+ new_valid = user_profile.is_ext_user_profile_valid
+ if not old_valid and new_valid:
+ utils.send_admin_user_completed_profile(request, user_profile)
+
+ serializer = self.get_serializer(values, many=True)
+ return Response(serializer.data)
diff --git a/django_airavata/apps/auth/yarn.lock b/django_airavata/apps/auth/yarn.lock
index eadbe09..817a0c7 100644
--- a/django_airavata/apps/auth/yarn.lock
+++ b/django_airavata/apps/auth/yarn.lock
@@ -8272,6 +8272,11 @@
resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.6.tgz#84100c13b943470660d0416642845cd2a1edf4b2"
integrity sha512-suzIuet1jGcyZ4oUSW8J27R2tNrJ9cIfklAh63EbAkFjE380iv97BAiIeolRYoB9bF9usBXCu4BxftWN1Dkn3g==
+vuex@^3.6.2:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
+ integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
+
watchpack-chokidar2@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
diff --git a/django_airavata/apps/workspace/package.json b/django_airavata/apps/workspace/package.json
index 2535328..b32f710 100644
--- a/django_airavata/apps/workspace/package.json
+++ b/django_airavata/apps/workspace/package.json
@@ -51,9 +51,9 @@
"cross-env": "^7.0.3",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0-0",
- "node-sass": "^6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
+ "sass": "^1.54.5",
"sass-loader": "^10",
"vue-loader": "^15.5.1",
"vue-template-compiler": "^2.5.22",
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 79e04a2..4485733 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
@@ -1,6 +1,6 @@
<template>
<div>
- <unsaved-changes-guard :dirty="dirty"/>
+ <unsaved-changes-guard :dirty="dirty" />
<div class="row">
<div class="col-auto mr-auto">
<h1 class="h4 mb-4">
@@ -89,9 +89,13 @@
</div>
<div class="row">
<div class="col">
- <workspace-notices-management-container class="mt-2"
+ <workspace-notices-management-container
+ class="mt-2"
v-if="appInterface && appInterface.applicationDescription"
- :data="[{notificationMessage: appInterface.applicationDescription}]"/>
+ :data="[
+ { notificationMessage: appInterface.applicationDescription },
+ ]"
+ />
</div>
</div>
<div class="row">
@@ -148,12 +152,8 @@
</div>
<div class="row">
<div class="col">
- <b-form-group
- label="Email Settings"
- >
- <b-form-checkbox
- v-model="localExperiment.enableEmailNotification"
- >
+ <b-form-group label="Email Settings">
+ <b-form-checkbox v-model="localExperiment.enableEmailNotification">
Receive email notification of experiment status
</b-form-checkbox>
</b-form-group>
@@ -186,9 +186,10 @@
import ExperimentDescriptionEditor from "./ExperimentDescriptionEditor.vue";
import GroupResourceProfileSelector from "./GroupResourceProfileSelector.vue";
import InputEditorContainer from "./input-editors/InputEditorContainer.vue";
-import {models, services} from "django-airavata-api";
-import {components, utils} from "django-airavata-common-ui";
+import { models, services } from "django-airavata-api";
+import { components, utils } from "django-airavata-common-ui";
import WorkspaceNoticesManagementContainer from "../notices/WorkspaceNoticesManagementContainer";
+import _ from "lodash";
export default {
name: "edit-experiment",
@@ -348,6 +349,32 @@
inputValueChanged: function () {
this.localExperiment.evaluateInputDependencies();
},
+ calculateQueueSettings: _.debounce(async function () {
+ const queueSettingsUpdate = await services.QueueSettingsCalculatorService.calculate(
+ {
+ lookup: this.appInterface.queueSettingsCalculatorId,
+ data: this.localExperiment,
+ },
+ { showSpinner: false }
+ );
+ // Override values in computationalResourceScheduling with the values
+ // returned from the queue settings calculator
+ Object.assign(
+ this.localExperiment.userConfigurationData
+ .computationalResourceScheduling,
+ queueSettingsUpdate
+ );
+ }, 500),
+ experimentInputsChanged() {
+ if (this.appInterface.queueSettingsCalculatorId) {
+ this.calculateQueueSettings();
+ }
+ },
+ resourceHostIdChanged() {
+ if (this.appInterface.queueSettingsCalculatorId) {
+ this.calculateQueueSettings();
+ }
+ },
},
watch: {
experiment: function (newValue) {
@@ -359,6 +386,15 @@
},
deep: true,
},
+ "experiment.experimentInputs": {
+ handler() {
+ this.experimentInputsChanged();
+ },
+ deep: true,
+ },
+ "experiment.userConfigurationData.computationalResourceScheduling.resourceHostId": function () {
+ this.resourceHostIdChanged();
+ },
},
};
</script>
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 da86e07..12aa9ef 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
@@ -2,10 +2,14 @@
<div v-if="showQueueSettings">
<div class="row">
<div class="col">
- <div class="card border-default" :class="{ 'border-danger': !valid }">
+ <div
+ class="card border-default"
+ :class="{ 'border-danger': !valid, 'is-disabled': disabled }"
+ >
<b-link
@click="showConfiguration = !showConfiguration"
class="card-link text-dark"
+ :disabled="disabled"
>
<div class="card-body">
<h5 class="card-title mb-4">
@@ -106,7 +110,8 @@
Max Allowed Cores = {{ maxCPUCount
}}<template
v-if="
- selectedQueueDefault && selectedQueueDefault.cpuPerNode > 0
+ selectedQueueDefault &&
+ selectedQueueDefault.cpuPerNode > 0
"
>. There are {{ selectedQueueDefault.cpuPerNode }} cores per
node.
@@ -114,16 +119,47 @@
</div>
</b-form-group>
</div>
- <div class="d-flex flex-column" v-if="selectedQueueDefault && selectedQueueDefault.cpuPerNode > 0">
- <div class="flex-fill"
- style="border: 1px solid #6c757d;border-top-right-radius: 10px;margin-top: 51px;border-left-width: 0px;border-bottom-width: 0px;margin-right: 15px;"></div>
- <b-button size="sm" pill variant="outline-secondary"
- v-on:click="enableNodeCountToCpuCheck = !enableNodeCountToCpuCheck">
- <i v-if="enableNodeCountToCpuCheck" class="fa fa-lock" aria-hidden="true"></i>
+ <div
+ class="d-flex flex-column"
+ v-if="selectedQueueDefault && selectedQueueDefault.cpuPerNode > 0"
+ >
+ <div
+ class="flex-fill"
+ style="
+ border: 1px solid #6c757d;
+ border-top-right-radius: 10px;
+ margin-top: 51px;
+ border-left-width: 0px;
+ border-bottom-width: 0px;
+ margin-right: 15px;
+ "
+ ></div>
+ <b-button
+ size="sm"
+ pill
+ variant="outline-secondary"
+ v-on:click="
+ enableNodeCountToCpuCheck = !enableNodeCountToCpuCheck
+ "
+ >
+ <i
+ v-if="enableNodeCountToCpuCheck"
+ class="fa fa-lock"
+ aria-hidden="true"
+ ></i>
<i v-else class="fa fa-unlock" aria-hidden="true"></i>
</b-button>
- <div class="flex-fill"
- style="border: 1px solid #6c757d;border-bottom-right-radius: 10px;margin-bottom: 57px;border-left-width: 0px;border-top-width: 0px;margin-right: 15px;"></div>
+ <div
+ class="flex-fill"
+ style="
+ border: 1px solid #6c757d;
+ border-bottom-right-radius: 10px;
+ margin-bottom: 57px;
+ border-left-width: 0px;
+ border-top-width: 0px;
+ margin-right: 15px;
+ "
+ ></div>
</div>
</div>
<b-form-group
@@ -337,6 +373,12 @@
? this.applicationInterface.showQueueSettings
: false;
},
+ disabled() {
+ return (
+ this.applicationInterface &&
+ !!this.applicationInterface.queueSettingsCalculatorId
+ );
+ },
},
methods: {
queueChanged: function (queueName) {
@@ -449,7 +491,10 @@
}
},
nodeCountChanged() {
- if (this.enableNodeCountToCpuCheck && this.selectedQueueDefault.cpuPerNode > 0) {
+ if (
+ this.enableNodeCountToCpuCheck &&
+ this.selectedQueueDefault.cpuPerNode > 0
+ ) {
const nodeCount = parseInt(this.data.nodeCount);
this.data.totalCPUCount = Math.min(
nodeCount * this.selectedQueueDefault.cpuPerNode,
@@ -458,7 +503,10 @@
}
},
cpuCountChanged() {
- if (this.enableNodeCountToCpuCheck && this.selectedQueueDefault.cpuPerNode > 0) {
+ if (
+ this.enableNodeCountToCpuCheck &&
+ this.selectedQueueDefault.cpuPerNode > 0
+ ) {
const cpuCount = parseInt(this.data.totalCPUCount);
if (cpuCount > 0) {
this.data.nodeCount = Math.min(
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue
index 96a2ffe..e480ba3 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue
@@ -24,8 +24,8 @@
{{ data.item.name }}</b-link
>
</template>
- <template slot="cell(createdTimestamp)" slot-scope="data">
- <human-date :date="data.item.createdTime" />
+ <template slot="cell(modifiedTimestamp)" slot-scope="data">
+ <human-date :date="data.item.modifiedTime" />
</template>
<template slot="cell(actions)" slot-scope="data">
<b-link
@@ -87,8 +87,8 @@
formatter: (value) => this.getFormattedSize(value),
},
{
- label: "Created Time",
- key: "createdTimestamp",
+ label: "Last Modified",
+ key: "modifiedTimestamp",
sortable: true,
},
{
@@ -106,8 +106,8 @@
name: d.name,
path: d.path,
type: "dir",
- createdTime: d.createdTime,
- createdTimestamp: d.createdTime.getTime(), // for sorting
+ modifiedTime: d.modifiedTime,
+ modifiedTimestamp: d.modifiedTime.getTime(), // for sorting
size: d.size,
};
});
@@ -118,8 +118,8 @@
type: "file",
dataProductURI: f.dataProductURI,
downloadURL: f.downloadURL,
- createdTime: f.createdTime,
- createdTimestamp: f.createdTime.getTime(), // for sorting
+ modifiedTime: f.modifiedTime,
+ modifiedTimestamp: f.modifiedTime.getTime(), // for sorting
size: f.size,
};
});
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 71f6991..974c27c 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
@@ -44,8 +44,8 @@
:allow-preview="allowPreview"
/>
</template>
- <template slot="cell(createdTimestamp)" slot-scope="data">
- <human-date :date="data.item.createdTime" />
+ <template slot="cell(modifiedTimestamp)" slot-scope="data">
+ <human-date :date="data.item.modifiedTime" />
</template>
<template slot="cell(actions)" slot-scope="data">
<b-button
@@ -159,8 +159,8 @@
formatter: (value) => this.getFormattedSize(value),
},
{
- label: "Created Time",
- key: "createdTimestamp",
+ label: "Last Modified",
+ key: "modifiedTimestamp",
sortable: true,
},
{
@@ -178,8 +178,8 @@
name: d.name,
path: d.path,
type: "dir",
- createdTime: d.createdTime,
- createdTimestamp: d.createdTime.getTime(), // for sorting
+ modifiedTime: d.modifiedTime,
+ modifiedTimestamp: d.modifiedTime.getTime(), // for sorting
size: d.size,
};
});
@@ -190,8 +190,8 @@
type: "file",
dataProductURI: f.dataProductURI,
downloadURL: f.downloadURL,
- createdTime: f.createdTime,
- createdTimestamp: f.createdTime.getTime(), // for sorting
+ modifiedTime: f.modifiedTime,
+ modifiedTimestamp: f.modifiedTime.getTime(), // for sorting
size: f.size,
};
});
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
index 40f6671..bd0e83d 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
@@ -81,6 +81,7 @@
files: [
{
createdTime: dataProduct.creationTime,
+ modifiedTime: dataProduct.lastModifiedTime,
dataProductURI: this.dataProductUri,
downloadURL: dataProduct.downloadURL,
mimeType: dataProduct.productMetadata["mime-type"],
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
index 1e1053d..1322499 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
@@ -264,7 +264,7 @@
} else {
commit("updateGroupResourceProfileId", { groupResourceProfileId });
}
- if (oldValue !== groupResourceProfileId) {
+ if (groupResourceProfileId && oldValue !== groupResourceProfileId) {
await dispatch("loadApplicationDeployments");
await dispatch("applyGroupResourceProfile");
}
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js b/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js
index 1b8346f..b9e4a01 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js
@@ -38,7 +38,14 @@
}
mutationCount++;
- if (mutationCount >= expectedMutations.length) {
+ checkIfDone();
+ };
+
+ const checkIfDone = () => {
+ if (
+ mutationCount >= expectedMutations.length &&
+ actionCount >= expectedActions.length
+ ) {
done();
}
};
@@ -55,9 +62,7 @@
}
actionCount++;
- if (actionCount >= expectedActions.length) {
- done();
- }
+ checkIfDone();
return action.result;
};
@@ -881,3 +886,50 @@
done,
});
});
+
+test("updateGroupResourceProfileId: test normal case where updated to a GRP id", (done) => {
+ const mockGetters = {
+ groupResourceProfileId: "old_grp_id",
+ };
+ const groupResourceProfileId = "new_grp_id";
+ const expectedMutations = [
+ {
+ type: "updateGroupResourceProfileId",
+ payload: { groupResourceProfileId },
+ },
+ ];
+ const expectedActions = [
+ { type: "loadApplicationDeployments" },
+ { type: "applyGroupResourceProfile" },
+ ];
+ testAction(actions.updateGroupResourceProfileId, {
+ payload: {
+ groupResourceProfileId,
+ },
+ getters: mockGetters,
+ expectedMutations,
+ done,
+ expectedActions,
+ });
+});
+
+test("updateGroupResourceProfileId: test case where GRP id is updated to null", (done) => {
+ const mockGetters = {
+ groupResourceProfileId: "old_grp_id",
+ };
+ const groupResourceProfileId = null;
+ const expectedMutations = [
+ {
+ type: "updateGroupResourceProfileId",
+ payload: { groupResourceProfileId },
+ },
+ ];
+ testAction(actions.updateGroupResourceProfileId, {
+ payload: {
+ groupResourceProfileId,
+ },
+ getters: mockGetters,
+ expectedMutations,
+ done,
+ });
+});
diff --git a/django_airavata/apps/workspace/vue.config.js b/django_airavata/apps/workspace/vue.config.js
index c08ea40..73a8dc6 100644
--- a/django_airavata/apps/workspace/vue.config.js
+++ b/django_airavata/apps/workspace/vue.config.js
@@ -28,6 +28,12 @@
path: __dirname,
},
},
+ sass: {
+ sassOptions: {
+ // Turn off deprecation warnings for sass dependencies
+ quietDeps: true,
+ },
+ },
},
},
configureWebpack: {
diff --git a/django_airavata/apps/workspace/yarn.lock b/django_airavata/apps/workspace/yarn.lock
index 550fba2..059c14a 100644
--- a/django_airavata/apps/workspace/yarn.lock
+++ b/django_airavata/apps/workspace/yarn.lock
@@ -812,11 +812,6 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
-"@types/minimist@^1.2.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
- integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
-
"@types/node@*":
version "12.12.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.9.tgz#0b5ae05516b757cbff2e82c04500190aef986c7b"
@@ -1415,7 +1410,7 @@
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ajv@^6.12.3, ajv@^6.12.5:
+ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -1430,11 +1425,6 @@
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
-amdefine@>=0.0.4:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
- integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
-
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -1503,6 +1493,14 @@
micromatch "^3.1.4"
normalize-path "^2.1.1"
+anymatch@~3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+ integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
append-transform@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
@@ -1643,11 +1641,6 @@
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
-async-foreach@^0.1.3:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
- integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
-
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
@@ -1977,6 +1970,11 @@
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5:
version "3.7.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de"
@@ -2074,6 +2072,13 @@
split-string "^3.0.2"
to-regex "^3.0.1"
+braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -2336,21 +2341,12 @@
no-case "^2.2.0"
upper-case "^1.1.1"
-camelcase-keys@^6.2.2:
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
- integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
- dependencies:
- camelcase "^5.3.1"
- map-obj "^4.0.0"
- quick-lru "^4.0.1"
-
camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2387,7 +2383,7 @@
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
-chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -2438,6 +2434,21 @@
resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
+"chokidar@>=3.0.0 <4.0.0":
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
chokidar@^2.0.2, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2462,11 +2473,6 @@
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==
-chownr@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
- integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
chrome-trace-event@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -2950,7 +2956,7 @@
shebang-command "^2.0.0"
which "^2.0.1"
-cross-spawn@^7.0.1, cross-spawn@^7.0.3:
+cross-spawn@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -3233,15 +3239,7 @@
dependencies:
ms "^2.1.1"
-decamelize-keys@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
- integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
- dependencies:
- decamelize "^1.1.0"
- map-obj "^1.0.0"
-
-decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0:
+decamelize@^1.1.1, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3675,11 +3673,6 @@
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
-env-paths@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
- integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
-
errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -4329,6 +4322,13 @@
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -4518,13 +4518,6 @@
dependencies:
minipass "^2.6.0"
-fs-minipass@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
- integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
- dependencies:
- minipass "^3.0.0"
-
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -4548,6 +4541,11 @@
nan "^2.12.1"
node-pre-gyp "^0.12.0"
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
function-bind@^1.0.2, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -4572,13 +4570,6 @@
strip-ansi "^3.0.1"
wide-align "^1.1.0"
-gaze@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
- integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
- dependencies:
- globule "^1.0.0"
-
get-caller-file@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
@@ -4589,11 +4580,6 @@
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-stdin@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
- integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
-
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4648,12 +4634,19 @@
is-glob "^3.1.0"
path-dirname "^1.0.0"
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1:
+glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -4712,15 +4705,6 @@
pify "^4.0.1"
slash "^2.0.0"
-globule@^1.0.0:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4"
- integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==
- dependencies:
- glob "~7.1.1"
- lodash "~4.17.10"
- minimatch "~3.0.2"
-
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -4733,11 +4717,6 @@
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
-graceful-fs@^4.2.3:
- version "4.2.6"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
- integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
-
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -4780,19 +4759,6 @@
ajv "^6.5.5"
har-schema "^2.0.0"
-har-validator@~5.1.3:
- version "5.1.5"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
- integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
- dependencies:
- ajv "^6.12.3"
- har-schema "^2.0.0"
-
-hard-rejection@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
- integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4926,13 +4892,6 @@
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
-hosted-git-info@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
- integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
- dependencies:
- lru-cache "^6.0.0"
-
hpack.js@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -5139,6 +5098,11 @@
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+immutable@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
+ integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5190,11 +5154,6 @@
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-indent-string@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
- integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -5358,6 +5317,13 @@
dependencies:
binary-extensions "^1.0.0"
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -5387,13 +5353,6 @@
rgb-regex "^1.0.1"
rgba-regex "^1.0.0"
-is-core-module@^2.5.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
- integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
- dependencies:
- has "^1.0.3"
-
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -5520,6 +5479,13 @@
dependencies:
is-extglob "^2.1.1"
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
is-number@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -5539,6 +5505,11 @@
resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -5563,7 +5534,7 @@
dependencies:
path-is-inside "^1.0.2"
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@@ -6099,11 +6070,6 @@
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
-js-base64@^2.1.8:
- version "2.6.4"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
- integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
-
js-base64@^2.4.9:
version "2.5.1"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
@@ -6314,11 +6280,6 @@
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
-kind-of@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
- integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
kleur@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300"
@@ -6549,11 +6510,6 @@
lodash._baseiteratee "~4.7.0"
lodash._baseuniq "~4.6.0"
-lodash@^4.0.0, lodash@~4.17.10:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
- integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
-
lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
@@ -6644,16 +6600,6 @@
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
-map-obj@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
- integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-obj@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
- integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
-
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -6715,24 +6661,6 @@
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
-meow@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
- integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
- dependencies:
- "@types/minimist" "^1.2.0"
- camelcase-keys "^6.2.2"
- decamelize "^1.2.0"
- decamelize-keys "^1.1.0"
- hard-rejection "^2.1.0"
- minimist-options "4.1.0"
- normalize-package-data "^3.0.0"
- read-pkg-up "^7.0.1"
- redent "^3.0.0"
- trim-newlines "^3.0.0"
- type-fest "^0.18.0"
- yargs-parser "^20.2.3"
-
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -6857,11 +6785,6 @@
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
-min-indent@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
- integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-
mini-css-extract-plugin@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
@@ -6882,22 +6805,13 @@
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
-minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
-minimist-options@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
- integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
- dependencies:
- arrify "^1.0.1"
- is-plain-obj "^1.1.0"
- kind-of "^6.0.3"
-
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -6926,13 +6840,6 @@
safe-buffer "^5.1.2"
yallist "^3.0.0"
-minipass@^3.0.0:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
- integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
- dependencies:
- yallist "^4.0.0"
-
minizlib@^1.2.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
@@ -6940,14 +6847,6 @@
dependencies:
minipass "^2.9.0"
-minizlib@^2.1.1:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
- integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
- dependencies:
- minipass "^3.0.0"
- yallist "^4.0.0"
-
mississippi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -6995,11 +6894,6 @@
dependencies:
minimist "0.0.8"
-mkdirp@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
- integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
moment@^2.21.0, moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -7069,11 +6963,6 @@
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
-nan@^2.13.2:
- version "2.14.2"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
- integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
-
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -7150,22 +7039,6 @@
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
-node-gyp@^7.1.0:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae"
- integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==
- dependencies:
- env-paths "^2.2.0"
- glob "^7.1.4"
- graceful-fs "^4.2.3"
- nopt "^5.0.0"
- npmlog "^4.1.2"
- request "^2.88.2"
- rimraf "^3.0.2"
- semver "^7.3.2"
- tar "^6.0.2"
- which "^2.0.2"
-
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -7248,27 +7121,6 @@
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
-node-sass@^6:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe"
- integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==
- dependencies:
- async-foreach "^0.1.3"
- chalk "^1.1.1"
- cross-spawn "^7.0.3"
- gaze "^1.0.0"
- get-stdin "^4.0.1"
- glob "^7.0.3"
- lodash "^4.17.15"
- meow "^9.0.0"
- nan "^2.13.2"
- node-gyp "^7.1.0"
- npmlog "^4.0.0"
- request "^2.88.0"
- sass-graph "2.2.5"
- stdout-stream "^1.4.0"
- "true-case-path" "^1.0.2"
-
nopt@^4.0.1, nopt@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -7277,13 +7129,6 @@
abbrev "1"
osenv "^0.1.4"
-nopt@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
- integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
- dependencies:
- abbrev "1"
-
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -7294,16 +7139,6 @@
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
-normalize-package-data@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
- integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
- dependencies:
- hosted-git-info "^4.0.1"
- is-core-module "^2.5.0"
- semver "^7.3.4"
- validate-npm-package-license "^3.0.1"
-
normalize-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
@@ -7316,7 +7151,7 @@
dependencies:
remove-trailing-separator "^1.0.1"
-normalize-path@^3.0.0:
+normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -7383,7 +7218,7 @@
dependencies:
path-key "^3.0.0"
-npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2:
+npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -7900,6 +7735,11 @@
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+picomatch@^2.0.4, picomatch@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
pidtree@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
@@ -8556,11 +8396,6 @@
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
-quick-lru@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
- integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-
randomatic@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
@@ -8618,15 +8453,6 @@
find-up "^1.0.0"
read-pkg "^1.0.0"
-read-pkg-up@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
- integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
- dependencies:
- find-up "^4.1.0"
- read-pkg "^5.2.0"
- type-fest "^0.8.1"
-
read-pkg@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -8645,7 +8471,7 @@
normalize-package-data "^2.3.2"
path-type "^3.0.0"
-read-pkg@^5.0.0, read-pkg@^5.2.0:
+read-pkg@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
@@ -8686,6 +8512,13 @@
micromatch "^3.1.10"
readable-stream "^2.0.2"
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
realpath-native@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
@@ -8693,14 +8526,6 @@
dependencies:
util.promisify "^1.0.0"
-redent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
- integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
- dependencies:
- indent-string "^4.0.0"
- strip-indent "^3.0.0"
-
regenerate-unicode-properties@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -8887,32 +8712,6 @@
tunnel-agent "^0.6.0"
uuid "^3.3.2"
-request@^2.88.0, request@^2.88.2:
- version "2.88.2"
- resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
- integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
- dependencies:
- aws-sign2 "~0.7.0"
- aws4 "^1.8.0"
- caseless "~0.12.0"
- combined-stream "~1.0.6"
- extend "~3.0.2"
- forever-agent "~0.6.1"
- form-data "~2.3.2"
- har-validator "~5.1.3"
- http-signature "~1.2.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.19"
- oauth-sign "~0.9.0"
- performance-now "^2.1.0"
- qs "~6.5.2"
- safe-buffer "^5.1.2"
- tough-cookie "~2.5.0"
- tunnel-agent "^0.6.0"
- uuid "^3.3.2"
-
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -9032,13 +8831,6 @@
dependencies:
glob "^7.1.3"
-rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -9123,16 +8915,6 @@
optionalDependencies:
fsevents "^1.2.3"
-sass-graph@2.2.5:
- version "2.2.5"
- resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8"
- integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==
- dependencies:
- glob "^7.0.0"
- lodash "^4.0.0"
- scss-tokenizer "^0.2.3"
- yargs "^13.3.2"
-
sass-loader@^10:
version "10.2.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716"
@@ -9144,6 +8926,15 @@
schema-utils "^3.0.0"
semver "^7.3.2"
+sass@^1.54.5:
+ version "1.54.5"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a"
+ integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==
+ dependencies:
+ chokidar ">=3.0.0 <4.0.0"
+ immutable "^4.0.0"
+ source-map-js ">=0.6.2 <2.0.0"
+
sax@^1.2.4, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -9167,14 +8958,6 @@
ajv "^6.12.5"
ajv-keywords "^3.5.2"
-scss-tokenizer@^0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
- integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
- dependencies:
- js-base64 "^2.1.8"
- source-map "^0.4.2"
-
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -9209,13 +8992,6 @@
dependencies:
lru-cache "^6.0.0"
-semver@^7.3.4:
- version "7.3.5"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
- integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
- dependencies:
- lru-cache "^6.0.0"
-
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -9445,6 +9221,11 @@
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+"source-map-js@>=0.6.2 <2.0.0":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
@@ -9481,13 +9262,6 @@
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
-source-map@^0.4.2:
- version "0.4.4"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
- integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
- dependencies:
- amdefine ">=0.0.4"
-
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -9640,13 +9414,6 @@
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-stdout-stream@^1.4.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
- integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
- dependencies:
- readable-stream "^2.0.1"
-
stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
@@ -9835,13 +9602,6 @@
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
-strip-indent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
- integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
- dependencies:
- min-indent "^1.0.0"
-
strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -9958,18 +9718,6 @@
safe-buffer "^5.1.2"
yallist "^3.0.3"
-tar@^6.0.2:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
- integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
- dependencies:
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- minipass "^3.0.0"
- minizlib "^2.1.1"
- mkdirp "^1.0.3"
- yallist "^4.0.0"
-
terser-webpack-plugin@^1.2.3, terser-webpack-plugin@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
@@ -10115,6 +9863,13 @@
is-number "^3.0.0"
repeat-string "^1.6.1"
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -10135,7 +9890,7 @@
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
-tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
+tough-cookie@^2.3.3, tough-cookie@^2.3.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -10158,23 +9913,11 @@
dependencies:
punycode "^2.1.0"
-trim-newlines@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
- integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
-
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
-"true-case-path@^1.0.2":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
- integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==
- dependencies:
- glob "^7.1.2"
-
tryer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -10232,21 +9975,11 @@
dependencies:
prelude-ls "~1.1.2"
-type-fest@^0.18.0:
- version "0.18.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
- integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
-
type-fest@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
-type-fest@^0.8.1:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
- integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -10855,7 +10588,7 @@
dependencies:
isexe "^2.0.0"
-which@^2.0.1, which@^2.0.2:
+which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -11002,14 +10735,6 @@
camelcase "^5.0.0"
decamelize "^1.2.0"
-yargs-parser@^13.1.2:
- version "13.1.2"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
- integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
- dependencies:
- camelcase "^5.0.0"
- decamelize "^1.2.0"
-
yargs-parser@^16.1.0:
version "16.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1"
@@ -11018,11 +10743,6 @@
camelcase "^5.0.0"
decamelize "^1.2.0"
-yargs-parser@^20.2.3:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
yargs-parser@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
@@ -11066,22 +10786,6 @@
y18n "^3.2.1"
yargs-parser "^9.0.2"
-yargs@^13.3.2:
- version "13.3.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
- integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
- dependencies:
- cliui "^5.0.0"
- find-up "^3.0.0"
- get-caller-file "^2.0.1"
- require-directory "^2.1.1"
- require-main-filename "^2.0.0"
- set-blocking "^2.0.0"
- string-width "^3.0.0"
- which-module "^2.0.0"
- y18n "^4.0.0"
- yargs-parser "^13.1.2"
-
yargs@^15.0.0:
version "15.0.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3"
diff --git a/django_airavata/static/common/js/errors/vuelidateHelpers.js b/django_airavata/static/common/js/errors/vuelidateHelpers.js
index 3f43183..1332e77 100644
--- a/django_airavata/static/common/js/errors/vuelidateHelpers.js
+++ b/django_airavata/static/common/js/errors/vuelidateHelpers.js
@@ -2,3 +2,17 @@
const { $dirty, $error } = validation;
return $dirty ? !$error : null;
}
+
+/**
+ * Return false if there is a validation error, null otherwise.
+ *
+ * This is just like validateState except it doesn't return true when valid
+ * which is useful if you only want to show invalid feedback.
+ *
+ * @param {*} validation
+ * @returns
+ */
+export function validateStateErrorOnly(validation) {
+ const { $dirty, $error } = validation;
+ return $dirty && $error ? false : null;
+}
diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js
index b4bcc5e..68c9719 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -31,6 +31,7 @@
import ListLayout from "./layouts/ListLayout.vue";
+import ValidationParent from "./mixins/ValidationParent";
import VModelMixin from "./mixins/VModelMixin";
import Notification from "./notifications/Notification";
@@ -80,6 +81,7 @@
};
const mixins = {
+ ValidationParent,
VModelMixin,
};
diff --git a/django_airavata/static/common/js/mixins/ValidationParent.js b/django_airavata/static/common/js/mixins/ValidationParent.js
new file mode 100644
index 0000000..6e89563
--- /dev/null
+++ b/django_airavata/static/common/js/mixins/ValidationParent.js
@@ -0,0 +1,30 @@
+/**
+ * Aggregate validation state of child components. Child components should
+ * dispatch 'valid' and 'invalid' events and component using this mixin should
+ * call recordValidChildComponent or recordInvalidChildComponent, respectively.
+ */
+export default {
+ data: function () {
+ return {
+ invalidChildComponents: [],
+ };
+ },
+ computed: {
+ childComponentsAreValid() {
+ return this.invalidChildComponents.length === 0;
+ },
+ },
+ methods: {
+ recordInvalidChildComponent(childComponentId) {
+ if (!this.invalidChildComponents.includes(childComponentId)) {
+ this.invalidChildComponents.push(childComponentId);
+ }
+ },
+ recordValidChildComponent(childComponentId) {
+ if (this.invalidChildComponents.includes(childComponentId)) {
+ const index = this.invalidChildComponents.indexOf(childComponentId);
+ this.invalidChildComponents.splice(index, 1);
+ }
+ },
+ },
+};
diff --git a/django_airavata/static/common/package.json b/django_airavata/static/common/package.json
index eced286..27e9970 100644
--- a/django_airavata/static/common/package.json
+++ b/django_airavata/static/common/package.json
@@ -44,8 +44,8 @@
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0-0",
"file-loader": "^3.0.1",
- "node-sass": "^6.0.1",
"prettier": "^2.0.5",
+ "sass": "^1.54.5",
"sass-loader": "^10",
"vue-loader": "^15.5.1",
"vue-template-compiler": "^2.5.22",
diff --git a/django_airavata/static/common/scss/main.scss b/django_airavata/static/common/scss/main.scss
index 72698d5..3314aa3 100644
--- a/django_airavata/static/common/scss/main.scss
+++ b/django_airavata/static/common/scss/main.scss
@@ -253,6 +253,9 @@
.fade-leave-to {
opacity: 0;
}
+.fade-move {
+ transition: transform 0.3s;
+}
/**
* spacing between buttons. .btn-container is a class applied to a wrapper
@@ -357,6 +360,8 @@
bottom: 0px;
left: 70px;
right: 0px;
+ // keep fixed-footer above form controls
+ z-index: 3;
padding-left: calc(1rem + 15px);
padding-right: calc(1rem + 15px);
padding-top: 1rem;
diff --git a/django_airavata/static/common/yarn.lock b/django_airavata/static/common/yarn.lock
index 2fa3c48..00b6b4e 100644
--- a/django_airavata/static/common/yarn.lock
+++ b/django_airavata/static/common/yarn.lock
@@ -788,11 +788,6 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
-"@types/minimist@^1.2.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
- integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
-
"@types/node@*":
version "12.12.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.9.tgz#0b5ae05516b757cbff2e82c04500190aef986c7b"
@@ -1356,11 +1351,6 @@
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
-amdefine@>=0.0.4:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
- integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
-
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -1434,6 +1424,14 @@
micromatch "^3.1.4"
normalize-path "^2.1.1"
+anymatch@~3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+ integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -1501,11 +1499,6 @@
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-arrify@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
- integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
-
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@@ -1550,11 +1543,6 @@
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
-async-foreach@^0.1.3:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
- integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
-
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
@@ -1709,6 +1697,11 @@
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5:
version "3.7.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de"
@@ -1797,6 +1790,13 @@
split-string "^3.0.2"
to-regex "^3.0.1"
+braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -2040,16 +2040,7 @@
no-case "^2.2.0"
upper-case "^1.1.1"
-camelcase-keys@^6.2.2:
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
- integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
- dependencies:
- camelcase "^5.3.1"
- map-obj "^4.0.0"
- quick-lru "^4.0.1"
-
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2079,7 +2070,7 @@
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
-chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -2130,6 +2121,21 @@
resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
+"chokidar@>=3.0.0 <4.0.0":
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
chokidar@^2.0.2, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2154,11 +2160,6 @@
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==
-chownr@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
- integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
chrome-trace-event@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -2615,7 +2616,7 @@
shebang-command "^2.0.0"
which "^2.0.1"
-cross-spawn@^7.0.1, cross-spawn@^7.0.3:
+cross-spawn@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -2867,15 +2868,7 @@
dependencies:
ms "^2.1.1"
-decamelize-keys@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
- integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
- dependencies:
- decamelize "^1.1.0"
- map-obj "^1.0.0"
-
-decamelize@^1.1.0, decamelize@^1.2.0:
+decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3255,11 +3248,6 @@
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
-env-paths@^2.2.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
- integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
-
errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -3809,6 +3797,13 @@
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -3986,13 +3981,6 @@
dependencies:
minipass "^2.6.0"
-fs-minipass@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
- integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
- dependencies:
- minipass "^3.0.0"
-
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -4016,6 +4004,11 @@
nan "^2.12.1"
node-pre-gyp "^0.12.0"
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
function-bind@^1.0.2, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -4040,13 +4033,6 @@
strip-ansi "^3.0.1"
wide-align "^1.1.0"
-gaze@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
- integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
- dependencies:
- globule "^1.0.0"
-
get-caller-file@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
@@ -4057,11 +4043,6 @@
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-stdin@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
- integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
-
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4101,12 +4082,19 @@
is-glob "^3.1.0"
path-dirname "^1.0.0"
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3:
+glob@^7.0.3, glob@^7.1.2, glob@^7.1.3:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@@ -4130,18 +4118,6 @@
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@~7.1.1:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
globals@^11.0.1, globals@^11.1.0, globals@^11.7.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -4184,15 +4160,6 @@
pify "^4.0.1"
slash "^2.0.0"
-globule@^1.0.0:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.3.tgz#811919eeac1ab7344e905f2e3be80a13447973c2"
- integrity sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==
- dependencies:
- glob "~7.1.1"
- lodash "~4.17.10"
- minimatch "~3.0.2"
-
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -4205,7 +4172,7 @@
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
-graceful-fs@^4.1.2, graceful-fs@^4.2.3:
+graceful-fs@^4.1.2:
version "4.2.8"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
@@ -4236,11 +4203,6 @@
ajv "^6.12.3"
har-schema "^2.0.0"
-hard-rejection@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
- integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4361,13 +4323,6 @@
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
-hosted-git-info@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
- integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
- dependencies:
- lru-cache "^6.0.0"
-
hpack.js@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -4567,6 +4522,11 @@
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+immutable@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
+ integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -4610,11 +4570,6 @@
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-indent-string@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
- integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -4778,6 +4733,13 @@
dependencies:
binary-extensions "^1.0.0"
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -4807,7 +4769,7 @@
rgb-regex "^1.0.1"
rgba-regex "^1.0.0"
-is-core-module@^2.2.0, is-core-module@^2.5.0:
+is-core-module@^2.2.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
@@ -4904,6 +4866,13 @@
dependencies:
is-extglob "^2.1.1"
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -4911,6 +4880,11 @@
dependencies:
kind-of "^3.0.2"
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -4935,7 +4909,7 @@
dependencies:
path-is-inside "^1.0.2"
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@@ -5040,11 +5014,6 @@
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
-js-base64@^2.1.8:
- version "2.6.4"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
- integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
-
js-base64@^2.4.9:
version "2.5.1"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
@@ -5207,11 +5176,6 @@
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
-kind-of@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
- integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
klona@^2.0.4:
version "2.0.5"
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
@@ -5401,16 +5365,16 @@
lodash._baseiteratee "~4.7.0"
lodash._baseuniq "~4.6.0"
-lodash@^4.0.0, lodash@^4.17.15, lodash@~4.17.10:
- version "4.17.21"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
- integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
@@ -5489,16 +5453,6 @@
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
-map-obj@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
- integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-obj@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
- integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
-
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -5550,24 +5504,6 @@
errno "^0.1.3"
readable-stream "^2.0.1"
-meow@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
- integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
- dependencies:
- "@types/minimist" "^1.2.0"
- camelcase-keys "^6.2.2"
- decamelize "^1.2.0"
- decamelize-keys "^1.1.0"
- hard-rejection "^2.1.0"
- minimist-options "4.1.0"
- normalize-package-data "^3.0.0"
- read-pkg-up "^7.0.1"
- redent "^3.0.0"
- trim-newlines "^3.0.0"
- type-fest "^0.18.0"
- yargs-parser "^20.2.3"
-
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -5673,11 +5609,6 @@
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
-min-indent@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
- integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-
mini-css-extract-plugin@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
@@ -5698,22 +5629,13 @@
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
-minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.2, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
-minimist-options@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
- integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
- dependencies:
- arrify "^1.0.1"
- is-plain-obj "^1.1.0"
- kind-of "^6.0.3"
-
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -5737,13 +5659,6 @@
safe-buffer "^5.1.2"
yallist "^3.0.0"
-minipass@^3.0.0:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732"
- integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==
- dependencies:
- yallist "^4.0.0"
-
minizlib@^1.2.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
@@ -5751,14 +5666,6 @@
dependencies:
minipass "^2.9.0"
-minizlib@^2.1.1:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
- integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
- dependencies:
- minipass "^3.0.0"
- yallist "^4.0.0"
-
mississippi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -5813,11 +5720,6 @@
dependencies:
minimist "^1.2.5"
-mkdirp@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
- integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -5887,11 +5789,6 @@
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
-nan@^2.13.2:
- version "2.15.0"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
- integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
-
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -5960,22 +5857,6 @@
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
-node-gyp@^7.1.0:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae"
- integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==
- dependencies:
- env-paths "^2.2.0"
- glob "^7.1.4"
- graceful-fs "^4.2.3"
- nopt "^5.0.0"
- npmlog "^4.1.2"
- request "^2.88.2"
- rimraf "^3.0.2"
- semver "^7.3.2"
- tar "^6.0.2"
- which "^2.0.2"
-
node-ipc@^9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.1.1.tgz#4e245ed6938e65100e595ebc5dc34b16e8dd5d69"
@@ -6042,27 +5923,6 @@
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
-node-sass@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe"
- integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==
- dependencies:
- async-foreach "^0.1.3"
- chalk "^1.1.1"
- cross-spawn "^7.0.3"
- gaze "^1.0.0"
- get-stdin "^4.0.1"
- glob "^7.0.3"
- lodash "^4.17.15"
- meow "^9.0.0"
- nan "^2.13.2"
- node-gyp "^7.1.0"
- npmlog "^4.0.0"
- request "^2.88.0"
- sass-graph "2.2.5"
- stdout-stream "^1.4.0"
- "true-case-path" "^1.0.2"
-
nopt@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -6071,13 +5931,6 @@
abbrev "1"
osenv "^0.1.4"
-nopt@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
- integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
- dependencies:
- abbrev "1"
-
normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -6088,16 +5941,6 @@
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
-normalize-package-data@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
- integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
- dependencies:
- hosted-git-info "^4.0.1"
- is-core-module "^2.5.0"
- semver "^7.3.4"
- validate-npm-package-license "^3.0.1"
-
normalize-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
@@ -6110,7 +5953,7 @@
dependencies:
remove-trailing-separator "^1.0.1"
-normalize-path@^3.0.0:
+normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -6162,7 +6005,7 @@
dependencies:
path-key "^3.0.0"
-npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2:
+npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -6634,6 +6477,11 @@
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+picomatch@^2.0.4, picomatch@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -7245,11 +7093,6 @@
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
-quick-lru@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
- integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -7290,16 +7133,7 @@
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-read-pkg-up@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
- integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
- dependencies:
- find-up "^4.1.0"
- read-pkg "^5.2.0"
- type-fest "^0.8.1"
-
-read-pkg@^5.0.0, read-pkg@^5.2.0:
+read-pkg@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
@@ -7353,13 +7187,12 @@
micromatch "^3.1.10"
readable-stream "^2.0.2"
-redent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
- integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
- indent-string "^4.0.0"
- strip-indent "^3.0.0"
+ picomatch "^2.2.1"
regenerate-unicode-properties@^8.1.0:
version "8.1.0"
@@ -7502,7 +7335,7 @@
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
-request@^2.87.0, request@^2.88.0, request@^2.88.2:
+request@^2.87.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -7650,13 +7483,6 @@
dependencies:
glob "^7.1.3"
-rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -7725,16 +7551,6 @@
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-sass-graph@2.2.5:
- version "2.2.5"
- resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8"
- integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==
- dependencies:
- glob "^7.0.0"
- lodash "^4.0.0"
- scss-tokenizer "^0.2.3"
- yargs "^13.3.2"
-
sass-loader@^10:
version "10.2.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716"
@@ -7746,6 +7562,15 @@
schema-utils "^3.0.0"
semver "^7.3.2"
+sass@^1.54.5:
+ version "1.54.5"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a"
+ integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==
+ dependencies:
+ chokidar ">=3.0.0 <4.0.0"
+ immutable "^4.0.0"
+ source-map-js ">=0.6.2 <2.0.0"
+
sax@^1.2.4, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -7769,14 +7594,6 @@
ajv "^6.12.5"
ajv-keywords "^3.5.2"
-scss-tokenizer@^0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
- integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
- dependencies:
- js-base64 "^2.1.8"
- source-map "^0.4.2"
-
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7804,7 +7621,7 @@
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-semver@^7.3.2, semver@^7.3.4:
+semver@^7.3.2:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -8030,6 +7847,11 @@
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+"source-map-js@>=0.6.2 <2.0.0":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
source-map-resolve@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
@@ -8059,13 +7881,6 @@
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
-source-map@^0.4.2:
- version "0.4.4"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
- integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
- dependencies:
- amdefine ">=0.0.4"
-
source-map@^0.5.0, source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -8213,13 +8028,6 @@
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-stdout-stream@^1.4.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
- integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
- dependencies:
- readable-stream "^2.0.1"
-
stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
@@ -8404,13 +8212,6 @@
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
-strip-indent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
- integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
- dependencies:
- min-indent "^1.0.0"
-
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -8515,18 +8316,6 @@
safe-buffer "^5.1.2"
yallist "^3.0.3"
-tar@^6.0.2:
- version "6.1.11"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
- integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
- dependencies:
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- minipass "^3.0.0"
- minizlib "^2.1.1"
- mkdirp "^1.0.3"
- yallist "^4.0.0"
-
terser-webpack-plugin@^1.2.3, terser-webpack-plugin@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
@@ -8646,6 +8435,13 @@
is-number "^3.0.0"
repeat-string "^1.6.1"
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -8674,18 +8470,6 @@
psl "^1.1.28"
punycode "^2.1.1"
-trim-newlines@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
- integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
-
-"true-case-path@^1.0.2":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
- integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==
- dependencies:
- glob "^7.1.2"
-
tryer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -8733,21 +8517,11 @@
dependencies:
prelude-ls "~1.1.2"
-type-fest@^0.18.0:
- version "0.18.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
- integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
-
type-fest@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
-type-fest@^0.8.1:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
- integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -9243,7 +9017,7 @@
dependencies:
isexe "^2.0.0"
-which@^2.0.1, which@^2.0.2:
+which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -9364,14 +9138,6 @@
camelcase "^5.0.0"
decamelize "^1.2.0"
-yargs-parser@^13.1.2:
- version "13.1.2"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
- integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
- dependencies:
- camelcase "^5.0.0"
- decamelize "^1.2.0"
-
yargs-parser@^16.1.0:
version "16.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1"
@@ -9380,11 +9146,6 @@
camelcase "^5.0.0"
decamelize "^1.2.0"
-yargs-parser@^20.2.3:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
yargs@12.0.5:
version "12.0.5"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
@@ -9403,22 +9164,6 @@
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
-yargs@^13.3.2:
- version "13.3.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
- integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
- dependencies:
- cliui "^5.0.0"
- find-up "^3.0.0"
- get-caller-file "^2.0.1"
- require-directory "^2.1.1"
- require-main-filename "^2.0.0"
- set-blocking "^2.0.0"
- string-width "^3.0.0"
- which-module "^2.0.0"
- y18n "^4.0.0"
- yargs-parser "^13.1.2"
-
yargs@^15.0.0:
version "15.0.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3"
diff --git a/docs/dev/developing_frontend.md b/docs/dev/developing_frontend.md
index 41913c1..f324c7b 100644
--- a/docs/dev/developing_frontend.md
+++ b/docs/dev/developing_frontend.md
@@ -2,7 +2,8 @@
Make sure you have
[the latest version of Node.js LTS installed](https://nodejs.org/en/download/).
-You also need to install [the Yarn package manager](https://yarnpkg.com).
+You also need to install
+[the Yarn 1 (Classic) package manager](https://classic.yarnpkg.com/en/docs/install).
Start the Django portal (`python manage.py runserver`). Navigate to the Django
app directory and run `yarn` and then `yarn` to start up the dev server. Now you
@@ -166,6 +167,6 @@
## Recommended tools
-- <https://github.com/vuejs/vue-devtools> - debugging/inspection in Firefox
- or Chrome
+- <https://github.com/vuejs/vue-devtools> - debugging/inspection in Firefox or
+ Chrome
- <https://vuejs.github.io/vetur/> - Vue tooling for Visual Studio Code
diff --git a/docs/dev/queue_settings_calculator.md b/docs/dev/queue_settings_calculator.md
new file mode 100644
index 0000000..55073f8
--- /dev/null
+++ b/docs/dev/queue_settings_calculator.md
@@ -0,0 +1,104 @@
+# Queue Settings Calculator
+
+A _Queue Settings Calculator_ is a Python function that computes the queue
+settings for an experiment. The function takes an instance of an experiment and
+returns:
+
+- queue name
+- core count
+- node count
+- walltime limit
+
+The Airavata Django Portal then uses these values to populate the Queue Settings
+fields when users are creating/editing experiments. This can greatly simplify
+experiment configuration for users and also lead to a better use of resources.
+
+## Getting started
+
+To add a Queue Settings Calculator function, you first will need a custom Django
+App. See the [Custom Django App](./custom_django_app.md) notes for how to create
+one.
+
+Next, add a `queue_settings_calculators.py` file to your custom Django app
+package if it doesn't already exist. You should add this file to the same folder
+that contains the `apps.py` file.
+
+In this file, you'll need to import the `@queue_settings_calculators` decorator.
+You'll use this to mark the queue settings calculator functions (you can define
+more than one).
+
+```python
+from airavata.model.experiment.ttypes import ExperimentModel
+from airavata_django_portal_sdk.decorators import queue_settings_calculator
+
+@queue_settings_calculator(
+ id="gateway_name-queue-settings-for-my-app", name="My Gateway: Queue Settings for My App"
+)
+def my_queue_settings_calculator(request, experiment_model: ExperimentModel):
+ # See https://airavata.apache.org/api-docs/master/experiment_model.html#Struct_ExperimentModel for ExperimentModel fields
+
+ total_core_count = 4
+ queue_name = "shared"
+ node_count = 1
+ walltime_limit = 30
+
+ # Return a dictionary with the queue settings values
+ result = {}
+ result["totalCPUCount"] = total_core_count
+ result["queueName"] = queue_name
+ result["nodeCount"] = node_count
+ result["wallTimeLimit"] = walltime_limit
+ return result
+```
+
+The `id` and `name` that are passed to the `@queue_settings_calculator`
+decorator are optional but highly recommended. The `id` will be used internally
+to associate applications with this function. Set the id to something that is
+unique to your gateway. The `name` is the value that will be displayed in the
+Settings UI for selecting this queue settings calculator.
+
+The queue settings calculator function is passed the Django `request` object and
+the `experiment_model`, an
+[ExperimentModel](https://airavata.apache.org/api-docs/master/experiment_model.html#Struct_ExperimentModel)
+instance.
+
+Primarily your function will inspect the `experiment_model` to determine the
+appropriate queue settings. For example you might look at one of the
+experiment's input files to determine how many cores are optimal for the job.
+
+Next, add the following import line to the `ready()` function of your apps.py
+AppConfig class. It will look something like this although the names of your
+AppConfig class and packages will be different:
+
+```python
+class CustomDjangoAppConfig(AppConfig):
+ name = 'custom_django_app'
+ label = name
+ verbose_name = "Custom Django App"
+ fa_icon_class = "fa-comment"
+ url_home = "custom_django_app:hello_world"
+
+ def ready(self) -> None:
+ from custom_django_app import queue_settings_calculators # noqa
+```
+
+Add the `ready(self)` function to your AppConfig if it is missing. Then add an
+import of your queue_settings_calculators module. Importing the module will
+register the calculator functions at startup time.
+
+## Configuring an application's queue settings calculator
+
+To have one of your applications use your queue settings calculator, first make
+sure to install the custom Django app in your portal instance. Then, go to
+**Settings > Application Catalog** and click on your application. Select the
+**Interface** tab. Under _Queue Settings Calculator_, select your queue settings
+calculator from the drop down. It should be listed with the name that you gave
+to it in your function's decorator
+(`@queue_settings_calculator(id='...', name='...')`).
+
+When an application is configured to use a queue settings calculator, the Queue
+Settings UI in the Create/Edit Experiment views is disabled and made read only.
+
+## Examples
+
+- <https://github.com/bio-miga/miga-autocomplete>
diff --git a/docs/tutorial/custom_ui_tutorial.md b/docs/tutorial/custom_ui_tutorial.md
index 6c5290a..755e8ec 100644
--- a/docs/tutorial/custom_ui_tutorial.md
+++ b/docs/tutorial/custom_ui_tutorial.md
@@ -15,7 +15,8 @@
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
- If you don't have Docker installed or can't install it, you'll also need:
- [Node LTS](https://nodejs.org/en/download/),
- - and [Yarn package manager](https://yarnpkg.com/getting-started/install).
+ - and
+ [Yarn 1 (Classic) package manager](https://classic.yarnpkg.com/en/docs/install).
### Installing Python
@@ -768,17 +769,13 @@
import io
import os
-import numpy as np
-from cclib.parser import ccopen
-from django.conf import settings
-from matplotlib.figure import Figure
+import numpy as np from cclib.parser import ccopen from django.conf import
+settings from matplotlib.figure import Figure
from airavata_django_portal_sdk import user_storage
-
-class GaussianEigenvaluesViewProvider:
- display_type = 'image'
- name = "Gaussian Eigenvalues"
+class GaussianEigenvaluesViewProvider: display_type = 'image' name = "Gaussian
+Eigenvalues"
def generate_data(self, request, experiment_output, experiment, output_file=None, **kwargs):
@@ -825,7 +822,7 @@
'mime-type': 'image/png'
}
-```
+````
</div>
5. Let's take a look at the implementation. First we added some imports at the
@@ -841,7 +838,7 @@
from matplotlib.figure import Figure
from airavata_django_portal_sdk import user_storage
-```
+````
6. Next we implemented the
[`generate_data` function](../dev/custom_output_view_provider.md#output-view-provider-interface).
@@ -1140,12 +1137,10 @@
from . import views
-app_name = 'custom_ui_tutorial_app'
-urlpatterns = [
- path('home/', views.home, name='home'),
- path('hello/', views.hello_world, name='hello_world'),
-]
-```
+app_name = 'custom_ui_tutorial_app' urlpatterns = [ path('home/', views.home,
+name='home'), path('hello/', views.hello_world, name='hello_world'), ]
+
+````
</div>
This maps the `/hello/` URL to the `hello_world` view.
@@ -1168,7 +1163,8 @@
verbose_name = "Custom UI Tutorial App"
fa_icon_class = "fa-comment"
url_home = "custom_ui_tutorial_app:hello_world"
-```
+````
+
</div>
This the main metadata for this custom Django app. Besides the normal metadata
@@ -1245,13 +1241,11 @@
from . import views
-app_name = 'custom_ui_tutorial_app'
-urlpatterns = [
- path('home/', views.home, name='home'),
- path('hello/', views.hello_world, name="hello_world"),
- path('languages/', views.languages, name="languages"),
-]
-```
+app_name = 'custom_ui_tutorial_app' urlpatterns = [ path('home/', views.home,
+name='home'), path('hello/', views.hello_world, name="hello_world"),
+path('languages/', views.languages, name="languages"), ]
+
+````
</div>
4. In
@@ -1282,7 +1276,7 @@
</main>
</div>
...
-```
+````
<button class="btn" data-clipboard-target="#greeting-select">
Copy to clipboard
@@ -1311,7 +1305,6 @@
Add to `hello.html` the code between the **STARTING HERE** and **ENDING
HERE** comments.
-
```html
{% block scripts %}
<script src="{% static 'django_airavata_api/dist/airavata-api.js' %}"></script>
@@ -1472,7 +1465,8 @@
loadExperiments();
$("#refresh-button").click(loadExperiments);
-```
+
+````
</div>
The user interface should now look something like:
@@ -1515,7 +1509,8 @@
}
$("#run-button").click(runClickHandler);
-```
+````
+
</div>
2. Going line by line we'll now take a look at this code. We added a click
@@ -1688,7 +1683,9 @@
const stdoutInput = experiment.getExperimentOutput("Echo-STDOUT");
const dataProductURI = stdoutInput.value;
try {
- const stdout = await utils.ExperimentUtils.readDataProduct(dataProductURI);
+ const stdout = await utils.ExperimentUtils.readDataProduct(
+ dataProductURI
+ );
// if stdout is null, it means the file wasn't found
if (stdout !== null) {
$(`#output_${index}`).text(stdout);
diff --git a/mkdocs.yml b/mkdocs.yml
index 3a0b01e..9ecc3b0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -16,6 +16,7 @@
- Custom UI Tutorial: tutorial/custom_ui_tutorial.md
- Adding a Custom Django App: dev/custom_django_app.md
- Adding a Custom Output View Provider: dev/custom_output_view_provider.md
+ - Adding a Queue Settings Calculator: dev/queue_settings_calculator.md
- HOWTOs: dev/customization_howto.md
- Administrator Guide:
- Application Input Customization: admin/app_inputs.md
diff --git a/requirements.txt b/requirements.txt
index e910a59..6fb3756 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
# Pin these dependencies
-Django==3.2.11
+Django==3.2.15
requests==2.25.1
requests-oauthlib==0.7.0
thrift==0.10.0
@@ -21,7 +21,7 @@
grpcio-tools==1.34.1
grpcio==1.34.1
-airavata-django-portal-sdk==1.4.0
+airavata-django-portal-sdk==1.6.0
airavata-python-sdk==1.0.2
-e "."