mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'release_25.1' into dev
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { BCardGroup } from "bootstrap-vue";
|
||||
import { BAlert, BCardGroup } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { GalaxyApi } from "@/api";
|
||||
@@ -11,6 +11,7 @@ import { forBuilder, type ForBuilderResponse } from "@/components/Collections/wi
|
||||
import { useWizard } from "@/components/Common/Wizard/useWizard";
|
||||
import { useToolRouting } from "@/composables/route";
|
||||
import localize from "@/utils/localization";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import type {
|
||||
ParsedFetchWorkbook,
|
||||
@@ -112,7 +113,7 @@ const wizard = useWizard({
|
||||
},
|
||||
"upload-workbook": {
|
||||
label: "Upload workbook",
|
||||
instructions: "Upload a workbook containing with URIs and metadata",
|
||||
instructions: "Upload a workbook containing URIs with metadata",
|
||||
isValid: () => sourceFrom.value === "workbook" && workbookCompleted.value,
|
||||
isSkippable: () => sourceFrom.value !== "workbook",
|
||||
},
|
||||
@@ -238,7 +239,7 @@ async function handleWorkbook(base64Content: string) {
|
||||
handleUploadedData(data);
|
||||
} else {
|
||||
console.log(error);
|
||||
uploadErrorMessage.value = "There was an error processing the file.";
|
||||
uploadErrorMessage.value = "There was an error processing the file. " + errorMessageAsString(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,17 +260,26 @@ const {
|
||||
<template>
|
||||
<GenericWizard :use="wizard" :submit-button-label="importButtonLabel" :title="title" @submit="submit">
|
||||
<template v-slot:header>
|
||||
<BAlert
|
||||
:show="!!uploadErrorMessage"
|
||||
variant="danger"
|
||||
class="my-2"
|
||||
dismissible
|
||||
@dismissed="uploadErrorMessage = ''">
|
||||
{{ uploadErrorMessage }}
|
||||
</BAlert>
|
||||
<h2 data-galaxy-file-drop-target>
|
||||
{{ title }}
|
||||
<FontAwesomeIcon
|
||||
class="workbook-upload-helper mr-1"
|
||||
:class="dropZoneClasses"
|
||||
:title="dropWorkbookTitle"
|
||||
:icon="faUpload"
|
||||
@click.prevent="browseFiles"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false" />
|
||||
<a v-b-tooltip.hover aria-label="Upload Completed Workbook" :title="dropWorkbookTitle" href="#">
|
||||
<FontAwesomeIcon
|
||||
class="workbook-upload-helper mr-1"
|
||||
:class="dropZoneClasses"
|
||||
:icon="faUpload"
|
||||
@click.prevent="browseFiles"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false" />
|
||||
</a>
|
||||
<HiddenWorkbookUploadInput ref="uploadRef" @onFileUpload="onFileUpload" />
|
||||
</h2>
|
||||
</template>
|
||||
@@ -336,5 +346,8 @@ const {
|
||||
// modeled a bit after upload-helper in the upload component...
|
||||
.workbook-upload-helper {
|
||||
color: $border-color;
|
||||
&:hover {
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,16 @@ import { faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BCard, BCardTitle, BLink } from "bootstrap-vue";
|
||||
|
||||
// We use this in two capacities - one where we have to do a post and do complex things
|
||||
// to generate the workbook and one where we just have an easy link. Support both.
|
||||
interface Props {
|
||||
generateWorkbookLink?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
generateWorkbookLink: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["download"]);
|
||||
</script>
|
||||
|
||||
@@ -11,6 +21,8 @@ const emit = defineEmits(["download"]);
|
||||
<BCardTitle>
|
||||
<b>Step 1: Download</b>
|
||||
</BCardTitle>
|
||||
<BLink @click="emit('download')"><FontAwesomeIcon size="xl" :icon="faDownload" /> Download workbook.</BLink>
|
||||
<BLink :href="generateWorkbookLink" @click="emit('download')"
|
||||
><FontAwesomeIcon size="xl" :icon="faDownload" /> Download workbook.</BLink
|
||||
>
|
||||
</BCard>
|
||||
</template>
|
||||
|
||||
@@ -19,6 +19,6 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<label style="display: none">
|
||||
<input ref="fileInputRef" type="file" accept=".xlsx" @change="onFileUpload" />
|
||||
<input ref="fileInputRef" type="file" accept=".xlsx,.xls,.tsv,.csv,.tabular" @change="onFileUpload" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -88,7 +88,9 @@ const COLUMN_TITLE_PREFIXES: Record<string, ColumnMappingType> = {
|
||||
deferredurl: "url_deferred",
|
||||
genome: "dbkey",
|
||||
dbkey: "dbkey",
|
||||
build: "dbkey",
|
||||
filetype: "file_type",
|
||||
type: "file_type",
|
||||
extension: "file_type",
|
||||
info: "info",
|
||||
tag: "tags",
|
||||
|
||||
@@ -393,9 +393,9 @@ export default {
|
||||
tool_id: this.formConfig.id,
|
||||
tool_version: this.formConfig.version,
|
||||
tool_uuid: this.toolUuid,
|
||||
__tags: this.tags,
|
||||
inputs: {
|
||||
...this.formData,
|
||||
__tags: this.tags,
|
||||
},
|
||||
};
|
||||
if (this.useEmail) {
|
||||
|
||||
@@ -257,43 +257,43 @@ function onToggleView(newView: ListViewMode) {
|
||||
</GButton>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<ToolsListSectionFilters
|
||||
:filter-class="ToolFilters"
|
||||
:filter-text="filterText"
|
||||
:disabled="loading"
|
||||
@apply-filter="applyFilter" />
|
||||
<ToolsListSectionFilters
|
||||
:filter-class="ToolFilters"
|
||||
:filter-text="filterText"
|
||||
:disabled="loading"
|
||||
@apply-filter="applyFilter">
|
||||
<template v-slot:list-view-controls>
|
||||
<!-- TODO: This div here and in ListHeader.vue needs to be a reusable component -->
|
||||
<div class="d-flex flex-gapx-1 align-items-center">
|
||||
Display:
|
||||
<GButtonGroup>
|
||||
<GButton
|
||||
id="view-grid"
|
||||
tooltip
|
||||
title="Grid view"
|
||||
size="small"
|
||||
:pressed="currentListViewMode === 'grid'"
|
||||
outline
|
||||
color="blue"
|
||||
@click="onToggleView('grid')">
|
||||
<FontAwesomeIcon :icon="faGripVertical" />
|
||||
</GButton>
|
||||
|
||||
<!-- TODO: This div here and in ListHeader.vue needs to be a reusable component -->
|
||||
<div>
|
||||
Display:
|
||||
<GButtonGroup>
|
||||
<GButton
|
||||
id="view-grid"
|
||||
tooltip
|
||||
title="Grid view"
|
||||
size="small"
|
||||
:pressed="currentListViewMode === 'grid'"
|
||||
outline
|
||||
color="blue"
|
||||
@click="onToggleView('grid')">
|
||||
<FontAwesomeIcon :icon="faGripVertical" />
|
||||
</GButton>
|
||||
|
||||
<GButton
|
||||
id="view-list"
|
||||
tooltip
|
||||
title="List view"
|
||||
size="small"
|
||||
:pressed="currentListViewMode === 'list'"
|
||||
outline
|
||||
color="blue"
|
||||
@click="onToggleView('list')">
|
||||
<FontAwesomeIcon :icon="faBars" />
|
||||
</GButton>
|
||||
</GButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<GButton
|
||||
id="view-list"
|
||||
tooltip
|
||||
title="List view"
|
||||
size="small"
|
||||
:pressed="currentListViewMode === 'list'"
|
||||
outline
|
||||
color="blue"
|
||||
@click="onToggleView('list')">
|
||||
<FontAwesomeIcon :icon="faBars" />
|
||||
</GButton>
|
||||
</GButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
</ToolsListSectionFilters>
|
||||
</div>
|
||||
|
||||
<div class="tools-list-body">
|
||||
|
||||
@@ -99,134 +99,147 @@ function searchWithinSections(sections: ToolSection[], query: string) {
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column flex-gapy-1">
|
||||
<div class="d-flex flex-gapx-1">
|
||||
<BDropdown
|
||||
block
|
||||
:disabled="props.disabled"
|
||||
variant="link"
|
||||
class="tool-section-dropdown"
|
||||
toggle-class="text-decoration-none"
|
||||
role="menu"
|
||||
aria-label="Select a tool section to filter by"
|
||||
size="sm">
|
||||
<template v-slot:button-content>
|
||||
<span class="sr-only">Select a tool section to filter by</span>
|
||||
<FontAwesomeIcon :icon="faLayerGroup" />
|
||||
<span v-if="selectedSection">
|
||||
{{ selectedSection.name }}
|
||||
</span>
|
||||
<i v-else> Select a section to filter by </i>
|
||||
</template>
|
||||
|
||||
<BDropdownGroup id="searchable-sections" class="sections-select-list" header-classes="search-header">
|
||||
<template v-slot:header>
|
||||
<BDropdownText>
|
||||
<BFormInput
|
||||
v-model="defaultToolSectionsFilter"
|
||||
type="text"
|
||||
placeholder="Filter sections..." />
|
||||
</BDropdownText>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex flex-gapx-1">
|
||||
<BDropdown
|
||||
block
|
||||
:disabled="props.disabled"
|
||||
variant="link"
|
||||
class="tool-section-dropdown"
|
||||
toggle-class="text-decoration-none"
|
||||
role="menu"
|
||||
aria-label="Select a tool section to filter by"
|
||||
size="sm">
|
||||
<template v-slot:button-content>
|
||||
<span class="sr-only">Select a tool section to filter by</span>
|
||||
<FontAwesomeIcon :icon="faLayerGroup" />
|
||||
<span v-if="selectedSection">
|
||||
{{ selectedSection.name }}
|
||||
</span>
|
||||
<i v-else> Select a section to filter by </i>
|
||||
</template>
|
||||
|
||||
<BDropdownItem
|
||||
v-for="sec in defaultToolSections"
|
||||
:key="sec.id"
|
||||
:title="sec.description"
|
||||
:active="selectedSection?.id === sec.id"
|
||||
@click="applyQuotedFilter('section', sec.name)">
|
||||
<span v-localize>{{ sec.name }}</span>
|
||||
</BDropdownItem>
|
||||
</BDropdownGroup>
|
||||
</BDropdown>
|
||||
<BDropdownGroup
|
||||
id="searchable-sections"
|
||||
class="sections-select-list"
|
||||
header-classes="search-header">
|
||||
<template v-slot:header>
|
||||
<BDropdownText>
|
||||
<BFormInput
|
||||
v-model="defaultToolSectionsFilter"
|
||||
type="text"
|
||||
placeholder="Filter sections..." />
|
||||
</BDropdownText>
|
||||
</template>
|
||||
|
||||
<GButton
|
||||
v-if="selectedSection"
|
||||
title="Remove section filter"
|
||||
icon-only
|
||||
inline
|
||||
transparent
|
||||
tooltip
|
||||
@click="applyQuotedFilter('section', selectedSection.name)">
|
||||
<FontAwesomeIcon :icon="faTimes" />
|
||||
</GButton>
|
||||
<BDropdownItem
|
||||
v-for="sec in defaultToolSections"
|
||||
:key="sec.id"
|
||||
:title="sec.description"
|
||||
:active="selectedSection?.id === sec.id"
|
||||
@click="applyQuotedFilter('section', sec.name)">
|
||||
<span v-localize>{{ sec.name }}</span>
|
||||
</BDropdownItem>
|
||||
</BDropdownGroup>
|
||||
</BDropdown>
|
||||
|
||||
<BDropdown
|
||||
block
|
||||
:disabled="props.disabled"
|
||||
variant="link"
|
||||
class="tool-section-dropdown"
|
||||
toggle-class="text-decoration-none"
|
||||
role="menu"
|
||||
aria-label="Select a tool ontology to filter by"
|
||||
size="sm">
|
||||
<template v-slot:button-content>
|
||||
<span class="sr-only">Select a tool ontology to filter by</span>
|
||||
<FontAwesomeIcon :icon="faSitemap" />
|
||||
<span v-if="selectedOntology">
|
||||
{{ selectedOntology.name }}
|
||||
</span>
|
||||
<i v-else> Select an ontology to filter by </i>
|
||||
</template>
|
||||
<GButton
|
||||
v-if="selectedSection"
|
||||
title="Remove section filter"
|
||||
icon-only
|
||||
inline
|
||||
transparent
|
||||
tooltip
|
||||
@click="applyQuotedFilter('section', selectedSection.name)">
|
||||
<FontAwesomeIcon :icon="faTimes" />
|
||||
</GButton>
|
||||
|
||||
<BDropdownGroup id="searchable-sections" class="sections-select-list" header-classes="search-header">
|
||||
<template v-slot:header>
|
||||
<BDropdownText>
|
||||
<BFormInput v-model="ontologiesFilter" type="text" placeholder="Filter ontologies..." />
|
||||
</BDropdownText>
|
||||
<BDropdown
|
||||
block
|
||||
:disabled="props.disabled"
|
||||
variant="link"
|
||||
class="tool-section-dropdown"
|
||||
toggle-class="text-decoration-none"
|
||||
role="menu"
|
||||
aria-label="Select a tool ontology to filter by"
|
||||
size="sm">
|
||||
<template v-slot:button-content>
|
||||
<span class="sr-only">Select a tool ontology to filter by</span>
|
||||
<FontAwesomeIcon :icon="faSitemap" />
|
||||
<span v-if="selectedOntology">
|
||||
{{ selectedOntology.name }}
|
||||
</span>
|
||||
<i v-else> Select an ontology to filter by </i>
|
||||
</template>
|
||||
|
||||
<BDropdownGroup v-if="Object.keys(edamOperations).length" id="edam-operations" class="unselectable">
|
||||
<BDropdownGroup
|
||||
id="searchable-sections"
|
||||
class="sections-select-list"
|
||||
header-classes="search-header">
|
||||
<template v-slot:header>
|
||||
<FontAwesomeIcon
|
||||
v-if="getPanelIcon('ontology:edam_operations')"
|
||||
:icon="getPanelIcon('ontology:edam_operations')"
|
||||
fixed-width
|
||||
size="sm" />
|
||||
<small class="font-weight-bold">{{ panels["ontology:edam_operations"]?.name }}</small>
|
||||
<BDropdownText>
|
||||
<BFormInput v-model="ontologiesFilter" type="text" placeholder="Filter ontologies..." />
|
||||
</BDropdownText>
|
||||
</template>
|
||||
<BDropdownItem
|
||||
v-for="ont in edamOperations"
|
||||
:key="ont.id"
|
||||
:title="ont.description"
|
||||
:active="selectedOntology?.id === ont.id"
|
||||
@click="applyQuotedFilter('ontology', ont.id)">
|
||||
<span v-localize>{{ ont.name }}</span>
|
||||
</BDropdownItem>
|
||||
|
||||
<BDropdownGroup
|
||||
v-if="Object.keys(edamOperations).length"
|
||||
id="edam-operations"
|
||||
class="unselectable">
|
||||
<template v-slot:header>
|
||||
<FontAwesomeIcon
|
||||
v-if="getPanelIcon('ontology:edam_operations')"
|
||||
:icon="getPanelIcon('ontology:edam_operations')"
|
||||
fixed-width
|
||||
size="sm" />
|
||||
<small class="font-weight-bold">{{ panels["ontology:edam_operations"]?.name }}</small>
|
||||
</template>
|
||||
<BDropdownItem
|
||||
v-for="ont in edamOperations"
|
||||
:key="ont.id"
|
||||
:title="ont.description"
|
||||
:active="selectedOntology?.id === ont.id"
|
||||
@click="applyQuotedFilter('ontology', ont.id)">
|
||||
<span v-localize>{{ ont.name }}</span>
|
||||
</BDropdownItem>
|
||||
</BDropdownGroup>
|
||||
|
||||
<BDropdownDivider />
|
||||
|
||||
<BDropdownGroup v-if="Object.keys(edamTopics).length" id="edam-topics" class="unselectable">
|
||||
<template v-slot:header>
|
||||
<FontAwesomeIcon
|
||||
v-if="getPanelIcon('ontology:edam_topics')"
|
||||
:icon="getPanelIcon('ontology:edam_topics')"
|
||||
fixed-width
|
||||
size="sm" />
|
||||
<small class="font-weight-bold">{{ panels["ontology:edam_topics"]?.name }}</small>
|
||||
</template>
|
||||
<BDropdownItem
|
||||
v-for="ont in edamTopics"
|
||||
:key="ont.id"
|
||||
:title="ont.description"
|
||||
:active="selectedOntology?.id === ont.id"
|
||||
@click="applyQuotedFilter('ontology', ont.id)">
|
||||
<span v-localize>{{ ont.name }}</span>
|
||||
</BDropdownItem>
|
||||
</BDropdownGroup>
|
||||
</BDropdownGroup>
|
||||
</BDropdown>
|
||||
|
||||
<BDropdownDivider />
|
||||
<GButton
|
||||
v-if="selectedOntology"
|
||||
title="Remove ontology filter"
|
||||
icon-only
|
||||
inline
|
||||
transparent
|
||||
tooltip
|
||||
@click="applyQuotedFilter('ontology', selectedOntology.id)">
|
||||
<FontAwesomeIcon :icon="faTimes" />
|
||||
</GButton>
|
||||
</div>
|
||||
|
||||
<BDropdownGroup v-if="Object.keys(edamTopics).length" id="edam-topics" class="unselectable">
|
||||
<template v-slot:header>
|
||||
<FontAwesomeIcon
|
||||
v-if="getPanelIcon('ontology:edam_topics')"
|
||||
:icon="getPanelIcon('ontology:edam_topics')"
|
||||
fixed-width
|
||||
size="sm" />
|
||||
<small class="font-weight-bold">{{ panels["ontology:edam_topics"]?.name }}</small>
|
||||
</template>
|
||||
<BDropdownItem
|
||||
v-for="ont in edamTopics"
|
||||
:key="ont.id"
|
||||
:title="ont.description"
|
||||
:active="selectedOntology?.id === ont.id"
|
||||
@click="applyQuotedFilter('ontology', ont.id)">
|
||||
<span v-localize>{{ ont.name }}</span>
|
||||
</BDropdownItem>
|
||||
</BDropdownGroup>
|
||||
</BDropdownGroup>
|
||||
</BDropdown>
|
||||
|
||||
<GButton
|
||||
v-if="selectedOntology"
|
||||
title="Remove ontology filter"
|
||||
icon-only
|
||||
inline
|
||||
transparent
|
||||
tooltip
|
||||
@click="applyQuotedFilter('ontology', selectedOntology.id)">
|
||||
<FontAwesomeIcon :icon="faTimes" />
|
||||
</GButton>
|
||||
<slot name="list-view-controls" />
|
||||
</div>
|
||||
|
||||
<ToolOntologyCard v-if="selectedOntology?.description" :ontology="selectedOntology" header />
|
||||
|
||||
@@ -119,6 +119,7 @@ markupsafe==3.0.3
|
||||
mdurl==0.1.2
|
||||
mercurial==7.1.1
|
||||
mistune==3.0.2
|
||||
more-itertools==10.8.0
|
||||
mrcfile==1.5.4
|
||||
msal==1.34.0
|
||||
msgpack==1.1.2
|
||||
|
||||
@@ -1242,9 +1242,16 @@ class WorkflowContentsManager(UsesAnnotations):
|
||||
"""Get workflow scheduling resource parameters for this user and workflow or None if not configured."""
|
||||
return self._resource_mapper_function(trans=trans, stored_workflow=stored, workflow=workflow)
|
||||
|
||||
def _workflow_to_dict_editor(self, trans, stored, workflow, tooltip=True, is_subworkflow=False):
|
||||
def _workflow_to_dict_editor(
|
||||
self,
|
||||
trans,
|
||||
stored: Optional[StoredWorkflow],
|
||||
workflow: Workflow,
|
||||
tooltip: bool = True,
|
||||
is_subworkflow: bool = False,
|
||||
):
|
||||
# Pack workflow data into a dictionary and return
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
data["name"] = workflow.name
|
||||
data["steps"] = {}
|
||||
data["upgrade_messages"] = {}
|
||||
@@ -1258,7 +1265,9 @@ class WorkflowContentsManager(UsesAnnotations):
|
||||
data["source_metadata"] = workflow.source_metadata
|
||||
data["annotation"] = self.get_item_annotation_str(trans.sa_session, trans.user, stored) or ""
|
||||
data["comments"] = [comment.to_dict() for comment in workflow.comments]
|
||||
data["tags"] = stored.make_tag_string_list()
|
||||
if stored:
|
||||
# subworkflow may not have StoredWorkflow
|
||||
data["tags"] = stored.make_tag_string_list()
|
||||
|
||||
output_label_index = set()
|
||||
input_step_types = set(workflow.input_step_types)
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
- doc: "dbkey maps to the dbkey target type"
|
||||
column_header: "dbkey"
|
||||
maps_to: "dbkey"
|
||||
- doc: "build maps to the dbkey target type"
|
||||
column_header: "build"
|
||||
maps_to: "dbkey"
|
||||
|
||||
|
||||
- doc: "filetype maps to the file_type target type"
|
||||
column_header: "file type"
|
||||
@@ -55,6 +59,9 @@
|
||||
- doc: "extension maps to the file_type target type"
|
||||
column_header: "extension"
|
||||
maps_to: "file_type"
|
||||
- doc: "Type maps to the file_type target type"
|
||||
column_header: "Type"
|
||||
maps_to: "file_type"
|
||||
|
||||
- doc: "info maps to the info target type"
|
||||
column_header: "info"
|
||||
|
||||
@@ -23,9 +23,11 @@ COLUMN_TITLE_PREFIXES: dict[str, RuleBuilderMappingTargetKey] = {
|
||||
"genome": "dbkey",
|
||||
"dbkey": "dbkey",
|
||||
"genomebuild": "dbkey",
|
||||
"build": "dbkey",
|
||||
"filetype": "file_type",
|
||||
"extension": "file_type",
|
||||
"fileextension": "file_type",
|
||||
"type": "file_type",
|
||||
"info": "info",
|
||||
"tag": "tags",
|
||||
"grouptag": "group_tags",
|
||||
|
||||
@@ -235,17 +235,24 @@ def load_workbook_from_base64(content: str) -> ReadOnlyWorkbook:
|
||||
is_excel = file_like.read(4) == b"\x50\x4b\x03\x04"
|
||||
workbook: ReadOnlyWorkbook
|
||||
file_like.seek(0)
|
||||
try:
|
||||
if is_excel:
|
||||
if is_excel:
|
||||
try:
|
||||
workbook = ExcelReadOnlyWorkbook(load_workbook(file_like, data_only=True))
|
||||
else:
|
||||
except Exception as e:
|
||||
extra_message = str(e)
|
||||
raise RequestParameterInvalidException(
|
||||
f"The provided content is not a valid Excel file (or at least not one Galaxy knows how to parse). Please check the content and try again. The underlying error was [{extra_message}]"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
tabular = decoded_content.decode("utf-8")
|
||||
file_like_as_utf8 = StringIO(tabular)
|
||||
workbook = CsvReaderReadOnlyWorkbook(file_like_as_utf8)
|
||||
except Exception:
|
||||
raise RequestParameterInvalidException(
|
||||
"The provided content is not a valid Excel file. Please check the content and try again."
|
||||
)
|
||||
except Exception as e:
|
||||
extra_message = str(e)
|
||||
raise RequestParameterInvalidException(
|
||||
f"The provided content is not a parsable as a valid CSV or TSV (or at least not one Galaxy knows how to parse). Please check the content and try again. The underlying error was [{extra_message}]"
|
||||
)
|
||||
return workbook
|
||||
|
||||
|
||||
|
||||
@@ -523,6 +523,12 @@ class WorkflowRunCrateProfileBuilder:
|
||||
)
|
||||
)
|
||||
crate.mainEntity.append_to("input", formal_param)
|
||||
|
||||
# Handle case where output_value is None (e.g., optional parameter not provided)
|
||||
output_value = None
|
||||
if step.output_value:
|
||||
output_value = step.output_value.value
|
||||
|
||||
return crate.add(
|
||||
ContextEntity(
|
||||
crate,
|
||||
@@ -530,7 +536,7 @@ class WorkflowRunCrateProfileBuilder:
|
||||
properties={
|
||||
"@type": "PropertyValue",
|
||||
"name": f"{param_id}",
|
||||
"value": step.output_value.value,
|
||||
"value": output_value,
|
||||
"exampleOfWork": {"@id": formal_param.id},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2325,6 +2325,7 @@ class Tool(UsesDictVisibleKeys, ToolParameterBundle):
|
||||
preferred_object_store_id: Optional[str] = DEFAULT_PREFERRED_OBJECT_STORE_ID,
|
||||
credentials_context: Optional[CredentialsContext] = None,
|
||||
input_format: InputFormatT = "legacy",
|
||||
tags: Optional[list[str]] = None,
|
||||
):
|
||||
"""
|
||||
Process incoming parameters for this tool from the dict `incoming`,
|
||||
@@ -2358,7 +2359,6 @@ class Tool(UsesDictVisibleKeys, ToolParameterBundle):
|
||||
|
||||
# Reserved global tags parameter. Applies to all tool outputs.
|
||||
# This may change in the future if per-output tags are introduced.
|
||||
tags = incoming.get("__tags", [])
|
||||
if tags:
|
||||
tag_handler = trans.tag_handler
|
||||
for _, hda in execution_tracker.output_datasets:
|
||||
@@ -4259,7 +4259,7 @@ class KeepSuccessDatasetsTool(FilterDatasetsTool):
|
||||
|
||||
class FilterEmptyDatasetsTool(FilterDatasetsTool):
|
||||
tool_type = "filter_empty_datasets_collection"
|
||||
require_dataset_ok = False
|
||||
require_dataset_ok = True
|
||||
|
||||
@staticmethod
|
||||
def element_is_valid(element: model.DatasetCollectionElement):
|
||||
|
||||
@@ -79,8 +79,8 @@ def on_text_for_numeric_ids(ids: Optional[list[int]], prefix: Optional[str] = No
|
||||
# and once as param_name1.
|
||||
groups = []
|
||||
unique_ids = sorted(set(ids))
|
||||
for group in consecutive_groups(unique_ids):
|
||||
group = list(group)
|
||||
for group_it in consecutive_groups(unique_ids):
|
||||
group = list(group_it)
|
||||
if len(group) == 1:
|
||||
groups.append(str(group[0]))
|
||||
elif len(group) == 2:
|
||||
|
||||
@@ -341,6 +341,7 @@ class ToolsService(ServiceBase):
|
||||
input_format = cast(InputFormatT, input_format) # https://github.com/python/mypy/issues/15106
|
||||
if "data_manager_mode" in payload:
|
||||
incoming["__data_manager_mode"] = payload["data_manager_mode"]
|
||||
tags = payload.get("__tags")
|
||||
vars = tool.handle_input(
|
||||
trans,
|
||||
incoming,
|
||||
@@ -349,6 +350,7 @@ class ToolsService(ServiceBase):
|
||||
input_format=input_format,
|
||||
preferred_object_store_id=preferred_object_store_id,
|
||||
credentials_context=CredentialsContext(root=credentials_context) if credentials_context else None,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
new_pja_flush = False
|
||||
|
||||
@@ -27,7 +27,6 @@ class TestToolOutputTaggingApi(ApiTestCase):
|
||||
"history_id": history_id,
|
||||
"inputs": {
|
||||
"input1": {"values": [{"src": "hda", "id": hda["id"]}]},
|
||||
"__tags": ["t1", "t2"],
|
||||
},
|
||||
"input_format": "21.01",
|
||||
"__tags": ["t1", "t2"],
|
||||
@@ -54,7 +53,6 @@ class TestToolOutputTaggingApi(ApiTestCase):
|
||||
"batch": True,
|
||||
"values": [{"src": "hdca", "id": hdca["id"]}],
|
||||
},
|
||||
"__tags": ["m1", "m2"],
|
||||
},
|
||||
"input_format": "21.01",
|
||||
"__tags": ["m1", "m2"],
|
||||
|
||||
@@ -60,6 +60,7 @@ install_requires =
|
||||
Markdown
|
||||
MarkupSafe
|
||||
mercurial>=6.8.2
|
||||
more-itertools
|
||||
nodejs-wheel>=22,<23
|
||||
packaging
|
||||
paramiko!=2.9.0,!=2.9.1
|
||||
|
||||
@@ -59,6 +59,7 @@ dependencies = [
|
||||
"MarkupSafe",
|
||||
"mercurial>=6.8.2", # Python 3.13 support
|
||||
"mrcfile",
|
||||
"more-itertools",
|
||||
"msal",
|
||||
"nodejs-wheel>=22,<23",
|
||||
"numpy>=1.26.0", # Python 3.12 support
|
||||
|
||||
@@ -74,6 +74,54 @@ class TestWorkflowTasksIntegration(PosixFileSourceSetup, IntegrationTestCase, Us
|
||||
bco = json.load(f)
|
||||
self.workflow_populator.validate_biocompute_object(bco)
|
||||
|
||||
def test_export_ro_crate_with_optional_parameter_without_value(self):
|
||||
"""Test exporting invocation with optional text parameter that has no value.
|
||||
|
||||
This tests the fix for the bug where step.output_value is None for optional
|
||||
parameters that weren't provided, which caused AttributeError when creating RO-Crate.
|
||||
"""
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
summary = self._run_workflow_with_optional_parameter_without_value(history_id)
|
||||
invocation_id = summary.invocation_id
|
||||
|
||||
# Export to RO-Crate - this should succeed without AttributeError
|
||||
ro_crate_path = self.workflow_populator.download_invocation_to_store(invocation_id, extension="rocrate.zip")
|
||||
|
||||
# Verify the RO-Crate was created successfully
|
||||
with CompressedFile(ro_crate_path) as cf:
|
||||
assert cf.file_type == "zip"
|
||||
|
||||
def _run_workflow_with_optional_parameter_without_value(self, history_id: str) -> RunJobsSummary:
|
||||
"""Run a workflow with an optional text parameter that is not provided."""
|
||||
workflow = """
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
input_data:
|
||||
type: data
|
||||
optional_text_param:
|
||||
type: text
|
||||
optional: true
|
||||
steps:
|
||||
cat_step:
|
||||
tool_id: cat
|
||||
in:
|
||||
input1: input_data
|
||||
outputs:
|
||||
output_data:
|
||||
outputSource: cat_step/out_file1
|
||||
"""
|
||||
test_data = """
|
||||
input_data:
|
||||
value: 1.bed
|
||||
type: File
|
||||
"""
|
||||
summary = self.workflow_populator.run_workflow(
|
||||
workflow,
|
||||
test_data=test_data,
|
||||
history_id=history_id,
|
||||
)
|
||||
return summary
|
||||
|
||||
def _export_invocation_to_format(self, extension: str, to_uri: bool):
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
summary = self._run_workflow_with_runtime_data_column_parameter(history_id)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
from galaxy.model.dataset_collections.workbook_util import index_to_excel_column
|
||||
import base64
|
||||
|
||||
from galaxy.model.dataset_collections.workbook_util import (
|
||||
index_to_excel_column,
|
||||
load_workbook_from_base64,
|
||||
)
|
||||
from galaxy.util.resources import resource_path
|
||||
|
||||
|
||||
def test_index_to_excel_column():
|
||||
@@ -8,3 +14,18 @@ def test_index_to_excel_column():
|
||||
assert index_to_excel_column(700) == "ZY"
|
||||
assert index_to_excel_column(701) == "ZZ"
|
||||
assert index_to_excel_column(702) == "AAA"
|
||||
|
||||
|
||||
def test_load_workbook_from_base64():
|
||||
workbook_base64 = resource_path_to_base64("filled_in_workbook_1.xlsx")
|
||||
workbook = load_workbook_from_base64(workbook_base64)
|
||||
assert workbook is not None
|
||||
|
||||
workbook_base64 = resource_path_to_base64("filled_in_workbook_1.tsv")
|
||||
workbook = load_workbook_from_base64(workbook_base64)
|
||||
assert workbook is not None
|
||||
|
||||
|
||||
def resource_path_to_base64(resource_name: str) -> str:
|
||||
resource_bytes = resource_path("galaxy.model.unittest_utils", resource_name).read_bytes()
|
||||
return base64.b64encode(resource_bytes).decode("utf-8")
|
||||
|
||||
Reference in New Issue
Block a user