diff --git a/HISTORY.rst b/HISTORY.rst
index c6a610fb9..932cb89d3 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -44,6 +44,7 @@ End-User Summary
- Fixing CADD annotation (#319)
- Adding mitochondrial inheritance to case phenotype annotation (#325)
- Fix issue with variant annotation export (#328)
+- Allowing direct update of variant annotations and ACMG ratings on case annotations details (#344)
Full Change List
================
@@ -96,6 +97,7 @@ Full Change List
- Make migrations compatible with Postgres 14 (#338)
- DgvSvs and DgvGoldStandardSvs are two different data sources now
- Adding deep linking into case details tab (#344)
+- Allowing direct update of variant annotations and ACMG ratings on case annotations details (#344)
-------
v0.23.9
diff --git a/svs/templates/svs/filter_result/table.html b/svs/templates/svs/filter_result/table.html
index b4e2bbe2f..a43988fa1 100644
--- a/svs/templates/svs/filter_result/table.html
+++ b/svs/templates/svs/filter_result/table.html
@@ -36,7 +36,7 @@
Multi-Variant Options
diff --git a/varfish/static/js/flags_comments.js b/varfish/static/js/flags_comments.js
index ae79d6d6e..6e5cb78e9 100644
--- a/varfish/static/js/flags_comments.js
+++ b/varfish/static/js/flags_comments.js
@@ -182,6 +182,9 @@ function clickVariantBookmark() {
flag_for_validation: false,
flag_candidate: false,
flag_final_causative: false,
+ flag_no_disease_association: false,
+ flag_segregates: false,
+ flag_doesnt_segregate: false,
flag_visual: "empty",
flag_molecular: "empty",
flag_validation: "empty",
@@ -196,6 +199,9 @@ function clickVariantBookmark() {
flag_for_validation: false,
flag_candidate: false,
flag_final_causative: false,
+ flag_no_disease_association: false,
+ flag_segregates: false,
+ flag_doesnt_segregate: false,
flag_visual: "empty",
flag_molecular: "empty",
flag_validation: "empty",
@@ -221,16 +227,21 @@ $('body').on('click', function (e) {
});
});
-function clickMultiVariantBookmark() {
+function clickMultiVariantBookmark(event) {
// Compile template.
var bookmarkModalTpl = $.templates("#multi-bookmark-flags-modal");
- var multiVars = $(".multivar-selector:checked");
- var variantList = [];
- var rowIds = [];
+
+ let selector = $(event.target).data('selector')
+ if (!selector) {
+ selector = $(event.target).closest('.btn').data('selector')
+ }
+ const multiVars = $(selector);
+ const variantList = [];
+ const rowIds = [];
multiVars.each(function(i, e) {
- if (structural_or_small == "small") {
- var dataVariant = $(e).val();
- var arrVariant = dataVariant.split("-");
+ if (structural_or_small === "small") {
+ const dataVariant = $(e).val();
+ const arrVariant = dataVariant.split("-");
variantList.push({
case: $(e).data("case"),
release: arrVariant[0],
@@ -260,17 +271,35 @@ function clickMultiVariantBookmark() {
data: "variant_list=" + JSON.stringify(variantList),
dataType: "json"
}).done(function(data) {
- var flags = {
+ var flags_bool = {
flag_bookmarked: true,
flag_for_validation: false,
flag_candidate: false,
flag_final_causative: false,
+ flag_no_disease_association: false,
+ flag_segregates: false,
+ flag_doesnt_segregate: false,
+ }
+ var flags_string = {
flag_visual: "empty",
flag_molecular: "empty",
flag_validation: "empty",
flag_phenotype_match: "empty",
flag_summary: "empty",
}
+ var flags = Object.assign({}, flags_bool, flags_string);
+ var flags_color = {
+ positive: "img-red",
+ uncertain: "img-gold",
+ negative: "img-green",
+ empty: "img-gray",
+ }
+ var flags_img = {
+ positive: "/icons/fa-solid/exclamation-circle.svg",
+ uncertain: "/icons/fa-solid/question.svg",
+ negative: "/icons/fa-solid/minus-circle.svg",
+ empty: "/icons/fa-solid/times.svg",
+ }
$.each(data["flags"], function(k, v) {
if ($.inArray(k, data["flags_interfering"]) > -1) {
flags[k + "_warning"] = true
@@ -285,7 +314,6 @@ function clickMultiVariantBookmark() {
$(modal).find(".save").click(function(event) {
event.preventDefault(); // we will handle everything
$(this).addClass("disabled");
-
// Save flags
var formData = $(this).closest(".modal-content").find("form").serialize();
$.ajax({
@@ -302,13 +330,14 @@ function clickMultiVariantBookmark() {
var iconBookmark = $(e).find(".variant-bookmark")
var iconComment = $(e).find(".variant-comment")
- if (d["flag_bookmarked"] || d["flag_for_validation"] || d["flag_candidate"] || d["flag_final_causative"]) {
- iconBookmark.attr("src", "/icons/fa-solid/bookmark.svg");
- } else {
- iconBookmark.attr("src", "/icons/fa-regular/bookmark.svg");
- }
+ iconBookmark.attr("src", "/icons/fa-regular/bookmark.svg");
+ $.each(flags_bool, function(flag, _) {
+ if (d[flag]) {
+ iconBookmark.attr("src", "/icons/fa-solid/bookmark.svg");
+ }
+ })
- if (data["comment"]) {
+ if (data["comment"]["text"]) {
iconComment.attr("src", "/icons/fa-solid/comment.svg");
}
@@ -323,16 +352,74 @@ function clickMultiVariantBookmark() {
}
else { // case_details
// update case detail page
- var flags = ["bookmarked", "for_validation", "candidate", "final_causative", "disease_association", "segregates", "doesnt_segregate"]
- $.each(flags, function (flag) {
- if (d["flag_" + flag]) {
- $(e + "-flag-" + flag).addClass("img-dark-gray")
- $(e + "-flag-" + flag).removeClass("img-light-gray")
+ $.each(flags_bool, function (flag, _) {
+ var iconFlag = $(e + "-" + flag)
+ if (d[flag]) {
+ iconFlag.addClass("img-dark-gray")
+ iconFlag.removeClass("img-light-gray")
} else {
- $(e + "-flag-" + flag).removeClass("img-dark-gray")
- $(e + "-flag-" + flag).addClass("img-light-gray")
+ iconFlag.addClass("img-light-gray")
+ iconFlag.removeClass("img-dark-gray")
}
})
+ $.each(flags_string, function(flag, _) {
+ var iconFlag = $(e + "-" + flag)
+
+ $.each(flags_color, function(color, _class) {
+ iconFlag.removeClass(_class)
+ })
+
+ iconFlag.addClass(flags_color[d[flag]])
+ iconFlag.attr("src", flags_img[d[flag]])
+ })
+
+ if (data["comment"]["text"]) {
+ var uuid = data["comment"]["uuids"][e.substring(1)]
+ $(e + "-small-variant-comment").append(`
+
+ `);
+ }
}
});
}
@@ -413,6 +500,154 @@ function updateAcmgRating(theForm) {
inputClassification.val(acmgClass)
}
+function clickVariantAcmgRatingModal(event) {
+ const outerThis = $(this)
+ // Compile template.
+ const acmgRatingModalTpl = $.templates("#single-acmg-criteria-modal")
+
+ let selector = $(event.target).data('selector')
+ if (!selector) {
+ selector = $(event.target).closest('.btn').data('selector')
+ }
+ const multiVars = $(selector)
+ const variantList = []
+ const rowIds = []
+ let caseUuid = null
+
+ multiVars.each(function(i, e) {
+ const dataVariant = $(e).val();
+ const arrVariant = dataVariant.split("-");
+ caseUuid = $(e).data("case")
+ variantList.push({
+ case: $(e).data("case"),
+ dataVariant: dataVariant,
+ release: arrVariant[0],
+ chromosome: arrVariant[1],
+ start: arrVariant[2],
+ end: arrVariant[3],
+ bin: arrVariant[4],
+ reference: arrVariant[5],
+ alternative: arrVariant[6],
+ })
+ rowIds.push("#" + $(e).data("case") + "-" + dataVariant)
+ })
+ // TODO: we only consider the first variant
+ const singleVar = variantList[0]
+
+ const modal = $("#singleVarAcmgRatingModal")
+
+ // Save ACMG rating.
+ function saveForm(event) {
+ console.log('saving form')
+ event.preventDefault() // we will handle everything
+
+ // Save flags
+ const form = $(this).closest("form")
+ let formValues = form.serializeArray()
+ // Because serializeArray() ignores unset checkboxes and radio buttons:
+ formValues = formValues.concat(
+ form.find('input[type=checkbox]:not(:checked)').map(
+ function() { return {"name": this.name, "value": false} }).get())
+ let formData = ""
+ for (let i = 0; i < formValues.length; ++i) {
+ let key = formValues[i].name
+ let value = formValues[i].value
+ if (key.startsWith("pvs") || key.startsWith("ps") || key.startsWith("pm") || key.startsWith("pp") ||
+ key.startsWith("ba") || key.startsWith("bs") || key.startsWith("bp")) {
+ value = value ? 2 : 0
+ }
+ if (formData.length > 0) {
+ formData += "&"
+ }
+ formData += key + "=" + value
+ }
+ $.ajax({
+ type: "POST",
+ url: acmg_rating_url.replace("--abcef--", caseUuid),
+ data: formData + "&csrfmiddlewaretoken=" + getCookie("csrftoken"),
+ dataType: "json",
+ }).done(function(data) {
+ let badge = $(outerThis).closest(".variant-row").find(".variant-acmg")
+ let acmgClass = data["class_override"] || data["class_auto"]
+ if (acmgClass && (acmgClass > 3)) {
+ badge.addClass("badge-danger text-white")
+ badge.removeClass("badge-light badge-warning badge-success text-black text-muted")
+ badge.text(acmgClass)
+ } else if (acmgClass && (acmgClass == 3)) {
+ badge.addClass("badge-warning text-black")
+ badge.removeClass("badge-light text-muted badge-danger badge-success text-white")
+ badge.text(acmgClass)
+ } else if (acmgClass) {
+ badge.addClass("badge-success text-white")
+ badge.removeClass("badge-light text-muted badge-danger badge-warning text-black")
+ badge.text(acmgClass)
+ } else {
+ badge.removeClass("badge-danger badge-warning badge-success text-white");
+ badge.addClass("badge-light text-black text-muted");
+ badge.text("-");
+ }
+ $(modal).modal("hide");
+ }).fail(function(xhr) {
+ // failed, notify user
+ alert("Updating ACMG classification failed");
+ $(modal).modal("hide");
+ });
+ }
+
+ // Show modal when ACMG rating has been retrieved.
+ function showModal(data) {
+ const rawHtml = acmgRatingModalTpl.render(data)
+ const html = $(rawHtml)
+ html.find('[data-toggle="tooltip"]').tooltip()
+ html.find('input').change(acmgCriterionChanged)
+ updateAcmgRating(html) // initial computation
+ html.find('.btn.save').click(saveForm)
+ html.find('.btn.clear').click(function(e) {
+ $(this).closest('form').find(':checkbox').prop('checked', false)
+ $(this).closest('form').find(':text').val('')
+ })
+
+ $("#singleVarAcmgRatingModalContent").html(html);
+ }
+
+ // Retrieve current small variant flags from server via AJAX.
+ $.ajax({
+ url: acmg_rating_url.replace("--abcef--", singleVar.case),
+ data: singleVar,
+ dataType: "json"
+ }).done(function(data) {
+ // found flags, show form with these
+ data["variant"] = singleVar.dataVariant
+ showModal(data)
+ }).fail(function(xhr) {
+ if (xhr.status == 404) {
+ // no flags found yet, show form with defaults
+ var data = {
+ variant: singleVar.dataVariant,
+ release: singleVar.release,
+ chromosome: singleVar.chromosome,
+ start: singleVar.start,
+ end: singleVar.end,
+ bin: singleVar.bin,
+ reference: singleVar.reference,
+ alternative: singleVar.alternative,
+ flag_bookmarked: true,
+ flag_for_validation: false,
+ flag_candidate: false,
+ flag_final_causative: false,
+ flag_visual: "empty",
+ flag_validation: "empty",
+ flag_phenotype_match: "empty",
+ flag_summary: "empty",
+ }
+ showModal(data)
+ } else {
+ // Non-404 status code, something else failed.
+ alert("Retrieving ACMG ratings failed failed")
+ }
+ })
+}
+
function acmgCriterionChanged(event) {
updateAcmgRating(event.target.form)
}
@@ -576,5 +811,7 @@ function toggleMultiVarOptionsDropdown() {
$(document).on("click", ".variant-bookmark-comment-group", clickVariantBookmark);
$(document).on("click", ".variant-acmg", clickVariantAcmgRating);
$(document).on("click", "#multivar-bookmark-comment", clickMultiVariantBookmark);
+$(document).on("click", ".singlevar-bookmark-comment", clickMultiVariantBookmark);
+$(document).on("click", ".singlevar-acmg-rating", clickVariantAcmgRatingModal);
$(document).on("click", ".multivar-selector", toggleMultiVarOptionsDropdown);
diff --git a/varfish/static/js/variant_comments.js b/varfish/static/js/variant_comments.js
index 7027f1e5a..ccdf0ccdd 100644
--- a/varfish/static/js/variant_comments.js
+++ b/varfish/static/js/variant_comments.js
@@ -131,12 +131,6 @@ function commentSubmit() {
`);
list.animate({scrollTop: list[0].scrollHeight}, 'slow');
textbox.val("");
- $('*[data-sodar-uuid="' + data["sodar_uuid"] + '"').find(".comment-button-delete").on("click", commentDeleteToggle);
- $('*[data-sodar-uuid="' + data["sodar_uuid"] + '"').find(".comment-button-delete-cancel").on("click", commentDeleteToggle);
- $('*[data-sodar-uuid="' + data["sodar_uuid"] + '"').find(".comment-button-delete-submit").on("click", commentDeleteSubmit);
- $('*[data-sodar-uuid="' + data["sodar_uuid"] + '"').find(".comment-button-edit").on("click", commentEditToggle);
- $('*[data-sodar-uuid="' + data["sodar_uuid"] + '"').find(".comment-button-edit-cancel").on("click", commentEditToggle);
- $('*[data-sodar-uuid="' + data["sodar_uuid"] + '"').find(".comment-button-edit-submit").on("click", commentEditSubmit);
handleEmptyMessage(list.attr("id"))
updateCaseCommentsCount();
},
diff --git a/variants/models.py b/variants/models.py
index aefa931fb..9c807476a 100644
--- a/variants/models.py
+++ b/variants/models.py
@@ -16,6 +16,7 @@
import math
import re
import requests
+from django.utils.timezone import localtime
from varfish.utils import JSONField
from variants.helpers import get_engine
@@ -1388,6 +1389,9 @@ def get_gene_symbols(self):
symbols2 = {o.symbol for o in Hgnc.objects.filter(ensembl_gene_id__in=gene_ids)}
return sorted(symbols1 | symbols2)
+ def get_date_created(self):
+ return localtime(self.date_created).strftime("%Y-%m-%d %H:%M")
+
def clean(self):
"""Make sure that the case has such a variant"""
# TODO: unit test me
diff --git a/variants/templates/variants/_acmg_criteria_form_tpl.html b/variants/templates/variants/_acmg_criteria_form_tpl.html
index 9820d91d2..cf39fd9e2 100644
--- a/variants/templates/variants/_acmg_criteria_form_tpl.html
+++ b/variants/templates/variants/_acmg_criteria_form_tpl.html
@@ -321,7 +321,7 @@ Benign
class override
-
+
diff --git a/variants/templates/variants/_single_acmg_criteria_form_tpl.html b/variants/templates/variants/_single_acmg_criteria_form_tpl.html
new file mode 100644
index 000000000..4d175be39
--- /dev/null
+++ b/variants/templates/variants/_single_acmg_criteria_form_tpl.html
@@ -0,0 +1,356 @@
+{# ACMG Criteria Form #}
+{% verbatim %}
+
+{% endverbatim %}
diff --git a/variants/templates/variants/case/detail_annotation.html b/variants/templates/variants/case/detail_annotation.html
index d44b1d8d1..bf8d98630 100644
--- a/variants/templates/variants/case/detail_annotation.html
+++ b/variants/templates/variants/case/detail_annotation.html
@@ -13,30 +13,32 @@
Annotated Variants
-
-
- Multi-Variant Options
-
-