Skip to content

Commit 436bea0

Browse files
Update the collection page table with the latest dynamic data table options (#1225)
Co-authored-by: Matthew Evans <[email protected]>
1 parent 2db7144 commit 436bea0

File tree

11 files changed

+365
-23
lines changed

11 files changed

+365
-23
lines changed

pydatalab/src/pydatalab/routes/v0_1/collections.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,44 @@ def add_items_to_collection(collection_id):
419419
)
420420

421421
return (jsonify({"status": "success"}), 200)
422+
423+
424+
@COLLECTIONS.route("/collections/<collection_id>/items", methods=["DELETE"])
425+
def remove_items_from_collection(collection_id):
426+
data = request.get_json()
427+
refcodes = data.get("refcodes", [])
428+
429+
collection = flask_mongo.db.collections.find_one(
430+
{"collection_id": collection_id, **get_default_permissions()}, projection={"_id": 1}
431+
)
432+
433+
if not collection:
434+
return jsonify({"error": "Collection not found"}), 404
435+
436+
if not refcodes:
437+
return jsonify({"error": "No refcodes provided"}), 400
438+
439+
update_result = flask_mongo.db.items.update_many(
440+
{"refcode": {"$in": refcodes}, **get_default_permissions()},
441+
{
442+
"$pull": {
443+
"relationships": {
444+
"immutable_id": ObjectId(collection["_id"]),
445+
"type": "collections",
446+
}
447+
}
448+
},
449+
)
450+
451+
if update_result.matched_count == 0:
452+
return jsonify({"status": "error", "message": "No matching items found."}), 404
453+
454+
elif update_result.matched_count != len(refcodes):
455+
return jsonify(
456+
{
457+
"status": "partial-success",
458+
"message": f"Only {update_result.matched_count} items updated",
459+
}
460+
), 207
461+
462+
return jsonify({"status": "success", "removed_count": update_result.modified_count}), 200

