Merge pull request #11799 from assuntad23/feature/6965/change-datatype-and-dbkey

Feature/6965/change datatype and dbkey
This commit is contained in:
Marius van den Beek
2021-09-16 10:40:04 +02:00
committed by GitHub
23 changed files with 970 additions and 67 deletions
@@ -0,0 +1,124 @@
<template>
<div>
<h4>{{ l("Edit Collection Attributes") }}</h4>
<b-alert show variant="info" dismissible>
{{ l("This will create a new collection in your History. Your quota usage will not increase.") }}
</b-alert>
<div v-if="jobError">
<b-alert show variant="danger" dismissible>
{{ errorMessage }}
</b-alert>
</div>
<b-tabs content-class="mt-3">
<b-tab>
<template v-slot:title> <font-awesome-icon icon="table" /> &nbsp; {{ l("Database/Build") }}</template>
<GenomeProvider v-slot="{ item, loading }">
<div v-if="loading"><b-spinner label="Loading Genomes..."></b-spinner></div>
<div v-else>
<database-edit-tab
v-if="item && databaseKeyFromElements"
:database-key-from-elements="databaseKeyFromElements"
:genomes="item"
@clicked-save="clickedSave"
/>
</div>
</GenomeProvider>
</b-tab>
<SuitableConvertersProvider :id="collection_id" v-slot="{ item }">
<b-tab v-if="item && item.length">
<template v-slot:title> <font-awesome-icon icon="cog" /> &nbsp; {{ l("Convert") }}</template>
<suitable-converters-tab :suitable-converters="item" @clicked-convert="clickedConvert" />
</b-tab>
</SuitableConvertersProvider>
</b-tabs>
</div>
</template>
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import axios from "axios";
import { prependPath } from "utils/redirect";
import _l from "utils/localization";
import { errorMessageAsString } from "utils/simple-error";
import DatabaseEditTab from "./DatabaseEditTab";
import SuitableConvertersTab from "./SuitableConvertersTab";
import { GenomeProvider, SuitableConvertersProvider } from "../../providers";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faDatabase, faTable, faBars, faUser, faCog } from "@fortawesome/free-solid-svg-icons";
library.add(faDatabase, faTable, faBars, faUser, faCog);
Vue.use(BootstrapVue);
export default {
created() {
this.getCollectionDataAndAttributes();
},
components: {
DatabaseEditTab,
SuitableConvertersTab,
FontAwesomeIcon,
GenomeProvider,
SuitableConvertersProvider,
},
data: function () {
return {
attributes_data: {},
errorMessage: null,
jobError: null,
};
},
props: {
collection_id: {
type: String,
required: true,
},
},
computed: {
databaseKeyFromElements: function () {
return this.attributes_data.dbkey;
},
},
methods: {
l(str) {
// _l conflicts private methods of Vue internals, expose as l instead
return _l(str);
},
getCollectionDataAndAttributes: async function () {
let attributesGet = this.$store.getters.getCollectionAttributes(this.collection_id);
if (attributesGet == null) {
await this.$store.dispatch("fetchCollectionAttributes", this.collection_id);
attributesGet = this.$store.getters.getCollectionAttributes(this.collection_id);
}
this.attributes_data = attributesGet;
},
clickedSave: function (attribute, newValue) {
const url = prependPath("/api/dataset_collections/" + this.collection_id);
const data = {};
if (attribute == "dbkey") {
data["dbkey"] = newValue.id;
} else if (attribute == "file_ext") {
data["file_ext"] = newValue.id;
}
axios.put(url, data).catch(this.handleError);
},
clickedConvert: function (selectedConverter) {
const url = prependPath(`/api/tools/${selectedConverter.tool_id}/convert`);
const data = {
src: "hdca",
id: this.collection_id,
source_type: selectedConverter.original_type,
target_type: selectedConverter.target_type,
};
axios.post(url, data).catch(this.handleError);
},
handleError: function (err) {
this.errorMessage = errorMessageAsString(err, "History import failed.");
if (err?.data?.stderr) {
this.jobError = err.data;
}
},
},
};
</script>
@@ -0,0 +1,59 @@
<template>
<div>
<div class="alert alert-secondary" role="alert">
<div class="float-left">Change Database/Build of all elements in collection</div>
<div class="text-right">
<button
class="save-collection-edit btn btn-primary"
@click="clickedSave"
:disabled="selectedGenome.id == databaseKeyFromElements"
>
{{ l("Save") }}
</button>
</div>
</div>
<b>{{ l("Database/Build") }}: </b>
<multiselect
v-model="selectedGenome"
deselect-label="Can't remove this value"
track-by="id"
label="text"
:options="genomes"
:searchable="true"
:allow-empty="false"
>
{{ selectedGenome.text }}
</multiselect>
</div>
</template>
<script>
import Multiselect from "vue-multiselect";
export default {
created() {
this.selectedGenome = this.genomes.find((element) => element.id == this.databaseKeyFromElements);
},
components: { Multiselect },
data: function () {
return {
selectedGenome: {},
};
},
props: {
genomes: {
type: Array,
required: true,
},
databaseKeyFromElements: {
type: String,
required: true,
},
},
methods: {
clickedSave: function () {
this.$emit("clicked-save", "dbkey", this.selectedGenome);
this.selectedGenome = this.genomes.find((element) => element.id == this.databaseKeyFromElements);
},
},
};
</script>
@@ -0,0 +1,51 @@
<template>
<div>
<div class="alert alert-secondary" role="alert">
<div class="float-left">Convert all datasets to new format</div>
<div class="text-right">
<button
class="run-tool-collection-edit btn btn-primary"
@click="clickedConvert"
:disabled="selectedConverter == {}"
>
{{ l("Convert Collection") }}
</button>
</div>
</div>
<b>{{ l("Converter Tool: ") }}</b>
<multiselect
v-model="selectedConverter"
deselect-label="Can't remove this value"
track-by="name"
label="name"
:options="suitableConverters"
:searchable="true"
:allow-empty="true"
>
</multiselect>
</div>
</template>
<script>
import Multiselect from "vue-multiselect";
export default {
components: { Multiselect },
data: function () {
return {
selectedConverter: {},
};
},
props: {
suitableConverters: {
type: Array,
required: true,
},
},
methods: {
clickedConvert: function () {
this.$emit("clicked-convert", this.selectedConverter);
this.selectedConverter = {};
},
},
};
</script>
@@ -1,45 +1,87 @@
<template>
<b-button-group>
<IconButton
icon="info-circle"
v-if="hasJob"
key="dataset-details"
title="View Dataset Collection Details"
@click.stop.prevent="backboneRoute(collectionDetailsPath)"
variant="link"
class="px-1"
>
</IconButton>
<b-dropdown right no-caret variant="link" size="sm" boundary="window" toggle-class="p-0">
<template v-slot:button-content>
<IconButton icon="trash" title="Delete Colletion" variant="link" class="px-1" />
</template>
<b-dropdown-item @click.stop="$emit('delete')">
<span v-localize>Delete Collection Only </span>
</b-dropdown-item>
<b-dropdown-item @click.stop="$emit('delete', { recursive: true })">
<span v-localize>Delete Contained Datasets</span>
</b-dropdown-item>
<b-dropdown-item @click.stop="$emit('delete', { recursive: true, purge: true })">
<span v-localize>Purge Contained Datasets</span>
</b-dropdown-item>
</b-dropdown>
</b-button-group>
<div class="collection-menu">
<b-button-group>
<IconButton
icon="info-circle"
v-if="hasJob"
key="dataset-details"
title="View Dataset Collection Details"
@click.stop.prevent="backboneRoute(collectionDetailsPath)"
variant="link"
class="px-1"
>
</IconButton>
<IconButton
v-if="notIn(STATES.DISCARDED)"
icon="pen"
:title="editButtonTitle"
:disabled="dsc.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
@click.stop="backboneRoute('collection/edit/' + dsc.hdca_id)"
variant="link"
class="px-1 dsc-edit-view"
/>
<b-dropdown v-if="notIn(STATES.DISCARDED)" no-caret right variant="link" size="sm" boundary="window">
<template v-slot:button-content>
<Icon icon="trash" variant="link" />
<span class="sr-only">Delete Collection</span>
</template>
<b-dropdown-item @click.stop="$emit('delete')">
<span v-localize>Delete Collection Only </span>
</b-dropdown-item>
<b-dropdown-item @click.stop="$emit('delete', { recursive: true })">
<span v-localize>Delete Contained Datasets</span>
</b-dropdown-item>
<b-dropdown-item @click.stop="$emit('delete', { recursive: true, purge: true })">
<span v-localize>Purge Contained Datasets</span>
</b-dropdown-item>
</b-dropdown>
</b-button-group>
</div>
</template>
<script>
import { DatasetCollection } from "../../model/DatasetCollection";
import { DatasetCollection } from "../../model";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import IconButton from "components/IconButton";
Vue.use(BootstrapVue);
export default {
mixins: [legacyNavigationMixin],
data() {
return {
deleteSelected: null,
deleteCollectionModalShow: false,
};
},
components: {
IconButton,
},
mixins: [legacyNavigationMixin],
inject: ["STATES"],
props: {
dsc: { type: DatasetCollection, required: true },
},
computed: {
editButtonTitle() {
if (this.dsc.deleted) {
return "Collection must not be deleted to edit";
}
if (this.dsc.purged) {
return "Cannot edit attributes of collections removed from disk";
}
const unreadyStates = new Set([this.STATES.UPLOAD, this.STATES.NEW]);
if (unreadyStates.has(this.dsc.state)) {
return "This collection is not yet editable";
}
return "Edit attributes";
},
deleteCollectionButtonTitle() {
return "Delete Collection";
},
hasJob() {
return this.dsc?.job_source_type == "Job";
},
@@ -47,5 +89,22 @@ export default {
return `jobs/${this.dsc.job_source_id}/view`;
},
},
methods: {
runDelete: function (kind) {
if (kind) {
this.$emit("delete", kind);
} else {
this.$emit("delete");
}
},
notIn(...states) {
const badStates = new Set(states);
return !badStates.has(this.dsc.state);
},
isIn(...states) {
const goodStates = new Set(states);
return goodStates.has(this.dsc.state);
},
},
};
</script>
+2
View File
@@ -2,6 +2,8 @@
export {
// DatasetProvider,
DatasetCollectionContentProvider,
GenomeProvider,
SuitableConvertersProvider,
JobProvider,
} from "./storeProviders";
@@ -80,6 +80,38 @@ export const DatasetCollectionProvider = {
},
};
export const GenomeProvider = {
mixins: [SimpleProviderMixin],
async mounted() {
await this.load();
},
methods: {
...mapCacheActions(["fetchUploadGenomes"]),
async load() {
this.loading = true;
let genomes = this.getUploadGenomes();
if (genomes == null || genomes.length == 0) {
await this.fetchUploadGenomes();
genomes = this.getUploadGenomes();
}
this.item = genomes;
this.loading = false;
},
},
computed: {
...mapGetters(["getUploadGenomes"]),
},
};
export const SuitableConvertersProvider = {
mixins: [SimpleProviderMixin],
computed: {
url() {
return prependPath(`/api/dataset_collections/${this.id}/suitable_converters`);
},
},
};
export const DatasetCollectionContentProvider = {
mixins: [SimpleProviderMixin],
computed: {
@@ -31,6 +31,7 @@ import TrsImport from "components/Workflow/TrsImport.vue";
import TrsSearch from "components/Workflow/TrsSearch.vue";
import InteractiveTools from "components/InteractiveTools/InteractiveTools.vue";
import WorkflowList from "components/Workflow/WorkflowList.vue";
import CollectionEditView from "components/Collections/common/CollectionEditView.vue";
import HistoryImport from "components/HistoryImport.vue";
import { HistoryExport } from "components/HistoryExport/index";
import HistoryView from "components/HistoryView.vue";
@@ -98,6 +99,7 @@ export const getAnalysisRouter = (Galaxy) => {
"(/)jobs(/)(:job_id)(/)view": "show_job",
"(/)custom_builds": "show_custom_builds",
"(/)datasets/edit": "show_dataset_edit_attributes",
"(/)collection(/)edit(/)(:collection_id)": "show_collection_edit_attributes",
"(/)datasets/error": "show_dataset_error",
"(/)datasets(/)(:dataset_id)/details": "show_dataset_details",
"(/)interactivetool_entry_points(/)list": "show_interactivetool_list",
@@ -388,6 +390,10 @@ export const getAnalysisRouter = (Galaxy) => {
}
},
show_collection_edit_attributes: function (collection_id) {
this._display_vue_helper(CollectionEditView, { collection_id: collection_id });
},
show_dataset_error: function (params) {
const datasetId = params.dataset_id;
this._display_vue_helper(DatasetError, { datasetId: datasetId });
@@ -0,0 +1,32 @@
export const state = {
collectionAttributes: {},
};
import Vue from "vue";
import { prependPath } from "utils/redirect";
import axios from "axios";
const getters = {
getCollectionAttributes: (state) => (collectionId) => {
return state.collectionAttributes[collectionId] || null;
},
};
const actions = {
fetchCollectionAttributes: async ({ commit }, collectionId) => {
const { data } = await axios.get(prependPath("api/dataset_collections/" + collectionId + "/attributes"));
commit("saveCollectionAttributes", { collectionId, collectionAttributes: data });
},
};
const mutations = {
saveCollectionAttributes: (state, { collectionId, collectionAttributes }) => {
Vue.set(state.collectionAttributes, collectionId, collectionAttributes);
},
};
export const collectionAttributesStore = {
state,
getters,
actions,
mutations,
};
+34
View File
@@ -0,0 +1,34 @@
export const state = {
uploadDatatypes: [],
};
import UploadUtils from "mvc/upload/upload-utils";
const getters = {
getUploadDatatypes: (state) => () => {
return state.uploadDatatypes;
},
};
const actions = {
fetchUploadDatatypes: async ({ commit }) => {
try {
const data = await UploadUtils.getUploadDatatypes(false, UploadUtils.AUTO_EXTENSION);
commit("saveUploadDatatypes", { datatypes: data });
} catch (err) {
console.log("Error: unable to load datatypes", err);
}
},
};
const mutations = {
saveUploadDatatypes: (state, { datatypes }) => {
state.uploadDatatypes = datatypes;
},
};
export const datatypeStore = {
state,
getters,
actions,
mutations,
};
+34
View File
@@ -0,0 +1,34 @@
export const state = {
uploadGenomes: [],
};
import UploadUtils from "mvc/upload/upload-utils";
const getters = {
getUploadGenomes: (state) => () => {
return state.uploadGenomes;
},
};
const actions = {
fetchUploadGenomes: async ({ commit }) => {
try {
const data = await UploadUtils.getUploadGenomes(UploadUtils.DEFAULT_GENOME);
commit("saveUploadGenomes", { genomes: data });
} catch (err) {
console.log("Error: unable to load genomes", err);
}
},
};
const mutations = {
saveUploadGenomes: (state, { genomes }) => {
state.uploadGenomes = genomes;
},
};
export const genomeStore = {
state,
getters,
actions,
mutations,
};
+6
View File
@@ -22,6 +22,9 @@ import { toolStore } from "./toolStore";
import { datasetPathDestinationStore } from "./datasetPathDestinationStore";
import { datasetExtFilesStore } from "./datasetExtFilesStore";
import { jobStore } from "./jobStore";
import { collectionAttributesStore } from "./collectionAttributesStore";
import { genomeStore } from "./genomeStore";
import { datatypeStore } from "./datatypeStore";
import { panelStore } from "./panelStore";
// beta features
@@ -57,6 +60,9 @@ export function createStore() {
workflows: workflowStore,
informationStore: jobStore,
tools: toolStore,
collectionAttributesStore: collectionAttributesStore,
genomeStore: genomeStore,
datatypeStore: datatypeStore,
},
};
+22 -14
View File
@@ -76,6 +76,25 @@ def validate(dataset_instance):
return datatype_validation
def get_params_and_input_name(converter, deps, target_context=None):
# Generate parameter dictionary
params = {}
# determine input parameter name and add to params
input_name = 'input1'
for key, value in converter.inputs.items():
if deps and value.name in deps:
params[value.name] = deps[value.name]
elif value.type == 'data':
input_name = key
# add potentially required/common internal tool parameters e.g. '__job_resource'
if target_context:
for key, value in target_context.items():
if key.startswith('__'):
params[key] = value
return params, input_name
class DataMeta(abc.ABCMeta):
"""
Metaclass for Data class. Sets up metadata spec.
@@ -620,20 +639,9 @@ class Data(metaclass=DataMeta):
if converter is None:
raise Exception(f"A converter does not exist for {original_dataset.ext} to {target_type}.")
# Generate parameter dictionary
params = {}
# determine input parameter name and add to params
input_name = 'input1'
for key, value in converter.inputs.items():
if deps and value.name in deps:
params[value.name] = deps[value.name]
elif value.type == 'data':
input_name = key
# add potentially required/common internal tool parameters e.g. '__job_resource'
if target_context:
for key, value in target_context.items():
if key.startswith('__'):
params[key] = value
params, input_name = get_params_and_input_name(converter, deps, target_context)
params[input_name] = original_dataset
# Make the target datatype available to the converter
params['__target_datatype__'] = target_type
+33
View File
@@ -3,6 +3,7 @@ import logging
from sqlalchemy.orm import joinedload, Query
from galaxy import model
from galaxy.datatypes.registry import Registry
from galaxy.exceptions import (
ItemAccessibilityException,
MessageException,
@@ -218,6 +219,38 @@ class DatasetCollectionManager:
dataset_collection.collection_type = collection_type
return dataset_collection
def get_converters_for_collection(self, trans, id, datatypes_registry: Registry, instance_type="history"):
dataset_collection_instance = self.get_dataset_collection_instance(
trans,
id=id,
instance_type=instance_type,
check_ownership=True
)
dbkeys_and_extensions = dataset_collection_instance.dataset_dbkeys_and_extensions_summary
suitable_converters = set()
first_extension = True
most_recent_datatype = None
# TODO error checking
for datatype in dbkeys_and_extensions[1]:
new_converters = datatypes_registry.get_converters_by_datatype(datatype)
set_of_new_converters = set()
for tgt_type, tgt_val in new_converters.items():
converter = (tgt_type, tgt_val)
set_of_new_converters.add(converter)
if (first_extension is True):
suitable_converters = set_of_new_converters
most_recent_datatype = datatype
first_extension = False
else:
suitable_converters = suitable_converters.intersection(set_of_new_converters)
if suitable_converters:
most_recent_datatype = datatype
suitable_tool_ids = list()
for tool in suitable_converters:
tool_info = {"tool_id": tool[1].id, "name": tool[1].name, "target_type": tool[0], "original_type": most_recent_datatype}
suitable_tool_ids.append(tool_info)
return suitable_tool_ids
def _element_identifiers_to_elements(self,
trans,
collection_type_description,
+9 -1
View File
@@ -5,6 +5,7 @@ HistoryDatasetCollectionAssociations (HDCAs) are datasets contained or created i
history.
"""
import logging
from typing import Dict
from galaxy import model
from galaxy.managers import (
@@ -39,6 +40,11 @@ def stream_dataset_collection(dataset_collection_instance, upstream_mod_zip=Fals
return archive
def set_collection_attributes(dataset_element, *payload):
for attribute, value in payload:
setattr(dataset_element, attribute[1], value[1])
# TODO: to DatasetCollectionInstanceManager
class HDCAManager(
base.ModelManager,
@@ -77,7 +83,9 @@ class HDCAManager(
returned.append(processed)
return returned
# TODO: un-stub
def update_attributes(self, content, payload: Dict):
# pre-requisite checked that attributes are valid
self.map_datasets(content, fn=lambda item, *args: set_collection_attributes(item, payload.items()))
# serializers
+36 -10
View File
@@ -5530,6 +5530,7 @@ class HistoryDatasetCollectionAssociation(
== HistoryDatasetCollectionAssociation.id), # type: ignore
back_populates='hidden_beneath_collection_instance')
dict_dbkeysandextensions_visible_keys = ['dbkeys', 'extensions']
editable_keys = ('name', 'deleted', 'visible')
def __init__(self, deleted=False, visible=True, **kwd):
@@ -5566,6 +5567,24 @@ class HistoryDatasetCollectionAssociation(
else:
return None
@property
def dataset_dbkeys_and_extensions_summary(self):
if not hasattr(self, '_dataset_dbkeys_and_extensions_summary'):
rows = self.collection._get_nested_collection_attributes(hda_attributes=('_metadata', 'extension'))
extensions = set()
dbkeys = set()
for row in rows:
if row is not None:
dbkey_field = row._metadata.get('dbkey')
if isinstance(dbkey_field, list):
for dbkey in dbkey_field:
dbkeys.add(dbkey)
else:
dbkeys.add(dbkey_field)
extensions.add(row.extension)
self._dataset_dbkeys_and_extensions_summary = (dbkeys, extensions)
return self._dataset_dbkeys_and_extensions_summary
@property
def job_source_id(self):
return self.implicit_collection_jobs_id or self.job_id
@@ -5620,16 +5639,23 @@ class HistoryDatasetCollectionAssociation(
def to_dict(self, view='collection'):
original_dict_value = super().to_dict(view=view)
dict_value = dict(
hid=self.hid,
history_id=self.history.id,
history_content_type=self.history_content_type,
visible=self.visible,
deleted=self.deleted,
job_source_id=self.job_source_id,
job_source_type=self.job_source_type,
**self._base_to_dict(view=view)
)
if (view == 'dbkeysandextensions'):
(dbkeys, extensions) = self.dataset_dbkeys_and_extensions_summary
dict_value = dict(
dbkey=dbkeys.pop() if len(dbkeys) == 1 else "?",
extension=extensions.pop() if len(extensions) == 1 else "auto"
)
else:
dict_value = dict(
hid=self.hid,
history_id=self.history.id,
history_content_type=self.history_content_type,
visible=self.visible,
deleted=self.deleted,
job_source_id=self.job_source_id,
job_source_type=self.job_source_type,
**self._base_to_dict(view=view)
)
dict_value.update(original_dict_value)
+18
View File
@@ -269,6 +269,14 @@ history_panel:
options_use_beta_history:
type: xpath
selector: '//a[text()="Use Beta History Panel"]'
options_use_legacy_history:
type: xpath
selector: '//button[contains(span, "Return to legacy history panel")]'
collection_menu_button: '.collection-menu'
collection_menu_edit_attributes:
type: xpath
selector: '//button[@title="Edit attributes"]'
new_history_button: '.history-new-button'
multi_view_button: '.history-view-multi-button'
histories_operation_menu: '.histories-operation-menu'
@@ -306,6 +314,16 @@ edit_dataset_attributes:
type: xpath
selector: '//li[normalize-space() = "${dbkey_text}"]'
edit_collection_attributes:
selectors:
database_genome_tab:
type: xpath
selector: '//a[contains(text(), "Database/Build")]'
database_value:
type: xpath
selector: '//span[contains(text(), "${dbkey}")]'
save_btn: '.save-collection-edit'
tool_panel:
@@ -3,14 +3,16 @@ from logging import getLogger
import routes
from galaxy import exceptions
from galaxy.datatypes.registry import Registry
from galaxy.managers.base import decode_id
from galaxy.managers.collections import DatasetCollectionManager
from galaxy.managers.collections_util import (
api_payload_to_create_params,
dictify_dataset_collection_instance,
dictify_element_reference
dictify_element_reference,
)
from galaxy.managers.context import ProvidesHistoryContext
from galaxy.managers.hdcas import HDCAManager
from galaxy.managers.histories import HistoryManager
from galaxy.web import expose_api
from galaxy.webapps.base.controller import UsesLibraryMixinItems
@@ -24,6 +26,9 @@ class DatasetCollectionsController(
UsesLibraryMixinItems,
):
history_manager: HistoryManager = depends(HistoryManager)
hdca_manager: HDCAManager = depends(HDCAManager)
collection_manager: DatasetCollectionManager = depends(DatasetCollectionManager)
datatypes_registry: Registry = depends(Registry)
@expose_api
def index(self, trans, **kwd):
@@ -64,6 +69,48 @@ class DatasetCollectionsController(
return dictify_dataset_collection_instance(dataset_collection_instance,
security=trans.security, parent=create_params["parent"])
@expose_api
def update(self, trans: ProvidesHistoryContext, payload: dict, id):
"""
Iterate over all datasets of a collection and copy datasets with new attributes to a new collection.
e.g attributes = {'dbkey': 'dm3'}
* PUT /api/dataset_collections/{hdca_id}:
create a new dataset collection instance.
"""
if len(payload) != 1:
raise exceptions.RequestParameterInvalidException("Update one attribute at a time.")
if 'dbkey' not in payload:
raise exceptions.RequestParameterInvalidException("This attribute cannot be modified.")
self.collection_manager.copy(trans, trans.history, "hdca", id, copy_elements=True, dataset_instance_attributes=payload)
trans.sa_session.flush()
@expose_api
def attributes(self, trans: ProvidesHistoryContext, id, instance_type='history'):
"""
GET /api/dataset_collections/{hdca_id}/attributes
Returns dbkey/extension for collection elements
"""
dataset_collection_instance = self.__service.get_dataset_collection_instance(
trans,
id=id,
instance_type=instance_type,
check_ownership=True
)
return dataset_collection_instance.to_dict(view="dbkeysandextensions")
@expose_api
def suitable_converters(self, trans: ProvidesHistoryContext, id, instance_type='history', **kwds):
"""
GET /api/dataset_collections/{hdca_id}/suitable_converters
Returns suitable converters for all datatypes in collection
"""
return self.collection_manager.get_converters_for_collection(trans, id, self.datatypes_registry, instance_type)
@expose_api
def show(self, trans: ProvidesHistoryContext, id, instance_type='history', **kwds):
"""
+66 -13
View File
@@ -4,6 +4,8 @@ from json import dumps, loads
from typing import Any, cast, Dict, Optional
from galaxy import exceptions, util, web
from galaxy.datatypes.data import get_params_and_input_name
from galaxy.managers.collections import DatasetCollectionManager
from galaxy.managers.collections_util import dictify_dataset_collection_instance
from galaxy.managers.hdas import HDAManager
from galaxy.managers.histories import HistoryManager
@@ -36,6 +38,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
"""
history_manager: HistoryManager = depends(HistoryManager)
hda_manager: HDAManager = depends(HDAManager)
hdca_manager: DatasetCollectionManager = depends(DatasetCollectionManager)
@expose_api_anonymous_and_sessionless
def index(self, trans: GalaxyWebTransaction, **kwds):
@@ -426,6 +429,51 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
rval.append(citation.to_dict('bibtex'))
return rval
@expose_api
def conversion(self, trans: GalaxyWebTransaction, tool_id, payload, **kwd):
converter = self._get_tool(tool_id, user=trans.user)
target_type = payload.get("target_type")
source_type = payload.get("source_type")
input_src = payload.get("src")
input_id = payload.get("id")
# List of string of dependencies
try:
deps = trans.app.datatypes_registry.converter_deps[source_type][target_type]
except KeyError:
deps = {}
# Generate parameter dictionary
params, input_name = get_params_and_input_name(converter, deps)
params = {}
# determine input parameter name and add to params
params[input_name] = {
"values": [
{
"id": input_id,
"src": input_src,
}
],
"batch": input_src == "hdca",
}
history_id = payload.get('history_id')
if history_id:
decoded_id = self.decode_id(history_id)
target_history = self.history_manager.get_owned(decoded_id, trans.user, current_history=trans.history)
else:
if input_src == "hdca":
target_history = self.hdca_manager.get_dataset_collection_instance(trans, instance_type='history', id=input_id).history
elif input_src == "hda":
decoded_id = trans.app.security.decode_id(input_id)
target_history = self.hda_manager.get_accessible(decoded_id, trans.user).history
self.history_manager.error_unless_owner(target_history, trans.user, current_history=trans.history)
else:
raise exceptions.RequestParameterInvalidException("Must run conversion on either hdca or hda.")
# Make the target datatype available to the converter
params['__target_datatype__'] = target_type
vars = converter.handle_input(trans, params, history=target_history)
return self._handle_inputs_output_to_api_response(trans, converter, target_history, vars)
@expose_api_anonymous_and_sessionless
def xrefs(self, trans: GalaxyWebTransaction, id, **kwds):
tool = self._get_tool(id, user=trans.user)
@@ -568,6 +616,24 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
vars = tool.handle_input(trans, incoming, history=target_history, use_cached_job=use_cached_job, input_format=input_format)
new_pja_flush = False
for job in vars.get('jobs', []):
if inputs.get('send_email_notification', False):
# Unless an anonymous user is invoking this via the API it
# should never be an option, but check and enforce that here
if trans.user is None:
raise exceptions.ToolExecutionError("Anonymously run jobs cannot send an email notification.")
else:
job_email_action = PostJobAction('EmailAction')
job.add_post_job_action(job_email_action)
new_pja_flush = True
if new_pja_flush:
trans.sa_session.flush()
return self._handle_inputs_output_to_api_response(trans, tool, target_history, vars)
def _handle_inputs_output_to_api_response(self, trans, tool, target_history, vars):
# TODO: check for errors and ensure that output dataset(s) are available.
output_datasets = vars.get('out_data', [])
rval: Dict[str, Any] = {'outputs': [], 'output_collections': [], 'jobs': [], 'implicit_collections': []}
@@ -587,21 +653,8 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
output_dict['output_name'] = output_name
outputs.append(trans.security.encode_dict_ids(output_dict, skip_startswith="metadata_"))
new_pja_flush = False
for job in vars.get('jobs', []):
rval['jobs'].append(self.encode_all_ids(trans, job.to_dict(view='collection'), recursive=True))
if inputs.get('send_email_notification', False):
# Unless an anonymous user is invoking this via the API it
# should never be an option, but check and enforce that here
if trans.user is None:
raise exceptions.ToolExecutionError("Anonymously run jobs cannot send an email notification.")
else:
job_email_action = PostJobAction('EmailAction')
job.add_post_job_action(job_email_action)
new_pja_flush = True
if new_pja_flush:
trans.sa_session.flush()
for output_name, collection_instance in vars.get('output_collections', []):
history = target_history or trans.history
+18
View File
@@ -178,6 +178,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
webapp.add_client_route('/histories/show_structure')
webapp.add_client_route('/datasets/list')
webapp.add_client_route('/datasets/edit')
webapp.add_client_route('/collection/edit/{collection_id}')
webapp.add_client_route('/datasets/error')
webapp.add_client_route('/jobs/{job_id}/view')
webapp.add_client_route('/datasets/{dataset_id}/details')
@@ -425,6 +426,7 @@ def populate_api_routes(webapp, app):
webapp.mapper.connect('/api/tools/{id:.+?}/test_data', action='test_data', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/diagnostics', action='diagnostics', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/citations', action='citations', controller="tools")
webapp.mapper.connect('/api/tools/{tool_id:.+?}/convert', action='conversion', controller="tools", conditions=dict(method=["POST"]))
webapp.mapper.connect('/api/tools/{id:.+?}/xrefs', action='xrefs', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/download', action='download', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/requirements', action='requirements', controller="tools")
@@ -550,6 +552,22 @@ def populate_api_routes(webapp, app):
action='download_dataset_collection',
conditions=dict(method=["GET"]))
webapp.mapper.connect("/api/dataset_collections/{id}",
controller='dataset_collections',
action='update',
conditions=dict(method=["PUT"]))
webapp.mapper.connect("/api/dataset_collections/{id}/attributes",
controller='dataset_collections',
action='attributes',
conditions=dict(method=["GET"]))
webapp.mapper.connect("api_suitable_converters",
"/api/dataset_collections/{id}/suitable_converters",
controller='dataset_collections',
action='suitable_converters',
conditions=dict(method=['GET']))
webapp.mapper.connect("/api/histories/{history_id}/jobs_summary",
action="index_jobs_summary",
controller='history_contents',
@@ -356,6 +356,94 @@ class DatasetCollectionApiTestCase(ApiTestCase):
assert len(offset_contents) == 1
assert offset_contents[0]['element_index'] == 1
def test_get_suitable_converters_single_datatype(self):
response = self.dataset_collection_populator.upload_collection(self.history_id, "list:paired", elements=[
{
"name": "test0",
"elements": [
{"src": "pasted", "paste_content": "123\n", "name": "forward", "ext": "bed"},
{"src": "pasted", "paste_content": "456\n", "name": "reverse", "ext": "bed"},
]
},
{
"name": "test1",
"elements": [
{"src": "pasted", "paste_content": "789\n", "name": "forward", "ext": "bed"},
{"src": "pasted", "paste_content": "0ab\n", "name": "reverse", "ext": "bed"},
]
}
])
self._assert_status_code_is(response, 200)
hdca_list_id = response.json()["outputs"][0]["id"]
converters = self._get("dataset_collections/" + hdca_list_id + "/suitable_converters")
expected = [
'CONVERTER_bed_to_fli_0',
'CONVERTER_interval_to_bed_0',
'CONVERTER_bed_gff_or_vcf_to_bigwig_0',
'CONVERTER_bed_to_gff_0',
'CONVERTER_interval_to_bgzip_0',
'tabular_to_csv',
'CONVERTER_interval_to_bed6_0',
'CONVERTER_interval_to_bedstrict_0',
'CONVERTER_interval_to_tabix_0',
'CONVERTER_interval_to_bed12_0']
actual = []
for converter in converters.json():
actual.append(converter["tool_id"])
assert sorted(actual) == sorted(expected)
def test_get_suitable_converters_different_datatypes_matches(self):
response = self.dataset_collection_populator.upload_collection(self.history_id, "list:paired", elements=[
{
"name": "test0",
"elements": [
{"src": "pasted", "paste_content": "123\n", "name": "forward", "ext": "bed"},
{"src": "pasted", "paste_content": "456\n", "name": "reverse", "ext": "bed"},
]
},
{
"name": "test1",
"elements": [
{"src": "pasted", "paste_content": "789\n", "name": "forward", "ext": "tabular"},
{"src": "pasted", "paste_content": "0ab\n", "name": "reverse", "ext": "tabular"},
]
}
])
self._assert_status_code_is(response, 200)
hdca_list_id = response.json()["outputs"][0]["id"]
converters = self._get("dataset_collections/" + hdca_list_id + "/suitable_converters")
expected = ['tabular_to_csv']
actual = []
for converter in converters.json():
actual.append(converter["tool_id"])
assert sorted(actual) == sorted(expected)
def test_get_suitable_converters_different_datatypes_no_matches(self):
response = self.dataset_collection_populator.upload_collection(self.history_id, "list:paired", elements=[
{
"name": "test0",
"elements": [
{"src": "pasted", "paste_content": "123\n", "name": "forward", "ext": "bed"},
{"src": "pasted", "paste_content": "456\n", "name": "reverse", "ext": "bed"},
]
},
{
"name": "test1",
"elements": [
{"src": "pasted", "paste_content": "789\n", "name": "forward", "ext": "fasta"},
{"src": "pasted", "paste_content": "0ab\n", "name": "reverse", "ext": "fasta"},
]
}
])
self._assert_status_code_is(response, 200)
hdca_list_id = response.json()["outputs"][0]["id"]
converters = self._get("dataset_collections/" + hdca_list_id + "/suitable_converters")
expected = []
actual = []
for converter in converters.json():
actual.append(converter["tool_id"])
assert sorted(actual) == sorted(expected)
def test_collection_tools_tag_propagation(self):
elements = [{"src": "files", "tags": ["name:element_tag"]}]
targets = [{
+76
View File
@@ -433,6 +433,82 @@ class ToolsTestCase(ApiTestCase, TestsTools):
expected_names = {'velveth_test1/Roadmaps', 'velveth_test1/output.html', 'velveth_test1/Sequences', 'velveth_test1/Log', 'velveth_test1/output/', 'velveth_test1/output/1'}
assert set(namelist) == expected_names
@uses_test_history(require_new=False)
def test_convert_dataset_explicit_history(self, history_id):
fasta1_contents = open(self.get_filename("1.fasta")).read()
hda1 = self.dataset_populator.new_dataset(history_id, content=fasta1_contents)
payload = {
"src": "hda",
"id": hda1["id"],
"source_type": "fasta",
"target_type": "tabular",
"history_id": history_id
}
create_response = self._post("tools/CONVERTER_fasta_to_tabular/convert", data=payload)
self.dataset_populator.wait_for_job(create_response.json()["jobs"][0]["id"], assert_ok=True)
create_response.raise_for_status()
assert len(create_response.json()["implicit_collections"]) == 0
for output in create_response.json()["outputs"]:
assert output["file_ext"] == "tabular"
@uses_test_history(require_new=False)
def test_convert_dataset_implicit_history(self, history_id):
fasta1_contents = open(self.get_filename("1.fasta")).read()
hda1 = self.dataset_populator.new_dataset(history_id, content=fasta1_contents)
payload = {
"src": "hda",
"id": hda1["id"],
"source_type": "fasta",
"target_type": "tabular"
}
create_response = self._post("tools/CONVERTER_fasta_to_tabular/convert", data=payload)
self.dataset_populator.wait_for_job(create_response.json()["jobs"][0]["id"], assert_ok=True)
create_response.raise_for_status()
assert len(create_response.json()["implicit_collections"]) == 0
for output in create_response.json()["outputs"]:
assert output["file_ext"] == "tabular"
@uses_test_history(require_new=False)
def test_convert_hdca(self, history_id):
data = [
{
"name": "test0",
"elements": [
{"src": "pasted", "paste_content": "123\n", "name": "forward", "ext": "fasta"},
{"src": "pasted", "paste_content": "456\n", "name": "reverse", "ext": "fasta"},
]
},
{
"name": "test1",
"elements": [
{"src": "pasted", "paste_content": "789\n", "name": "forward", "ext": "fasta"},
{"src": "pasted", "paste_content": "0ab\n", "name": "reverse", "ext": "fasta"},
]
}
]
hdca1 = self.dataset_collection_populator.upload_collection(history_id, "list:paired", elements=data)
self._assert_status_code_is(hdca1, 200)
payload = {
"src": "hdca",
"id": hdca1.json()["outputs"][0]["id"],
"source_type": "fasta",
"target_type": "tabular",
"history_id": history_id
}
create_response = self._post("tools/CONVERTER_fasta_to_tabular/convert", payload)
self.dataset_populator.wait_for_job(create_response.json()["jobs"][0]["id"], assert_ok=True)
create_response.raise_for_status()
assert create_response.json()["implicit_collections"] != []
hdca_id = create_response.json()["implicit_collections"][0]["hid"]
fetchedResponse = self.dataset_populator.get_history_collection_details(history_id, hid=hdca_id)
for element in fetchedResponse["elements"][0]["object"]["elements"]:
assert element["object"]["file_ext"] == "tabular"
def test_unzip_collection(self):
with self.dataset_populator.test_history() as history_id:
hdca_id = self._build_pair(history_id, ["123", "456"])
@@ -0,0 +1,74 @@
from selenium.webdriver.common.keys import Keys
from .framework import (
selenium_test,
SeleniumTestCase
)
class CollectionEditTestCase(SeleniumTestCase):
ensure_registered = True
@selenium_test
def test_change_dbkey_simple_list(self):
self.use_beta_history()
self.create_simple_list_collection()
self.open_collection_edit_view()
self.navigate_to_database_tab()
dbkeyValue = "Additional"
self.check_current_dbkey_value(dbkeyValue)
dbkeyNew = "hg17"
self.change_dbkey_value_and_click_submit(dbkeyValue, dbkeyNew)
self.history_panel_wait_for_hid_ok(4)
self.use_beta_history()
self.open_collection_edit_view()
self.navigate_to_database_tab()
self.check_current_dbkey_value(dbkeyNew)
def create_simple_list_collection(self):
self.perform_upload(self.get_filename("1.fasta"))
self._wait_for_and_select([1])
self._collection_dropdown("build list")
self.collection_builder_set_name("my cool list")
self.screenshot("collection_builder_list")
self.collection_builder_create()
self._wait_for_hid_visible(2)
def open_collection_edit_view(self):
self.components.history_panel.collection_menu_edit_attributes.wait_for_and_click()
def navigate_to_database_tab(self):
self.components.edit_collection_attributes.database_genome_tab.wait_for_and_click()
def check_current_dbkey_value(self, dbkeyValue):
self.components.edit_collection_attributes.database_value(dbkey=dbkeyValue).wait_for_visible()
def change_dbkey_value_and_click_submit(self, dbkeyValue, dbkeyNew):
self.components.edit_collection_attributes.database_value(dbkey=dbkeyValue).wait_for_and_click()
self.driver.find_element_by_css_selector("input.multiselect__input").send_keys(dbkeyNew)
self.driver.find_element_by_css_selector("input.multiselect__input").send_keys(Keys.ENTER)
self.components.edit_collection_attributes.save_btn.wait_for_and_click()
def _wait_for_and_select(self, hids):
"""
Waits for uploads to pass through queued, running, ok. Not all the states are not guaranteed
depending on how fast the upload goes compared to the history polling updates, it might just
skip to the end for a really fast upload
"""
for hid in hids:
timeout = self.wait_length(self.wait_types.JOB_COMPLETION)
row_selector = self.content_item_by_attributes(hid=hid, state="ok")
row = self.wait_for_present(row_selector, timeout=timeout)
row.send_keys(" ")
def _collection_dropdown(self, option_description):
return self.use_bootstrap_dropdown(option=option_description, menu="new content menu")
def _wait_for_hid_visible(self, hid, state="ok"):
timeout = self.wait_length(self.wait_types.JOB_COMPLETION)
row_selector = self.content_item_by_attributes(hid=hid, state=state)
self.wait_for_visible(row_selector, timeout=timeout)
+15
View File
@@ -417,6 +417,21 @@ class MappingTests(BaseModelTestCase):
assert c4.dataset_elements == [dce1, dce2]
assert c4.element_identifiers_extensions_and_paths == [(('outer_list', 'inner_list', 'forward'), 'bam', 'mock_dataset_14.dat'), (('outer_list', 'inner_list', 'reverse'), 'txt', 'mock_dataset_14.dat')]
def test_dataset_dbkeys_and_extensions_summary(self):
model = self.model
u = model.User(email="mary2@example.com", password="password")
h1 = model.History(name="History 1", user=u)
d1 = model.HistoryDatasetAssociation(extension="bam", dbkey="hg19", history=h1, create_dataset=True, sa_session=model.session)
d2 = model.HistoryDatasetAssociation(extension="txt", dbkey="hg19", history=h1, create_dataset=True, sa_session=model.session)
c1 = model.DatasetCollection(collection_type='paired')
dce1 = model.DatasetCollectionElement(collection=c1, element=d1, element_identifier="forward", element_index=0)
dce2 = model.DatasetCollectionElement(collection=c1, element=d2, element_identifier="reverse", element_index=1)
hdca = model.HistoryDatasetCollectionAssociation(collection=c1, history=h1)
model.session.add_all([d1, d2, c1, dce1, dce2, hdca])
model.session.flush()
assert hdca.dataset_dbkeys_and_extensions_summary[0] == {"hg19"}
assert hdca.dataset_dbkeys_and_extensions_summary[1] == {"bam", "txt"}
def test_default_disk_usage(self):
model = self.model