From bd0e960ea99f5a662629e82b89412aed14a93545 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Fri, 3 Oct 2025 11:13:49 +0200 Subject: [PATCH] clean up relationships and constituents on item deletion --- pydatalab/src/pydatalab/routes/v0_1/items.py | 70 +++++++++++++++++++- pydatalab/tests/server/test_graph.py | 58 ++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/pydatalab/src/pydatalab/routes/v0_1/items.py b/pydatalab/src/pydatalab/routes/v0_1/items.py index b51e0a739..01c5ad1c1 100644 --- a/pydatalab/src/pydatalab/routes/v0_1/items.py +++ b/pydatalab/src/pydatalab/routes/v0_1/items.py @@ -808,9 +808,30 @@ def update_item_permissions(refcode: str): @ITEMS.route("/delete-sample/", methods=["POST"]) def delete_sample(): - request_json = request.get_json() # noqa: F821 pylint: disable=undefined-variable + request_json = request.get_json() item_id = request_json["item_id"] + item_to_delete = flask_mongo.db.items.find_one( + {"item_id": item_id, **get_default_permissions(user_only=True, deleting=True)}, + projection={"refcode": 1, "_id": 1, "name": 1, "chemform": 1}, + ) + + if not item_to_delete: + return ( + jsonify( + { + "status": "error", + "message": f"Authorization required to attempt to delete sample with {item_id=} from the database.", + } + ), + 401, + ) + + refcode = item_to_delete.get("refcode") + immutable_id = item_to_delete["_id"] + item_name = item_to_delete.get("name") or item_id + item_chemform = item_to_delete.get("chemform") + result = flask_mongo.db.items.delete_one( {"item_id": item_id, **get_default_permissions(user_only=True, deleting=True)} ) @@ -825,6 +846,53 @@ def delete_sample(): ), 401, ) + + flask_mongo.db.items.update_many( + {"relationships.item_id": item_id}, {"$pull": {"relationships": {"item_id": item_id}}} + ) + + if refcode: + flask_mongo.db.items.update_many( + {"relationships.refcode": refcode}, {"$pull": {"relationships": {"refcode": refcode}}} + ) + + flask_mongo.db.items.update_many( + {"relationships.immutable_id": immutable_id}, + {"$pull": {"relationships": {"immutable_id": immutable_id}}}, + ) + + inline_item = {"name": item_name} + if item_chemform: + inline_item["chemform"] = item_chemform + + items_with_constituent = flask_mongo.db.items.find( + { + "$or": [ + {"synthesis_constituents.item.item_id": item_id}, + {"synthesis_constituents.item.refcode": refcode}, + {"synthesis_constituents.item.immutable_id": immutable_id}, + ] + } + ) + + for item in items_with_constituent: + updated_constituents = [] + for constituent in item.get("synthesis_constituents", []): + constituent_item = constituent.get("item", {}) + + if ( + constituent_item.get("item_id") == item_id + or constituent_item.get("refcode") == refcode + or constituent_item.get("immutable_id") == immutable_id + ): + constituent["item"] = inline_item.copy() + + updated_constituents.append(constituent) + + flask_mongo.db.items.update_one( + {"_id": item["_id"]}, {"$set": {"synthesis_constituents": updated_constituents}} + ) + return ( jsonify( { diff --git a/pydatalab/tests/server/test_graph.py b/pydatalab/tests/server/test_graph.py index e916af7b2..f4f2f0c35 100644 --- a/pydatalab/tests/server/test_graph.py +++ b/pydatalab/tests/server/test_graph.py @@ -158,3 +158,61 @@ def test_simple_graph(admin_client): graph = admin_client.get("/item-graph/parent").json assert len(graph["nodes"]) == 6 assert len(graph["edges"]) == 5 + + +def test_delete_item_cleans_relationships_and_constituents(admin_client): + """Test that deleting an item removes relationships and transforms synthesis_constituents to inline.""" + + parent = Sample(item_id="parent_to_delete", name="Test Parent", chemform="NaCl") + response = admin_client.post( + "/new-sample/", + json={"new_sample_data": json.loads(parent.json())}, + ) + assert response.status_code == 201 + + child = Sample( + item_id="child_with_constituent", + synthesis_constituents=[ + Constituent(item={"type": "samples", "item_id": "parent_to_delete"}, quantity=5.0) + ], + ) + response = admin_client.post( + "/new-sample/", + json={"new_sample_data": json.loads(child.json())}, + ) + assert response.status_code == 201 + + response = admin_client.get("/get-item-data/child_with_constituent") + assert response.status_code == 200 + assert "parent_to_delete" in response.json["parent_items"] + relationships = response.json["item_data"]["relationships"] + assert any(r.get("item_id") == "parent_to_delete" for r in relationships) + + constituents = response.json["item_data"]["synthesis_constituents"] + assert len(constituents) == 1 + assert constituents[0]["item"]["item_id"] == "parent_to_delete" + assert constituents[0]["item"]["type"] == "samples" + + response = admin_client.post("/delete-sample/", json={"item_id": "parent_to_delete"}) + assert response.status_code == 200 + + response = admin_client.get("/get-item-data/child_with_constituent") + assert response.status_code == 200 + assert "parent_to_delete" not in response.json["parent_items"] + relationships = response.json["item_data"]["relationships"] + assert not any(r.get("item_id") == "parent_to_delete" for r in relationships) + + constituents = response.json["item_data"]["synthesis_constituents"] + assert len(constituents) == 1 + constituent_item = constituents[0]["item"] + assert constituent_item["name"] == "Test Parent" + assert constituent_item["chemform"] == "NaCl" + assert "item_id" not in constituent_item + assert "refcode" not in constituent_item + assert "type" not in constituent_item + assert "immutable_id" not in constituent_item + + graph = admin_client.get("/item-graph/child_with_constituent").json + assert graph["status"] == "success" + assert len(graph["nodes"]) == 1 + assert graph["nodes"][0]["data"]["id"] == "child_with_constituent"