pydatalab/tests/server/test_samples.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,3 +750,186 @@ def test_add_items_to_collection_success(client, default_collection, example_ite
750750
child_refcodes = [item["refcode"] for item in collection_data["child_items"]]
751751

752752
assert all(refcode in child_refcodes for refcode in refcodes)
753+
754+
755+
@pytest.mark.dependency()
756+
def test_remove_items_from_collection_success(
757+
client, database, default_sample_dict, default_collection
758+
):
759+
"""Test successfully removing items from a collection."""
760+
sample_1_dict = default_sample_dict.copy()
761+
sample_1_dict["item_id"] = "test_sample_remove_1"
762+
sample_1_dict["collections"] = []
763+
764+
sample_2_dict = default_sample_dict.copy()
765+
sample_2_dict["item_id"] = "test_sample_remove_2"
766+
sample_2_dict["collections"] = []
767+
768+
for sample_dict in [sample_1_dict, sample_2_dict]:
769+
response = client.post("/new-sample/", json=sample_dict)
770+
assert response.status_code == 201
771+
772+
collection_dict = default_collection.dict()
773+
collection_dict["collection_id"] = "test_collection_remove"
774+
response = client.put("/collections", json={"data": collection_dict})
775+
assert response.status_code == 201
776+
777+
collection_from_db = database.collections.find_one({"collection_id": "test_collection_remove"})
778+
collection_object_id = collection_from_db["_id"]
779+
780+
item_ids = [sample_1_dict["item_id"], sample_2_dict["item_id"]]
781+
782+
for item_id in item_ids:
783+
database.items.update_one(
784+
{"item_id": item_id},
785+
{
786+
"$push": {
787+
"relationships": {
788+
"type": "collections",
789+
"immutable_id": collection_object_id,
790+
}
791+
}
792+
},
793+
)
794+
795+
for item_id in item_ids:
796+
item = database.items.find_one({"item_id": item_id})
797+
assert item is not None
798+
collection_relationships = [
799+
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
800+
]
801+
assert len(collection_relationships) == 1
802+
803+
item_1_refcode = client.get(f"/get-item-data/{sample_1_dict['item_id']}").json["item_data"][
804+
"refcode"
805+
]
806+
item_2_refcode = client.get(f"/get-item-data/{sample_2_dict['item_id']}").json["item_data"][
807+
"refcode"
808+
]
809+
refcodes = [item_1_refcode, item_2_refcode]
810+
811+
response = client.delete(
812+
"/collections/test_collection_remove/items", json={"refcodes": refcodes}
813+
)
814+
815+
assert response.status_code == 200
816+
data = response.get_json()
817+
assert data["status"] == "success"
818+
assert data["removed_count"] == 2
819+
820+
for item_id in item_ids:
821+
item = database.items.find_one({"item_id": item_id})
822+
assert item is not None
823+
collection_relationships = [
824+
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
825+
]
826+
assert len(collection_relationships) == 0
827+
828+
829+
@pytest.mark.dependency()
830+
def test_remove_items_from_collection_not_found(client):
831+
"""Test removing items from non-existent collection."""
832+
response = client.delete(
833+
"/collections/nonexistent_collection/items", json={"refcodes": ["refcode1", "refcode2"]}
834+
)
835+
836+
assert response.status_code == 404
837+
data = response.get_json()
838+
assert data["error"] == "Collection not found"
839+
840+
841+
@pytest.mark.dependency()
842+
def test_remove_items_from_collection_no_items_provided(client, default_collection):
843+
"""Test removing with no item IDs provided."""
844+
collection_dict = default_collection.dict()
845+
collection_dict["collection_id"] = "test_collection_empty_items"
846+
response = client.put("/collections", json={"data": collection_dict})
847+
assert response.status_code == 201
848+
849+
collection_id = collection_dict["collection_id"]
850+
response = client.delete(f"/collections/{collection_id}/items", json={"refcodes": []})
851+
852+
assert response.status_code == 400
853+
data = response.get_json()
854+
assert data["error"] == "No refcodes provided"
855+
856+
857+
@pytest.mark.dependency()
858+
def test_remove_items_from_collection_no_matching_items(client, default_collection):
859+
"""Test removing items that don't exist."""
860+
collection_dict = default_collection.dict()
861+
collection_dict["collection_id"] = "test_collection_no_match"
862+
response = client.put("/collections", json={"data": collection_dict})
863+
assert response.status_code == 201
864+
865+
collection_id = collection_dict["collection_id"]
866+
response = client.delete(
867+
f"/collections/{collection_id}/items",
868+
json={"refcodes": ["nonexistent_refcode_1", "nonexistent_refcode_2"]},
869+
)
870+
871+
assert response.status_code == 404
872+
data = response.get_json()
873+
assert data["status"] == "error"
874+
assert data["message"] == "No matching items found."
875+
876+
877+
@pytest.mark.dependency()
878+
def test_remove_items_from_collection_partial_success(
879+
client, database, default_sample_dict, default_collection
880+
):
881+
"""Test removing items where some exist in collection and some don't."""
882+
sample_dict = default_sample_dict.copy()
883+
sample_dict["item_id"] = "test_sample_partial"
884+
sample_dict["collections"] = []
885+
886+
response = client.post("/new-sample/", json=sample_dict)
887+
assert response.status_code == 201
888+
889+
collection_dict = default_collection.dict()
890+
collection_dict["collection_id"] = "test_collection_partial"
891+
response = client.put("/collections", json={"data": collection_dict})
892+
assert response.status_code == 201
893+
894+
collection_from_db = database.collections.find_one({"collection_id": "test_collection_partial"})
895+
collection_object_id = collection_from_db["_id"]
896+
897+
item_id = sample_dict["item_id"]
898+
899+
database.items.update_one(
900+
{"item_id": item_id},
901+
{
902+
"$push": {
903+
"relationships": {
904+
"type": "collections",
905+
"immutable_id": collection_object_id,
906+
}
907+
}
908+
},
909+
)
910+
911+
item = database.items.find_one({"item_id": item_id})
912+
collection_relationships = [
913+
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
914+
]
915+
assert len(collection_relationships) == 1
916+
917+
item_refcode = client.get(f"/get-item-data/{sample_dict['item_id']}").json["item_data"][
918+
"refcode"
919+
]
920+
921+
response = client.delete(
922+
"/collections/test_collection_partial/items",
923+
json={"refcodes": [item_refcode, "nonexistent_refcode"]},
924+
)
925+
926+
assert response.status_code == 207
927+
data = response.get_json()
928+
assert data["status"] == "partial-success"
929+
assert "Only 1 items updated" in data["message"]
930+
931+
item = database.items.find_one({"item_id": item_id})
932+
collection_relationships = [
933+
rel for rel in item.get("relationships", []) if rel.get("type") == "collections"
934+
]
935+
assert len(collection_relationships) == 0

webapp/src/components/CollectionInformation.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,18 @@
3838
<DynamicDataTable
3939
:data="children"
4040
:columns="collectionTableColumns"
41-
:data-type="'samples'"
41+
:data-type="'collectionItems'"
4242
:global-filter-fields="[
4343
'item_id',
4444
'name',
4545
'refcode',
46-
'chemform',
4746
'blocks',
47+
'chemform',
4848
'characteristic_chemical_formula',
4949
]"
50-
:show-buttons="false"
50+
:show-buttons="true"
51+
:collection-id="collection_id"
52+
@remove-selected-items-from-collection="handleItemsRemovedFromCollection"
5153
/>
5254
</div>
5355
</template>
@@ -115,6 +117,9 @@ export default {
115117
this.fetchError = true;
116118
});
117119
},
120+
handleItemsRemovedFromCollection() {
121+
this.getCollectionChildren();
122+
},
118123
},
119124
};
120125
</script>

