Skip to content

Commit

Permalink
Added feature to select multiple rows in results to create same annot…
Browse files Browse the repository at this point in the history
…ation (#259) (#280)
  • Loading branch information
stolpeo committed Jan 24, 2022
1 parent 705b554 commit 7538557
Show file tree
Hide file tree
Showing 21 changed files with 1,423 additions and 28 deletions.
2 changes: 2 additions & 0 deletions HISTORY.rst
Expand Up @@ -32,6 +32,7 @@ End-User Summary
For this, GRCh38 background data must be imported.
Kiosk mode does not support GRCh38 yet.
**This is a breaking change, new data and CLI must be used!**
- Added feature to select multiple rows in results to create same annotation (#259)

Full Change List
================
Expand Down Expand Up @@ -65,6 +66,7 @@ Full Change List
- Setting ``VARFISH_CADD_SUBMISSION_RELEASE`` is called ``VARFISH_CADD_SUBMISSION_VERSION`` now (**breaking change**).
- ``import_info.tsv`` expected as in data release from ``20210728`` as built from varfish-db-downloader ``1b03e97`` or later.
- Extending columns of ``Hgnc`` to upstream update.
- Added feature to select multiple rows in results to create same annotation (#259)

-------
v0.23.9
Expand Down
3 changes: 3 additions & 0 deletions svs/models.py
Expand Up @@ -430,6 +430,9 @@ def no_flags_set(self):
self.flag_candidate,
self.flag_final_causative,
self.flag_for_validation,
self.flag_no_disease_association,
self.flag_segregates,
self.flag_doesnt_segregate,
self.flag_molecular != "empty",
self.flag_visual != "empty",
self.flag_validation != "empty",
Expand Down
3 changes: 2 additions & 1 deletion svs/templates/svs/filter_result/header.html
Expand Up @@ -2,8 +2,9 @@

{# Variant filter table header #}
<tr>
<th style="width:0px; background-color: #f0f0f0;" class="p-0 m-0"></th> {# rank #}
<th style="width:0px; background-color: #f0f0f0;" class="p-0 m-0"></th> {# fold-out #}
<th style="width:0px; background-color: #f0f0f0;" class="p-0 m-0"></th> {# rank #}
<th style="width:0px; background-color: #f0f0f0;" class="p-0 m-0"></th> {# checkbox #}
<th style="width:0px; background-color: #f0f0f0;" class="p-0 m-0"></th> {# bookmark #}

<th style="width: 65px;" class="text-center">SV type / caller</th> {# sv_type + caller #}
Expand Down
11 changes: 7 additions & 4 deletions svs/templates/svs/filter_result/row_sv.html
Expand Up @@ -5,7 +5,7 @@
{% load regmaps_tags %}
{% load dict %}

<tr class="variant-row {% if not training_mode %}variant-row-{{ first_entry|flag_class }}{% endif %}">
<tr class="variant-row {% if not training_mode %}variant-row-{{ first_entry|flag_class }}{% endif %}" id="{{ first_entry.case_uuid }}-{{ first_entry.sv_uuid }}">
{# fold-out #}
<td class="clickable toggle-variant-details pl-0 pr-0"
data-url="{% url "svs:sv-details" project=project.sodar_uuid case=object.sodar_uuid sv=first_entry.sv_uuid %}?database={{ database }}&regulatory_general_padding={{ form.cleaned_data|keyvalue:"regulatory_general_padding" }}"
Expand All @@ -16,13 +16,16 @@
<td class="text-nowrap text-right text-muted">
#{{ forloop.counter }}
</td>

{# checkbox #}
<td class="pl-0 pr-1">
<input type="checkbox" value="{{ first_entry.sv_uuid }}" class="multivar-selector" data-case="{{ first_entry.case_uuid }}">
</td>
{# bookmark #}
{% if not training_mode %}
<td class="bookmark pl-0 pr-0 text-nowrap">
<a style="text-decoration: none" class="variant-bookmark-comment-group" data-sv="{{ first_entry.sv_uuid }}" data-project="{{ project.sodar_uuid }}" data-case="{{ first_entry.case_uuid }}">
<img class="text-muted variant-bookmark" src="/icons/fa-{% if first_entry.flag_count %}solid{% else %}regular{% endif %}/bookmark.svg" />
<img class="text-muted variant-comment" src="/icons/fa-{% if first_entry.comment_count %}solid{% else %}regular{% endif %}/comment.svg" />
<img class="text-muted variant-bookmark" style="filter: invert(54%) sepia(0%) saturate(0%) hue-rotate(321deg) brightness(95%) contrast(89%);" src="/icons/fa-{% if first_entry.flag_count %}solid{% else %}regular{% endif %}/bookmark.svg" />
<img class="text-muted variant-comment" style="filter: invert(54%) sepia(0%) saturate(0%) hue-rotate(321deg) brightness(95%) contrast(89%);" src="/icons/fa-{% if first_entry.comment_count %}solid{% else %}regular{% endif %}/comment.svg" />
</a>
</td>
{% endif %}
Expand Down
48 changes: 40 additions & 8 deletions svs/templates/svs/filter_result/table.html
Expand Up @@ -22,15 +22,25 @@ <h2>
{{ rows_by_sv|length|intcomma }} SV calls
{% if object %}(case has a total of {{ object.num_svs|intcomma }}{% endif %} SV calls)
</div>
<div class="pl-3">
Using
{% if database == "refseq" %}
<strong>RefSeq</strong>
{% else %}
<strong>ENSEMBL</strong>
{% endif %}
transcripts.
<div class="pl-3">
Using
{% if database == "refseq" %}
<strong>RefSeq</strong>
{% else %}
<strong>ENSEMBL</strong>
{% endif %}
transcripts.
</div>
<div class="dropdown pl-2">
<button type="button" id="multiVarButton" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="iconify" data-icon="fluent:bookmark-multiple-16-filled"></i> Multi-Variant Options
</button>
<div class="dropdown-menu dropdown-menu-left" style="z-index: 1031;">
<a class="dropdown-item" id="multivar-bookmark-comment" data-toggle="modal" data-target="#multiVarBookmarkCommentModal">
<i class="iconify" data-icon="fluent:bookmark-multiple-16-filled"></i> Flag & Comment
</a>
</div>
</div>
</div>
</div>

Expand All @@ -54,5 +64,27 @@ <h2>
<p>
Query completed in {{ elapsed_seconds }} sec.
</p>

{# Multi-var bookmark and comment modal #}
<div class="modal fade" id="multiVarBookmarkCommentModal" aria-hidden="true">
<div class="modal-dialog" style="width: 434px" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="multiVarBookmarkCommentModal">Multi-Variant Comments &amp; Flags</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container-fluid" id="multiVarBookmarkCommentModalContent">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary save">Save</button>
</div>
</div>
</div>
</div>
</div> <!-- sodar-page-container -->
{% endif %}
2 changes: 2 additions & 0 deletions svs/templates/svs/scripts.html
Expand Up @@ -30,9 +30,11 @@
let structural_or_small = "structural";
let variant_flags_url = "{% url 'svs:sv-flags-api' project=project.sodar_uuid case="--abcef--" sv="--bbccee--" %}";
let variant_comment_url = "{% url 'svs:sv-comment-api' project=project.sodar_uuid case="--abcef--" sv="--bbccee--" %}";
let multi_variant_flags_comment_url = "{% url 'svs:multi-sv-flags-comment-api' project=project.sodar_uuid %}";
</script>
<script type="text/javascript" src="{% static "js/helpers.js" %}"></script>
<script type="text/javascript" src="{% static "js/filter_form.js" %}"></script>
<script type="text/javascript" src="{% static "js/flags_comments.js" %}"></script>

{% include "variants/_variant_flag_form_tpl.html" %}
{% include "variants/_multi_variant_flag_form_tpl.html" %}
5 changes: 5 additions & 0 deletions svs/urls.py
Expand Up @@ -28,6 +28,11 @@
view=views.StructuralVariantCommentApiView.as_view(),
name="sv-comment-api",
),
url(
regex=r"^(?P<project>[0-9a-f-]+)/multi-sv-flags-comment",
view=views.MultiStructuralVariantFlagsAndCommentApiView.as_view(),
name="multi-sv-flags-comment-api",
),
# Views for variants import job.
url(
regex=r"^(?P<project>[0-9a-f-]+)/import/(?P<job>[0-9a-f-]+)/$",
Expand Down
159 changes: 156 additions & 3 deletions svs/views.py
Expand Up @@ -8,7 +8,7 @@
from projectroles.views import LoginRequiredMixin
from django.db import transaction
from django.forms import model_to_dict
from django.http import HttpResponse, Http404
from django.http import HttpResponse, Http404, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404, reverse
from django.utils import timezone
from django.views import View
Expand Down Expand Up @@ -341,10 +341,10 @@ def post(self, *_args, **kwargs):
if timeline:
tl_event = timeline.add_event(
project=self.get_project(self.request, self.kwargs),
app_name="variants",
app_name="svs",
user=self.request.user,
event_name="comment_add",
description="add comment for variant %s in case {case}: {text}"
description="add comment for structural variant %s in case {case}: {text}"
% comment.get_variant_description(),
status_type="OK",
)
Expand All @@ -353,6 +353,159 @@ def post(self, *_args, **kwargs):
return HttpResponse(json.dumps({"result": "OK"}), content_type="application/json")


class MultiStructuralVariantFlagsAndCommentApiView(
LoginRequiredMixin, LoggedInPermissionMixin, ProjectPermissionMixin, ProjectContextMixin, View,
):
"""A view that returns JSON for the ``SmallVariantFlags`` for a variant of a case and allows updates."""

# TODO: create new permission
permission_required = "variants.view_data"

def get(self, *_args, **_kwargs):
get_data = dict(self.request.GET)
variant_list = json.loads(get_data.get("variant_list")[0])
flags_keys = [
"flag_bookmarked",
"flag_candidate",
"flag_final_causative",
"flag_for_validation",
"flag_no_disease_association",
"flag_segregates",
"flag_doesnt_segregate",
"flag_visual",
"flag_molecular",
"flag_validation",
"flag_phenotype_match",
"flag_summary",
]
flags = {i: None for i in flags_keys}
flags_interfering = set()

for variant in reversed(variant_list):
case = get_object_or_404(Case, sodar_uuid=variant.get("case"))

try:
flag_data = model_to_dict(self._get_flags_for_variant(variant.get("sv_uuid"), case))

for flag in flags_keys:
if flags[flag] is None:
flags[flag] = flag_data[flag]

if not flags[flag] == flag_data[flag]:
flags_interfering.add(flag)

flags[flag] = flag_data[flag]

except StructuralVariantFlags.DoesNotExist:
continue

results = {
"flags": flags,
"flags_interfering": sorted(flags_interfering),
"variant_list": variant_list,
}

return JsonResponse(results, UUIDEncoder)

def post(self, *_args, **_kwargs):
timeline = get_backend_api("timeline_backend")
post_data = dict(self.request.POST)
variant_list = post_data.pop("variant_list")[0]
post_data.pop("csrfmiddlewaretoken")
post_data_clean = {k: v[0] for k, v in post_data.items()}
text = post_data_clean.pop("text")

for variant in json.loads(variant_list):
case = get_object_or_404(Case, sodar_uuid=variant.get("case"))
sv = StructuralVariant.objects.get(sv_uuid=variant.get("sv_uuid"))

try:
flags = self._get_flags_for_variant(variant.get("sv_uuid"), case)

except StructuralVariantFlags.DoesNotExist:
flags = StructuralVariantFlags(
case=case,
bin=sv.bin,
release=sv.release,
chromosome=sv.chromosome,
start=sv.start,
end=sv.end,
sv_type=sv.sv_type,
sv_sub_type=sv.sv_sub_type,
)
flags.save()

form = StructuralVariantFlagsForm({**variant, **post_data_clean}, instance=flags)

try:
flags = form.save()

except ValueError as e:
raise Exception(str(form.errors)) from e

if timeline:
tl_event = timeline.add_event(
project=self.get_project(self.request, self.kwargs),
app_name="svs",
user=self.request.user,
event_name="flags_set",
description="set flags for structural variant %s in case {case}: {extra-flag_values}"
% sv,
status_type="OK",
extra_data={"flag_values": flags.human_readable()},
)
tl_event.add_object(obj=case, label="case", name=case.name)

if flags.no_flags_set():
flags.delete()

if text:
comment = StructuralVariantComment(
case=case,
user=self.request.user,
bin=sv.bin,
release=sv.release,
chromosome=sv.chromosome,
start=sv.start,
end=sv.end,
sv_type=sv.sv_type,
sv_sub_type=sv.sv_sub_type,
sodar_uuid=uuid.uuid4(),
)
form = StructuralVariantCommentForm({**variant, "text": text}, instance=comment)

try:
comment = form.save()

except ValueError as e:
raise Exception(str(form.errors)) from e

if timeline:
tl_event = timeline.add_event(
project=self.get_project(self.request, self.kwargs),
app_name="svs",
user=self.request.user,
event_name="comment_add",
description="add comment for structural variant %s in case {case}: {text}"
% comment.get_variant_description(),
status_type="OK",
)
tl_event.add_object(obj=case, label="case", name=case.name)
tl_event.add_object(obj=comment, label="text", name=comment.shortened_text())

return JsonResponse({"message": "OK", "flags": post_data_clean, "comment": text})

def _get_flags_for_variant(self, sv_uuid, case):
with contextlib.closing(best_matching_flags(get_engine(), case.id, sv_uuid)) as results:
result = results.first()
if not result:
raise StructuralVariantFlags.DoesNotExist()
else:
return StructuralVariantFlags.objects.get(
case_id=case.id, sodar_uuid=result.flags_uuid
)


class ImportStructuralVariantsJobDetailView(
LoginRequiredMixin,
LoggedInPermissionMixin,
Expand Down

0 comments on commit 7538557

Please sign in to comment.