Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge release_23.1 into release_23.2 #17471

Merged
merged 14 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ const TASKS_CONFIG = {
enable_celery_tasks: true,
};

const getPurgedContentSelection = () => new Map([["FAKE_ID", { purged: true }]]);
const getNonPurgedContentSelection = () => new Map([["FAKE_ID", { purged: false }]]);
const getMenuSelectorFor = (option) => `[data-description="${option} option"]`;

const getPurgedSelection = () => new Map([["FAKE_ID", { purged: true }]]);
const getNonPurgedSelection = () => new Map([["FAKE_ID", { purged: false }]]);
const getVisibleSelection = () => new Map([["FAKE_ID", { visible: true }]]);
const getHiddenSelection = () => new Map([["FAKE_ID", { visible: false }]]);
const getDeletedSelection = () => new Map([["FAKE_ID", { deleted: true }]]);
const getActiveSelection = () => new Map([["FAKE_ID", { deleted: false }]]);

async function mountSelectionOperationsWrapper(config) {
mockFetcher.path("/api/configuration").method("get").mock({ data: config });
Expand Down Expand Up @@ -77,117 +83,155 @@ describe("History Selection Operations", () => {
expect(wrapper.find('[data-description="selected count"]').text()).toContain("10");
});

it("should display 'hide' option only on visible items", async () => {
const option = '[data-description="hide option"]';
it("should display 'hide' option on visible items", async () => {
const option = getMenuSelectorFor("hide");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "visible:true" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display 'hide' option when visible and hidden items are mixed", async () => {
const option = getMenuSelectorFor("hide");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "visible:any" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should not display 'hide' option when only hidden items are selected", async () => {
const option = getMenuSelectorFor("hide");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "visible:any", contentSelection: getHiddenSelection() });
expect(wrapper.find(option).exists()).toBe(false);
await wrapper.setProps({ filterText: "visible:false" });
expect(wrapper.find(option).exists()).toBe(false);
});

it("should display 'unhide' option on hidden items", async () => {
const option = '[data-description="unhide option"]';
expect(wrapper.find(option).exists()).toBe(false);
const option = getMenuSelectorFor("unhide");
await wrapper.setProps({ filterText: "visible:false" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display 'unhide' option when hidden and visible items are mixed", async () => {
const option = '[data-description="unhide option"]';
expect(wrapper.find(option).exists()).toBe(false);
const option = getMenuSelectorFor("unhide");
await wrapper.setProps({ filterText: "visible:any" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should not display 'unhide' option when only visible items are selected", async () => {
const option = getMenuSelectorFor("unhide");
await wrapper.setProps({
filterText: "visible:any",
contentSelection: getVisibleSelection(),
});
expect(wrapper.find(option).exists()).toBe(false);
});

it("should display 'delete' option on non-deleted items", async () => {
const option = '[data-description="delete option"]';
const option = getMenuSelectorFor("delete");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "deleted:false" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display 'delete' option on non-deleted items", async () => {
const option = getMenuSelectorFor("delete");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "deleted:false" });
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "deleted:true" });
expect(wrapper.find(option).exists()).toBe(false);
});

it("should display 'delete' option when non-deleted and deleted items are mixed", async () => {
const option = '[data-description="delete option"]';
const option = getMenuSelectorFor("delete");
await wrapper.setProps({ filterText: "deleted:any" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should not display 'delete' option when only deleted items are selected", async () => {
const option = getMenuSelectorFor("delete");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "deleted:any", contentSelection: getDeletedSelection() });
expect(wrapper.find(option).exists()).toBe(false);
});

it("should display 'permanently delete' option always", async () => {
const option = '[data-description="purge option"]';
const option = getMenuSelectorFor("purge");
expect(wrapper.find(option).exists()).toBe(true);
await wrapper.setProps({ filterText: "deleted:true" });
await wrapper.setProps({ filterText: "deleted:any visible:any" });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display 'undelete' option on deleted and non-purged items", async () => {
const option = '[data-description="undelete option"]';
const option = getMenuSelectorFor("undelete");
expect(wrapper.find(option).exists()).toBe(false);
await wrapper.setProps({
filterText: "deleted:true",
contentSelection: getNonPurgedContentSelection(),
contentSelection: getNonPurgedSelection(),
});
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display 'undelete' option when non-purged items (deleted or not) are mixed", async () => {
const option = '[data-description="undelete option"]';
const option = getMenuSelectorFor("undelete");
await wrapper.setProps({
filterText: "deleted:any",
contentSelection: getNonPurgedContentSelection(),
contentSelection: getNonPurgedSelection(),
});
expect(wrapper.find(option).exists()).toBe(true);
});

it("should not display 'undelete' when is manual selection mode and all selected items are purged", async () => {
const option = '[data-description="undelete option"]';
it("should not display 'undelete' when only non-deleted items are selected", async () => {
const option = getMenuSelectorFor("undelete");
await wrapper.setProps({
filterText: "deleted:true",
contentSelection: getPurgedContentSelection(),
filterText: "deleted:any",
contentSelection: getActiveSelection(),
});
expect(wrapper.find(option).exists()).toBe(false);
});

it("should not display 'undelete' when only purged items are selected", async () => {
const option = getMenuSelectorFor("undelete");
await wrapper.setProps({
contentSelection: getPurgedSelection(),
isQuerySelection: false,
});
expect(wrapper.find(option).exists()).toBe(false);
});

it("should display 'undelete' option when is query selection mode and filtering by deleted", async () => {
const option = '[data-description="undelete option"]';
const option = getMenuSelectorFor("undelete");
// In query selection mode we don't know if some items may not be purged, so we allow to undelete
await wrapper.setProps({
filterText: "deleted:true",
contentSelection: getPurgedContentSelection(),
contentSelection: getPurgedSelection(),
isQuerySelection: true,
});
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display 'undelete' option when is query selection mode and filtering by any deleted state", async () => {
const option = '[data-description="undelete option"]';
const option = getMenuSelectorFor("undelete");
// In query selection mode we don't know if some items may not be purged, so we allow to undelete
await wrapper.setProps({
filterText: "deleted:any",
contentSelection: getPurgedContentSelection(),
isQuerySelection: true,
});
await wrapper.setProps({ filterText: "deleted:any", isQuerySelection: true });
expect(wrapper.find(option).exists()).toBe(true);
});

it("should display collection building options only on visible and non-deleted items", async () => {
it("should display collection building options only on active (non-deleted) items", async () => {
const buildListOption = '[data-description="build list"]';
const buildPairOption = '[data-description="build pair"]';
const buildListOfPairsOption = '[data-description="build list of pairs"]';
await wrapper.setProps({ filterText: "visible:true deleted:false" });
expect(wrapper.find(buildListOption).exists()).toBe(true);
expect(wrapper.find(buildPairOption).exists()).toBe(true);
expect(wrapper.find(buildListOfPairsOption).exists()).toBe(true);
await wrapper.setProps({ filterText: "visible:false" });
expect(wrapper.find(buildListOption).exists()).toBe(false);
expect(wrapper.find(buildPairOption).exists()).toBe(false);
expect(wrapper.find(buildListOfPairsOption).exists()).toBe(false);
await wrapper.setProps({ filterText: "deleted:true" });
expect(wrapper.find(buildListOption).exists()).toBe(false);
expect(wrapper.find(buildPairOption).exists()).toBe(false);
expect(wrapper.find(buildListOfPairsOption).exists()).toBe(false);
await wrapper.setProps({ filterText: "visible:any" });
expect(wrapper.find(buildListOption).exists()).toBe(false);
expect(wrapper.find(buildPairOption).exists()).toBe(false);
expect(wrapper.find(buildListOfPairsOption).exists()).toBe(false);
await wrapper.setProps({ filterText: "visible:any deleted:false" });
expect(wrapper.find(buildListOption).exists()).toBe(true);
expect(wrapper.find(buildPairOption).exists()).toBe(true);
expect(wrapper.find(buildListOfPairsOption).exists()).toBe(true);
await wrapper.setProps({ filterText: "deleted:any" });
expect(wrapper.find(buildListOption).exists()).toBe(false);
expect(wrapper.find(buildPairOption).exists()).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
<b-dropdown-text>
<span v-localize data-description="selected count">With {{ numSelected }} selected...</span>
</b-dropdown-text>
<b-dropdown-item v-if="showHidden" v-b-modal:show-selected-content data-description="unhide option">
<b-dropdown-item v-if="canUnhideSelection" v-b-modal:show-selected-content data-description="unhide option">
<span v-localize>Unhide</span>
</b-dropdown-item>
<b-dropdown-item v-else v-b-modal:hide-selected-content data-description="hide option">
<b-dropdown-item v-if="canHideSelection" v-b-modal:hide-selected-content data-description="hide option">
<span v-localize>Hide</span>
</b-dropdown-item>
<b-dropdown-item
Expand All @@ -25,7 +25,7 @@
<span v-localize>Undelete</span>
</b-dropdown-item>
<b-dropdown-item
v-if="!showStrictDeleted"
v-if="canDeleteSelection"
v-b-modal:delete-selected-content
data-description="delete option">
<span v-localize>Delete</span>
Expand Down Expand Up @@ -193,20 +193,28 @@ export default {
},
computed: {
/** @returns {Boolean} */
showHidden() {
return !HistoryFilters.checkFilter(this.filterText, "visible", true);
canUnhideSelection() {
return this.areAllSelectedHidden || (this.isAnyVisibilityAllowed && !this.areAllSelectedVisible);
},
/** @returns {Boolean} */
canHideSelection() {
return this.areAllSelectedVisible || (this.isAnyVisibilityAllowed && !this.areAllSelectedHidden);
},
/** @returns {Boolean} */
showDeleted() {
return !HistoryFilters.checkFilter(this.filterText, "deleted", false);
},
/** @returns {Boolean} */
showStrictDeleted() {
return HistoryFilters.checkFilter(this.filterText, "deleted", true);
canDeleteSelection() {
return this.areAllSelectedActive || (this.isAnyDeletedStateAllowed && !this.areAllSelectedDeleted);
},
/** @returns {Boolean} */
canUndeleteSelection() {
return this.showDeleted && (this.isQuerySelection || !this.areAllSelectedPurged);
},
/** @returns {Boolean} */
showBuildOptions() {
return !this.isQuerySelection && !this.showHidden && !this.showDeleted;
return !this.isQuerySelection && this.areAllSelectedActive && !this.showDeleted;
},
/** @returns {Boolean} */
showBuildOptionForAll() {
Expand All @@ -227,9 +235,6 @@ export default {
noTagsSelected() {
return this.selectedTags.length === 0;
},
canUndeleteSelection() {
return this.showDeleted && (this.isQuerySelection || !this.areAllSelectedPurged);
},
areAllSelectedPurged() {
for (const item of this.contentSelection.values()) {
if (Object.prototype.hasOwnProperty.call(item, "purged") && !item["purged"]) {
Expand All @@ -238,6 +243,44 @@ export default {
}
return true;
},
areAllSelectedVisible() {
for (const item of this.contentSelection.values()) {
if (Object.prototype.hasOwnProperty.call(item, "visible") && !item["visible"]) {
return false;
}
}
return true;
},
areAllSelectedHidden() {
for (const item of this.contentSelection.values()) {
if (Object.prototype.hasOwnProperty.call(item, "visible") && item["visible"]) {
return false;
}
}
return true;
},
areAllSelectedActive() {
for (const item of this.contentSelection.values()) {
if (Object.prototype.hasOwnProperty.call(item, "deleted") && item["deleted"]) {
return false;
}
}
return true;
},
areAllSelectedDeleted() {
for (const item of this.contentSelection.values()) {
if (Object.prototype.hasOwnProperty.call(item, "deleted") && !item["deleted"]) {
return false;
}
}
return true;
},
isAnyVisibilityAllowed() {
return HistoryFilters.checkFilter(this.filterText, "visible", "any");
},
isAnyDeletedStateAllowed() {
return HistoryFilters.checkFilter(this.filterText, "deleted", "any");
},
},
watch: {
hasSelection(newVal) {
Expand Down
9 changes: 7 additions & 2 deletions lib/galaxy/files/sources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import abc
import os
import time
from dataclasses import (
dataclass,
field,
)
from enum import Enum
from typing import (
Any,
Expand Down Expand Up @@ -88,19 +92,20 @@ class FilesSourceProperties(TypedDict):
browsable: NotRequired[bool]


@dataclass
class FilesSourceOptions:
"""Options to control behavior of file source operations, such as realize_to, write_from and list."""

# Indicates access to the FS operation with intent to write.
# Even if a file source is "writeable" some directories (or elements) may be restricted or read-only
# so those should be skipped while browsing with writeable=True.
writeable: Optional[bool]
writeable: Optional[bool] = False

# Property overrides for values initially configured through the constructor. For example
# the HTTPFilesSource passes in additional http_headers through these properties, which
# are merged with constructor defined http_headers. The interpretation of these properties
# are filesystem specific.
extra_props: Optional[FilesSourceProperties]
extra_props: Optional[FilesSourceProperties] = field(default_factory=lambda: FilesSourceProperties())


class EntryData(TypedDict):
Expand Down
4 changes: 2 additions & 2 deletions lib/galaxy/webapps/galaxy/services/dataset_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ def contents(
raise exceptions.RequestParameterInvalidException(
"Parameter instance_type not being 'history' is not yet implemented."
)
hdca: HistoryDatasetCollectionAssociation = self.collection_manager.get_dataset_collection_instance(
trans, "history", hdca_id, check_ownership=True
hdca: "HistoryDatasetCollectionAssociation" = self.collection_manager.get_dataset_collection_instance(
trans, "history", hdca_id
)

# check to make sure the dsc is part of the validated hdca
Expand Down
12 changes: 12 additions & 0 deletions lib/galaxy_test/api/test_dataset_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,18 @@ def test_collection_contents_security(self, history_id):
contents_response = self._get(contents_url)
self._assert_status_code_is(contents_response, 403)

@requires_new_user
def test_published_collection_contents_accessible(self, history_id):
# request contents on an hdca that is in a published history
hdca, contents_url = self._create_collection_contents_pair(history_id)
with self._different_user():
contents_response = self._get(contents_url)
self._assert_status_code_is(contents_response, 403)
self.dataset_populator.make_public(history_id)
with self._different_user():
contents_response = self._get(contents_url)
self._assert_status_code_is(contents_response, 200)

def test_collection_contents_invalid_collection(self, history_id):
# request an invalid collection from a valid hdca, should get 404
hdca, contents_url = self._create_collection_contents_pair(history_id)
Expand Down
Loading