Skip to content

Commit b80f418

Browse files
Merge remote-tracking branch 'upstream/release_24.2' into dev
2 parents 5024847 + 5180c13 commit b80f418

12 files changed

Lines changed: 157 additions & 69 deletions

File tree

client/src/components/Form/Elements/FormData/FormData.vue

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { type EventData, useEventStore } from "@/stores/eventStore";
2222
import { orList } from "@/utils/strings";
2323
2424
import type { DataOption } from "./types";
25+
import { containsDataOption } from "./types";
2526
import { BATCH, SOURCE, VARIANTS } from "./variants";
2627
2728
import FormSelection from "../FormSelection.vue";
@@ -384,49 +385,24 @@ function handleIncoming(incoming: Record<string, unknown> | Record<string, unkno
384385
const incomingValues: Array<DataOption> = [];
385386
values.forEach((currVal) => {
386387
// Map incoming objects to data option values
387-
const { newSrc, datasetCollectionDataset } = getSrcAndContentType(currVal);
388-
let v: HistoryOrCollectionItem | HDAObject;
389-
if (datasetCollectionDataset) {
390-
v = datasetCollectionDataset;
391-
} else {
392-
v = currVal;
393-
}
394-
const newHid = isHistoryItem(v) ? v.hid : undefined;
395-
const newId = v.id;
396-
const newName = isHistoryItem(v) && v.name ? v.name : newId;
397-
const newValue: DataOption = {
398-
id: newId,
399-
src: newSrc,
400-
batch: false,
401-
map_over_type: undefined,
402-
hid: newHid,
403-
name: newName,
404-
keep: true,
405-
tags: [],
406-
};
407-
if (isHistoryItem(v) && isHDCA(v) && props.collectionTypes?.length > 0) {
408-
const itemCollectionType = v.collection_type;
409-
if (!props.collectionTypes.includes(itemCollectionType as CollectionType)) {
410-
const mapOverType = props.collectionTypes.find((collectionType) =>
411-
itemCollectionType.endsWith(collectionType)
412-
);
413-
if (!mapOverType) {
414-
return false;
415-
}
416-
newValue["batch"] = true;
417-
newValue["map_over_type"] = mapOverType;
418-
}
388+
const newValue = toDataOption(currVal);
389+
if (!newValue) {
390+
return false;
419391
}
420392
// Verify that new value has corresponding option
421-
const keepKey = `${newId}_${newSrc}`;
422-
const existingOptions = props.options && props.options[newSrc];
423-
const foundOption = existingOptions && existingOptions.find((option) => option.id === newId);
393+
const keepKey = `${newValue.id}_${newValue.src}`;
394+
const existingOptions = props.options && props.options[newValue.src];
395+
const foundOption = existingOptions && existingOptions.find((option) => option.id === newValue.id);
424396
if (!foundOption && !(keepKey in keepOptions)) {
425-
keepOptions[keepKey] = { label: `${newHid || "Selected"}: ${newName}`, value: newValue };
397+
keepOptions[keepKey] = {
398+
label: `${newValue.hid || "Selected"}: ${newValue.name}`,
399+
value: newValue,
400+
};
426401
}
427402
// Add new value to list
428403
incomingValues.push(newValue);
429404
});
405+
let hasDuplicates = false;
430406
if (incomingValues.length > 0 && incomingValues[0]) {
431407
// Set new value
432408
const config = currentVariant.value;
@@ -435,21 +411,68 @@ function handleIncoming(incoming: Record<string, unknown> | Record<string, unkno
435411
if (config.multiple) {
436412
const newValues = currentValue.value ? currentValue.value.slice() : [];
437413
incomingValues.forEach((v) => {
438-
newValues.push(v);
414+
if (containsDataOption(newValues, v)) {
415+
hasDuplicates = true;
416+
} else {
417+
newValues.push(v);
418+
}
439419
});
440420
currentValue.value = newValues;
441421
} else {
422+
if (containsDataOption(currentValue.value ?? [], firstValue)) {
423+
hasDuplicates = true;
424+
}
442425
currentValue.value = [firstValue];
443426
}
444427
} else {
445428
currentValue.value = incomingValues;
446429
}
447430
}
431+
if (hasDuplicates) {
432+
return false;
433+
}
448434
}
449435
}
450436
return true;
451437
}
452438
439+
function toDataOption(item: HistoryOrCollectionItem): DataOption | null {
440+
const { newSrc, datasetCollectionDataset } = getSrcAndContentType(item);
441+
let v: HistoryOrCollectionItem | HDAObject;
442+
if (datasetCollectionDataset) {
443+
v = datasetCollectionDataset;
444+
} else {
445+
v = item;
446+
}
447+
const newHid = isHistoryItem(v) ? v.hid : undefined;
448+
const newId = v.id;
449+
const newName = isHistoryItem(v) && v.name ? v.name : newId;
450+
const newValue: DataOption = {
451+
id: newId,
452+
src: newSrc,
453+
batch: false,
454+
map_over_type: undefined,
455+
hid: newHid,
456+
name: newName,
457+
keep: true,
458+
tags: [],
459+
};
460+
if (isHistoryItem(v) && isHDCA(v) && props.collectionTypes?.length > 0) {
461+
const itemCollectionType = v.collection_type;
462+
if (!props.collectionTypes.includes(itemCollectionType as CollectionType)) {
463+
const mapOverType = props.collectionTypes.find((collectionType) =>
464+
itemCollectionType.endsWith(collectionType)
465+
);
466+
if (!mapOverType) {
467+
return null;
468+
}
469+
newValue["batch"] = true;
470+
newValue["map_over_type"] = mapOverType;
471+
}
472+
}
473+
return newValue;
474+
}
475+
453476
/**
454477
* Open file dialog
455478
*/
@@ -585,6 +608,16 @@ function isHistoryOrCollectionItem(item: EventData): item is HistoryOrCollection
585608
return isHistoryItem(item) || isDCE(item);
586609
}
587610
611+
function getNameForItem(item: HistoryOrCollectionItem): string {
612+
if (isHistoryItem(item)) {
613+
return item.name ?? `Item ${item.hid}`;
614+
} else if (isDCE(item)) {
615+
return item.element_identifier;
616+
} else {
617+
throw new Error("Unknown item type");
618+
}
619+
}
620+
588621
// Drag/Drop event handlers
589622
function onDragEnter(evt: DragEvent) {
590623
const eventData = eventStore.getDragItems();
@@ -605,6 +638,13 @@ function onDragEnter(evt: DragEvent) {
605638
highlightingState = "warning";
606639
$emit("alert", `${historyContentType} is not an acceptable input type for this parameter.`);
607640
}
641+
// Check if the item is already in the current value
642+
const option = toDataOption(item);
643+
const isAlreadyInValue = containsDataOption(currentValue.value ?? [], option);
644+
if (isAlreadyInValue) {
645+
highlightingState = "warning";
646+
$emit("alert", `${getNameForItem(item)} is already selected.`);
647+
}
608648
}
609649
}
610650
currentHighlighting.value = highlightingState;

