Skip to content

Update the collection page table with the latest dynamic data table options #1225

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
41 changes: 41 additions & 0 deletions pydatalab/src/pydatalab/routes/v0_1/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,44 @@ def add_items_to_collection(collection_id):
)

return (jsonify({"status": "success"}), 200)


@COLLECTIONS.route("/collections/<collection_id>/items", methods=["DELETE"])
def remove_items_from_collection(collection_id):
data = request.get_json()
refcodes = data.get("refcodes", [])

collection = flask_mongo.db.collections.find_one(
{"collection_id": collection_id, **get_default_permissions()}, projection={"_id": 1}
)

if not collection:
return jsonify({"error": "Collection not found"}), 404

if not refcodes:
return jsonify({"error": "No refcodes provided"}), 400

update_result = flask_mongo.db.items.update_many(
{"refcode": {"$in": refcodes}, **get_default_permissions()},
{
"$pull": {
"relationships": {
"immutable_id": ObjectId(collection["_id"]),
"type": "collections",
}
}
},
)

if update_result.matched_count == 0:
return jsonify({"status": "error", "message": "No matching items found."}), 404

elif update_result.matched_count != len(refcodes):
return jsonify(
{
"status": "partial-success",
"message": f"Only {update_result.matched_count} items updated",
}
), 207

return jsonify({"status": "success", "removed_count": update_result.modified_count}), 200
183 changes: 183 additions & 0 deletions pydatalab/tests/server/test_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,3 +750,186 @@ def test_add_items_to_collection_success(client, default_collection, example_ite
child_refcodes = [item["refcode"] for item in collection_data["child_items"]]

assert all(refcode in child_refcodes for refcode in refcodes)


@pytest.mark.dependency()
def test_remove_items_from_collection_success(
client, database, default_sample_dict, default_collection
):
"""Test successfully removing items from a collection."""
sample_1_dict = default_sample_dict.copy()
sample_1_dict["item_id"] = "test_sample_remove_1"
sample_1_dict["collections"] = []

sample_2_dict = default_sample_dict.copy()
sample_2_dict["item_id"] = "test_sample_remove_2"
sample_2_dict["collections"] = []

for sample_dict in [sample_1_dict, sample_2_dict]:
response = client.post("/new-sample/", json=sample_dict)
assert response.status_code == 201

collection_dict = default_collection.dict()
collection_dict["collection_id"] = "test_collection_remove"
response = client.put("/collections", json={"data": collection_dict})
assert response.status_code == 201

collection_from_db = database.collections.find_one({"collection_id": "test_collection_remove"})
collection_object_id = collection_from_db["_id"]

item_ids = [sample_1_dict["item_id"], sample_2_dict["item_id"]]

for item_id in item_ids:
database.items.update_one(
{"item_id": item_id},
{
"$push": {
"relationships": {
"type": "collections",
"immutable_id": collection_object_id,
}
}
},
)

for item_id in item_ids:
item = database.items.find_one({"item_id": item_id})
assert item is not None
collection_relationships = [
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
]
assert len(collection_relationships) == 1

item_1_refcode = client.get(f"/get-item-data/{sample_1_dict['item_id']}").json["item_data"][
"refcode"
]
item_2_refcode = client.get(f"/get-item-data/{sample_2_dict['item_id']}").json["item_data"][
"refcode"
]
refcodes = [item_1_refcode, item_2_refcode]

response = client.delete(
"/collections/test_collection_remove/items", json={"refcodes": refcodes}
)

assert response.status_code == 200
data = response.get_json()
assert data["status"] == "success"
assert data["removed_count"] == 2

for item_id in item_ids:
item = database.items.find_one({"item_id": item_id})
assert item is not None
collection_relationships = [
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
]
assert len(collection_relationships) == 0


@pytest.mark.dependency()
def test_remove_items_from_collection_not_found(client):
"""Test removing items from non-existent collection."""
response = client.delete(
"/collections/nonexistent_collection/items", json={"refcodes": ["refcode1", "refcode2"]}
)

assert response.status_code == 404
data = response.get_json()
assert data["error"] == "Collection not found"


@pytest.mark.dependency()
def test_remove_items_from_collection_no_items_provided(client, default_collection):
"""Test removing with no item IDs provided."""
collection_dict = default_collection.dict()
collection_dict["collection_id"] = "test_collection_empty_items"
response = client.put("/collections", json={"data": collection_dict})
assert response.status_code == 201

collection_id = collection_dict["collection_id"]
response = client.delete(f"/collections/{collection_id}/items", json={"refcodes": []})

assert response.status_code == 400
data = response.get_json()
assert data["error"] == "No refcodes provided"


@pytest.mark.dependency()
def test_remove_items_from_collection_no_matching_items(client, default_collection):
"""Test removing items that don't exist."""
collection_dict = default_collection.dict()
collection_dict["collection_id"] = "test_collection_no_match"
response = client.put("/collections", json={"data": collection_dict})
assert response.status_code == 201

collection_id = collection_dict["collection_id"]
response = client.delete(
f"/collections/{collection_id}/items",
json={"refcodes": ["nonexistent_refcode_1", "nonexistent_refcode_2"]},
)

assert response.status_code == 404
data = response.get_json()
assert data["status"] == "error"
assert data["message"] == "No matching items found."


@pytest.mark.dependency()
def test_remove_items_from_collection_partial_success(
client, database, default_sample_dict, default_collection
):
"""Test removing items where some exist in collection and some don't."""
sample_dict = default_sample_dict.copy()
sample_dict["item_id"] = "test_sample_partial"
sample_dict["collections"] = []

response = client.post("/new-sample/", json=sample_dict)
assert response.status_code == 201

collection_dict = default_collection.dict()
collection_dict["collection_id"] = "test_collection_partial"
response = client.put("/collections", json={"data": collection_dict})
assert response.status_code == 201

collection_from_db = database.collections.find_one({"collection_id": "test_collection_partial"})
collection_object_id = collection_from_db["_id"]

item_id = sample_dict["item_id"]

database.items.update_one(
{"item_id": item_id},
{
"$push": {
"relationships": {
"type": "collections",
"immutable_id": collection_object_id,
}
}
},
)

item = database.items.find_one({"item_id": item_id})
collection_relationships = [
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
]
assert len(collection_relationships) == 1

item_refcode = client.get(f"/get-item-data/{sample_dict['item_id']}").json["item_data"][
"refcode"
]

response = client.delete(
"/collections/test_collection_partial/items",
json={"refcodes": [item_refcode, "nonexistent_refcode"]},
)

assert response.status_code == 207
data = response.get_json()
assert data["status"] == "partial-success"
assert "Only 1 items updated" in data["message"]

item = database.items.find_one({"item_id": item_id})
collection_relationships = [
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
]
assert len(collection_relationships) == 0
11 changes: 8 additions & 3 deletions webapp/src/components/CollectionInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,18 @@
<DynamicDataTable
:data="children"
:columns="collectionTableColumns"
:data-type="'samples'"
:data-type="'collectionItems'"
:global-filter-fields="[
'item_id',
'name',
'refcode',
'chemform',
'blocks',
'chemform',
'characteristic_chemical_formula',
]"
:show-buttons="false"
:show-buttons="true"
:collection-id="collection_id"
@remove-selected-items-from-collection="handleItemsRemovedFromCollection"
/>
</div>
</template>
Expand Down Expand Up @@ -115,6 +117,9 @@ export default {
this.fetchError = true;
});
},
handleItemsRemovedFromCollection() {
this.getCollectionChildren();
},
},
};
</script>
28 changes: 27 additions & 1 deletion webapp/src/components/CollectionSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
<script>
import vSelect from "vue-select";
import FormattedCollectionName from "@/components/FormattedCollectionName.vue";
import { searchCollections, createNewCollection } from "@/server_fetch_utils.js";
import {
searchCollections,
createNewCollection,
removeItemsFromCollection,
} from "@/server_fetch_utils.js";
import { validateEntryID } from "@/field_utils.js";
import { debounceTime } from "@/resources.js";

