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

[23.1] Fix delete collection + elements #16879

Merged
5 changes: 4 additions & 1 deletion client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
:state="state"
:item-urls="itemUrls"
:keyboard-selectable="expandDataset"
@delete="$emit('delete')"
@delete="onDelete"
@display="onDisplay"
@showCollectionInfo="onShowCollectionInfo"
@edit="onEdit"
Expand Down Expand Up @@ -234,6 +234,9 @@ export default {
this.$router.push(this.itemUrls.display, { title: this.name });
}
},
onDelete(recursive = false) {
this.$emit("delete", this.item, recursive);
},
onDragStart(evt) {
setDrag(evt, this.item);
},
Expand Down
39 changes: 36 additions & 3 deletions client/src/components/History/Content/ContentOptions.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<span class="align-self-start btn-group">
<span class="align-self-start btn-group align-items-baseline">
<!-- Special case for collections -->
<b-button
v-if="isCollection && canShowCollectionDetails"
Expand Down Expand Up @@ -43,8 +43,21 @@
title="Delete"
size="sm"
variant="link"
@click.stop="$emit('delete')">
<icon icon="trash" />
@click.stop="onDelete($event)">
<icon v-if="isDataset" icon="trash" />
<b-dropdown v-else ref="deleteCollectionMenu" size="sm" variant="link" no-caret toggle-class="p-0 m-0">
<template v-slot:button-content>
<icon icon="trash" />
</template>
<b-dropdown-item title="Delete collection Only" @click.prevent.stop="onDeleteItem">
davelopez marked this conversation as resolved.
Show resolved Hide resolved
<icon icon="file" />
Collection only
</b-dropdown-item>
<b-dropdown-item title="Delete collection and elements" @click.prevent.stop="onDeleteItemRecursively">
<icon icon="copy" />
Collection and elements
</b-dropdown-item>
</b-dropdown>
</b-button>
<b-button
v-if="writable && isHistoryItem && isDeleted"
Expand Down Expand Up @@ -130,6 +143,26 @@ export default {
this.$emit("display");
}
},
onDelete() {
if (this.isCollection) {
this.$refs.deleteCollectionMenu.show();
} else {
this.onDeleteItem();
}
},
onDeleteItem() {
this.$emit("delete");
},
onDeleteItemRecursively() {
const recursive = true;
this.$emit("delete", recursive);
},
},
};
</script>
<style lang="css">
.dropdown-menu .dropdown-item {
font-weight: normal;
}
</style>
```
6 changes: 3 additions & 3 deletions client/src/components/History/Content/GenericItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
:is-dataset="item.history_content_type == 'dataset' || item.element_type == 'hda'"
@update:expand-dataset="expandDataset = $event"
@view-collection="viewCollection = !viewCollection"
@delete="onDelete(item)"
@delete="onDelete"
@toggleHighlights="onHighlight(item)"
@undelete="onUndelete(item)"
@unhide="onUnhide(item)" />
Expand Down Expand Up @@ -74,8 +74,8 @@ export default {
},
methods: {
...mapActions(useHistoryStore, ["applyFilters"]),
onDelete(item) {
deleteContent(item);
onDelete(item, recursive = false) {
deleteContent(item, { recursive: recursive });
},
onUndelete(item) {
updateContentFields(item, { deleted: false });
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/History/CurrentHistory/HistoryPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
@update:expand-dataset="setExpanded(item, $event)"
@update:selected="setSelected(item, $event)"
@view-collection="$emit('view-collection', item, currentOffset)"
@delete="onDelete(item)"
@delete="onDelete"
@undelete="onUndelete(item)"
@unhide="onUnhide(item)" />
</template>
Expand Down Expand Up @@ -357,9 +357,9 @@ export default {
this.loading = false;
}
},
onDelete(item) {
onDelete(item, recursive = false) {
this.setInvisible(item);
deleteContent(item);
deleteContent(item, { recursive: recursive });
},
onHideSelection(selectedItems) {
selectedItems.forEach((item) => {
Expand Down
32 changes: 32 additions & 0 deletions lib/galaxy/managers/hdcas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import logging
from typing import Dict

from sqlalchemy import (
false,
func,
select,
)

from galaxy import model
from galaxy.managers import (
annotatable,
Expand Down Expand Up @@ -103,6 +109,32 @@ 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()))

def delete(self, item, flush=True, **kwargs):
super().delete(item, flush=flush, **kwargs)
self.map_datasets(item, fn=self._delete_hda_if_orphaned_and_hidden)

def _delete_hda_if_orphaned_and_hidden(self, dataset, *parents):
if isinstance(dataset, model.HistoryDatasetAssociation):
if not dataset.deleted and not dataset.purged and not dataset.visible:
if not self._is_hda_used_in_multiple_active_collections(dataset):
super().delete(dataset, flush=False)

def _is_hda_used_in_multiple_active_collections(self, dataset: model.HistoryDatasetAssociation):
stmt = (
select(func.count(model.DatasetCollectionElement.id))
.join(
model.HistoryDatasetCollectionAssociation,
model.DatasetCollectionElement.dataset_collection_id
== model.HistoryDatasetCollectionAssociation.collection_id,
)
.where(
model.DatasetCollectionElement.hda_id == dataset.id,
model.HistoryDatasetCollectionAssociation.deleted == false(),
)
)
usage_count = self.session().execute(stmt).scalar()
return usage_count > 1


# serializers
# -----------------------------------------------------------------------------
Expand Down
67 changes: 67 additions & 0 deletions lib/galaxy_test/api/test_history_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,73 @@ def test_purging_collection_should_purge_contents(self):
if item["history_content_type"] == "dataset":
self.dataset_populator.wait_for_purge(history_id=history_id, content_id=item["id"])

def test_deleting_collection_should_delete_contents_only_if_orphaned(self):
with self.dataset_populator.test_history() as history_id:
num_expected_collections = 1
num_expected_datasets = 2
collection_ids = self._create_collection_in_history(history_id, num_expected_collections)
original_collection_id = collection_ids[0]
# Check datasets are hidden and not deleted
history_contents = self._get_history_contents(history_id)
datasets = list(filter(lambda item: item["history_content_type"] == "dataset", history_contents))
assert len(datasets) == num_expected_datasets
for dataset in datasets:
assert dataset["deleted"] is False
assert dataset["visible"] is False

# Create a new list collection with the same datasets
payload = {
"name": "New list with same datasets",
"type": "dataset_collection",
"collection_type": "list",
"copy_elements": False, # Reference the same datasets
"hide_source_items": True,
"element_identifiers": [
{"name": "el1", "src": "hda", "id": datasets[0]["id"]},
{"name": "el2", "src": "hda", "id": datasets[1]["id"]},
],
}
create_response = self._post(f"histories/{history_id}/contents", payload, json=True)
self._assert_status_code_is_ok(create_response)
new_collection_id = create_response.json()["id"]

# Delete the original collection
payload = {
"operation": "delete",
"items": [
{
"id": original_collection_id,
"history_content_type": "dataset_collection",
},
],
}
bulk_operation_result = self._apply_bulk_operation(history_id, payload)
self._assert_bulk_success(bulk_operation_result, 1)

# Check datasets are still not deleted
history_contents = self._get_history_contents(history_id)
for item in history_contents:
davelopez marked this conversation as resolved.
Show resolved Hide resolved
if item["history_content_type"] == "dataset":
assert item["deleted"] is False

# Delete the new collection too
payload = {
"operation": "delete",
"items": [
{
"id": new_collection_id,
"history_content_type": "dataset_collection",
},
],
}
bulk_operation_result = self._apply_bulk_operation(history_id, payload)
self._assert_bulk_success(bulk_operation_result, 1)

# Check datasets are deleted (and collections too)
history_contents = self._get_history_contents(history_id)
davelopez marked this conversation as resolved.
Show resolved Hide resolved
for item in history_contents:
assert item["deleted"] is True

@requires_new_user
def test_only_owner_can_apply_bulk_operations(self):
with self.dataset_populator.test_history() as history_id:
Expand Down
Loading