client/src/components/Form/Elements/FormData/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ export type DataOption = {
1313
export function isDataOption(item: object): item is DataOption {
1414
return !!item && "src" in item;
1515
}
16+
17+
export function itemUniqueKey(item: DataOption): string {
18+
return `${item.src}-${item.id}`;
19+
}
20+
21+
export function containsDataOption(items: DataOption[], item: DataOption | null): boolean {
22+
return item !== null && items.some((i) => itemUniqueKey(i) === itemUniqueKey(item));
23+
}

client/src/components/Form/Elements/FormSelect.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useFilterObjectArray } from "@/composables/filter";
99
import { useMultiselect } from "@/composables/useMultiselect";
1010
import { uid } from "@/utils/utils";
1111
12-
import { type DataOption, isDataOption } from "./FormData/types";
12+
import { type DataOption, isDataOption, itemUniqueKey } from "./FormData/types";
1313
1414
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
1515
@@ -154,10 +154,6 @@ const currentValue = computed({
154154
},
155155
});
156156
157-
function itemUniqueKey(item: DataOption): string {
158-
return `${item.src}-${item.id}`;
159-
}
160-
161157
/**
162158
* Ensures that an initial value is selected for non-optional inputs
163159
*/

lib/galaxy/datatypes/binary.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -930,11 +930,12 @@ def set_meta(
930930
index_file = dataset.metadata.spec[spec_key].param.new_file(
931931
dataset=dataset, metadata_tmp_files_dir=metadata_tmp_files_dir
932932
)
933+
extra_threads = int(os.environ.get("GALAXY_SLOTS", 1)) - 1
933934
if index_flag == "-b":
934935
# IOError: No such file or directory: '-b' if index_flag is set to -b (pysam 0.15.4)
935-
pysam.index("-o", index_file.get_file_name(), dataset.get_file_name())
936+
pysam.index("-o", index_file.get_file_name(), f"-@{extra_threads}", dataset.get_file_name()) # type: ignore[attr-defined, unused-ignore]
936937
else:
937-
pysam.index(index_flag, "-o", index_file.get_file_name(), dataset.get_file_name())
938+
pysam.index(index_flag, "-o", index_file.get_file_name(), f"-@{extra_threads}", dataset.get_file_name()) # type: ignore[attr-defined, unused-ignore]
938939
dataset.metadata.bam_index = index_file
939940

940941
def sniff(self, filename: str) -> bool:
@@ -1124,8 +1125,9 @@ def get_cram_version(self, filename: str) -> Tuple[int, int]:
11241125
return -1, -1
11251126

11261127
def set_index_file(self, dataset: HasFileName, index_file) -> bool:
1128+
extra_threads = int(os.environ.get("GALAXY_SLOTS", 1)) - 1
11271129
try:
1128-
pysam.index("-o", index_file.get_file_name(), dataset.get_file_name())
1130+
pysam.index("-o", index_file.get_file_name(), f"-@{extra_threads}", dataset.get_file_name()) # type: ignore[attr-defined, unused-ignore]
11291131
return True
11301132
except Exception as exc:
11311133
log.warning("%s, set_index_file Exception: %s", self, exc)

lib/galaxy/files/sources/invenio.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,10 @@ def _get_download_file_url(self, record_id: str, filename: str, user_context: Op
403403
if is_draft_record:
404404
file_details_url = self._to_draft_url(file_details_url)
405405
download_file_content_url = self._to_draft_url(download_file_content_url)
406-
file_details = self._get_response(user_context, file_details_url)
407-
if not self._can_download_from_api(file_details):
408-
# TODO: This is a temporary workaround for the fact that the "content" API
409-
# does not support downloading files from S3 or other remote storage classes.
410-
# More info: https://inveniordm.docs.cern.ch/reference/file_storage/#remote-files-r
411-
download_file_content_url = f"{file_details_url.replace('/api', '')}?download=1"
406+
# Downloading through the API is only supported for local files and depends on how
407+
# the InvenioRDM instance file storage is configured.
408+
# So this is the most reliable way to download files for now it.
409+
download_file_content_url = f"{file_details_url.replace('/api', '')}?download=1"
412410
return download_file_content_url
413411

414412
def _is_api_url(self, url: str) -> bool:
@@ -417,11 +415,6 @@ def _is_api_url(self, url: str) -> bool:
417415
def _to_draft_url(self, url: str) -> str:
418416
return url.replace("/files/", "/draft/files/")
419417

420-
def _can_download_from_api(self, file_details: dict) -> bool:
421-
# Only files stored locally seems to be fully supported by the API for now
422-
# More info: https://inveniordm.docs.cern.ch/reference/file_storage/
423-
return file_details["storage_class"] == "L"
424-
425418
def _is_draft_record(self, record_id: str, user_context: OptionalUserContext = None):
426419
request_url = self._get_draft_record_url(record_id)
427420
headers = self._get_request_headers(user_context)

lib/galaxy/model/__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,12 @@
229229
from galaxy.util.sanitize_html import sanitize_html
230230

231231
if TYPE_CHECKING:
232+
from sqlalchemy.sql.expression import BindParameter
233+
232234
from galaxy.objectstore import (
233235
BaseObjectStore,
234236
ObjectStorePopulator,
237+
QuotaSourceMap,
235238
)
236239
from galaxy.schema.invocation import InvocationMessageUnion
237240

@@ -678,9 +681,9 @@ def stderr(self, stderr):
678681
"""
679682

680683

681-
def calculate_user_disk_usage_statements(user_id, quota_source_map, for_sqlite=False):
684+
def calculate_user_disk_usage_statements(user_id: int, quota_source_map: "QuotaSourceMap", for_sqlite: bool = False):
682685
"""Standalone function so can be reused for postgres directly in pgcleanup.py."""
683-
statements = []
686+
statements: List[Tuple[str, Dict[str, Any]]] = []
684687
default_quota_enabled = quota_source_map.default_quota_enabled
685688
default_exclude_ids = quota_source_map.default_usage_excluded_ids()
686689
default_cond = "dataset.object_store_id IS NULL" if default_quota_enabled and default_exclude_ids else ""
@@ -696,7 +699,7 @@ def calculate_user_disk_usage_statements(user_id, quota_source_map, for_sqlite=F
696699
UPDATE galaxy_user SET disk_usage = ({default_usage})
697700
WHERE id = :id
698701
"""
699-
params = {"id": user_id}
702+
params: Dict[str, Any] = {"id": user_id}
700703
if default_exclude_ids:
701704
params["exclude_object_store_ids"] = default_exclude_ids
702705
statements.append((default_usage, params))
@@ -1162,25 +1165,27 @@ def calculate_disk_usage_default_source(self, object_store):
11621165
usage = sa_session.scalar(sql_calc, params)
11631166
return usage
11641167

1165-
def calculate_and_set_disk_usage(self, object_store):
1168+
def calculate_and_set_disk_usage(self, object_store: "BaseObjectStore"):
11661169
"""
11671170
Calculates and sets user disk usage.
11681171
"""
11691172
self._calculate_or_set_disk_usage(object_store=object_store)
11701173

1171-
def _calculate_or_set_disk_usage(self, object_store):
1174+
def _calculate_or_set_disk_usage(self, object_store: "BaseObjectStore"):
11721175
"""
11731176
Utility to calculate and return the disk usage. If dryrun is False,
11741177
the new value is set immediately.
11751178
"""
11761179
assert object_store is not None
11771180
quota_source_map = object_store.get_quota_source_map()
11781181
sa_session = object_session(self)
1182+
assert sa_session
1183+
assert sa_session.bind
11791184
for_sqlite = "sqlite" in sa_session.bind.dialect.name
11801185
statements = calculate_user_disk_usage_statements(self.id, quota_source_map, for_sqlite)
11811186
for sql, args in statements:
11821187
statement = text(sql)
1183-
binds = []
1188+
binds: List[BindParameter] = []
11841189
for key, _ in args.items():
11851190
expand_binding = key.endswith("s")
11861191
binds.append(bindparam(key, expanding=expand_binding))

lib/galaxy/objectstore/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def to_dict(self) -> Dict[str, Any]:
403403
raise NotImplementedError()
404404

405405
@abc.abstractmethod
406-
def get_quota_source_map(self):
406+
def get_quota_source_map(self) -> "QuotaSourceMap":
407407
"""Return QuotaSourceMap describing mapping of object store IDs to quota sources."""
408408

409409
@abc.abstractmethod
@@ -715,7 +715,7 @@ def parse_badges_from_config_xml(clazz, badges_xml):
715715
badges.append({"type": type, "message": message})
716716
return badges
717717

718-
def get_quota_source_map(self):
718+
def get_quota_source_map(self) -> "QuotaSourceMap":
719719
# I'd rather keep this abstract... but register_singleton wants it to be instantiable...
720720
raise NotImplementedError()
721721

lib/galaxy/tool_util/linters/tests.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
TYPE_CHECKING,
99
)
1010

11+
from packaging.version import Version
12+
1113
from galaxy.tool_util.lint import Linter
1214
from galaxy.tool_util.parameters import validate_test_cases_for_tool_source
1315
from galaxy.tool_util.verify.assertion_models import assertion_list
@@ -166,10 +168,13 @@ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
166168
class TestsCaseValidation(Linter):
167169
@classmethod
168170
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
171+
profile = tool_source.parse_profile()
172+
lint_log = lint_ctx.warn if Version(profile) < Version("24.2") else lint_ctx.error
173+
169174
try:
170175
validation_results = validate_test_cases_for_tool_source(tool_source, use_latest_profile=True)
171176
except Exception as e:
172-
lint_ctx.warn(
177+
lint_log(
173178
f"Serious problem parsing tool source or tests - cannot validate test cases. The exception is [{e}]",
174179
linter=cls.name(),
175180
)
@@ -178,7 +183,7 @@ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
178183
error = validation_result.validation_error
179184
if error:
180185
error_str = _cleanup_pydantic_error(error)
181-
lint_ctx.warn(
186+
lint_log(
182187
f"Test {test_idx}: failed to validate test parameters against inputs - tests won't run on a modern Galaxy tool profile version. Validation errors are [{error_str}]",
183188
linter=cls.name(),
184189
)

0 commit comments

Comments
 (0)