Skip to content

Commit c3ea5ee

Browse files
committed
Fix merge records to merge variant types publications and comments
1 parent cad5d65 commit c3ea5ee

6 files changed

Lines changed: 232 additions & 15 deletions

File tree

gene2phenotype_project/gene2phenotype_app/fixtures/lgd_variant_type.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,29 @@
4646
"unknown_inheritance": false,
4747
"is_deleted": 1
4848
}
49+
},
50+
{
51+
"model": "gene2phenotype_app.lgdvarianttype",
52+
"pk": 5,
53+
"fields": {
54+
"lgd": 11,
55+
"variant_type_ot": 13,
56+
"inherited": false,
57+
"de_novo": false,
58+
"unknown_inheritance": false,
59+
"is_deleted": 0
60+
}
61+
},
62+
{
63+
"model": "gene2phenotype_app.lgdvarianttype",
64+
"pk": 6,
65+
"fields": {
66+
"lgd": 10,
67+
"variant_type_ot": 13,
68+
"inherited": false,
69+
"de_novo": false,
70+
"unknown_inheritance": false,
71+
"is_deleted": 0
72+
}
4973
}
5074
]

gene2phenotype_project/gene2phenotype_app/fixtures/lgd_variant_type_comment.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,17 @@
2222
"user": 1,
2323
"date": "2025-05-03T11:38:52Z"
2424
}
25+
},
26+
{
27+
"model": "gene2phenotype_app.lgdvarianttypecomment",
28+
"pk": 3,
29+
"fields": {
30+
"lgd_variant_type": 6,
31+
"comment": "Comment only on the source record's variant type",
32+
"is_public": 0,
33+
"is_deleted": 0,
34+
"user": 1,
35+
"date": "2025-06-01T09:00:00Z"
36+
}
2537
}
2638
]

gene2phenotype_project/gene2phenotype_app/fixtures/lgd_variant_type_publication.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,23 @@
3434
"publication": 2,
3535
"is_deleted": 1
3636
}
37+
},
38+
{
39+
"model": "gene2phenotype_app.lgdvarianttypepublication",
40+
"pk": 5,
41+
"fields": {
42+
"lgd_variant_type": 5,
43+
"publication": 2,
44+
"is_deleted": 0
45+
}
46+
},
47+
{
48+
"model": "gene2phenotype_app.lgdvarianttypepublication",
49+
"pk": 6,
50+
"fields": {
51+
"lgd_variant_type": 6,
52+
"publication": 1,
53+
"is_deleted": 0
54+
}
3755
}
3856
]

gene2phenotype_project/gene2phenotype_app/fixtures/locus_genotype_disease.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,39 @@
151151
"is_reviewed": 1,
152152
"is_deleted": 0
153153
}
154+
},
155+
{
156+
"model": "gene2phenotype_app.locusgenotypedisease",
157+
"pk": 10,
158+
"fields": {
159+
"stable_id": 12,
160+
"locus": 1,
161+
"genotype": 9,
162+
"disease": 6,
163+
"mechanism": 1,
164+
"mechanism_support": 17,
165+
"confidence": 1,
166+
"confidence_support": null,
167+
"date_review": "2025-06-01T09:00:00Z",
168+
"is_reviewed": 1,
169+
"is_deleted": 0
170+
}
171+
},
172+
{
173+
"model": "gene2phenotype_app.locusgenotypedisease",
174+
"pk": 11,
175+
"fields": {
176+
"stable_id": 13,
177+
"locus": 1,
178+
"genotype": 9,
179+
"disease": 1,
180+
"mechanism": 1,
181+
"mechanism_support": 17,
182+
"confidence": 1,
183+
"confidence_support": null,
184+
"date_review": "2025-06-01T09:00:00Z",
185+
"is_reviewed": 1,
186+
"is_deleted": 0
187+
}
154188
}
155189
]

