mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 15:37:32 +08:00
Merge pull request #11701 from OlegZharkov/share-to-users
Refactor Sharing
This commit is contained in:
@@ -1,263 +0,0 @@
|
||||
<template>
|
||||
<div v-if="ready">
|
||||
<h3>Share or Publish {{ model_class }} `{{ item.title }}`</h3>
|
||||
<b-alert :show="showDanger" variant="danger" dismissible> {{ errMsg }} </b-alert>
|
||||
<br />
|
||||
<div v-if="!hasUsername">
|
||||
<div>To make a {{ model_class }} accessible via link or publish it, you must create a public username:</div>
|
||||
<form class="form-group" @submit.prevent="setUsername()">
|
||||
<input class="form-control" type="text" v-model="newUsername" />
|
||||
</form>
|
||||
<b-button type="submit" variant="primary" @click="setUsername()">Set Username</b-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<b-form-checkbox switch class="make-accessible" v-model="item.importable" @change="onImportable">
|
||||
Make {{ model_class }} accessible
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-if="item.importable"
|
||||
class="make-publishable"
|
||||
switch
|
||||
v-model="item.published"
|
||||
@change="onPublish"
|
||||
>
|
||||
Make {{ model_class }} publicly available in
|
||||
<a :href="published_url" target="_top">Published {{ plural_name }}</a>
|
||||
</b-form-checkbox>
|
||||
<br />
|
||||
<div v-if="item.importable">
|
||||
<div>
|
||||
This {{ model_class }} is currently <strong>{{ itemStatus }}</strong
|
||||
>.
|
||||
</div>
|
||||
<p>Anyone can view and import this {{ model_class }} by visiting the following URL:</p>
|
||||
<blockquote>
|
||||
<b-button title="Edit URL" @click="onEdit" v-b-tooltip.hover variant="link" size="sm">
|
||||
<font-awesome-icon icon="edit" />
|
||||
</b-button>
|
||||
<b-button id="tooltip-clipboard" @click="onCopy" @mouseout="onCopyOut" variant="link" size="sm">
|
||||
<font-awesome-icon icon="link" />
|
||||
</b-button>
|
||||
<b-tooltip target="tooltip-clipboard" triggers="hover">
|
||||
{{ tooltipClipboard }}
|
||||
</b-tooltip>
|
||||
<a v-if="showUrl" id="item-url" :href="itemUrl" target="_top" class="ml-2">
|
||||
url:
|
||||
{{ itemUrl }}
|
||||
</a>
|
||||
<span v-else id="item-url-text">
|
||||
slug:
|
||||
{{ itemUrlParts[0] }}<SlugInput class="ml-1" :slug="itemUrlParts[1]" @onChange="onChange" />
|
||||
</span>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div v-else>
|
||||
Access to this {{ model_class }} is currently restricted so that only you and the users listed below can
|
||||
access it. Note that sharing a History will also allow access to all of its datasets.
|
||||
</div>
|
||||
<br />
|
||||
<h4>Share {{ model_class }} with Individual Users</h4>
|
||||
<div>
|
||||
<div v-if="item.users_shared_with && item.users_shared_with.length > 0">
|
||||
<b-table small caption-top :fields="shareFields" :items="item.users_shared_with">
|
||||
<template v-slot:table-caption>
|
||||
The following users will see this {{ model_class }} in their {{ model_class }} list and will
|
||||
be able to view, import and run it.
|
||||
</template>
|
||||
<template v-slot:cell(id)="cell">
|
||||
<b-button
|
||||
class="unshare_user"
|
||||
size="sm"
|
||||
@click.stop="setSharing('unshare_user', cell.value)"
|
||||
>Remove</b-button
|
||||
>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>You have not shared this {{ model_class }} with any users.</p>
|
||||
</div>
|
||||
<b-button :href="shareUrl" id="share_with_a_user"> <span>Share with a user</span> </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faLink, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import SlugInput from "components/Common/SlugInput";
|
||||
import axios from "axios";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
library.add(faLink);
|
||||
library.add(faEdit);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
SlugInput,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
plural_name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
model_class: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return {
|
||||
ready: false,
|
||||
hasUsername: Galaxy.user.get("username"),
|
||||
newUsername: "",
|
||||
errMsg: null,
|
||||
item: {
|
||||
title: "title",
|
||||
username_and_slug: "username/slug",
|
||||
importable: false,
|
||||
published: false,
|
||||
users_shared_with: [],
|
||||
},
|
||||
shareFields: ["email", { key: "id", label: "" }],
|
||||
makeMembersPublic: false,
|
||||
showUrl: true,
|
||||
tooltipClipboard: "Copy URL",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modelClassLower() {
|
||||
return this.model_class.toLowerCase();
|
||||
},
|
||||
pluralNameLower() {
|
||||
return this.plural_name.toLowerCase();
|
||||
},
|
||||
itemStatus() {
|
||||
return this.item.published ? "accessible via link and published" : "accessible via link";
|
||||
},
|
||||
itemRoot() {
|
||||
const port = window.location.port ? `:${window.location.port}` : "";
|
||||
return `${window.location.protocol}//${window.location.hostname}${port}${getAppRoot()}`;
|
||||
},
|
||||
itemUrl() {
|
||||
return `${this.itemRoot}${this.item.username_and_slug}`;
|
||||
},
|
||||
itemSlugParts() {
|
||||
const str = this.item.username_and_slug;
|
||||
const index = str.lastIndexOf("/");
|
||||
return [str.substring(0, index + 1), str.substring(index + 1)];
|
||||
},
|
||||
itemUrlParts() {
|
||||
const str = this.itemUrl;
|
||||
const index = str.lastIndexOf("/");
|
||||
return [str.substring(0, index + 1), str.substring(index + 1)];
|
||||
},
|
||||
published_url() {
|
||||
return `${getAppRoot()}${this.pluralNameLower}/list_published`;
|
||||
},
|
||||
shareUrl() {
|
||||
return `${getAppRoot()}${this.modelClassLower}/share/?id=${this.id}`;
|
||||
},
|
||||
slugUrl() {
|
||||
return `${getAppRoot()}${this.modelClassLower}/set_slug_async/?id=${this.id}`;
|
||||
},
|
||||
showDanger() {
|
||||
return this.errMsg !== null;
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
this.getModel();
|
||||
},
|
||||
methods: {
|
||||
onCopy() {
|
||||
const clipboard = document.createElement("input");
|
||||
document.body.appendChild(clipboard);
|
||||
clipboard.value = this.itemUrl;
|
||||
clipboard.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(clipboard);
|
||||
this.tooltipClipboard = "Copied!";
|
||||
},
|
||||
onCopyOut() {
|
||||
this.tooltipClipboard = "Copy URL";
|
||||
},
|
||||
onEdit() {
|
||||
this.showUrl = false;
|
||||
},
|
||||
onChange(newSlug) {
|
||||
this.showUrl = true;
|
||||
this.item.username_and_slug = `${this.itemSlugParts[0]}${newSlug}`;
|
||||
const requestUrl = `${this.slugUrl}&new_slug=${newSlug}`;
|
||||
axios.get(requestUrl).catch((error) => (this.errMsg = error.response.data.err_msg));
|
||||
},
|
||||
onImportable(importable) {
|
||||
if (importable) {
|
||||
this.setSharing(`make_accessible_via_link-${this.item.published ? "publish" : "unpublish"}`);
|
||||
} else {
|
||||
this.item.published = false;
|
||||
this.setSharing("disable_link_access-unpublish");
|
||||
}
|
||||
},
|
||||
onPublish(published) {
|
||||
if (published) {
|
||||
this.item.importable = true;
|
||||
this.setSharing("make_accessible_and_publish");
|
||||
} else {
|
||||
this.setSharing("unpublish");
|
||||
}
|
||||
},
|
||||
getModel() {
|
||||
this.ready = false;
|
||||
axios
|
||||
.get(`${getAppRoot()}api/${this.pluralNameLower}/${this.id}/sharing`)
|
||||
.then((response) => {
|
||||
this.item = response.data;
|
||||
this.ready = true;
|
||||
})
|
||||
.catch((error) => (this.errMsg = error.response.data.err_msg));
|
||||
},
|
||||
setUsername() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
axios
|
||||
.put(`${getAppRoot()}api/users/${Galaxy.user.id}/information/inputs`, {
|
||||
username: this.newUsername || "",
|
||||
})
|
||||
.then((response) => {
|
||||
this.errMsg = null;
|
||||
this.hasUsername = true;
|
||||
this.getModel();
|
||||
})
|
||||
.catch((error) => (this.errMsg = error.response.data.err_msg));
|
||||
},
|
||||
setSharing(action, user_id) {
|
||||
const data = {
|
||||
action: action,
|
||||
user_id: user_id,
|
||||
};
|
||||
return axios
|
||||
.post(`${getAppRoot()}api/${this.pluralNameLower}/${this.id}/sharing`, data)
|
||||
.then((response) => {
|
||||
if (response.data.skipped) {
|
||||
this.errMsg = "Some of the items within this object were not published due to an error.";
|
||||
}
|
||||
this.item = response.data;
|
||||
this.ready = true;
|
||||
})
|
||||
.catch((error) => (this.errMsg = error.response.data.err_msg));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,593 @@
|
||||
<template>
|
||||
<div v-if="ready">
|
||||
<h3>Share or Publish {{ modelClass }} `{{ item.title }}`</h3>
|
||||
<div v-for="error in errors" :key="error">
|
||||
<b-alert show variant="danger" dismissible @dismissed="errors = errors.filter((e) => e !== error)">
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="!hasUsername">
|
||||
<div>To make a {{ modelClass }} accessible via link or publish it, you must create a public username:</div>
|
||||
<form class="form-group" @submit.prevent="setUsername()">
|
||||
<input class="form-control" type="text" v-model="newUsername" />
|
||||
</form>
|
||||
<b-button type="submit" variant="primary" @click="setUsername()">Set Username</b-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<b-form-checkbox switch class="make-accessible" v-model="item.importable" @change="onImportable">
|
||||
Make {{ modelClass }} accessible
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-if="item.importable"
|
||||
class="make-publishable"
|
||||
switch
|
||||
v-model="item.published"
|
||||
@change="onPublish"
|
||||
>
|
||||
Make {{ modelClass }} publicly available in
|
||||
<a :href="published_url" target="_top">Published {{ pluralName }}</a>
|
||||
</b-form-checkbox>
|
||||
<br />
|
||||
<div v-if="item.importable">
|
||||
<div>
|
||||
This {{ modelClass }} is currently <strong>{{ itemStatus }}</strong
|
||||
>.
|
||||
</div>
|
||||
<p>Anyone can view and import this {{ modelClass }} by visiting the following URL:</p>
|
||||
<blockquote>
|
||||
<b-button title="Edit URL" @click="onEdit" v-b-tooltip.hover variant="link" size="sm">
|
||||
<font-awesome-icon icon="edit" />
|
||||
</b-button>
|
||||
<b-button id="tooltip-clipboard" @click="onCopy" @mouseout="onCopyOut" variant="link" size="sm">
|
||||
<font-awesome-icon :icon="['far', 'copy']" />
|
||||
</b-button>
|
||||
<b-tooltip target="tooltip-clipboard" triggers="hover">
|
||||
{{ tooltipClipboard }}
|
||||
</b-tooltip>
|
||||
<a v-if="showUrl" id="item-url" :href="itemUrl" target="_top" class="ml-2">
|
||||
url:
|
||||
{{ itemUrl }}
|
||||
</a>
|
||||
<span v-else id="item-url-text">
|
||||
slug:
|
||||
{{ itemUrlParts[0] }}<SlugInput class="ml-1" :slug="itemUrlParts[1]" @onChange="onChange" />
|
||||
</span>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div v-else>
|
||||
Access to this {{ modelClass }} is currently restricted so that only you and the users listed below can
|
||||
access it. Note that sharing a History will also allow access to all of its datasets.
|
||||
</div>
|
||||
<br />
|
||||
<b-card no-body>
|
||||
<b-button
|
||||
class="share-with-collapse"
|
||||
@click="isCollapseVisible = !isCollapseVisible"
|
||||
v-b-toggle.accordion-1
|
||||
variant="light"
|
||||
>
|
||||
Share {{ modelClass }} with Individual Users
|
||||
<font-awesome-icon :icon="isCollapseVisible ? `caret-up` : `caret-down`" />
|
||||
</b-button>
|
||||
<b-collapse id="accordion-1" accordion="main-accordion" role="tabpanel">
|
||||
<ConfigProvider v-slot="{ config }">
|
||||
<CurrentUser v-slot="{ user }">
|
||||
<div v-if="user && config && !permissionsChangeRequired(item)">
|
||||
<p class="share_with_title" v-if="item.users_shared_with.length === 0">
|
||||
You have not shared this {{ modelClass }} with any users.
|
||||
</p>
|
||||
<p v-else class="share_with_title">
|
||||
The following users will see this {{ modelClass }} in their {{ modelClass }} list
|
||||
and will be able to view, import and run it.
|
||||
</p>
|
||||
|
||||
<b-alert
|
||||
:show="dismissCountDown"
|
||||
dismissible
|
||||
class="success-alert"
|
||||
variant="success"
|
||||
@dismissed="dismissCountDown = 0"
|
||||
@dismiss-count-down="dismissCountDown = $event"
|
||||
>
|
||||
Sharing preferences are saved!
|
||||
</b-alert>
|
||||
|
||||
<div class="share_with_view">
|
||||
<multiselect
|
||||
class="multiselect-users"
|
||||
v-model="multiselectValues.sharingCandidates"
|
||||
:options="multiselectValues.userOptions"
|
||||
:clear-on-select="true"
|
||||
:multiple="true"
|
||||
:internal-search="false"
|
||||
:max-height="config.expose_user_email || user.is_admin ? 300 : 0"
|
||||
label="email"
|
||||
@close="onMultiselectBlur(config.expose_user_email || user.is_admin)"
|
||||
track-by="email"
|
||||
@search-change="
|
||||
searchChanged($event, config.expose_user_email || user.is_admin)
|
||||
"
|
||||
placeholder="Please specify user email"
|
||||
>
|
||||
<template slot="caret" v-if="!(config.expose_user_email || user.is_admin)">
|
||||
<div></div>
|
||||
</template>
|
||||
<template slot="noResult" v-if="config.expose_user_email || user.is_admin">
|
||||
<div v-if="threeCharactersEntered">
|
||||
{{ elementsNotFoundWarning }}
|
||||
</div>
|
||||
<div v-else>{{ charactersThresholdWarning }}</div>
|
||||
</template>
|
||||
<template slot="tag" slot-scope="{ option, remove }">
|
||||
<span class="multiselect__tag">
|
||||
<span>{{ option.email }}</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
@click="remove(option)"
|
||||
tabindex="1"
|
||||
class="multiselect__tag-icon"
|
||||
></i>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="noOptions">
|
||||
<div v-if="threeCharactersEntered">
|
||||
{{ charactersThresholdWarning }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ elementsNotFoundWarning }}
|
||||
</div>
|
||||
</template>
|
||||
</multiselect>
|
||||
<div class="share-with-card-buttons">
|
||||
<!--submit/cancel buttons-->
|
||||
<b-button
|
||||
@click="getSharing()"
|
||||
variant="outline-danger"
|
||||
class="sharing_icon cancel-sharing-with"
|
||||
>
|
||||
Cancel
|
||||
</b-button>
|
||||
<b-button
|
||||
variant="outline-primary"
|
||||
:disabled="
|
||||
!(sharedWithUsersChanged || !!multiselectValues.currentUserSearch)
|
||||
"
|
||||
@click.stop="
|
||||
setSharing(
|
||||
actions.share_with,
|
||||
multiselectValues.sharingCandidates.map(({ email }) => email)
|
||||
)
|
||||
"
|
||||
v-b-tooltip.hover.bottom
|
||||
:title="submitBtnTitle"
|
||||
class="sharing_icon submit-sharing-with"
|
||||
>
|
||||
{{ multiselectValues.currentUserSearch ? `Add` : `Save` }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CurrentUser>
|
||||
</ConfigProvider>
|
||||
<b-alert variant="warning" dismissible fade :show="permissionsChangeRequired(item)">
|
||||
<div class="text-center">
|
||||
{{
|
||||
item.extra.can_change.length > 0
|
||||
? `${item.extra.can_change.length} datasets are exclusively private to you`
|
||||
: `You are not authorized to share ${item.extra.cannot_change.length} datasets`
|
||||
}}
|
||||
</div>
|
||||
</b-alert>
|
||||
<b-row v-if="permissionsChangeRequired(item)">
|
||||
<b-col v-if="item.extra.can_change.length > 0">
|
||||
<b-card>
|
||||
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||
<b-button block v-b-toggle.can-share variant="warning">
|
||||
Datasets can be shared by updating their permissions
|
||||
</b-button>
|
||||
</b-card-header>
|
||||
<b-collapse id="can-share" visible accordion="can-share-accordion" role="tabpanel">
|
||||
<b-list-group>
|
||||
<b-list-group-item :key="dataset.id" v-for="dataset in item.extra.can_change">{{
|
||||
dataset.name
|
||||
}}</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</b-col>
|
||||
<b-col v-if="item.extra.cannot_change.length > 0">
|
||||
<b-card>
|
||||
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||
<b-button block v-b-toggle.cannot-share variant="danger"
|
||||
>Datasets cannot be shared, you are not authorized to change
|
||||
permissions</b-button
|
||||
>
|
||||
</b-card-header>
|
||||
<b-collapse id="cannot-share" visible accordion="cannot-accordion2" role="tabpanel">
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
:key="dataset.id"
|
||||
v-for="dataset in item.extra.cannot_change"
|
||||
>{{ dataset.name }}</b-list-group-item
|
||||
>
|
||||
</b-list-group>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-card
|
||||
border-variant="primary"
|
||||
header="How would you like to proceed?"
|
||||
header-bg-variant="primary"
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
>
|
||||
<b-button
|
||||
@click="
|
||||
setSharing(
|
||||
actions.share_with,
|
||||
multiselectValues.sharingCandidates.map(({ email }) => email),
|
||||
share_option.make_public
|
||||
)
|
||||
"
|
||||
v-if="item.extra.can_change.length > 0"
|
||||
block
|
||||
variant="outline-primary"
|
||||
>Make datasets public</b-button
|
||||
>
|
||||
<b-button
|
||||
@click="
|
||||
setSharing(
|
||||
actions.share_with,
|
||||
multiselectValues.sharingCandidates.map(({ email }) => email),
|
||||
share_option.make_accessible_to_shared
|
||||
)
|
||||
"
|
||||
v-if="item.extra.can_change.length > 0"
|
||||
block
|
||||
variant="outline-primary"
|
||||
>Make datasets private to me and
|
||||
{{ multiselectValues.sharingCandidates.map(({ email }) => email).join() }}</b-button
|
||||
>
|
||||
<b-button
|
||||
@click="
|
||||
setSharing(
|
||||
actions.share_with,
|
||||
multiselectValues.sharingCandidates.map(({ email }) => email),
|
||||
share_option.no_changes
|
||||
)
|
||||
"
|
||||
block
|
||||
variant="outline-primary"
|
||||
>
|
||||
Share Anyway
|
||||
</b-button>
|
||||
<b-button @click="getSharing()" block variant="outline-danger">Cancel </b-button>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faCopy, faEdit, faUserPlus, faUserSlash, faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import SlugInput from "components/Common/SlugInput";
|
||||
import axios from "axios";
|
||||
import Multiselect from "vue-multiselect";
|
||||
import { copy } from "utils/clipboard";
|
||||
import ConfigProvider from "components/providers/ConfigProvider";
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
library.add(faCopy, faEdit, faUserPlus, faUserSlash, faCaretDown, faCaretUp);
|
||||
const defaultExtra = () => {
|
||||
return {
|
||||
cannot_change: [],
|
||||
can_change: [],
|
||||
can_share: true,
|
||||
};
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
ConfigProvider,
|
||||
FontAwesomeIcon,
|
||||
SlugInput,
|
||||
Multiselect,
|
||||
CurrentUser,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
pluralName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return {
|
||||
isCollapseVisible: false,
|
||||
dismissCountDown: 0,
|
||||
charactersThresholdWarning: "Enter at least 3 characters to see suggestions",
|
||||
elementsNotFoundWarning: "No elements found. Consider changing the search query.",
|
||||
ready: false,
|
||||
threeCharactersEntered: true,
|
||||
hasUsername: Galaxy.user.get("username"),
|
||||
newUsername: "",
|
||||
errors: [],
|
||||
multiselectValues: {
|
||||
sharingCandidates: [],
|
||||
userOptions: [],
|
||||
currentUserSearch: "",
|
||||
},
|
||||
item: {
|
||||
title: "title",
|
||||
username_and_slug: "username/slug",
|
||||
importable: false,
|
||||
published: false,
|
||||
users_shared_with: [],
|
||||
extra: defaultExtra(),
|
||||
},
|
||||
shareFields: ["email", { key: "id", label: "" }],
|
||||
makeMembersPublic: false,
|
||||
showUrl: true,
|
||||
tooltipClipboard: "Copy URL",
|
||||
actions: {
|
||||
enable_link_access: "enable_link_access",
|
||||
disable_link_access: "disable_link_access",
|
||||
publish: "publish",
|
||||
unpublish: "unpublish",
|
||||
share_with: "share_with_users",
|
||||
},
|
||||
share_option: {
|
||||
make_public: "make_public",
|
||||
make_accessible_to_shared: "make_accessible_to_shared",
|
||||
no_changes: "no_changes",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sharedWithUsersChanged() {
|
||||
if (this.item.users_shared_with.length !== this.multiselectValues.sharingCandidates.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.multiselectValues.sharingCandidates.every(({ email }) =>
|
||||
this.item.users_shared_with.some((user) => user.email === email)
|
||||
);
|
||||
},
|
||||
submitBtnTitle() {
|
||||
if (this.multiselectValues.currentUserSearch) {
|
||||
return "";
|
||||
} else {
|
||||
return this.multiselectValues.sharingCandidates && this.multiselectValues.sharingCandidates.length > 0
|
||||
? `Share with ${this.multiselectValues.sharingCandidates.map(({ email }) => email)}`
|
||||
: "Please enter user email";
|
||||
}
|
||||
},
|
||||
|
||||
pluralNameLower() {
|
||||
return this.pluralName.toLowerCase();
|
||||
},
|
||||
itemStatus() {
|
||||
return this.item.published ? "accessible via link and published" : "accessible via link";
|
||||
},
|
||||
itemRoot() {
|
||||
const port = window.location.port ? `:${window.location.port}` : "";
|
||||
return `${window.location.protocol}//${window.location.hostname}${port}${getAppRoot()}`;
|
||||
},
|
||||
itemUrl() {
|
||||
return `${this.itemRoot}${this.item.username_and_slug}`;
|
||||
},
|
||||
itemSlugParts() {
|
||||
const str = this.item.username_and_slug;
|
||||
const index = str.lastIndexOf("/");
|
||||
return [str.substring(0, index + 1), str.substring(index + 1)];
|
||||
},
|
||||
itemUrlParts() {
|
||||
const str = this.itemUrl;
|
||||
const index = str.lastIndexOf("/");
|
||||
return [str.substring(0, index + 1), str.substring(index + 1)];
|
||||
},
|
||||
published_url() {
|
||||
return `${getAppRoot()}${this.pluralNameLower}/list_published`;
|
||||
},
|
||||
slugUrl() {
|
||||
return `${getAppRoot()}api/${this.pluralNameLower}/${this.id}/slug`;
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
this.getSharing();
|
||||
},
|
||||
methods: {
|
||||
permissionsChangeRequired(item) {
|
||||
if (!item.extra) {
|
||||
return false;
|
||||
}
|
||||
return item.extra && (item.extra.can_change.length > 0 || item.extra.cannot_change.length > 0);
|
||||
},
|
||||
onMultiselectBlur(isAdmin) {
|
||||
const isValueChosen = this.multiselectValues.sharingCandidates.some(
|
||||
(item) => item.email === this.multiselectValues.currentUserSearch
|
||||
);
|
||||
if (this.multiselectValues.currentUserSearch && !isValueChosen && !isAdmin) {
|
||||
this.multiselectValues.sharingCandidates.push({ email: this.multiselectValues.currentUserSearch });
|
||||
}
|
||||
},
|
||||
addError(newError) {
|
||||
// temporary turning Set into Array, until we update till Vue 3.0, that supports Set reactivity
|
||||
this.errors = Array.from(new Set(this.errors).add(newError));
|
||||
},
|
||||
onCopy() {
|
||||
copy(this.itemUrl);
|
||||
this.tooltipClipboard = "Copied!";
|
||||
},
|
||||
onCopyOut() {
|
||||
this.tooltipClipboard = "Copy URL";
|
||||
},
|
||||
onEdit() {
|
||||
this.showUrl = false;
|
||||
},
|
||||
assignItem(newItem, overwriteCandidates) {
|
||||
if (newItem.errors) {
|
||||
this.errors = newItem.errors;
|
||||
}
|
||||
this.item = newItem;
|
||||
if (overwriteCandidates) {
|
||||
this.multiselectValues.sharingCandidates = Array.from(newItem.users_shared_with);
|
||||
}
|
||||
if (!this.item.extra || newItem.errors.length > 0) {
|
||||
this.item.extra = defaultExtra();
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
},
|
||||
onChange(newSlug) {
|
||||
this.showUrl = true;
|
||||
const requestUrl = `${this.slugUrl}`;
|
||||
axios
|
||||
.put(requestUrl, {
|
||||
new_slug: newSlug,
|
||||
})
|
||||
.then(() => (this.item.username_and_slug = `${this.itemSlugParts[0]}${newSlug}`))
|
||||
.catch((error) => this.addError(error.response.data.err_msg));
|
||||
},
|
||||
onImportable(importable) {
|
||||
if (importable) {
|
||||
this.setSharing(this.actions.enable_link_access);
|
||||
} else {
|
||||
this.item.published = false;
|
||||
this.setSharing(this.actions.disable_link_access);
|
||||
}
|
||||
},
|
||||
onPublish(published) {
|
||||
if (published) {
|
||||
this.item.importable = true;
|
||||
this.setSharing(this.actions.publish);
|
||||
} else {
|
||||
this.setSharing(this.actions.unpublish);
|
||||
}
|
||||
},
|
||||
getSharing() {
|
||||
this.ready = false;
|
||||
this.dismissCountDown = 0;
|
||||
axios
|
||||
.get(`${getAppRoot()}api/${this.pluralNameLower}/${this.id}/sharing`)
|
||||
.then((response) => this.assignItem(response.data, true))
|
||||
.catch((error) => this.addError(error.response.data.err_msg));
|
||||
},
|
||||
setUsername() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
axios
|
||||
.put(`${getAppRoot()}api/users/${Galaxy.user.id}/information/inputs`, {
|
||||
username: this.newUsername || "",
|
||||
})
|
||||
.then((response) => {
|
||||
this.hasUsername = true;
|
||||
this.getSharing();
|
||||
})
|
||||
.catch((error) => this.addError(error.response.data.err_msg));
|
||||
},
|
||||
setSharing(action, user_id, share_option) {
|
||||
let user_ids = undefined;
|
||||
if (Array.isArray(user_id)) {
|
||||
user_ids = user_id;
|
||||
} else {
|
||||
user_ids = user_id ? user_id.replace(/ /g, "").split(",") : undefined;
|
||||
}
|
||||
|
||||
const data = {
|
||||
user_ids: user_ids,
|
||||
share_option: share_option ? share_option : undefined,
|
||||
};
|
||||
return axios
|
||||
.put(`${getAppRoot()}api/${this.pluralNameLower}/${this.id}/${action}`, data)
|
||||
.then(({ data }) => {
|
||||
this.errors = [];
|
||||
const userIdsSaved = user_ids && !this.permissionsChangeRequired(data) && data.errors.length === 0;
|
||||
this.assignItem(data, userIdsSaved);
|
||||
|
||||
if (userIdsSaved) {
|
||||
this.dismissCountDown = 3;
|
||||
}
|
||||
})
|
||||
.catch((error) => this.addError(error.response.data.err_msg));
|
||||
},
|
||||
searchChanged(searchValue, exposedUsers) {
|
||||
this.multiselectValues.currentUserSearch = searchValue;
|
||||
if (!exposedUsers) {
|
||||
this.multiselectValues.userOptions = [{ email: searchValue }];
|
||||
} else if (searchValue.length < 3) {
|
||||
this.threeCharactersEntered = false;
|
||||
this.multiselectValues.userOptions = [];
|
||||
} else {
|
||||
this.threeCharactersEntered = true;
|
||||
axios
|
||||
.get(`${getAppRoot()}api/users?f_email=${searchValue}`)
|
||||
.then((response) => {
|
||||
this.multiselectValues.userOptions = response.data.filter(
|
||||
({ email }) =>
|
||||
!this.multiselectValues.sharingCandidates.map(({ email }) => email).includes(email)
|
||||
);
|
||||
})
|
||||
.catch((error) => this.addError(error.response.data.err_msg));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sharing_icon {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.share_with_view {
|
||||
margin: 1rem 1rem;
|
||||
}
|
||||
.share_with_title {
|
||||
text-align: center;
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.multiselect-users {
|
||||
font-weight: normal;
|
||||
}
|
||||
.multiselect-users::v-deep .multiselect__option--highlight {
|
||||
background: #dee2e6;
|
||||
color: #2c3143;
|
||||
}
|
||||
.multiselect__tag {
|
||||
background: #dee2e6;
|
||||
color: #2c3143;
|
||||
}
|
||||
.multiselect__tag-icon:after {
|
||||
color: white;
|
||||
}
|
||||
.multiselect__tag-icon:focus,
|
||||
.multiselect__tag-icon:hover {
|
||||
background: #132c40;
|
||||
}
|
||||
.success-alert {
|
||||
margin: 0.3rem 2rem 0.9rem;
|
||||
}
|
||||
.share-with-card-buttons {
|
||||
margin: 0.5rem 0;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@ import decodeUriComponent from "decode-uri-component";
|
||||
import Router from "layout/router";
|
||||
import ToolForm from "components/Tool/ToolForm";
|
||||
import FormGeneric from "components/Form/FormGeneric";
|
||||
import Sharing from "components/Sharing.vue";
|
||||
import Sharing from "components/Sharing/Sharing.vue";
|
||||
import UserPreferences from "components/User/UserPreferences.vue";
|
||||
import DatasetList from "components/Dataset/DatasetList.vue";
|
||||
import { getUserPreferencesModel } from "components/User/UserPreferencesModel";
|
||||
@@ -192,8 +192,8 @@ export const getAnalysisRouter = (Galaxy) => {
|
||||
show_visualizations_sharing: function () {
|
||||
this._display_vue_helper(Sharing, {
|
||||
id: QueryStringParsing.get("id"),
|
||||
plural_name: "Visualizations",
|
||||
model_class: "Visualization",
|
||||
pluralName: "Visualizations",
|
||||
modelClass: "Visualization",
|
||||
});
|
||||
},
|
||||
|
||||
@@ -246,8 +246,8 @@ export const getAnalysisRouter = (Galaxy) => {
|
||||
show_histories_sharing: function () {
|
||||
this._display_vue_helper(Sharing, {
|
||||
id: QueryStringParsing.get("id"),
|
||||
plural_name: "Histories",
|
||||
model_class: "History",
|
||||
pluralName: "Histories",
|
||||
modelClass: "History",
|
||||
});
|
||||
},
|
||||
|
||||
@@ -318,8 +318,8 @@ export const getAnalysisRouter = (Galaxy) => {
|
||||
show_pages_sharing: function () {
|
||||
this._display_vue_helper(Sharing, {
|
||||
id: QueryStringParsing.get("id"),
|
||||
plural_name: "Pages",
|
||||
model_class: "Page",
|
||||
pluralName: "Pages",
|
||||
modelClass: "Page",
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Toast } from "ui/toast";
|
||||
export function copy(text, notificationText) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
Toast.info(notificationText);
|
||||
if (notificationText) {
|
||||
Toast.info(notificationText);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
||||
|
||||
@@ -206,6 +206,7 @@ class ConfigSerializer(base.ModelSerializer):
|
||||
'default_panel_view': _use_config,
|
||||
'upload_from_form_button': _use_config,
|
||||
'release_doc_base_url': _use_config,
|
||||
'expose_user_email': _use_config,
|
||||
'user_library_import_dir_available': lambda config, key, **context: bool(config.get('user_library_import_dir')),
|
||||
'welcome_directory': _use_config,
|
||||
}
|
||||
|
||||
@@ -8,13 +8,20 @@ import glob
|
||||
import logging
|
||||
import os
|
||||
from typing import (
|
||||
cast,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
)
|
||||
from sqlalchemy import (
|
||||
and_,
|
||||
asc,
|
||||
desc,
|
||||
false,
|
||||
@@ -60,6 +67,37 @@ from galaxy.util import restore_text
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HDABasicInfo(BaseModel):
|
||||
id: EncodedDatabaseIdField
|
||||
name: str
|
||||
|
||||
|
||||
class ShareHistoryExtra(sharable.ShareWithExtra):
|
||||
can_change: List[HDABasicInfo] = Field(
|
||||
[],
|
||||
title="Can Change",
|
||||
description=(
|
||||
"A collection of datasets that are not accessible by one or more of the target users "
|
||||
"and that can be made accessible for others by the user sharing the history."
|
||||
),
|
||||
)
|
||||
cannot_change: List[HDABasicInfo] = Field(
|
||||
[],
|
||||
title="Cannot Change",
|
||||
description=(
|
||||
"A collection of datasets that are not accessible by one or more of the target users "
|
||||
"and that cannot be made accessible for others by the user sharing the history."
|
||||
),
|
||||
)
|
||||
accessible_count: int = Field(
|
||||
0,
|
||||
title="Accessible Count",
|
||||
description=(
|
||||
"The number of datasets in the history that are public or accessible by all the target users."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMixin, SortableManager):
|
||||
|
||||
model_class = model.History
|
||||
@@ -249,6 +287,68 @@ class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMix
|
||||
job, _ = history_exp_tool.execute(trans, incoming=params, history=history, set_output_hid=True)
|
||||
return job
|
||||
|
||||
def get_sharing_extra_information(
|
||||
self, trans, item, users: Set[model.User], errors: Set[str], option: Optional[sharable.SharingOptions] = None
|
||||
) -> Optional[sharable.ShareWithExtra]:
|
||||
"""Returns optional extra information about the datasets of the history that can be accessed by the users."""
|
||||
extra = ShareHistoryExtra()
|
||||
history = cast(model.History, item)
|
||||
if history.empty:
|
||||
errors.add("You cannot share an empty history.")
|
||||
return extra
|
||||
|
||||
owner = trans.user
|
||||
owner_roles = owner.all_roles()
|
||||
can_change_dict = {}
|
||||
cannot_change_dict = {}
|
||||
share_anyway = option is not None and option == sharable.SharingOptions.no_changes
|
||||
datasets = history.activatable_datasets
|
||||
total_dataset_count = len(datasets)
|
||||
for user in users:
|
||||
if self.is_history_shared_with(history, user):
|
||||
continue
|
||||
|
||||
user_roles = user.all_roles()
|
||||
# TODO: Handle this is a more performant way
|
||||
# Only deal with datasets that have not been purged
|
||||
for hda in datasets:
|
||||
if trans.app.security_agent.can_access_dataset(user_roles, hda.dataset):
|
||||
continue
|
||||
# The user with which we are sharing the history does not have access permission on the current dataset
|
||||
owner_can_manage_dataset = (
|
||||
trans.app.security_agent.can_manage_dataset(owner_roles, hda.dataset)
|
||||
and not hda.dataset.library_associations
|
||||
)
|
||||
if option and owner_can_manage_dataset:
|
||||
if option == sharable.SharingOptions.make_accessible_to_shared:
|
||||
trans.app.security_agent.privately_share_dataset(hda.dataset, users=[owner, user])
|
||||
elif option == sharable.SharingOptions.make_public:
|
||||
trans.app.security_agent.make_dataset_public(hda.dataset)
|
||||
else:
|
||||
hda_id = trans.security.encode_id(hda.id)
|
||||
hda_info = HDABasicInfo(id=hda_id, name=hda.name)
|
||||
if owner_can_manage_dataset:
|
||||
can_change_dict[hda_id] = hda_info
|
||||
else:
|
||||
cannot_change_dict[hda_id] = hda_info
|
||||
|
||||
extra.can_change = list(can_change_dict.values())
|
||||
extra.cannot_change = list(cannot_change_dict.values())
|
||||
extra.accessible_count = total_dataset_count - len(extra.can_change) - len(extra.cannot_change)
|
||||
if not extra.accessible_count and not extra.can_change and not share_anyway:
|
||||
errors.add("The history you are sharing do not contain any datasets that can be accessed by the users with which you are sharing.")
|
||||
|
||||
extra.can_share = not errors and (extra.accessible_count == total_dataset_count or option is not None)
|
||||
return extra
|
||||
|
||||
def is_history_shared_with(self, history, user) -> bool:
|
||||
return bool(self.session().query(self.user_share_model).filter(
|
||||
and_(
|
||||
self.user_share_model.table.c.user_id == user.id,
|
||||
self.user_share_model.table.c.history_id == history.id,
|
||||
)
|
||||
).first())
|
||||
|
||||
|
||||
class HistoryExportView:
|
||||
|
||||
@@ -955,12 +1055,6 @@ class HistoriesService(ServiceBase):
|
||||
fasta_hdas=[LabelValuePair(label=f'{hda.hid}: {hda.name}', value=trans.security.encode_id(hda.id)) for hda in fasta_hdas],
|
||||
)
|
||||
|
||||
def sharing(self, trans, id: EncodedDatabaseIdField, payload: Optional[sharable.SharingPayload] = None) -> sharable.SharingStatus:
|
||||
"""Allows to publish or share with other users the given resource (by id) and returns the current sharing
|
||||
status of the resource.
|
||||
"""
|
||||
return self.shareable_service.sharing(trans, id, payload)
|
||||
|
||||
def _serialize_history(
|
||||
self,
|
||||
trans,
|
||||
|
||||
@@ -286,12 +286,6 @@ class PagesService:
|
||||
trans.response.set_content_type("application/pdf")
|
||||
return internal_galaxy_markdown_to_pdf(trans, internal_galaxy_markdown, 'page')
|
||||
|
||||
def sharing(self, trans, id: EncodedDatabaseIdField, payload: Optional[sharable.SharingPayload] = None) -> sharable.SharingStatus:
|
||||
"""Allows to publish or share with other users the given resource (by id) and returns the current sharing
|
||||
status of the resource.
|
||||
"""
|
||||
return self.shareable_service.sharing(trans, id, payload)
|
||||
|
||||
|
||||
class PageManager(sharable.SharableModelManager, UsesAnnotations):
|
||||
"""Provides operations for managing a Page."""
|
||||
|
||||
+249
-181
@@ -11,17 +11,25 @@ A sharable Galaxy object:
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Extra,
|
||||
Field,
|
||||
)
|
||||
from sqlalchemy import true
|
||||
from sqlalchemy import (
|
||||
false,
|
||||
true,
|
||||
)
|
||||
|
||||
from galaxy import exceptions
|
||||
from galaxy.managers import (
|
||||
@@ -32,13 +40,128 @@ from galaxy.managers import (
|
||||
taggable,
|
||||
users
|
||||
)
|
||||
from galaxy.model import UserShareAssociation
|
||||
from galaxy.model import (
|
||||
User,
|
||||
UserShareAssociation,
|
||||
)
|
||||
from galaxy.schema.fields import EncodedDatabaseIdField
|
||||
from galaxy.structured_app import MinimalManagerApp
|
||||
from galaxy.util import ready_name_for_url
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
UserIdentifier = Union[EncodedDatabaseIdField, str]
|
||||
|
||||
|
||||
class SharingOptions(str, Enum):
|
||||
"""Options for sharing resources that may have restricted access to all or part of their contents."""
|
||||
make_public = "make_public"
|
||||
make_accessible_to_shared = "make_accessible_to_shared"
|
||||
no_changes = "no_changes"
|
||||
|
||||
|
||||
class ShareWithExtra(BaseModel):
|
||||
can_share: bool = Field(
|
||||
False,
|
||||
title="Can Share",
|
||||
description="Indicates whether the resource can be directly shared or requires further actions.",
|
||||
)
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
|
||||
|
||||
class ShareWithPayload(BaseModel):
|
||||
user_ids: List[UserIdentifier] = Field(
|
||||
...,
|
||||
title="User Identifiers",
|
||||
description=(
|
||||
"A collection of encoded IDs (or email addresses) of users "
|
||||
"that this resource will be shared with."
|
||||
),
|
||||
)
|
||||
share_option: Optional[SharingOptions] = Field(
|
||||
None,
|
||||
title="Share Option",
|
||||
description=(
|
||||
"User choice for sharing resources which its contents may be restricted:\n"
|
||||
" - None: The user did not choose anything yet or no option is needed.\n"
|
||||
f" - {SharingOptions.make_public}: The contents of the resource will be made publicly accessible.\n"
|
||||
f" - {SharingOptions.make_accessible_to_shared}: This will automatically create a new `sharing role` allowing protected contents to be accessed only by the desired users.\n"
|
||||
f" - {SharingOptions.no_changes}: This won't change the current permissions for the contents. The user which this resource will be shared may not be able to access all its contents.\n"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SetSlugPayload(BaseModel):
|
||||
new_slug: str = Field(
|
||||
...,
|
||||
title="New Slug",
|
||||
description="The slug that will be used to access this shared item.",
|
||||
)
|
||||
|
||||
|
||||
class UserEmail(BaseModel):
|
||||
id: EncodedDatabaseIdField = Field(
|
||||
...,
|
||||
title="User ID",
|
||||
description="The encoded ID of the user.",
|
||||
)
|
||||
email: str = Field(
|
||||
...,
|
||||
title="Email",
|
||||
description="The email of the user.",
|
||||
)
|
||||
|
||||
|
||||
class SharingStatus(BaseModel):
|
||||
id: EncodedDatabaseIdField = Field(
|
||||
...,
|
||||
title="ID",
|
||||
description="The encoded ID of the resource to be shared.",
|
||||
)
|
||||
title: str = Field(
|
||||
...,
|
||||
title="Title",
|
||||
description="The title or name of the resource.",
|
||||
)
|
||||
importable: bool = Field(
|
||||
...,
|
||||
title="Importable",
|
||||
description="Whether this resource can be published using a link.",
|
||||
)
|
||||
published: bool = Field(
|
||||
...,
|
||||
title="Published",
|
||||
description="Whether this resource is currently published.",
|
||||
)
|
||||
users_shared_with: List[UserEmail] = Field(
|
||||
[],
|
||||
title="Users shared with",
|
||||
description="The list of encoded ids for users the resource has been shared.",
|
||||
)
|
||||
username_and_slug: Optional[str] = Field(
|
||||
None,
|
||||
title="Username and slug",
|
||||
description="The relative URL in the form of /u/{username}/{resource_single_char}/{slug}",
|
||||
)
|
||||
|
||||
|
||||
class ShareWithStatus(SharingStatus):
|
||||
errors: List[str] = Field(
|
||||
[],
|
||||
title="Errors",
|
||||
description="Collection of messages indicating that the resource was not shared with some (or all users) due to an error.",
|
||||
)
|
||||
extra: Optional[ShareWithExtra] = Field(
|
||||
None,
|
||||
title="Extra",
|
||||
description=(
|
||||
"Optional extra information about this shareable resource that may be of interest. "
|
||||
"The contents of this field depend on the particular resource."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SharableModelManager(base.ModelManager, secured.OwnableManagerMixin, secured.AccessibleManagerMixin,
|
||||
taggable.TaggableManagerMixin, annotatable.AnnotatableManagerMixin, ratable.RatableManagerMixin):
|
||||
@@ -158,14 +281,11 @@ class SharableModelManager(base.ModelManager, secured.OwnableManagerMixin, secur
|
||||
query = query.filter_by(user=user)
|
||||
return query.all()
|
||||
|
||||
def share_with(self, item, user, flush=True):
|
||||
def share_with(self, item, user: User, flush: bool = True):
|
||||
"""
|
||||
Get or create a share for the given user (or users if `user` is a list).
|
||||
Get or create a share for the given user.
|
||||
"""
|
||||
# precondition: user has been validated
|
||||
# allow user to be a list and call recursivly
|
||||
if isinstance(user, list):
|
||||
return [self.share_with(item, _, flush=False) for _ in user]
|
||||
# get or create
|
||||
existing = self.get_share_assocs(item, user=user)
|
||||
if existing:
|
||||
@@ -189,12 +309,10 @@ class SharableModelManager(base.ModelManager, secured.OwnableManagerMixin, secur
|
||||
self.session().flush()
|
||||
return user_share_assoc
|
||||
|
||||
def unshare_with(self, item, user, flush=True):
|
||||
def unshare_with(self, item, user: User, flush: bool = True):
|
||||
"""
|
||||
Delete a user share (or list of shares) from the database.
|
||||
Delete a user share from the database.
|
||||
"""
|
||||
if isinstance(user, list):
|
||||
return [self.unshare_with(item, _, flush=False) for _ in user]
|
||||
# Look for and delete sharing relation for user.
|
||||
user_share_assoc = self.get_share_assocs(item, user=user)[0]
|
||||
self.session().delete(user_share_assoc)
|
||||
@@ -233,6 +351,33 @@ class SharableModelManager(base.ModelManager, secured.OwnableManagerMixin, secur
|
||||
items = self._apply_fn_filters_gen(query.all(), fn_filters)
|
||||
return list(self._apply_fn_limit_offset_gen(items, limit, offset))
|
||||
|
||||
def get_sharing_extra_information(
|
||||
self, trans, item, users: Set[User], errors: Set[str], option: Optional[SharingOptions] = None
|
||||
) -> Optional[ShareWithExtra]:
|
||||
"""Returns optional extra information about the shareability of the given item.
|
||||
|
||||
This function should be overridden in the particular manager class that wants
|
||||
to provide the extra information, otherwise, it will be None by default."""
|
||||
return None
|
||||
|
||||
def update_current_sharing_with_users(self, item, new_users_shared_with: Set[User], flush=True):
|
||||
"""Updates the currently list of users this item is shared with by adding new
|
||||
users and removing missing ones."""
|
||||
current_shares = self.get_share_assocs(item)
|
||||
currently_shared_with = {share.user for share in current_shares}
|
||||
|
||||
needs_adding = new_users_shared_with - currently_shared_with
|
||||
for user in needs_adding:
|
||||
current_shares.append(self.share_with(item, user, flush=False))
|
||||
|
||||
needs_removing = currently_shared_with - new_users_shared_with
|
||||
for user in needs_removing:
|
||||
current_shares.remove(self.unshare_with(item, user, flush=False))
|
||||
|
||||
if flush:
|
||||
self.session().flush()
|
||||
return current_shares
|
||||
|
||||
# .... slugs
|
||||
# slugs are human readable strings often used to link to sharable resources (replacing ids)
|
||||
# TODO: as validator, deserializer, etc. (maybe another object entirely?)
|
||||
@@ -244,6 +389,9 @@ class SharableModelManager(base.ModelManager, secured.OwnableManagerMixin, secur
|
||||
if not self.is_valid_slug(new_slug):
|
||||
raise exceptions.RequestParameterInvalidException("Invalid slug", slug=new_slug)
|
||||
|
||||
if item.slug == new_slug:
|
||||
return item
|
||||
|
||||
# error if slug is already in use
|
||||
if self._slug_exists(user, new_slug):
|
||||
raise exceptions.Conflict("Slug already exists", slug=new_slug)
|
||||
@@ -439,18 +587,7 @@ class SharableModelDeserializer(base.ModelDeserializer,
|
||||
"""
|
||||
unencoded_ids = [self.app.security.decode_id(id_) for id_ in val]
|
||||
new_users_shared_with = set(self.manager.user_manager.by_ids(unencoded_ids))
|
||||
current_shares = self.manager.get_share_assocs(item)
|
||||
currently_shared_with = {share.user for share in current_shares}
|
||||
|
||||
needs_adding = new_users_shared_with - currently_shared_with
|
||||
for user in needs_adding:
|
||||
current_shares.append(self.manager.share_with(item, user, flush=False))
|
||||
|
||||
needs_removing = currently_shared_with - new_users_shared_with
|
||||
for user in needs_removing:
|
||||
current_shares.remove(self.manager.unshare_with(item, user, flush=False))
|
||||
|
||||
self.manager.session().flush()
|
||||
current_shares = self.manager.update_current_sharing_with_users(item, new_users_shared_with)
|
||||
# TODO: or should this return the list of ids?
|
||||
return current_shares
|
||||
|
||||
@@ -473,78 +610,6 @@ class SharableModelFilters(base.ModelFilterParser,
|
||||
})
|
||||
|
||||
|
||||
class SharingPayload(BaseModel):
|
||||
action: str = Field( # TODO: this seems like it should be a list of actions instead of separating them by '-'
|
||||
..., # Mark this field as required
|
||||
title="Action",
|
||||
description=(
|
||||
"The name of the sharing action. "
|
||||
"Can be one (or multiple values separated by '-') of the following: "
|
||||
"make_accessible_via_link, make_accessible_and_publish, publish, "
|
||||
"unpublish, disable_link_access, disable_link_access_and_unpublish, unshare_user"
|
||||
),
|
||||
)
|
||||
user_id: Optional[EncodedDatabaseIdField] = Field(
|
||||
None,
|
||||
title="User ID",
|
||||
description=(
|
||||
"The ID of the user with whom this resource will be shared. "
|
||||
"*Required* when the action is `unshare_user`."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class UserEmail(BaseModel):
|
||||
id: EncodedDatabaseIdField = Field(
|
||||
..., # Mark this field as required
|
||||
title="User ID",
|
||||
description="The encoded ID of the user.",
|
||||
)
|
||||
email: str = Field(
|
||||
..., # Mark this field as required
|
||||
title="Email",
|
||||
description="The email of the user.",
|
||||
)
|
||||
|
||||
|
||||
class SharingStatus(BaseModel):
|
||||
id: EncodedDatabaseIdField = Field(
|
||||
..., # Mark this field as required
|
||||
title="ID",
|
||||
description="The encoded ID of the resource to be shared.",
|
||||
)
|
||||
title: str = Field(
|
||||
..., # Mark this field as required
|
||||
title="Title",
|
||||
description="The title or name of the resource.",
|
||||
)
|
||||
importable: bool = Field(
|
||||
..., # Mark this field as required
|
||||
title="Importable",
|
||||
description="Whether this resource can be published using a link.",
|
||||
)
|
||||
published: bool = Field(
|
||||
..., # Mark this field as required
|
||||
title="Published",
|
||||
description="Whether this resource is currently published.",
|
||||
)
|
||||
users_shared_with: List[UserEmail] = Field(
|
||||
[],
|
||||
title="Users shared with",
|
||||
description="The list of encoded ids for users the resource has been shared.",
|
||||
)
|
||||
username_and_slug: Optional[str] = Field(
|
||||
None,
|
||||
title="Username and slug",
|
||||
description="The relative URL in the form of /u/{username}/{resource_single_char}/{slug}",
|
||||
)
|
||||
skipped: Optional[bool] = Field(
|
||||
None,
|
||||
title="Skipped",
|
||||
description="Indicates that some of the resources within this object were not published due to an error.",
|
||||
)
|
||||
|
||||
|
||||
class SlugBuilder:
|
||||
"""Builder for creating slugs out of items."""
|
||||
|
||||
@@ -596,97 +661,100 @@ class ShareableService:
|
||||
def __init__(self, manager: SharableModelManager, serializer: SharableModelSerializer) -> None:
|
||||
self.manager = manager
|
||||
self.serializer = serializer
|
||||
self.slug_builder = SlugBuilder()
|
||||
|
||||
def sharing(self, trans, id: EncodedDatabaseIdField, payload: Optional[SharingPayload] = None) -> SharingStatus:
|
||||
"""Allows to publish or share with other users the given resource (by id) and returns the current sharing
|
||||
status of the resource.
|
||||
def set_slug(self, trans, id: EncodedDatabaseIdField, payload: SetSlugPayload):
|
||||
item = self._get_item_by_id(trans, id)
|
||||
self.manager.set_slug(item, payload.new_slug, trans.user)
|
||||
|
||||
:param id: The encoded ID of the resource to share.
|
||||
:type id: EncodedDatabaseIdField
|
||||
:param payload: The options to share this resource, defaults to None
|
||||
:type payload: Optional[sharable.SharingPayload], optional
|
||||
:return: The current sharing status of the resource.
|
||||
:rtype: sharable.SharingStatus
|
||||
"""
|
||||
skipped = False
|
||||
def sharing(self, trans, id: EncodedDatabaseIdField) -> SharingStatus:
|
||||
"""Gets the current sharing status of the item with the given id."""
|
||||
item = self._get_item_by_id(trans, id)
|
||||
return self._get_sharing_status(trans, item)
|
||||
|
||||
def enable_link_access(self, trans, id: EncodedDatabaseIdField) -> SharingStatus:
|
||||
item = self._get_item_by_id(trans, id)
|
||||
self.manager.make_importable(item)
|
||||
return self._get_sharing_status(trans, item)
|
||||
|
||||
def disable_link_access(self, trans, id: EncodedDatabaseIdField) -> SharingStatus:
|
||||
item = self._get_item_by_id(trans, id)
|
||||
self.manager.make_non_importable(item)
|
||||
return self._get_sharing_status(trans, item)
|
||||
|
||||
def publish(self, trans, id: EncodedDatabaseIdField) -> SharingStatus:
|
||||
item = self._get_item_by_id(trans, id)
|
||||
self.manager.publish(item)
|
||||
return self._get_sharing_status(trans, item)
|
||||
|
||||
def unpublish(self, trans, id: EncodedDatabaseIdField) -> SharingStatus:
|
||||
item = self._get_item_by_id(trans, id)
|
||||
self.manager.unpublish(item)
|
||||
return self._get_sharing_status(trans, item)
|
||||
|
||||
def share_with_users(self, trans, id: EncodedDatabaseIdField, payload: ShareWithPayload) -> ShareWithStatus:
|
||||
item = self._get_item_by_id(trans, id)
|
||||
users, errors = self._get_users(trans, payload.user_ids)
|
||||
extra = self._share_with_options(trans, item, users, errors, payload.share_option)
|
||||
base_status = self._get_sharing_status(trans, item)
|
||||
status = ShareWithStatus.parse_obj(base_status)
|
||||
status.extra = extra
|
||||
status.errors.extend(errors)
|
||||
return status
|
||||
|
||||
def _share_with_options(
|
||||
self,
|
||||
trans,
|
||||
item,
|
||||
users: Set[User],
|
||||
errors: Set[str],
|
||||
share_option: Optional[SharingOptions] = None,
|
||||
):
|
||||
extra = self.manager.get_sharing_extra_information(trans, item, users, errors, share_option)
|
||||
if not extra or extra.can_share:
|
||||
self.manager.update_current_sharing_with_users(item, users)
|
||||
extra = None
|
||||
return extra
|
||||
|
||||
def _get_item_by_id(self, trans, id: EncodedDatabaseIdField):
|
||||
class_name = self.manager.model_class.__name__
|
||||
item = base.get_object(trans, id, class_name, check_ownership=True, check_accessible=True, deleted=False)
|
||||
actions = []
|
||||
if payload:
|
||||
actions += payload.action.split("-")
|
||||
for action in actions:
|
||||
if action == "make_accessible_via_link":
|
||||
self._make_item_accessible(trans.sa_session, item)
|
||||
if hasattr(item, "has_possible_members") and item.has_possible_members:
|
||||
skipped = self._make_members_public(trans, item)
|
||||
elif action == "make_accessible_and_publish":
|
||||
self._make_item_accessible(trans.sa_session, item)
|
||||
if hasattr(item, "has_possible_members") and item.has_possible_members:
|
||||
skipped = self._make_members_public(trans, item)
|
||||
item.published = True
|
||||
elif action == "publish":
|
||||
if item.importable:
|
||||
item.published = True
|
||||
if hasattr(item, "has_possible_members") and item.has_possible_members:
|
||||
skipped = self._make_members_public(trans, item)
|
||||
else:
|
||||
raise exceptions.MessageException(f"{class_name} not importable.")
|
||||
elif action == "disable_link_access":
|
||||
item.importable = False
|
||||
elif action == "unpublish":
|
||||
item.published = False
|
||||
elif action == "disable_link_access_and_unpublish":
|
||||
item.importable = item.published = False
|
||||
elif action == "unshare_user":
|
||||
if payload is None or payload.user_id is None:
|
||||
raise exceptions.MessageException(f"Missing required user_id to perform {action}")
|
||||
user = trans.sa_session.query(trans.app.model.User).get(trans.app.security.decode_id(payload.user_id))
|
||||
class_name_lc = class_name.lower()
|
||||
ShareAssociation = getattr(trans.app.model, f"{class_name}UserShareAssociation")
|
||||
usas = trans.sa_session.query(ShareAssociation).filter_by(**{"user": user, class_name_lc: item}).all()
|
||||
if not usas:
|
||||
raise exceptions.MessageException(f"{class_name} was not shared with user.")
|
||||
for usa in usas:
|
||||
trans.sa_session.delete(usa)
|
||||
trans.sa_session.add(item)
|
||||
trans.sa_session.flush()
|
||||
if item.importable and not item.slug:
|
||||
self._make_item_accessible(trans.sa_session, item)
|
||||
item_dict = self.serializer.serialize_to_view(item,
|
||||
return item
|
||||
|
||||
def _get_sharing_status(self, trans, item):
|
||||
status = self.serializer.serialize_to_view(item,
|
||||
user=trans.user, trans=trans, default_view="sharing")
|
||||
item_dict["users_shared_with"] = [{"id": self.manager.app.security.encode_id(a.user.id), "email": a.user.email} for a in item.users_shared_with]
|
||||
if skipped:
|
||||
item_dict["skipped"] = True
|
||||
return SharingStatus.parse_obj(item_dict)
|
||||
status["users_shared_with"] = [{"id": self.manager.app.security.encode_id(a.user.id), "email": a.user.email} for a in item.users_shared_with]
|
||||
return SharingStatus.parse_obj(status)
|
||||
|
||||
def _is_valid_slug(self, slug):
|
||||
""" Returns true if slug is valid. """
|
||||
return base.is_valid_slug(slug)
|
||||
def _get_users(self, trans, emails_or_ids: Optional[List] = None) -> Tuple[Set[User], Set[str]]:
|
||||
if emails_or_ids is None:
|
||||
raise exceptions.MessageException("Missing required user IDs or emails")
|
||||
send_to_users: Set[User] = set()
|
||||
send_to_err: Set[str] = set()
|
||||
for email_or_id in set(emails_or_ids):
|
||||
email_or_id = email_or_id.strip()
|
||||
if not email_or_id:
|
||||
continue
|
||||
|
||||
def _make_item_accessible(self, sa_session, item):
|
||||
""" Makes item accessible--viewable and importable--and sets item's slug.
|
||||
Does not flush/commit changes, however. Item must have name, user,
|
||||
importable, and slug attributes. """
|
||||
item.importable = True
|
||||
self.slug_builder.create_item_slug(sa_session, item)
|
||||
send_to_user = None
|
||||
if '@' in email_or_id:
|
||||
email_address = email_or_id
|
||||
send_to_user = self.manager.user_manager.by_email(email_address,
|
||||
filters=[User.table.c.deleted == false()])
|
||||
else:
|
||||
try:
|
||||
decoded_user_id = trans.security.decode_id(email_or_id)
|
||||
send_to_user = self.manager.user_manager.by_id(decoded_user_id)
|
||||
if send_to_user.deleted:
|
||||
send_to_user = None
|
||||
except exceptions.MalformedId:
|
||||
send_to_user = None
|
||||
|
||||
def _make_members_public(self, trans, item):
|
||||
""" Make the non-purged datasets in history public
|
||||
Performs pemissions check.
|
||||
"""
|
||||
# TODO eventually we should handle more classes than just History
|
||||
skipped = False
|
||||
for hda in item.activatable_datasets:
|
||||
dataset = hda.dataset
|
||||
if not trans.app.security_agent.dataset_is_public(dataset):
|
||||
if trans.app.security_agent.can_manage_dataset(trans.user.all_roles(), dataset):
|
||||
try:
|
||||
trans.app.security_agent.make_dataset_public(hda.dataset)
|
||||
except Exception:
|
||||
log.warning("Unable to make dataset with id: %s public", dataset.id)
|
||||
skipped = True
|
||||
else:
|
||||
log.warning("User without permissions tried to make dataset with id: %s public", dataset.id)
|
||||
skipped = True
|
||||
return skipped
|
||||
if not send_to_user:
|
||||
send_to_err.add(f"{email_or_id} is not a valid Galaxy user.")
|
||||
elif send_to_user == trans.user:
|
||||
send_to_err.add("You cannot share resources with yourself.")
|
||||
else:
|
||||
send_to_users.add(send_to_user)
|
||||
|
||||
return send_to_users, send_to_err
|
||||
|
||||
@@ -5,11 +5,9 @@ Visualizations are saved configurations/variables used to
|
||||
reproduce a specific view in a Galaxy visualization.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from galaxy import model
|
||||
from galaxy.managers import sharable
|
||||
from galaxy.schema.fields import EncodedDatabaseIdField
|
||||
from galaxy.structured_app import MinimalManagerApp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -89,9 +87,3 @@ class VisualizationsService:
|
||||
self.shareable_service = sharable.ShareableService(self.manager, self.serializer)
|
||||
|
||||
# TODO: add the rest of the API actions here and call them directly from the API controller
|
||||
|
||||
def sharing(self, trans, id: EncodedDatabaseIdField, payload: Optional[sharable.SharingPayload] = None) -> sharable.SharingStatus:
|
||||
"""Allows to publish or share with other users the given resource (by id) and returns the current sharing
|
||||
status of the resource.
|
||||
"""
|
||||
return self.shareable_service.sharing(trans, id, payload)
|
||||
|
||||
@@ -1667,17 +1667,23 @@ class NavigatesGalaxy(HasDriver):
|
||||
assert text == expected, f"Tooltip text [{text}] was not expected text [{expected}]."
|
||||
|
||||
def assert_error_message(self, contains=None):
|
||||
element = self.components._.messages["error"]
|
||||
return self.assert_message(element, contains=contains)
|
||||
self.components._.messages.error.wait_for_visible()
|
||||
elements = self.find_elements(self.components._.messages.selectors.error)
|
||||
return self.assert_message(elements, contains=contains)
|
||||
|
||||
def assert_warning_message(self, contains=None):
|
||||
element = self.components._.messages["warning"]
|
||||
return self.assert_message(element, contains=contains)
|
||||
|
||||
def assert_message(self, element, contains=None):
|
||||
element = element.wait_for_visible()
|
||||
assert element, "No error message found, one expected."
|
||||
if contains is not None:
|
||||
|
||||
if type(element) == list:
|
||||
assert any([contains in el.text for el in element]), \
|
||||
f"{contains} was not found in {[el.text for el in element]}"
|
||||
return
|
||||
|
||||
element = element.wait_for_visible()
|
||||
text = element.text
|
||||
if contains not in text:
|
||||
message = f"Text [{contains}] expected inside of [{text}] but not found."
|
||||
|
||||
@@ -394,8 +394,12 @@ histories:
|
||||
|
||||
sharing:
|
||||
selectors:
|
||||
unshare_user_button: '.unshare_user'
|
||||
share_with_a_user_button: '#share_with_a_user'
|
||||
unshare_user_button: '.share_with_view .multiselect__tag-icon'
|
||||
user_email_input: '.user-email-input-form'
|
||||
submit_sharing_with: '.submit-sharing-with'
|
||||
share_with_collapse: '.share-with-collapse'
|
||||
share_with_multiselect: '.share_with_view > .multiselect'
|
||||
share_with_input: '.share_with_view input'
|
||||
make_accessible: '.make-accessible label'
|
||||
make_publishable: '.make-publishable label'
|
||||
labels:
|
||||
|
||||
@@ -6,6 +6,13 @@ API operations on a history.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import (
|
||||
Body,
|
||||
Path,
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
|
||||
from galaxy import (
|
||||
util
|
||||
)
|
||||
@@ -13,8 +20,12 @@ from galaxy.managers import (
|
||||
histories,
|
||||
sharable,
|
||||
)
|
||||
from galaxy.managers.context import ProvidesUserContext
|
||||
from galaxy.schema import FilterQueryParams
|
||||
from galaxy.schema.fields import OrderParamField
|
||||
from galaxy.schema.fields import (
|
||||
EncodedDatabaseIdField,
|
||||
OrderParamField,
|
||||
)
|
||||
from galaxy.schema.schema import (
|
||||
CreateHistoryPayload,
|
||||
ExportHistoryArchivePayload,
|
||||
@@ -29,10 +40,116 @@ from galaxy.web import (
|
||||
expose_api_raw,
|
||||
)
|
||||
from galaxy.webapps.galaxy.api.configuration import parse_serialization_params
|
||||
from . import BaseGalaxyAPIController, depends
|
||||
from . import (
|
||||
BaseGalaxyAPIController,
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
Router,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = Router(tags=['histories'])
|
||||
|
||||
HistoryIdPathParam: EncodedDatabaseIdField = Path(
|
||||
...,
|
||||
title="History ID",
|
||||
description="The encoded database identifier of the History."
|
||||
)
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIHistories:
|
||||
service: histories.HistoriesService = depends(histories.HistoriesService)
|
||||
|
||||
@router.get(
|
||||
'/api/histories/{id}/sharing',
|
||||
summary="Get the current sharing status of the given item.",
|
||||
)
|
||||
def sharing(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
) -> sharable.SharingStatus:
|
||||
"""Return the sharing status of the item."""
|
||||
return self.service.shareable_service.sharing(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/histories/{id}/enable_link_access',
|
||||
summary="Makes this item accessible by a URL link.",
|
||||
)
|
||||
def enable_link_access(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
) -> sharable.SharingStatus:
|
||||
"""Makes this item accessible by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.enable_link_access(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/histories/{id}/disable_link_access',
|
||||
summary="Makes this item inaccessible by a URL link.",
|
||||
)
|
||||
def disable_link_access(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
) -> sharable.SharingStatus:
|
||||
"""Makes this item inaccessible by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.disable_link_access(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/histories/{id}/publish',
|
||||
summary="Makes this item public and accessible by a URL link.",
|
||||
)
|
||||
def publish(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
) -> sharable.SharingStatus:
|
||||
"""Makes this item publicly available by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.publish(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/histories/{id}/unpublish',
|
||||
summary="Removes this item from the published list.",
|
||||
)
|
||||
def unpublish(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
) -> sharable.SharingStatus:
|
||||
"""Removes this item from the published list and return the current sharing status."""
|
||||
return self.service.shareable_service.unpublish(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/histories/{id}/share_with_users',
|
||||
summary="Share this item with specific users.",
|
||||
)
|
||||
def share_with_users(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
payload: sharable.ShareWithPayload = Body(...)
|
||||
) -> sharable.ShareWithStatus:
|
||||
"""Shares this item with specific users and return the current sharing status."""
|
||||
return self.service.shareable_service.share_with_users(trans, id, payload)
|
||||
|
||||
@router.put(
|
||||
'/api/histories/{id}/slug',
|
||||
summary="Set a new slug for this shared item.",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def set_slug(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = HistoryIdPathParam,
|
||||
payload: sharable.SetSlugPayload = Body(...),
|
||||
):
|
||||
"""Sets a new slug to access this item by URL. The new slug must be unique."""
|
||||
self.service.shareable_service.set_slug(trans, id, payload)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class HistoryFilterQueryParams(FilterQueryParams):
|
||||
order: Optional[str] = OrderParamField(default_order="create_time-dsc")
|
||||
@@ -367,11 +484,52 @@ class HistoriesController(BaseGalaxyAPIController):
|
||||
return self.service.get_custom_builds_metadata(trans, id)
|
||||
|
||||
@expose_api
|
||||
def sharing(self, trans, id, payload=None, **kwd):
|
||||
def sharing(self, trans, id, **kwd):
|
||||
"""
|
||||
* GET/POST /api/pages/{id}/sharing
|
||||
View/modify sharing options for the page with the given id.
|
||||
* GET /api/histories/{id}/sharing
|
||||
"""
|
||||
if payload:
|
||||
payload = sharable.SharingPayload(**payload)
|
||||
return self.service.sharing(trans, id, payload)
|
||||
return self.service.shareable_service.sharing(trans, id)
|
||||
|
||||
@expose_api
|
||||
def enable_link_access(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/histories/{id}/enable_link_access
|
||||
"""
|
||||
return self.service.shareable_service.enable_link_access(trans, id)
|
||||
|
||||
@expose_api
|
||||
def disable_link_access(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/histories/{id}/disable_link_access
|
||||
"""
|
||||
return self.service.shareable_service.disable_link_access(trans, id)
|
||||
|
||||
@expose_api
|
||||
def publish(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/histories/{id}/publish
|
||||
"""
|
||||
return self.service.shareable_service.publish(trans, id)
|
||||
|
||||
@expose_api
|
||||
def unpublish(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/histories/{id}/unpublish
|
||||
"""
|
||||
return self.service.shareable_service.unpublish(trans, id)
|
||||
|
||||
@expose_api
|
||||
def share_with_users(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
* PUT /api/histories/{id}/share_with_users
|
||||
"""
|
||||
payload = sharable.ShareWithPayload(**payload)
|
||||
return self.service.shareable_service.share_with_users(trans, id, payload)
|
||||
|
||||
@expose_api
|
||||
def set_slug(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
* PUT /api/histories/{id}/slug
|
||||
"""
|
||||
payload = sharable.SetSlugPayload(**payload)
|
||||
self.service.shareable_service.set_slug(trans, id, payload)
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi import (
|
||||
Body,
|
||||
Path,
|
||||
Query,
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
from starlette.responses import StreamingResponse
|
||||
@@ -20,7 +21,12 @@ from galaxy.managers.pages import (
|
||||
PageSummary,
|
||||
PageSummaryList,
|
||||
)
|
||||
from galaxy.managers.sharable import SharingPayload, SharingStatus
|
||||
from galaxy.managers.sharable import (
|
||||
SetSlugPayload,
|
||||
ShareWithPayload,
|
||||
ShareWithStatus,
|
||||
SharingStatus,
|
||||
)
|
||||
from galaxy.schema.fields import EncodedDatabaseIdField
|
||||
from galaxy.web import (
|
||||
expose_api,
|
||||
@@ -47,7 +53,7 @@ DeletedQueryParam: bool = Query(
|
||||
PageIdPathParam: EncodedDatabaseIdField = Path(
|
||||
..., # Required
|
||||
title="Page ID",
|
||||
description="The encoded indentifier of the Page."
|
||||
description="The encoded database identifier of the Page."
|
||||
)
|
||||
|
||||
|
||||
@@ -135,28 +141,91 @@ class FastAPIPages:
|
||||
|
||||
@router.get(
|
||||
'/api/pages/{id}/sharing',
|
||||
summary="Get sharing the status of the given Page.",
|
||||
summary="Get the current sharing status of the given Page.",
|
||||
)
|
||||
def get_sharing(
|
||||
def sharing(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Return the sharing status of the Page."""
|
||||
return self.service.sharing(trans, id)
|
||||
"""Return the sharing status of the item."""
|
||||
return self.service.shareable_service.sharing(trans, id)
|
||||
|
||||
@router.post(
|
||||
'/api/pages/{id}/sharing',
|
||||
summary="Set sharing options for the given Page.",
|
||||
@router.put(
|
||||
'/api/pages/{id}/enable_link_access',
|
||||
summary="Makes this item accessible by a URL link.",
|
||||
)
|
||||
def post_sharing(
|
||||
def enable_link_access(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
payload: SharingPayload = Body(...),
|
||||
) -> SharingStatus:
|
||||
"""Return the sharing status of the Page after the changes."""
|
||||
return self.service.sharing(trans, id, payload)
|
||||
"""Makes this item accessible by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.enable_link_access(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/pages/{id}/disable_link_access',
|
||||
summary="Makes this item inaccessible by a URL link.",
|
||||
)
|
||||
def disable_link_access(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Makes this item inaccessible by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.disable_link_access(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/pages/{id}/publish',
|
||||
summary="Makes this item public and accessible by a URL link.",
|
||||
)
|
||||
def publish(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Makes this item publicly available by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.publish(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/pages/{id}/unpublish',
|
||||
summary="Removes this item from the published list.",
|
||||
)
|
||||
def unpublish(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Removes this item from the published list and return the current sharing status."""
|
||||
return self.service.shareable_service.unpublish(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/pages/{id}/share_with_users',
|
||||
summary="Share this item with specific users.",
|
||||
)
|
||||
def share_with_users(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
payload: ShareWithPayload = Body(...)
|
||||
) -> ShareWithStatus:
|
||||
"""Shares this item with specific users and return the current sharing status."""
|
||||
return self.service.shareable_service.share_with_users(trans, id, payload)
|
||||
|
||||
@router.put(
|
||||
'/api/pages/{id}/slug',
|
||||
summary="Set a new slug for this shared item.",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def set_slug(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = PageIdPathParam,
|
||||
payload: SetSlugPayload = Body(...),
|
||||
):
|
||||
"""Sets a new slug to access this item by URL. The new slug must be unique."""
|
||||
self.service.shareable_service.set_slug(trans, id, payload)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class PagesController(BaseGalaxyAPIController):
|
||||
@@ -242,11 +311,52 @@ class PagesController(BaseGalaxyAPIController):
|
||||
return self.service.show_pdf(trans, id)
|
||||
|
||||
@expose_api
|
||||
def sharing(self, trans, id, payload=None, **kwd):
|
||||
def sharing(self, trans, id, **kwd):
|
||||
"""
|
||||
* GET/POST /api/pages/{id}/sharing
|
||||
View/modify sharing options for the page with the given id.
|
||||
* GET /api/pages/{id}/sharing
|
||||
"""
|
||||
if payload:
|
||||
payload = SharingPayload(**payload)
|
||||
return self.service.sharing(trans, id, payload)
|
||||
return self.service.shareable_service.sharing(trans, id)
|
||||
|
||||
@expose_api
|
||||
def enable_link_access(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/pages/{id}/enable_link_access
|
||||
"""
|
||||
return self.service.shareable_service.enable_link_access(trans, id)
|
||||
|
||||
@expose_api
|
||||
def disable_link_access(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/pages/{id}/disable_link_access
|
||||
"""
|
||||
return self.service.shareable_service.disable_link_access(trans, id)
|
||||
|
||||
@expose_api
|
||||
def publish(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/pages/{id}/publish
|
||||
"""
|
||||
return self.service.shareable_service.publish(trans, id)
|
||||
|
||||
@expose_api
|
||||
def unpublish(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/pages/{id}/unpublish
|
||||
"""
|
||||
return self.service.shareable_service.unpublish(trans, id)
|
||||
|
||||
@expose_api
|
||||
def share_with_users(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
* PUT /api/pages/{id}/share_with_users
|
||||
"""
|
||||
payload = ShareWithPayload(**payload)
|
||||
return self.service.shareable_service.share_with_users(trans, id, payload)
|
||||
|
||||
@expose_api
|
||||
def set_slug(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
* PUT /api/pages/{id}/slug
|
||||
"""
|
||||
payload = SetSlugPayload(**payload)
|
||||
self.service.shareable_service.set_slug(trans, id, payload)
|
||||
|
||||
@@ -7,24 +7,143 @@ may change often.
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import (
|
||||
Body,
|
||||
Path,
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
|
||||
from galaxy import (
|
||||
exceptions,
|
||||
util,
|
||||
web
|
||||
)
|
||||
from galaxy.managers.sharable import SharingPayload
|
||||
from galaxy.managers.context import ProvidesUserContext
|
||||
from galaxy.managers.sharable import (
|
||||
SetSlugPayload,
|
||||
ShareWithPayload,
|
||||
ShareWithStatus,
|
||||
SharingStatus,
|
||||
)
|
||||
from galaxy.managers.visualizations import VisualizationsService
|
||||
from galaxy.model.item_attrs import UsesAnnotations
|
||||
from galaxy.schema.fields import EncodedDatabaseIdField
|
||||
from galaxy.web import expose_api
|
||||
from galaxy.webapps.base.controller import (
|
||||
UsesVisualizationMixin
|
||||
)
|
||||
from galaxy.webapps.base.webapp import GalaxyWebTransaction
|
||||
from . import BaseGalaxyAPIController, depends
|
||||
from . import (
|
||||
BaseGalaxyAPIController,
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
Router,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = Router(tags=['visualizations'])
|
||||
|
||||
VisualizationIdPathParam: EncodedDatabaseIdField = Path(
|
||||
...,
|
||||
title="Visualization ID",
|
||||
description="The encoded database identifier of the Visualization."
|
||||
)
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIVisualizations:
|
||||
service: VisualizationsService = depends(VisualizationsService)
|
||||
|
||||
@router.get(
|
||||
'/api/visualizations/{id}/sharing',
|
||||
summary="Get the current sharing status of the given Page.",
|
||||
)
|
||||
def sharing(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Return the sharing status of the item."""
|
||||
return self.service.shareable_service.sharing(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/visualizations/{id}/enable_link_access',
|
||||
summary="Makes this item accessible by a URL link.",
|
||||
)
|
||||
def enable_link_access(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Makes this item accessible by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.enable_link_access(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/visualizations/{id}/disable_link_access',
|
||||
summary="Makes this item inaccessible by a URL link.",
|
||||
)
|
||||
def disable_link_access(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Makes this item inaccessible by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.disable_link_access(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/visualizations/{id}/publish',
|
||||
summary="Makes this item public and accessible by a URL link.",
|
||||
)
|
||||
def publish(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Makes this item publicly available by a URL link and return the current sharing status."""
|
||||
return self.service.shareable_service.publish(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/visualizations/{id}/unpublish',
|
||||
summary="Removes this item from the published list.",
|
||||
)
|
||||
def unpublish(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
) -> SharingStatus:
|
||||
"""Removes this item from the published list and return the current sharing status."""
|
||||
return self.service.shareable_service.unpublish(trans, id)
|
||||
|
||||
@router.put(
|
||||
'/api/visualizations/{id}/share_with_users',
|
||||
summary="Share this item with specific users.",
|
||||
)
|
||||
def share_with_users(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
payload: ShareWithPayload = Body(...)
|
||||
) -> ShareWithStatus:
|
||||
"""Shares this item with specific users and return the current sharing status."""
|
||||
return self.service.shareable_service.share_with_users(trans, id, payload)
|
||||
|
||||
@router.put(
|
||||
'/api/visualizations/{id}/slug',
|
||||
summary="Set a new slug for this shared item.",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def set_slug(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = VisualizationIdPathParam,
|
||||
payload: SetSlugPayload = Body(...),
|
||||
):
|
||||
"""Sets a new slug to access this item by URL. The new slug must be unique."""
|
||||
self.service.shareable_service.set_slug(trans, id, payload)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class VisualizationsController(BaseGalaxyAPIController, UsesVisualizationMixin, UsesAnnotations):
|
||||
"""
|
||||
@@ -152,14 +271,55 @@ class VisualizationsController(BaseGalaxyAPIController, UsesVisualizationMixin,
|
||||
return rval
|
||||
|
||||
@expose_api
|
||||
def sharing(self, trans, id, payload=None, **kwd):
|
||||
def sharing(self, trans, id, **kwd):
|
||||
"""
|
||||
* GET/POST /api/pages/{id}/sharing
|
||||
View/modify sharing options for the page with the given id.
|
||||
* GET /api/visualizations/{id}/sharing
|
||||
"""
|
||||
if payload:
|
||||
payload = SharingPayload(**payload)
|
||||
return self.service.sharing(trans, id, payload)
|
||||
return self.service.shareable_service.sharing(trans, id)
|
||||
|
||||
@expose_api
|
||||
def enable_link_access(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/visualizations/{id}/enable_link_access
|
||||
"""
|
||||
return self.service.shareable_service.enable_link_access(trans, id)
|
||||
|
||||
@expose_api
|
||||
def disable_link_access(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/visualizations/{id}/disable_link_access
|
||||
"""
|
||||
return self.service.shareable_service.disable_link_access(trans, id)
|
||||
|
||||
@expose_api
|
||||
def publish(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/visualizations/{id}/publish
|
||||
"""
|
||||
return self.service.shareable_service.publish(trans, id)
|
||||
|
||||
@expose_api
|
||||
def unpublish(self, trans, id, **kwd):
|
||||
"""
|
||||
* PUT /api/visualizations/{id}/unpublish
|
||||
"""
|
||||
return self.service.shareable_service.unpublish(trans, id)
|
||||
|
||||
@expose_api
|
||||
def share_with_users(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
* PUT /api/visualizations/{id}/share_with_users
|
||||
"""
|
||||
payload = ShareWithPayload(**payload)
|
||||
return self.service.shareable_service.share_with_users(trans, id, payload)
|
||||
|
||||
@expose_api
|
||||
def set_slug(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
* PUT /api/visualizations/{id}/slug
|
||||
"""
|
||||
payload = SetSlugPayload(**payload)
|
||||
self.service.shareable_service.set_slug(trans, id, payload)
|
||||
|
||||
def _validate_and_parse_payload(self, payload):
|
||||
"""
|
||||
|
||||
@@ -479,7 +479,13 @@ def populate_api_routes(webapp, app):
|
||||
webapp.mapper.connect('/api/genomes/{id}/indexes', controller='genomes', action='indexes')
|
||||
webapp.mapper.connect('/api/genomes/{id}/sequences', controller='genomes', action='sequences')
|
||||
webapp.mapper.resource('visualization', 'visualizations', path_prefix='/api')
|
||||
webapp.mapper.connect('/api/visualizations/{id}/sharing', action='sharing', controller="visualizations", conditions=dict(method=["GET", "POST"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/sharing', action='sharing', controller="visualizations", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/enable_link_access', action='enable_link_access', controller="visualizations", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/disable_link_access', action='disable_link_access', controller="visualizations", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/publish', action='publish', controller="visualizations", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/unpublish', action='unpublish', controller="visualizations", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/share_with_users', action='share_with_users', controller="visualizations", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/visualizations/{id}/slug', action='set_slug', controller="visualizations", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.resource('plugins', 'plugins', path_prefix='/api')
|
||||
webapp.mapper.connect('/api/workflows/build_module', action='build_module', controller="workflows")
|
||||
webapp.mapper.connect('/api/workflows/menu', action='get_workflow_menu', controller="workflows", conditions=dict(method=["GET"]))
|
||||
@@ -490,7 +496,13 @@ def populate_api_routes(webapp, app):
|
||||
webapp.mapper.connect('/api/licenses/{id}', controller='licenses', action='get', conditions=dict(method="GET"))
|
||||
webapp.mapper.resource_with_deleted('history', 'histories', path_prefix='/api')
|
||||
webapp.mapper.connect('/api/histories/{history_id}/citations', action='citations', controller="histories")
|
||||
webapp.mapper.connect('/api/histories/{id}/sharing', action='sharing', controller="histories", conditions=dict(method=["GET", "POST"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/sharing', action='sharing', controller="histories", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/enable_link_access', action='enable_link_access', controller="histories", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/disable_link_access', action='disable_link_access', controller="histories", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/publish', action='publish', controller="histories", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/unpublish', action='unpublish', controller="histories", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/share_with_users', action='share_with_users', controller="histories", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/histories/{id}/slug', action='set_slug', controller="histories", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect(
|
||||
'dynamic_tool_confs',
|
||||
'/api/configuration/dynamic_tool_confs',
|
||||
@@ -529,7 +541,13 @@ def populate_api_routes(webapp, app):
|
||||
webapp.mapper.resource('search', 'search', path_prefix='/api')
|
||||
webapp.mapper.connect('/api/pages/{id}.pdf', action='show_pdf', controller="pages", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.resource('page', 'pages', path_prefix="/api")
|
||||
webapp.mapper.connect('/api/pages/{id}/sharing', action='sharing', controller="pages", conditions=dict(method=["GET", "POST"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/sharing', action='sharing', controller="pages", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/enable_link_access', action='enable_link_access', controller="pages", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/disable_link_access', action='disable_link_access', controller="pages", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/publish', action='publish', controller="pages", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/unpublish', action='unpublish', controller="pages", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/share_with_users', action='share_with_users', controller="pages", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/pages/{id}/slug', action='set_slug', controller="pages", conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.resource('revision', 'revisions',
|
||||
path_prefix='/api/pages/{page_id}',
|
||||
controller='page_revisions',
|
||||
|
||||
@@ -2,7 +2,6 @@ import logging
|
||||
|
||||
from markupsafe import escape
|
||||
from sqlalchemy import (
|
||||
and_,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
@@ -13,7 +12,6 @@ from sqlalchemy.orm import (
|
||||
undefer
|
||||
)
|
||||
|
||||
import galaxy.util
|
||||
from galaxy import exceptions
|
||||
from galaxy import model
|
||||
from galaxy import web
|
||||
@@ -26,7 +24,6 @@ from galaxy.model.item_attrs import (
|
||||
from galaxy.structured_app import StructuredApp
|
||||
from galaxy.util import (
|
||||
listify,
|
||||
Params,
|
||||
parse_int,
|
||||
sanitize_text,
|
||||
string_as_bool,
|
||||
@@ -717,79 +714,6 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
|
||||
raise exceptions.InternalServerError('An error occurred and the dataset is NOT private.')
|
||||
return {'message': f"Success, requested permissions have been changed in {'all histories' if all_histories else history.name}."}
|
||||
|
||||
@web.expose
|
||||
@web.require_login("share histories with other users")
|
||||
def share(self, trans, id=None, email="", **kwd):
|
||||
# If a history contains both datasets that can be shared and others that cannot be shared with the desired user,
|
||||
# then the entire history is shared, and the protected datasets will be visible, but inaccessible ( greyed out )
|
||||
# in the copyd history
|
||||
params = Params(kwd)
|
||||
user = trans.get_user()
|
||||
# TODO: we have too many error messages floating around in here - we need
|
||||
# to incorporate the messaging system used by the libraries that will display
|
||||
# a message on any page.
|
||||
err_msg = galaxy.util.restore_text(params.get('err_msg', ''))
|
||||
if not email:
|
||||
if not id:
|
||||
# Default to the current history
|
||||
id = trans.security.encode_id(trans.history.id)
|
||||
id = listify(id)
|
||||
send_to_err = err_msg
|
||||
histories = []
|
||||
for history_id in id:
|
||||
history_id = self.decode_id(history_id)
|
||||
history = self.history_manager.get_owned(history_id, trans.user, current_history=trans.history)
|
||||
histories.append(history)
|
||||
return trans.fill_template("/history/share.mako",
|
||||
histories=histories,
|
||||
email=email,
|
||||
send_to_err=send_to_err)
|
||||
|
||||
histories = self._get_histories(trans, id)
|
||||
send_to_users, send_to_err = self._get_users(trans, user, email)
|
||||
if not send_to_users:
|
||||
if not send_to_err:
|
||||
send_to_err += f"{email} is not a valid Galaxy user. {err_msg}"
|
||||
return trans.fill_template("/history/share.mako",
|
||||
histories=histories,
|
||||
email=email,
|
||||
send_to_err=send_to_err)
|
||||
|
||||
if params.get('share_button', False):
|
||||
|
||||
# The user has not yet made a choice about how to share, so dictionaries will be built for display
|
||||
can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \
|
||||
self._populate_restricted(trans, user, histories, send_to_users, None, send_to_err, unique=True)
|
||||
|
||||
send_to_err += err_msg
|
||||
if cannot_change and not no_change_needed and not can_change:
|
||||
send_to_err = "The histories you are sharing do not contain any datasets that can be accessed by the users with which you are sharing."
|
||||
return trans.fill_template("/history/share.mako",
|
||||
histories=histories,
|
||||
email=email,
|
||||
send_to_err=send_to_err)
|
||||
|
||||
if can_change or cannot_change:
|
||||
return trans.fill_template("/history/share.mako",
|
||||
histories=histories,
|
||||
email=email,
|
||||
send_to_err=send_to_err,
|
||||
can_change=can_change,
|
||||
cannot_change=cannot_change,
|
||||
no_change_needed=unique_no_change_needed)
|
||||
|
||||
if no_change_needed:
|
||||
return self._share_histories(trans, user, send_to_err, histories=no_change_needed)
|
||||
|
||||
elif not send_to_err:
|
||||
# User seems to be sharing an empty history
|
||||
send_to_err = "You cannot share an empty history. "
|
||||
|
||||
return trans.fill_template("/history/share.mako",
|
||||
histories=histories,
|
||||
email=email,
|
||||
send_to_err=send_to_err)
|
||||
|
||||
@web.expose
|
||||
def adjust_hidden(self, trans, id=None, **kwd):
|
||||
""" THIS METHOD IS A TEMPORARY ADDITION. It'll allow us to fix the
|
||||
@@ -804,248 +728,8 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
|
||||
trans.history.unhide_datasets()
|
||||
trans.sa_session.flush()
|
||||
|
||||
@web.expose
|
||||
@web.require_login("share restricted histories with other users")
|
||||
def share_restricted(self, trans, id=None, email="", **kwd):
|
||||
if 'action' in kwd:
|
||||
action = kwd['action']
|
||||
else:
|
||||
err_msg = "Select an action. "
|
||||
return trans.response.send_redirect(url_for(controller='history',
|
||||
action='share',
|
||||
id=id,
|
||||
email=email,
|
||||
err_msg=err_msg,
|
||||
share_button=True))
|
||||
user = trans.get_user()
|
||||
user_roles = user.all_roles()
|
||||
histories = self._get_histories(trans, id)
|
||||
send_to_users, send_to_err = self._get_users(trans, user, email)
|
||||
send_to_err = ''
|
||||
# The user has made a choice, so dictionaries will be built for sharing
|
||||
can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \
|
||||
self._populate_restricted(trans, user, histories, send_to_users, action, send_to_err)
|
||||
# Now that we've populated the can_change, cannot_change, and no_change_needed dictionaries,
|
||||
# we'll populate the histories_for_sharing dictionary from each of them.
|
||||
histories_for_sharing = {}
|
||||
if no_change_needed:
|
||||
# Don't need to change anything in cannot_change, so populate as is
|
||||
histories_for_sharing, send_to_err = \
|
||||
self._populate(trans, histories_for_sharing, no_change_needed, send_to_err)
|
||||
if cannot_change:
|
||||
# Can't change anything in cannot_change, so populate as is
|
||||
histories_for_sharing, send_to_err = \
|
||||
self._populate(trans, histories_for_sharing, cannot_change, send_to_err)
|
||||
# The action here is either 'public' or 'private', so we'll continue to populate the
|
||||
# histories_for_sharing dictionary from the can_change dictionary.
|
||||
for send_to_user, history_dict in can_change.items():
|
||||
for history in history_dict:
|
||||
# Make sure the current history has not already been shared with the current send_to_user
|
||||
if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation) \
|
||||
.filter(and_(trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id,
|
||||
trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id)) \
|
||||
.count() > 0:
|
||||
send_to_err += f"History ({history.name}) already shared with user ({send_to_user.email})"
|
||||
else:
|
||||
# Only deal with datasets that have not been purged
|
||||
for hda in history.activatable_datasets:
|
||||
# If the current dataset is not public, we may need to perform an action on it to
|
||||
# make it accessible by the other user.
|
||||
if not trans.app.security_agent.can_access_dataset(send_to_user.all_roles(), hda.dataset):
|
||||
# The user with which we are sharing the history does not have access permission on the current dataset
|
||||
if trans.app.security_agent.can_manage_dataset(user_roles, hda.dataset) and not hda.dataset.library_associations:
|
||||
# The current user has authority to change permissions on the current dataset because
|
||||
# they have permission to manage permissions on the dataset and the dataset is not associated
|
||||
# with a library.
|
||||
if action == "private":
|
||||
trans.app.security_agent.privately_share_dataset(hda.dataset, users=[user, send_to_user])
|
||||
elif action == "public":
|
||||
trans.app.security_agent.make_dataset_public(hda.dataset)
|
||||
# Populate histories_for_sharing with the history after performing any requested actions on
|
||||
# its datasets to make them accessible by the other user.
|
||||
if send_to_user not in histories_for_sharing:
|
||||
histories_for_sharing[send_to_user] = [history]
|
||||
elif history not in histories_for_sharing[send_to_user]:
|
||||
histories_for_sharing[send_to_user].append(history)
|
||||
return self._share_histories(trans, user, send_to_err, histories=histories_for_sharing)
|
||||
|
||||
def _get_histories(self, trans, ids):
|
||||
if not ids:
|
||||
# Default to the current history
|
||||
ids = trans.security.encode_id(trans.history.id)
|
||||
ids = listify(ids)
|
||||
histories = []
|
||||
for history_id in ids:
|
||||
history_id = self.decode_id(history_id)
|
||||
history = self.history_manager.get_owned(history_id, trans.user, current_history=trans.history)
|
||||
histories.append(history)
|
||||
return histories
|
||||
|
||||
def _get_users(self, trans, user, emails_or_ids):
|
||||
send_to_users = []
|
||||
send_to_err = ""
|
||||
for string in listify(emails_or_ids):
|
||||
string = string.strip()
|
||||
if not string:
|
||||
continue
|
||||
|
||||
send_to_user = None
|
||||
if '@' in string:
|
||||
email_address = string
|
||||
send_to_user = self.user_manager.by_email(email_address,
|
||||
filters=[trans.app.model.User.table.c.deleted == false()])
|
||||
|
||||
else:
|
||||
try:
|
||||
decoded_user_id = self.decode_id(string)
|
||||
send_to_user = self.user_manager.by_id(decoded_user_id)
|
||||
if send_to_user.deleted:
|
||||
send_to_user = None
|
||||
# TODO: in an ideal world, we would let this bubble up to web.expose which would handle it
|
||||
except exceptions.MalformedId:
|
||||
send_to_user = None
|
||||
|
||||
if not send_to_user:
|
||||
send_to_err += f"{string} is not a valid Galaxy user. "
|
||||
elif send_to_user == user:
|
||||
send_to_err += "You cannot send histories to yourself. "
|
||||
else:
|
||||
send_to_users.append(send_to_user)
|
||||
|
||||
return send_to_users, send_to_err
|
||||
|
||||
def _populate(self, trans, histories_for_sharing, other, send_to_err):
|
||||
# This method will populate the histories_for_sharing dictionary with the users and
|
||||
# histories in other, eliminating histories that have already been shared with the
|
||||
# associated user. No security checking on datasets is performed.
|
||||
# If not empty, the histories_for_sharing dictionary looks like:
|
||||
# { userA: [ historyX, historyY ], userB: [ historyY ] }
|
||||
# other looks like:
|
||||
# { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} }
|
||||
for send_to_user, history_dict in other.items():
|
||||
for history in history_dict:
|
||||
# Make sure the current history has not already been shared with the current send_to_user
|
||||
if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation) \
|
||||
.filter(and_(trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id,
|
||||
trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id)) \
|
||||
.count() > 0:
|
||||
send_to_err += f"History ({history.name}) already shared with user ({send_to_user.email})"
|
||||
else:
|
||||
# Build the dict that will be used for sharing
|
||||
if send_to_user not in histories_for_sharing:
|
||||
histories_for_sharing[send_to_user] = [history]
|
||||
elif history not in histories_for_sharing[send_to_user]:
|
||||
histories_for_sharing[send_to_user].append(history)
|
||||
return histories_for_sharing, send_to_err
|
||||
|
||||
def _populate_restricted(self, trans, user, histories, send_to_users, action, send_to_err, unique=False):
|
||||
# The user may be attempting to share histories whose datasets cannot all be accessed by other users.
|
||||
# If this is the case, the user sharing the histories can:
|
||||
# 1) action=='public': choose to make the datasets public if he is permitted to do so
|
||||
# 2) action=='private': automatically create a new "sharing role" allowing protected
|
||||
# datasets to be accessed only by the desired users
|
||||
# This method will populate the can_change, cannot_change and no_change_needed dictionaries, which
|
||||
# are used for either displaying to the user, letting them make 1 of the choices above, or sharing
|
||||
# after the user has made a choice. They will be used for display if 'unique' is True, and will look
|
||||
# like: {historyX : [hda, hda], historyY : [hda] }
|
||||
# For sharing, they will look like:
|
||||
# { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} }
|
||||
can_change = {}
|
||||
cannot_change = {}
|
||||
no_change_needed = {}
|
||||
unique_no_change_needed = {}
|
||||
user_roles = user.all_roles()
|
||||
for history in histories:
|
||||
for send_to_user in send_to_users:
|
||||
# Make sure the current history has not already been shared with the current send_to_user
|
||||
if trans.sa_session.query(trans.app.model.HistoryUserShareAssociation) \
|
||||
.filter(and_(trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id,
|
||||
trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id)) \
|
||||
.count() > 0:
|
||||
send_to_err += f"History ({history.name}) already shared with user ({send_to_user.email})"
|
||||
else:
|
||||
# Only deal with datasets that have not been purged
|
||||
for hda in history.activatable_datasets:
|
||||
if trans.app.security_agent.can_access_dataset(send_to_user.all_roles(), hda.dataset):
|
||||
# The no_change_needed dictionary is a special case. If both of can_change
|
||||
# and cannot_change are empty, no_change_needed will used for sharing. Otherwise
|
||||
# unique_no_change_needed will be used for displaying, so we need to populate both.
|
||||
# Build the dictionaries for display, containing unique histories only
|
||||
if history not in unique_no_change_needed:
|
||||
unique_no_change_needed[history] = [hda]
|
||||
else:
|
||||
unique_no_change_needed[history].append(hda)
|
||||
# Build the dictionaries for sharing
|
||||
if send_to_user not in no_change_needed:
|
||||
no_change_needed[send_to_user] = {}
|
||||
if history not in no_change_needed[send_to_user]:
|
||||
no_change_needed[send_to_user][history] = [hda]
|
||||
else:
|
||||
no_change_needed[send_to_user][history].append(hda)
|
||||
else:
|
||||
# The user with which we are sharing the history does not have access permission on the current dataset
|
||||
if trans.app.security_agent.can_manage_dataset(user_roles, hda.dataset):
|
||||
# The current user has authority to change permissions on the current dataset because
|
||||
# they have permission to manage permissions on the dataset.
|
||||
# NOTE: ( gvk )There may be problems if the dataset also has an ldda, but I don't think so
|
||||
# because the user with which we are sharing will not have the "manage permission" permission
|
||||
# on the dataset in their history. Keep an eye on this though...
|
||||
if unique:
|
||||
# Build the dictionaries for display, containing unique histories only
|
||||
if history not in can_change:
|
||||
can_change[history] = [hda]
|
||||
else:
|
||||
can_change[history].append(hda)
|
||||
else:
|
||||
# Build the dictionaries for sharing
|
||||
if send_to_user not in can_change:
|
||||
can_change[send_to_user] = {}
|
||||
if history not in can_change[send_to_user]:
|
||||
can_change[send_to_user][history] = [hda]
|
||||
else:
|
||||
can_change[send_to_user][history].append(hda)
|
||||
else:
|
||||
if action in ["private", "public"]:
|
||||
# The user has made a choice, so 'unique' doesn't apply. Don't change stuff
|
||||
# that the user doesn't have permission to change
|
||||
continue
|
||||
if unique:
|
||||
# Build the dictionaries for display, containing unique histories only
|
||||
if history not in cannot_change:
|
||||
cannot_change[history] = [hda]
|
||||
else:
|
||||
cannot_change[history].append(hda)
|
||||
else:
|
||||
# Build the dictionaries for sharing
|
||||
if send_to_user not in cannot_change:
|
||||
cannot_change[send_to_user] = {}
|
||||
if history not in cannot_change[send_to_user]:
|
||||
cannot_change[send_to_user][history] = [hda]
|
||||
else:
|
||||
cannot_change[send_to_user][history].append(hda)
|
||||
return can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err
|
||||
|
||||
def _share_histories(self, trans, user, send_to_err, histories=None):
|
||||
# histories looks like: { userA: [ historyX, historyY ], userB: [ historyY ] }
|
||||
histories = histories or {}
|
||||
if not histories:
|
||||
send_to_err += "No users have been specified or no histories can be sent without changing permissions or associating a sharing role. "
|
||||
return trans.response.send_redirect(web.url_for(f"/histories/list?status=error&message={send_to_err}"))
|
||||
else:
|
||||
shared_histories = []
|
||||
for send_to_user, send_to_user_histories in histories.items():
|
||||
for history in send_to_user_histories:
|
||||
share = trans.app.model.HistoryUserShareAssociation()
|
||||
share.history = history
|
||||
share.user = send_to_user
|
||||
trans.sa_session.add(share)
|
||||
self.slug_builder.create_item_slug(trans.sa_session, history)
|
||||
trans.sa_session.flush()
|
||||
if history not in shared_histories:
|
||||
shared_histories.append(history)
|
||||
return trans.response.send_redirect(web.url_for(f"/histories/sharing?id={trans.security.encode_id(shared_histories[0].id)}"))
|
||||
|
||||
# ......................................................................... actions/orig. async
|
||||
|
||||
@web.expose
|
||||
def purge_deleted_datasets(self, trans):
|
||||
count = 0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from markupsafe import escape
|
||||
from sqlalchemy import (
|
||||
and_,
|
||||
desc,
|
||||
false,
|
||||
true
|
||||
@@ -445,46 +444,6 @@ class PageController(BaseUIController, SharableMixin,
|
||||
"""
|
||||
return trans.fill_template("page/editor.mako", id=id)
|
||||
|
||||
@web.expose
|
||||
@web.require_login("use Galaxy pages")
|
||||
def share(self, trans, id, email="", use_panels=False):
|
||||
""" Handle sharing with an individual user. """
|
||||
msg = mtype = None
|
||||
page = trans.sa_session.query(model.Page).get(self.decode_id(id))
|
||||
if email:
|
||||
other = trans.sa_session.query(model.User) \
|
||||
.filter(and_(model.User.table.c.email == email,
|
||||
model.User.table.c.deleted == false())) \
|
||||
.first()
|
||||
if not other:
|
||||
mtype = "error"
|
||||
msg = f"User '{escape(email)}' does not exist"
|
||||
elif other == trans.get_user():
|
||||
mtype = "error"
|
||||
msg = ("You cannot share a page with yourself")
|
||||
elif trans.sa_session.query(model.PageUserShareAssociation) \
|
||||
.filter_by(user=other, page=page).count() > 0:
|
||||
mtype = "error"
|
||||
msg = f"Page already shared with '{escape(email)}'"
|
||||
else:
|
||||
share = model.PageUserShareAssociation()
|
||||
share.page = page
|
||||
share.user = other
|
||||
session = trans.sa_session
|
||||
session.add(share)
|
||||
self.slug_builder.create_item_slug(session, page)
|
||||
session.flush()
|
||||
page_title = escape(page.title)
|
||||
other_email = escape(other.email)
|
||||
trans.set_message(f"Page '{page_title}' shared with user '{other_email}'")
|
||||
return trans.response.send_redirect(url_for(f"/pages/sharing?id={id}"))
|
||||
return trans.fill_template("/ind_share_base.mako",
|
||||
message=msg,
|
||||
messagetype=mtype,
|
||||
item=page,
|
||||
email=email,
|
||||
use_panels=use_panels)
|
||||
|
||||
@web.expose
|
||||
@web.require_login()
|
||||
def display(self, trans, id):
|
||||
|
||||
@@ -9,7 +9,6 @@ from paste.httpexceptions import (
|
||||
HTTPNotFound
|
||||
)
|
||||
from sqlalchemy import (
|
||||
and_,
|
||||
desc,
|
||||
false,
|
||||
or_,
|
||||
@@ -414,46 +413,6 @@ class VisualizationController(BaseUIController, SharableMixin, UsesVisualization
|
||||
message="""Visualization "%s" has been imported. <br>You can <a href="%s">start using this visualization</a> or %s."""
|
||||
% (visualization.title, web.url_for('/visualizations/list'), referer_message), use_panels=True)
|
||||
|
||||
@web.expose
|
||||
@web.require_login("share Galaxy visualizations")
|
||||
def share(self, trans, id=None, email="", use_panels=False):
|
||||
""" Handle sharing a visualization with a particular user. """
|
||||
msg = mtype = None
|
||||
visualization = self.get_visualization(trans, id, check_ownership=True)
|
||||
if email:
|
||||
other = trans.sa_session.query(model.User) \
|
||||
.filter(and_(model.User.table.c.email == email,
|
||||
model.User.table.c.deleted == false())) \
|
||||
.first()
|
||||
if not other:
|
||||
mtype = "error"
|
||||
msg = f"User '{escape(email)}' does not exist"
|
||||
elif other == trans.get_user():
|
||||
mtype = "error"
|
||||
msg = ("You cannot share a visualization with yourself")
|
||||
elif trans.sa_session.query(model.VisualizationUserShareAssociation) \
|
||||
.filter_by(user=other, visualization=visualization).count() > 0:
|
||||
mtype = "error"
|
||||
msg = f"Visualization already shared with '{escape(email)}'"
|
||||
else:
|
||||
share = model.VisualizationUserShareAssociation()
|
||||
share.visualization = visualization
|
||||
share.user = other
|
||||
session = trans.sa_session
|
||||
session.add(share)
|
||||
self.slug_builder.create_item_slug(session, visualization)
|
||||
session.flush()
|
||||
viz_title = escape(visualization.title)
|
||||
other_email = escape(other.email)
|
||||
trans.set_message(f"Visualization '{viz_title}' shared with user '{other_email}'")
|
||||
return trans.response.send_redirect(web.url_for(f"/visualizations/sharing?id={id}"))
|
||||
return trans.fill_template("/ind_share_base.mako",
|
||||
message=msg,
|
||||
messagetype=mtype,
|
||||
item=visualization,
|
||||
email=email,
|
||||
use_panels=use_panels)
|
||||
|
||||
@web.expose
|
||||
def display_by_username_and_slug(self, trans, username, slug):
|
||||
""" Display visualization based on a username and slug. """
|
||||
|
||||
+114
-60
@@ -7,102 +7,156 @@ from galaxy_test.base.api import UsesApiTestCaseMixin
|
||||
class SharingApiTests(UsesApiTestCaseMixin):
|
||||
""" Includes some tests for the sharing functionality of a particular resource type."""
|
||||
|
||||
# The api_name has to be set to the appropiate value in the class using these tests.
|
||||
# The api_name has to be set to the appropriate value in the class using these tests.
|
||||
api_name: str
|
||||
|
||||
def create(self, name: str) -> str:
|
||||
"""Creates a shareable resource with the given name and returns it's ID.
|
||||
|
||||
:param name: The name of the shareable resource to create.
|
||||
:type name: str
|
||||
:return: The ID of the resource.
|
||||
:rtype: str
|
||||
"""
|
||||
raise SkipTest("Abstract")
|
||||
|
||||
def test_get_sharing_info(self):
|
||||
def test_sharing_get_status(self):
|
||||
resource_id = self.create("resource-to-share")
|
||||
sharing_response = self._get_resource_sharing_info(resource_id)
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
self._assert_has_keys(sharing_response, "title", "importable", "id", "username_and_slug", "published", "users_shared_with")
|
||||
|
||||
def test_sharing_make_accessible_via_link(self):
|
||||
resource_id = self.create("resource-to-make-accessible-via-link")
|
||||
def test_sharing_access(self):
|
||||
resource_id = self.create("resource-to-enable-link-access")
|
||||
|
||||
sharing_response = self._get_resource_sharing_info(resource_id)
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
assert sharing_response["importable"] is False
|
||||
|
||||
payload = {"action": "make_accessible_via_link"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
sharing_response = self._set_resource_sharing(resource_id, "enable_link_access")
|
||||
assert sharing_response["importable"] is True
|
||||
|
||||
def test_sharing_make_accessible_and_publish(self):
|
||||
sharing_response = self._set_resource_sharing(resource_id, "disable_link_access")
|
||||
assert sharing_response["importable"] is False
|
||||
|
||||
def test_sharing_publish(self):
|
||||
resource_id = self.create("resource-to-publish")
|
||||
|
||||
sharing_response = self._get_resource_sharing_info(resource_id)
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
assert sharing_response["importable"] is False
|
||||
assert sharing_response["published"] is False
|
||||
|
||||
payload = {"action": "make_accessible_and_publish"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
sharing_response = self._set_resource_sharing(resource_id, "publish")
|
||||
assert sharing_response["importable"] is True
|
||||
assert sharing_response["published"] is True
|
||||
|
||||
def test_sharing_publish_not_accessible_raises_400(self):
|
||||
resource_id = self.create("resource-to-publish-not-accesible")
|
||||
|
||||
sharing_response = self._get_resource_sharing_info(resource_id)
|
||||
assert sharing_response["importable"] is False
|
||||
|
||||
payload = {"action": "publish"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload, expect_response_status=400)
|
||||
|
||||
def test_sharing_disable_link_access(self):
|
||||
resource_id = self.create("resource-to-disable-link-access")
|
||||
|
||||
payload = {"action": "make_accessible_via_link"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
assert sharing_response["importable"] is True
|
||||
|
||||
payload = {"action": "disable_link_access"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
assert sharing_response["importable"] is False
|
||||
|
||||
def test_sharing_unpublish(self):
|
||||
resource_id = self.create("resource-to-unpublish")
|
||||
|
||||
sharing_response = self._get_resource_sharing_info(resource_id)
|
||||
|
||||
payload = {"action": "make_accessible_and_publish"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
assert sharing_response["importable"] is True
|
||||
assert sharing_response["published"] is True
|
||||
|
||||
payload = {"action": "unpublish"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
sharing_response = self._set_resource_sharing(resource_id, "unpublish")
|
||||
assert sharing_response["importable"] is True
|
||||
assert sharing_response["published"] is False
|
||||
|
||||
def test_sharing_disable_link_access_and_unpublish(self):
|
||||
resource_id = self.create("resource-to-disable-link-access-and-unpublish")
|
||||
def test_sharing_without_user(self):
|
||||
resource_id = self.create("resource-to-share-with-empty")
|
||||
|
||||
sharing_response = self._get_resource_sharing_info(resource_id)
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
payload = {"action": "make_accessible_and_publish"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
assert sharing_response["importable"] is True
|
||||
assert sharing_response["published"] is True
|
||||
payload = {"user_ids": []}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
payload = {"action": "disable_link_access_and_unpublish"}
|
||||
sharing_response = self._set_resource_sharing(resource_id, payload)
|
||||
assert sharing_response["importable"] is False
|
||||
assert sharing_response["published"] is False
|
||||
def test_sharing_with_user_id(self):
|
||||
target_user = self._setup_user("target@user.com")
|
||||
target_user_id = target_user["id"]
|
||||
|
||||
def _get_resource_sharing_info(self, resource_id: str):
|
||||
resource_id = self.create("resource-to-share-user-id")
|
||||
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
payload = {"user_ids": [target_user_id]}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert sharing_response["users_shared_with"]
|
||||
assert sharing_response["users_shared_with"][0]["id"] == target_user_id
|
||||
|
||||
payload = {"user_ids": []}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
def test_sharing_with_user_email(self):
|
||||
target_user = self._setup_user("target@user.com")
|
||||
target_user_email = target_user["email"]
|
||||
|
||||
resource_id = self.create("resource-to-share-user-email")
|
||||
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
payload = {"user_ids": [target_user_email]}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert sharing_response["users_shared_with"]
|
||||
assert sharing_response["users_shared_with"][0]["email"] == target_user_email
|
||||
|
||||
payload = {"user_ids": []}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
def test_update_sharing_with_users(self):
|
||||
target_user_list = ["target01@user.com", "target02@user.com"]
|
||||
additional_user_list = ["add01@user.com", "add02@user.com"]
|
||||
all_user_emails = target_user_list + additional_user_list
|
||||
for email in all_user_emails:
|
||||
self._setup_user(email)
|
||||
|
||||
resource_id = self.create("resource-to-share-and-update")
|
||||
|
||||
payload = {"user_ids": all_user_emails}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert sharing_response["users_shared_with"]
|
||||
assert len(sharing_response["users_shared_with"]) == len(all_user_emails)
|
||||
|
||||
# We just keep target users so additional users should be removed
|
||||
payload = {"user_ids": target_user_list}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert len(sharing_response["users_shared_with"]) == len(target_user_list)
|
||||
assert additional_user_list not in sharing_response["users_shared_with"]
|
||||
|
||||
def test_sharing_with_invalid_user(self):
|
||||
invalid_user_email = "unknown@user.com"
|
||||
|
||||
resource_id = self.create("resource-to-share-user-unknown")
|
||||
|
||||
sharing_response = self._get_resource_sharing_status(resource_id)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
payload = {"user_ids": [invalid_user_email]}
|
||||
sharing_response = self._set_resource_sharing(resource_id, action="share_with_users", payload=payload)
|
||||
assert not sharing_response["users_shared_with"]
|
||||
assert sharing_response["errors"]
|
||||
assert invalid_user_email in sharing_response["errors"][0]
|
||||
|
||||
def test_set_slug(self):
|
||||
resource_id = self.create("resource-to-set-slug")
|
||||
other_resource_id = self.create("other-resource-to-set-slug")
|
||||
|
||||
response = self._set_slug(resource_id, "new-slug")
|
||||
self._assert_status_code_is_ok(response)
|
||||
|
||||
# Slugs must be unique for the same user/resource
|
||||
response = self._set_slug(other_resource_id, "new-slug")
|
||||
self._assert_status_code_is(response, 409)
|
||||
|
||||
# Other users cannot change the slug if they don't own the resource
|
||||
with self._different_user():
|
||||
response = self._set_slug(resource_id, "another-slug")
|
||||
self._assert_status_code_is(response, 403)
|
||||
|
||||
def _get_resource_sharing_status(self, resource_id: str):
|
||||
sharing_response = self._get(f"{self.api_name}/{resource_id}/sharing")
|
||||
self._assert_status_code_is(sharing_response, 200)
|
||||
return sharing_response.json()
|
||||
|
||||
def _set_resource_sharing(self, resource_id: str, payload: Any, expect_response_status: int = 200):
|
||||
sharing_response = self._post(f"{self.api_name}/{resource_id}/sharing", data=payload, json=True)
|
||||
def _set_resource_sharing(self, resource_id: str, action: str, payload: Any = None, expect_response_status: int = 200):
|
||||
sharing_response = self._put(f"{self.api_name}/{resource_id}/{action}", data=payload, json=True)
|
||||
self._assert_status_code_is(sharing_response, expect_response_status)
|
||||
return sharing_response.json()
|
||||
|
||||
def _set_slug(self, resource_id: str, new_slug: str):
|
||||
payload = {"new_slug": new_slug}
|
||||
response = self._put(f"{self.api_name}/{resource_id}/slug", data=payload, json=True)
|
||||
return response
|
||||
|
||||
@@ -32,13 +32,7 @@ class BaseHistories:
|
||||
return create_response
|
||||
|
||||
|
||||
class HistoriesApiTestCase(ApiTestCase, BaseHistories, SharingApiTests):
|
||||
|
||||
api_name = "histories"
|
||||
|
||||
def create(self, name: str) -> str:
|
||||
response_json = self._create_history(name)
|
||||
return response_json["id"]
|
||||
class HistoriesApiTestCase(ApiTestCase, BaseHistories):
|
||||
|
||||
def test_create_history(self):
|
||||
# Create a history.
|
||||
@@ -425,3 +419,134 @@ class ImportExportHistoryTestCase(ApiTestCase, BaseHistories):
|
||||
|
||||
if elements_checker is not None:
|
||||
elements_checker(imported_collection_metadata["elements"])
|
||||
|
||||
|
||||
class SharingHistoryTestCase(ApiTestCase, BaseHistories, SharingApiTests):
|
||||
"""Tests specific for the particularities of sharing Histories."""
|
||||
|
||||
api_name = "histories"
|
||||
|
||||
def create(self, name: str) -> str:
|
||||
response_json = self._create_history(name)
|
||||
history_id = response_json["id"]
|
||||
# History to share cannot be empty
|
||||
populator = DatasetPopulator(self.galaxy_interactor)
|
||||
populator.new_dataset(history_id)
|
||||
return history_id
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
|
||||
|
||||
def test_sharing_with_private_datasets(self):
|
||||
history_id = self.dataset_populator.new_history()
|
||||
hda = self.dataset_populator.new_dataset(history_id)
|
||||
hda_id = hda["id"]
|
||||
|
||||
self.dataset_populator.make_private(history_id, hda_id)
|
||||
|
||||
with self._different_user():
|
||||
target_user_id = self.dataset_populator.user_id()
|
||||
|
||||
payload = {"user_ids": [target_user_id]}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
|
||||
# If no share_option is provided, the extra field will contain the
|
||||
# datasets that need to be accessible before sharing
|
||||
assert sharing_response["extra"]
|
||||
assert sharing_response["extra"]["can_share"] is False
|
||||
assert sharing_response["extra"]["can_change"][0]["id"] == hda_id
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
# Now we provide the share_option
|
||||
payload = {
|
||||
"user_ids": [target_user_id],
|
||||
"share_option": "make_accessible_to_shared"
|
||||
}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
assert sharing_response["users_shared_with"]
|
||||
assert sharing_response["users_shared_with"][0]["id"] == target_user_id
|
||||
|
||||
def test_sharing_without_manage_permissions(self):
|
||||
history_id = self.dataset_populator.new_history()
|
||||
hda = self.dataset_populator.new_dataset(history_id)
|
||||
hda_id = hda["id"]
|
||||
owner_role_id = self.dataset_populator.user_private_role_id()
|
||||
|
||||
with self._different_user():
|
||||
target_user_id = self.dataset_populator.user_id()
|
||||
|
||||
with self._different_user("alice@test.com"):
|
||||
alice_role_id = self.dataset_populator.user_private_role_id()
|
||||
|
||||
# We have one dataset that we cannot manage
|
||||
payload = {"access": [owner_role_id], "manage": [alice_role_id]}
|
||||
update_response = self._update_permissions(history_id, hda_id, payload)
|
||||
self._assert_status_code_is(update_response, 200)
|
||||
|
||||
# We will get an error if none of the datasets can be made accessible
|
||||
payload = {"user_ids": [target_user_id]}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
assert sharing_response["extra"]
|
||||
assert sharing_response["extra"]["can_share"] is False
|
||||
assert sharing_response["extra"]["cannot_change"][0]["id"] == hda_id
|
||||
assert sharing_response["errors"]
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
# Trying to change the permissions when sharing should fail
|
||||
# because we don't have manage permissions
|
||||
payload = {
|
||||
"user_ids": [target_user_id],
|
||||
"share_option": "make_public"
|
||||
}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
assert sharing_response["extra"]
|
||||
assert sharing_response["extra"]["can_share"] is False
|
||||
assert sharing_response["errors"]
|
||||
assert not sharing_response["users_shared_with"]
|
||||
|
||||
# we can share if we don't try to make any permission changes
|
||||
payload = {
|
||||
"user_ids": [target_user_id],
|
||||
"share_option": "no_changes"
|
||||
}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
assert not sharing_response["errors"]
|
||||
assert sharing_response["users_shared_with"]
|
||||
assert sharing_response["users_shared_with"][0]["id"] == target_user_id
|
||||
|
||||
def test_sharing_empty_not_allowed(self):
|
||||
history_id = self.dataset_populator.new_history()
|
||||
|
||||
with self._different_user():
|
||||
target_user_id = self.dataset_populator.user_id()
|
||||
|
||||
payload = {"user_ids": [target_user_id]}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
assert sharing_response["extra"]["can_share"] is False
|
||||
assert sharing_response["errors"]
|
||||
assert "empty" in sharing_response["errors"][0]
|
||||
|
||||
def test_sharing_with_duplicated_users(self):
|
||||
history_id = self.create("HistoryToShareWithDuplicatedUser")
|
||||
|
||||
with self._different_user():
|
||||
target_user_id = self.dataset_populator.user_id()
|
||||
|
||||
# Ignore repeated users in the same request
|
||||
payload = {"user_ids": [target_user_id, target_user_id]}
|
||||
sharing_response = self._share_history_with_payload(history_id, payload)
|
||||
assert sharing_response["users_shared_with"]
|
||||
assert len(sharing_response["users_shared_with"]) == 1
|
||||
assert sharing_response["users_shared_with"][0]["id"] == target_user_id
|
||||
|
||||
def _share_history_with_payload(self, history_id, payload):
|
||||
sharing_response = self._put(f"histories/{history_id}/share_with_users", data=payload, json=True)
|
||||
self._assert_status_code_is(sharing_response, 200)
|
||||
return sharing_response.json()
|
||||
|
||||
def _update_permissions(self, history_id: str, dataset_id: str, payload):
|
||||
url = f"histories/{history_id}/contents/{dataset_id}/permissions"
|
||||
update_url = self._api_url(url, **{"use_admin_key": True})
|
||||
update_response = put(update_url, json=payload)
|
||||
return update_response
|
||||
|
||||
@@ -28,9 +28,11 @@ class HistorySharingTestCase(SeleniumTestCase):
|
||||
user1_email, user2_email, history_id = self.setup_two_users_with_one_shared_history()
|
||||
self.submit_login(user1_email, retries=VALID_LOGIN_RETRIES)
|
||||
self.navigate_to_history_share_page()
|
||||
self.components.histories.sharing.share_with_collapse.wait_for_and_click()
|
||||
|
||||
unshare_user_button = self.components.histories.sharing.unshare_user_button
|
||||
unshare_user_button.wait_for_and_click()
|
||||
self.components.histories.sharing.submit_sharing_with.wait_for_and_click()
|
||||
|
||||
self.navigate_to_history_share_page()
|
||||
unshare_user_button.assert_absent()
|
||||
@@ -66,7 +68,7 @@ class HistorySharingTestCase(SeleniumTestCase):
|
||||
user1_email = self._get_random_email()
|
||||
self.register(user1_email)
|
||||
self.share_history_with_user(user_email=user1_email)
|
||||
self.assert_error_message(contains='You cannot send histories to yourself')
|
||||
self.assert_error_message(contains='You cannot share resources with yourself')
|
||||
|
||||
def setup_two_users_with_one_shared_history(self, share_by_id=False):
|
||||
user1_email = self._get_random_email()
|
||||
@@ -84,6 +86,7 @@ class HistorySharingTestCase(SeleniumTestCase):
|
||||
self.wait_for_history()
|
||||
|
||||
history_id = self.current_history_id()
|
||||
|
||||
if share_by_id:
|
||||
self.share_history_with_user(user_email=user2_email, assert_valid=True)
|
||||
else:
|
||||
@@ -96,10 +99,6 @@ class HistorySharingTestCase(SeleniumTestCase):
|
||||
self.home()
|
||||
self.click_history_option("Share or Publish")
|
||||
|
||||
def navigate_to_history_user_share_page(self):
|
||||
self.navigate_to_history_share_page()
|
||||
self.components.histories.sharing.share_with_a_user_button.wait_for_and_click()
|
||||
|
||||
def share_history_with_user(self, user_id=None, user_email=None, assert_valid=False, screenshot=False):
|
||||
"""Share the current history with a target user by ID or email.
|
||||
|
||||
@@ -107,18 +106,20 @@ class HistorySharingTestCase(SeleniumTestCase):
|
||||
is also specified. The ``user_email`` however is always used to check
|
||||
the result if ``assert_valid`` is True.
|
||||
"""
|
||||
self.navigate_to_history_user_share_page()
|
||||
form_selector = "form#share"
|
||||
form = self.wait_for_selector(form_selector)
|
||||
# If expose_user_info is on would fill form out with this
|
||||
# line, in future dispatch on actual select2 div present or not.
|
||||
# self.select2_set_value(form_selector, email)
|
||||
self.fill(form, {"email": user_id or user_email})
|
||||
self.navigate_to_history_share_page()
|
||||
self.components.histories.sharing.share_with_collapse.wait_for_and_click()
|
||||
self.components.histories.sharing.share_with_multiselect.wait_for_and_click()
|
||||
self.components.histories.sharing.share_with_input.wait_for_and_send_keys(user_id or user_email)
|
||||
|
||||
if screenshot:
|
||||
self.screenshot("history_sharing_user")
|
||||
self.click_submit(form)
|
||||
# first click to add the item
|
||||
self.components.histories.sharing.submit_sharing_with.wait_for_and_click()
|
||||
# second click to save the sharing preferences
|
||||
self.components.histories.sharing.submit_sharing_with.wait_for_and_click()
|
||||
|
||||
if assert_valid:
|
||||
self.assert_no_error_message()
|
||||
|
||||
xpath = f'//td[contains(text(), "{user_email}")]'
|
||||
xpath = f'//span[contains(text(), "{user_email}")]'
|
||||
self.wait_for_xpath_visible(xpath)
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
<% _=n_ %>
|
||||
<%inherit file="/base.mako"/>
|
||||
<%def name="title()">Share histories</%def>
|
||||
|
||||
<div class="toolForm">
|
||||
<div class="toolFormTitle">Share ${len( histories)} histories</div>
|
||||
<div class="toolFormBody">
|
||||
%if not can_change and not cannot_change and not no_change_needed:
|
||||
## We are sharing histories that contain only public datasets
|
||||
<form name='share' id='share' action="${h.url_for( controller='history', action='share' )}" method="post" >
|
||||
<div class="form-title-row"><b>Histories to be shared:</b></div>
|
||||
<div class="form-row" style="padding-left: 2em;">
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<th>${_('History Name')}</th>
|
||||
<th>${_('Number of Datasets')}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for history in histories:
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="id" value="${trans.security.encode_id( history.id )}">
|
||||
${ util.unicodify( history.name ) | h }
|
||||
</td>
|
||||
<td>
|
||||
%if len( history.datasets ) < 1:
|
||||
<div class="warningmark">${_('This history contains no data.')}</div>
|
||||
%else:
|
||||
${len(history.datasets)}
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<% existing_emails = ','.join([ d.user.email for d in history.users_shared_with ]) %>
|
||||
<label>Galaxy user emails with which to share histories</label>
|
||||
%if trans.app.config.expose_user_email or trans.app.config.expose_user_name or trans.user_is_admin:
|
||||
<input type="hidden" id="email_select" name="email" value="${ existing_emails }" style="float: left; width: 250px; margin-right: 10px;">
|
||||
</input>
|
||||
%else:
|
||||
<input type="text" name="email" value="${ existing_emails }" size="40">
|
||||
</input>
|
||||
%endif
|
||||
<div class="toolParamHelp" style="clear: both;">
|
||||
Enter a Galaxy user email address or a comma-separated list of addresses if sharing with multiple users
|
||||
</div>
|
||||
</div>
|
||||
%if send_to_err:
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<div class="alert alert-danger">${send_to_err}</div>
|
||||
</div>
|
||||
%endif
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<input type="submit" name="share_button" value="Submit">
|
||||
</div>
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
// stolen from templates/admin/impersonate.mako
|
||||
/* This should be ripped out and made generic at some point for the
|
||||
* various API bindings available, and once the API can filter list
|
||||
* queries (term, below) */
|
||||
|
||||
var user_id = "${trans.security.encode_id(trans.user.id)}";
|
||||
var history_id = "${trans.security.encode_id( history.id )}";
|
||||
|
||||
function item_to_label(item){
|
||||
var text = "";
|
||||
if(typeof(item.username) === "string" && typeof(item.email) === "string"){
|
||||
text = item.username + " <" + item.email + ">";
|
||||
}else if(typeof(item.username) === "string"){
|
||||
text = item.username;
|
||||
}else{
|
||||
text = item.email;
|
||||
}
|
||||
return text;
|
||||
//return "id:" + item.id + "|e:" + item.email + "|u:" + item.username;
|
||||
}
|
||||
|
||||
$("#email_select").select2({
|
||||
placeholder: "Select a user",
|
||||
multiple: true,
|
||||
initSelection: function(element, callback) {
|
||||
var data = [
|
||||
// Must be here to loop across the users that this has been shared with.
|
||||
%for i, association in enumerate( history.users_shared_with ):
|
||||
<% shared_with = association.user %>
|
||||
{
|
||||
email: "${ shared_with.email }",
|
||||
id: "${trans.security.encode_id(shared_with.id)}",
|
||||
text: item_to_label({"email": "${ shared_with.email }", "username": "${ shared_with.username }" })
|
||||
},
|
||||
%endfor
|
||||
];
|
||||
callback(data);
|
||||
},
|
||||
tokenSeparators: [',', ' '],
|
||||
// Required for initSelection
|
||||
id: function(object) {
|
||||
return object.id;
|
||||
},
|
||||
ajax: {
|
||||
url: "${h.url_for(controller="/api/users", action="index")}",
|
||||
data: function (term) {
|
||||
return {
|
||||
f_any: term,
|
||||
};
|
||||
},
|
||||
dataType: 'json',
|
||||
quietMillis: 250,
|
||||
results: function (data) {
|
||||
var results = [];
|
||||
// For every user returned by the API call,
|
||||
$.each(data, function(index, item){
|
||||
// If they aren't the requesting user, add to the
|
||||
// list that will populate the select
|
||||
if(item.id != "${trans.security.encode_id(trans.user.id)}"){
|
||||
if(item.email !== undefined){
|
||||
results.push({
|
||||
id: item.id,
|
||||
name: item.username,
|
||||
text: item_to_label(item),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
results: results
|
||||
};
|
||||
}
|
||||
},
|
||||
createSearchChoice: function(term, data) {
|
||||
// Check for a user with a matching email.
|
||||
var matches = _.filter(data, function(user){
|
||||
return user.text.indexOf(term) > -1;
|
||||
});
|
||||
// If there aren't any users with matching object labels, then
|
||||
// display a "default" entry with whatever text they're entering.
|
||||
// id is set to term as that will be used in
|
||||
if(matches.length == 0){
|
||||
return {id: term, text:term};
|
||||
}else{
|
||||
// No extra needed
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
%else:
|
||||
## We are sharing restricted histories
|
||||
%if no_change_needed or can_change:
|
||||
<form name='share_restricted' id=share_restricted' action="${h.url_for( controller='history', action='share_restricted' )}" method="post">
|
||||
%if send_to_err:
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<div class="alert alert-danger">${send_to_err}</div>
|
||||
</div>
|
||||
%endif
|
||||
## Needed for rebuilding dicts
|
||||
<input type="hidden" name="email" value="${email}" size="40">
|
||||
%for history in histories:
|
||||
<input type="hidden" name="id" value="${trans.security.encode_id( history.id )}">
|
||||
%endfor
|
||||
%if no_change_needed:
|
||||
## no_change_needed looks like: {historyX : [hda, hda], historyY : [hda] }
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<div class="donemessage">
|
||||
The following datasets can be shared with ${email} with no changes
|
||||
</div>
|
||||
</div>
|
||||
%for history, hdas in no_change_needed.items():
|
||||
<div class="form-row">
|
||||
<label>History</label>
|
||||
${util.unicodify( history.name )}
|
||||
</div>
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<label>Datasets</label>
|
||||
</div>
|
||||
%for hda in hdas:
|
||||
<div class="form-row">
|
||||
${util.unicodify( hda.name )}
|
||||
%if hda.deleted:
|
||||
(deleted)
|
||||
%endif
|
||||
</div>
|
||||
%endfor
|
||||
%endfor
|
||||
%endif
|
||||
%if can_change:
|
||||
## can_change looks like: {historyX : [hda, hda], historyY : [hda] }
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<div class="warningmessage">
|
||||
The following datasets can be shared with ${email} by updating their permissions
|
||||
</div>
|
||||
</div>
|
||||
%for history, hdas in can_change.items():
|
||||
<div class="form-row">
|
||||
<label>History</label>
|
||||
${util.unicodify( history.name )}
|
||||
</div>
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<label>Datasets</label>
|
||||
</div>
|
||||
%for hda in hdas:
|
||||
<div class="form-row">
|
||||
${util.unicodify( hda.name )}
|
||||
%if hda.deleted:
|
||||
(deleted)
|
||||
%endif
|
||||
</div>
|
||||
%endfor
|
||||
%endfor
|
||||
%endif
|
||||
%if cannot_change:
|
||||
## cannot_change looks like: {historyX : [hda, hda], historyY : [hda] }
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<div class="alert alert-danger">
|
||||
The following datasets cannot be shared with ${email} because you are not authorized to
|
||||
change the permissions on them
|
||||
</div>
|
||||
</div>
|
||||
%for history, hdas in cannot_change.items():
|
||||
<div class="form-row">
|
||||
<label>History</label>
|
||||
${util.unicodify( history.name )}
|
||||
</div>
|
||||
<div style="clear: both"></div>
|
||||
<div class="form-row">
|
||||
<label>Datasets</label>
|
||||
</div>
|
||||
%for hda in hdas:
|
||||
<div class="form-row">
|
||||
${util.unicodify( hda.name )}
|
||||
%if hda.deleted:
|
||||
(deleted)
|
||||
%endif
|
||||
</div>
|
||||
%endfor
|
||||
%endfor
|
||||
%endif
|
||||
<div class="toolFormTitle"></div>
|
||||
<div class="form-row">
|
||||
<label>How would you like to proceed?</label>
|
||||
</div>
|
||||
%if can_change:
|
||||
<div class="form-row">
|
||||
<input type="radio" name="action" value="public"> Make datasets public so anyone can access them
|
||||
%if cannot_change:
|
||||
(where possible)
|
||||
%endif
|
||||
</div>
|
||||
<div class="form-row">
|
||||
%if no_change_needed:
|
||||
<input type="radio" name="action" value="private"> Make datasets private to me and the user(s) with whom I am sharing
|
||||
%else:
|
||||
<input type="radio" name="action" value="private" checked> Make datasets private to me and the user(s) with whom I am sharing
|
||||
%endif
|
||||
%if cannot_change:
|
||||
(where possible)
|
||||
%endif
|
||||
</div>
|
||||
%endif
|
||||
%if no_change_needed:
|
||||
<div class="form-row">
|
||||
<input type="radio" name="action" value="share_anyway" checked> Share anyway
|
||||
%if can_change:
|
||||
(don't change any permissions)
|
||||
%endif
|
||||
</div>
|
||||
%endif
|
||||
<div class="form-row">
|
||||
<input type="submit" name="share_restricted_button" value="Go"><br/>
|
||||
</div>
|
||||
</form>
|
||||
%endif
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user