webapp/src/components/CollectionSelect.vue

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@
3939
<script>
4040
import vSelect from "vue-select";
4141
import FormattedCollectionName from "@/components/FormattedCollectionName.vue";
42-
import { searchCollections, createNewCollection } from "@/server_fetch_utils.js";
42+
import {
43+
searchCollections,
44+
createNewCollection,
45+
removeItemsFromCollection,
46+
} from "@/server_fetch_utils.js";
4347
import { validateEntryID } from "@/field_utils.js";
4448
import { debounceTime } from "@/resources.js";
4549
@@ -65,6 +69,7 @@ export default {
6569
collections: [],
6670
isSearchFetchError: false,
6771
searchQuery: "",
72+
pendingRemovals: [],
6873
};
6974
},
7075
computed: {
@@ -74,6 +79,14 @@ export default {
7479
return this.modelValue;
7580
},
7681
set(newValue) {
82+
const oldIds = this.modelValue?.map((c) => c.collection_id) || [];
83+
const newIds = newValue?.map((c) => c.collection_id) || [];
84+
const removedIds = oldIds.filter((id) => !newIds.includes(id));
85+
86+
if (removedIds.length > 0) {
87+
this.pendingRemovals.push(...removedIds);
88+
}
89+
7790
this.$emit("update:modelValue", newValue);
7891
},
7992
},
@@ -151,6 +164,19 @@ export default {
151164
}
152165
}
153166
},
167+
async processPendingRemovals() {
168+
if (this.pendingRemovals.length > 0) {
169+
const item_id = this.item_id;
170+
for (const collection_id of this.pendingRemovals) {
171+
try {
172+
await removeItemsFromCollection(collection_id, [item_id]);
173+
} catch (error) {
174+
console.error("Error removing item from collection:", error);
175+
}
176+
}
177+
this.pendingRemovals = [];
178+
}
179+
},
154180
},
155181
};
156182
</script>

webapp/src/components/DynamicDataTable.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
:show-buttons="showButtons"
4646
:available-columns="availableColumns"
4747
:selected-columns="selectedColumns"
48+
:collection-id="collectionId"
4849
@update:filters="updateFilters"
4950
@update:selected-columns="onToggleColumns"
5051
@open-create-item-modal="createItemModalIsOpen = true"
@@ -54,6 +55,7 @@
5455
@open-create-equipment-modal="createEquipmentModalIsOpen = true"
5556
@open-add-to-collection-modal="addToCollectionModalIsOpen = true"
5657
@delete-selected-items="deleteSelectedItems"
58+
@remove-selected-items-from-collection="removeSelectedItemsFromCollection"
5759
@reset-table="handleResetTable"
5860
/>
5961
</template>
@@ -322,7 +324,13 @@ export default {
322324
required: false,
323325
default: "edit",
324326
},
327+
collectionId: {
328+
type: String,
329+
required: false,
330+
default: null,
331+
},
325332
},
333+
emits: ["remove-selected-items-from-collection"],
326334
data() {
327335
return {
328336
createItemModalIsOpen: false,
@@ -711,6 +719,10 @@ export default {
711719
deleteSelectedItems() {
712720
this.itemsSelected = [];
713721
},
722+
removeSelectedItemsFromCollection() {
723+
this.itemsSelected = [];
724+
this.$emit("remove-selected-items-from-collection");
725+
},
714726
handleItemsUpdated() {
715727
this.itemsSelected = [];
716728
},

0 commit comments

Comments
 (0)