gene2phenotype_project/gene2phenotype_app/tests/update_data/test_merge_records.py

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
LocusGenotypeDisease,
1010
LGDVariantType,
1111
LGDVariantTypeComment,
12+
LGDVariantTypePublication,
1213
LGDPublication,
1314
LGDMinedPublication,
1415
)
@@ -192,24 +193,32 @@ def test_merge_records(self):
192193
)
193194
# Check if all variant types were merged correctly
194195
# Note: a variant type is unique per (lgd, variant_type_ot), so the source
195-
# record's "intron_variant" row is skipped during the merge since the
196-
# target record already has one (with a different supporting publication)
196+
# record's "intron_variant" row is not itself moved (the target already has
197+
# one), but its supporting publication is merged onto the target's existing
198+
# "intron_variant" row instead of being lost
197199
lgd_variant_type_list = LGDVariantType.objects.filter(lgd=lgd_obj.id)
198200
self.assertEqual(len(lgd_variant_type_list), 2)
199201
for variant_type in lgd_variant_type_list:
200-
if (
201-
variant_type.publications.filter(
202-
publication_id=1, is_deleted=0
203-
).exists()
204-
and variant_type.variant_type_ot.term == "intron_variant"
205-
):
202+
if variant_type.variant_type_ot.term == "intron_variant":
203+
pub_ids = set(
204+
variant_type.publications.filter(is_deleted=0).values_list(
205+
"publication_id", flat=True
206+
)
207+
)
208+
self.assertEqual(pub_ids, {1, 2})
206209
variant_type_comment_list = LGDVariantTypeComment.objects.filter(
207210
lgd_variant_type=variant_type.id
208211
)
209212
self.assertEqual(
210213
variant_type_comment_list[0].comment,
211214
"Recurrent c.340C>T; other variant",
212215
)
216+
217+
# Check the source record's duplicate "intron_variant" publication was
218+
# reparented onto the target's row instead of being discarded
219+
moved_pub = LGDVariantTypePublication.objects.get(pk=2)
220+
self.assertEqual(moved_pub.lgd_variant_type_id, 3)
221+
self.assertEqual(moved_pub.is_deleted, 0)
213222
# Count the number of curated publications
214223
lgd_publication_list = LGDPublication.objects.filter(lgd=lgd_obj.id)
215224
self.assertEqual(len(lgd_publication_list), 4)
@@ -284,3 +293,72 @@ def test_merge_records_different_genotype(self):
284293
# Check mined publications
285294
lgd_mined_publication_list = LGDMinedPublication.objects.filter(lgd=lgd_obj.id)
286295
self.assertEqual(len(lgd_mined_publication_list), 1)
296+
297+
def test_merge_records_variant_type_comment_migration(self):
298+
"""
299+
Test that when a source record's variant type duplicates one already on
300+
the target record, the source's LGDVariantTypeComment and
301+
LGDVariantTypePublication are merged onto the target's existing row
302+
instead of being discarded.
303+
"""
304+
url_merge = reverse("merge_records")
305+
306+
records_to_merge = [
307+
{"g2p_ids": ["G2P00012"], "final_g2p_id": "G2P00013"}
308+
]
309+
310+
# Login
311+
user = User.objects.get(email="user5@test.ac.uk")
312+
refresh = RefreshToken.for_user(user)
313+
access_token = str(refresh.access_token)
314+
315+
# Authenticate by setting cookie on the test client
316+
self.client.cookies[settings.SIMPLE_JWT["AUTH_COOKIE"]] = access_token
317+
318+
response = self.client.post(
319+
url_merge, records_to_merge, content_type="application/json"
320+
)
321+
self.assertEqual(response.status_code, 200)
322+
323+
response_data = response.json()
324+
self.assertEqual(
325+
response_data["merged_records"], [["G2P00012 merged into G2P00013"]]
326+
)
327+
328+
stable_id_obj = G2PStableID.objects.get(stable_id="G2P00013")
329+
lgd_obj = LocusGenotypeDisease.objects.get(
330+
stable_id=stable_id_obj.id, is_deleted=0
331+
)
332+
333+
# Only one "missense_variant" row should survive on the target - the
334+
# source's duplicate row is not moved, it is soft-deleted
335+
lgd_variant_type_list = LGDVariantType.objects.filter(
336+
lgd=lgd_obj.id, is_deleted=0
337+
)
338+
self.assertEqual(len(lgd_variant_type_list), 1)
339+
surviving_variant_type = lgd_variant_type_list[0]
340+
self.assertEqual(surviving_variant_type.variant_type_ot.term, "missense_variant")
341+
342+
# The target's own publication and the source's publication should both
343+
# end up attached to the surviving row
344+
pub_ids = set(
345+
surviving_variant_type.publications.filter(is_deleted=0).values_list(
346+
"publication_id", flat=True
347+
)
348+
)
349+
self.assertEqual(pub_ids, {1, 2})
350+
351+
# The comment that only existed on the source's duplicate row should now
352+
# be attached to the target's surviving row, not deleted
353+
variant_type_comment_list = LGDVariantTypeComment.objects.filter(
354+
lgd_variant_type=surviving_variant_type.id, is_deleted=0
355+
)
356+
self.assertEqual(len(variant_type_comment_list), 1)
357+
self.assertEqual(
358+
variant_type_comment_list[0].comment,
359+
"Comment only on the source record's variant type",
360+
)
361+
362+
# The source's duplicate variant type row itself was soft-deleted
363+
source_variant_type = LGDVariantType.objects.get(pk=6)
364+
self.assertEqual(source_variant_type.is_deleted, 1)