Expand All @@ -65,6 +69,7 @@ export default {
collections: [],
isSearchFetchError: false,
searchQuery: "",
pendingRemovals: [],
};
},
computed: {
Expand All @@ -74,6 +79,14 @@ export default {
return this.modelValue;
},
set(newValue) {
const oldIds = this.modelValue?.map((c) => c.collection_id) || [];
const newIds = newValue?.map((c) => c.collection_id) || [];
const removedIds = oldIds.filter((id) => !newIds.includes(id));

if (removedIds.length > 0) {
this.pendingRemovals.push(...removedIds);
}

this.$emit("update:modelValue", newValue);
},
},
Expand Down Expand Up @@ -151,6 +164,19 @@ export default {
}
}
},
async processPendingRemovals() {
if (this.pendingRemovals.length > 0) {
const item_id = this.item_id;
for (const collection_id of this.pendingRemovals) {
try {
await removeItemsFromCollection(collection_id, [item_id]);
} catch (error) {
console.error("Error removing item from collection:", error);
}
}
this.pendingRemovals = [];
}
},
},
};
</script>
Expand Down
12 changes: 12 additions & 0 deletions webapp/src/components/DynamicDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
:show-buttons="showButtons"
:available-columns="availableColumns"
:selected-columns="selectedColumns"
:collection-id="collectionId"
@update:filters="updateFilters"
@update:selected-columns="onToggleColumns"
@open-create-item-modal="createItemModalIsOpen = true"
Expand All @@ -54,6 +55,7 @@
@open-create-equipment-modal="createEquipmentModalIsOpen = true"
@open-add-to-collection-modal="addToCollectionModalIsOpen = true"
@delete-selected-items="deleteSelectedItems"
@remove-selected-items-from-collection="removeSelectedItemsFromCollection"
@reset-table="handleResetTable"
/>
</template>
Expand Down Expand Up @@ -322,7 +324,13 @@ export default {
required: false,
default: "edit",
},
collectionId: {
type: String,
required: false,
default: null,
},
},
emits: ["remove-selected-items-from-collection"],
data() {
return {
createItemModalIsOpen: false,
Expand Down Expand Up @@ -711,6 +719,10 @@ export default {
deleteSelectedItems() {
this.itemsSelected = [];
},
removeSelectedItemsFromCollection() {
this.itemsSelected = [];
this.$emit("remove-selected-items-from-collection");
},
handleItemsUpdated() {
this.itemsSelected = [];
},
Expand Down
Loading