Skip to content

Commit

Permalink
Merge branch 'release_23.1' into release_23.2
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Feb 14, 2024
2 parents ed7c257 + 91d9d49 commit ccb3e90
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 54 deletions.
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

0 comments on commit ccb3e90

Please sign in to comment.