gene2phenotype_project/gene2phenotype_app/views/locus_genotype_disease.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,7 +1918,7 @@ def MergeRecords(request):
19181918
LGDVariantTypeDescription, lgd_obj, lgd_obj_keep
19191919
)
19201920
move_related_objects(
1921-
LGDComment, lgd_obj, lgd_obj_keep
1921+
LGDComment, lgd_obj, lgd_obj_keep, ["comment"]
19221922
)
19231923
move_related_objects(
19241924
LGDMolecularMechanismSynopsis,
@@ -1937,12 +1937,8 @@ def MergeRecords(request):
19371937
lgd_obj_keep,
19381938
["ccm"],
19391939
)
1940-
move_related_objects(
1941-
LGDVariantType,
1942-
lgd_obj,
1943-
lgd_obj_keep,
1944-
["variant_type_ot"],
1945-
)
1940+
# Merge the variant types and their related objects (comments, publications)
1941+
merge_lgd_variant_types(lgd_obj, lgd_obj_keep)
19461942
move_related_objects(
19471943
LGDMolecularMechanismEvidence,
19481944
lgd_obj,
@@ -2138,6 +2134,61 @@ def delete_lgd_record(lgd_obj: Model) -> None:
21382134
lgd_obj.save()
21392135

21402136

2137+
@extend_schema(exclude=True)
2138+
def merge_lgd_variant_types(lgd_obj: Model, lgd_obj_keep: Model) -> None:
2139+
"""
2140+
Method to reassign LGDVariantType objects and their linked publications (LGDVariantTypePublication)
2141+
and comments (LGDVariantTypeComment) from a source LGD record to a target LGD record.
2142+
2143+
A variant type is unique per lgd+variant_type. When the source record has
2144+
a variant type that already exists on the target record, the source row itself
2145+
is left for delete_lgd_record() to soft-delete, but its LGDVariantTypePublication
2146+
and LGDVariantTypeComment children are reassigned onto the target's existing row
2147+
for that variant type.
2148+
2149+
Args:
2150+
lgd_obj (Model): The source LocusGenotypeDisease object (to merge from)
2151+
lgd_obj_keep (Model): The target LocusGenotypeDisease object (to merge into)
2152+
"""
2153+
variant_types: QuerySet = LGDVariantType.objects.filter(lgd=lgd_obj, is_deleted=0)
2154+
2155+
for variant_type_obj in variant_types:
2156+
if variant_type_obj.lgd_id == lgd_obj_keep.id:
2157+
continue
2158+
2159+
target_variant_type = LGDVariantType.objects.filter(
2160+
lgd=lgd_obj_keep,
2161+
variant_type_ot=variant_type_obj.variant_type_ot,
2162+
is_deleted=0,
2163+
).first()
2164+
2165+
if target_variant_type is None:
2166+
# If variant type not linked to the target record, move the row as-is.
2167+
# Its children move implicitly since they reference this row by id, not by lgd.
2168+
variant_type_obj.lgd = lgd_obj_keep
2169+
variant_type_obj.save()
2170+
continue
2171+
2172+
# If variant type already linked to the target record, don't move variant_type_obj itself,
2173+
# instead move its supporting publications/comments onto the target's existing row.
2174+
for pub in LGDVariantTypePublication.objects.filter(
2175+
lgd_variant_type=variant_type_obj, is_deleted=0
2176+
):
2177+
if not LGDVariantTypePublication.objects.filter(
2178+
lgd_variant_type=target_variant_type,
2179+
publication=pub.publication,
2180+
is_deleted=0,
2181+
).exists():
2182+
pub.lgd_variant_type = target_variant_type
2183+
pub.save()
2184+
2185+
for comment in LGDVariantTypeComment.objects.filter(
2186+
lgd_variant_type=variant_type_obj, is_deleted=0
2187+
):
2188+
comment.lgd_variant_type = target_variant_type
2189+
comment.save()
2190+
2191+
21412192
@extend_schema(exclude=True)
21422193
def move_related_objects(
21432194
model_class: Type[Model],

0 commit comments

Comments
 (0)