scout-browser 4.91.2__py3-none-any.whl → 4.93.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scout/__version__.py +1 -1
- scout/adapter/mongo/base.py +3 -0
- scout/adapter/mongo/case.py +27 -2
- scout/adapter/mongo/ccv.py +131 -0
- scout/adapter/mongo/query.py +13 -16
- scout/adapter/mongo/variant.py +4 -3
- scout/adapter/mongo/variant_events.py +45 -1
- scout/build/ccv.py +59 -0
- scout/commands/delete/delete_command.py +27 -4
- scout/commands/serve.py +2 -1
- scout/constants/__init__.py +2 -0
- scout/constants/case_tags.py +2 -0
- scout/constants/ccv.py +244 -0
- scout/demo/643594.config.yaml +2 -2
- scout/demo/images/custom_images/1300x1000.jpg +0 -0
- scout/models/ccv_evaluation.py +26 -0
- scout/models/variant/variant.py +1 -0
- scout/parse/panelapp.py +3 -1
- scout/server/blueprints/cases/templates/cases/case_report.html +45 -0
- scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +2 -2
- scout/server/blueprints/cases/templates/cases/index.html +0 -2
- scout/server/blueprints/genes/templates/genes/gene.html +6 -0
- scout/server/blueprints/institutes/templates/overview/causatives.html +1 -1
- scout/server/blueprints/institutes/templates/overview/utils.html +12 -1
- scout/server/blueprints/institutes/templates/overview/verified.html +1 -1
- scout/server/blueprints/institutes/views.py +4 -0
- scout/server/blueprints/panels/controllers.py +5 -6
- scout/server/blueprints/panels/templates/panels/panel.html +5 -5
- scout/server/blueprints/variant/controllers.py +148 -1
- scout/server/blueprints/variant/templates/variant/cancer-variant.html +1 -1
- scout/server/blueprints/variant/templates/variant/ccv.html +183 -0
- scout/server/blueprints/variant/templates/variant/components.html +61 -3
- scout/server/blueprints/variant/templates/variant/variant.html +1 -1
- scout/server/blueprints/variant/templates/variant/variant_details.html +29 -11
- scout/server/blueprints/variant/utils.py +21 -1
- scout/server/blueprints/variant/views.py +114 -3
- scout/server/blueprints/variants/controllers.py +31 -0
- scout/server/blueprints/variants/templates/variants/cancer-variants.html +2 -1
- scout/server/blueprints/variants/templates/variants/components.html +63 -73
- scout/server/blueprints/variants/templates/variants/indicators.html +11 -0
- scout/server/extensions/panelapp_extension.py +2 -2
- scout/server/static/custom_images.js +19 -2
- scout/utils/ccv.py +201 -0
- {scout_browser-4.91.2.dist-info → scout_browser-4.93.1.dist-info}/METADATA +6 -5
- {scout_browser-4.91.2.dist-info → scout_browser-4.93.1.dist-info}/RECORD +49 -43
- {scout_browser-4.91.2.dist-info → scout_browser-4.93.1.dist-info}/WHEEL +1 -1
- scout/demo/images/custom_images/640x480_two.jpg +0 -0
- {scout_browser-4.91.2.dist-info → scout_browser-4.93.1.dist-info}/LICENSE +0 -0
- {scout_browser-4.91.2.dist-info → scout_browser-4.93.1.dist-info}/entry_points.txt +0 -0
- {scout_browser-4.91.2.dist-info → scout_browser-4.93.1.dist-info}/top_level.txt +0 -0
@@ -13,13 +13,26 @@ from flask import (
|
|
13
13
|
from flask_login import current_user
|
14
14
|
from markupsafe import Markup
|
15
15
|
|
16
|
-
from scout.constants import
|
17
|
-
|
16
|
+
from scout.constants import (
|
17
|
+
ACMG_CRITERIA,
|
18
|
+
ACMG_MAP,
|
19
|
+
ACMG_OPTIONS,
|
20
|
+
CCV_CRITERIA,
|
21
|
+
CCV_MAP,
|
22
|
+
CCV_OPTIONS,
|
23
|
+
)
|
24
|
+
from scout.server.blueprints.variant.controllers import ccv_evaluation as ccv_evaluation_controller
|
25
|
+
from scout.server.blueprints.variant.controllers import (
|
26
|
+
check_reset_variant_ccv_classification,
|
27
|
+
check_reset_variant_classification,
|
28
|
+
)
|
18
29
|
from scout.server.blueprints.variant.controllers import evaluation as evaluation_controller
|
19
30
|
from scout.server.blueprints.variant.controllers import observations, str_variant_reviewer
|
20
31
|
from scout.server.blueprints.variant.controllers import variant as variant_controller
|
21
32
|
from scout.server.blueprints.variant.controllers import variant_acmg as acmg_controller
|
22
33
|
from scout.server.blueprints.variant.controllers import variant_acmg_post
|
34
|
+
from scout.server.blueprints.variant.controllers import variant_ccv as ccv_controller
|
35
|
+
from scout.server.blueprints.variant.controllers import variant_ccv_post
|
23
36
|
from scout.server.blueprints.variant.verification_controllers import (
|
24
37
|
MissingVerificationRecipientError,
|
25
38
|
variant_verification,
|
@@ -27,6 +40,7 @@ from scout.server.blueprints.variant.verification_controllers import (
|
|
27
40
|
from scout.server.extensions import loqusdb, store
|
28
41
|
from scout.server.utils import institute_and_case, public_endpoint, templated
|
29
42
|
from scout.utils.acmg import get_acmg, get_acmg_conflicts, get_acmg_temperature
|
43
|
+
from scout.utils.ccv import get_ccv, get_ccv_conflicts, get_ccv_temperature
|
30
44
|
from scout.utils.ensembl_rest_clients import EnsemblRestApiClient
|
31
45
|
|
32
46
|
LOG = logging.getLogger(__name__)
|
@@ -205,9 +219,40 @@ def variant_acmg(institute_id, case_name, variant_id):
|
|
205
219
|
)
|
206
220
|
|
207
221
|
|
222
|
+
@variant_bp.route("/<institute_id>/<case_name>/<variant_id>/ccv", methods=["GET", "POST"])
|
223
|
+
@templated("variant/ccv.html")
|
224
|
+
def variant_ccv(institute_id, case_name, variant_id):
|
225
|
+
"""ClinGen-CCG-VICC classification form."""
|
226
|
+
if request.method == "GET":
|
227
|
+
data = ccv_controller(store, institute_id, case_name, variant_id)
|
228
|
+
return data
|
229
|
+
|
230
|
+
criteria = []
|
231
|
+
criteria_terms = request.form.getlist("criteria")
|
232
|
+
for term in criteria_terms:
|
233
|
+
criteria.append(
|
234
|
+
dict(
|
235
|
+
term=term,
|
236
|
+
comment=request.form.get("comment-{}".format(term)),
|
237
|
+
links=[request.form.get("link-{}".format(term))],
|
238
|
+
)
|
239
|
+
)
|
240
|
+
ccv = variant_ccv_post(store, institute_id, case_name, variant_id, current_user.email, criteria)
|
241
|
+
flash("classified as: {}".format(ccv), "info")
|
242
|
+
return redirect(
|
243
|
+
url_for(
|
244
|
+
".variant",
|
245
|
+
institute_id=institute_id,
|
246
|
+
case_name=case_name,
|
247
|
+
variant_id=variant_id,
|
248
|
+
)
|
249
|
+
)
|
250
|
+
|
251
|
+
|
208
252
|
@variant_bp.route("/<institute_id>/<case_name>/<variant_id>/update", methods=["POST"])
|
209
253
|
def variant_update(institute_id, case_name, variant_id):
|
210
|
-
"""Update user-defined information about a variant: manual rank
|
254
|
+
"""Update user-defined information about a variant: manual rank, cancer tier,
|
255
|
+
CLinGen-CGC-VICC classification, dismissal & mosaic tags."""
|
211
256
|
institute_obj, case_obj = institute_and_case(store, institute_id, case_name)
|
212
257
|
variant_obj = store.variant(variant_id)
|
213
258
|
user_obj = store.user(current_user.email)
|
@@ -250,6 +295,7 @@ def variant_update(institute_id, case_name, variant_id):
|
|
250
295
|
flash("Variant tag was updated", "info")
|
251
296
|
else:
|
252
297
|
flash("Variant tag was reset", "info")
|
298
|
+
|
253
299
|
elif request.form.get("acmg_classification"):
|
254
300
|
new_acmg = request.form["acmg_classification"]
|
255
301
|
acmg_classification = variant_obj.get("acmg_classification")
|
@@ -268,6 +314,24 @@ def variant_update(institute_id, case_name, variant_id):
|
|
268
314
|
)
|
269
315
|
flash("updated ACMG classification: {}".format(new_acmg), "info")
|
270
316
|
|
317
|
+
elif request.form.get("ccv_classification"):
|
318
|
+
new_ccv = request.form["ccv_classification"]
|
319
|
+
ccv_classification = variant_obj.get("ccv_classification")
|
320
|
+
# If there already is a classification and the same one is sent again this means that
|
321
|
+
# We want to remove the classification
|
322
|
+
if isinstance(ccv_classification, int) and (new_ccv == CCV_MAP[ccv_classification]):
|
323
|
+
new_ccv = None
|
324
|
+
|
325
|
+
store.submit_ccv_evaluation(
|
326
|
+
variant_obj=variant_obj,
|
327
|
+
user_obj=user_obj,
|
328
|
+
institute_obj=institute_obj,
|
329
|
+
case_obj=case_obj,
|
330
|
+
link=link,
|
331
|
+
classification=new_ccv,
|
332
|
+
)
|
333
|
+
flash("updated ClinGen-CGC-VIGG classification: {}".format(new_ccv), "info")
|
334
|
+
|
271
335
|
new_dismiss = request.form.getlist("dismiss_variant")
|
272
336
|
if new_dismiss:
|
273
337
|
store.update_dismiss_variant(
|
@@ -353,6 +417,53 @@ def acmg():
|
|
353
417
|
return jsonify({"classification": classification, "conflicts": acmg_conflicts, **acmg_bayesian})
|
354
418
|
|
355
419
|
|
420
|
+
@variant_bp.route("/ccv_evaluations/<evaluation_id>", methods=["GET", "POST"])
|
421
|
+
@templated("variant/ccv.html")
|
422
|
+
def ccv_evaluation(evaluation_id):
|
423
|
+
"""Show, edit or delete an ClinGen-CGC-VIGG evaluation."""
|
424
|
+
|
425
|
+
evaluation_obj = store.get_ccv_evaluation(evaluation_id)
|
426
|
+
if evaluation_obj is None:
|
427
|
+
flash("Evaluation was not found in database", "warning")
|
428
|
+
return redirect(request.referrer)
|
429
|
+
ccv_evaluation_controller(store, evaluation_obj)
|
430
|
+
if request.method == "POST":
|
431
|
+
link = url_for(
|
432
|
+
".variant",
|
433
|
+
institute_id=evaluation_obj["institute"]["_id"],
|
434
|
+
case_name=evaluation_obj["case"]["display_name"],
|
435
|
+
variant_id=evaluation_obj["variant_specific"],
|
436
|
+
)
|
437
|
+
store.delete_ccv_evaluation(evaluation_obj)
|
438
|
+
|
439
|
+
if check_reset_variant_ccv_classification(store, evaluation_obj, link):
|
440
|
+
flash("Cleared ClinGen-CGC-VIGG classification.", "info")
|
441
|
+
|
442
|
+
return redirect(link)
|
443
|
+
|
444
|
+
return dict(
|
445
|
+
evaluation=evaluation_obj,
|
446
|
+
edit=bool(request.args.get("edit")),
|
447
|
+
institute=evaluation_obj["institute"],
|
448
|
+
case=evaluation_obj["case"],
|
449
|
+
variant=evaluation_obj["variant"],
|
450
|
+
CRITERIA=CCV_CRITERIA,
|
451
|
+
CCV_OPTIONS=CCV_OPTIONS,
|
452
|
+
)
|
453
|
+
|
454
|
+
|
455
|
+
@variant_bp.route("/api/v1/ccv")
|
456
|
+
@public_endpoint
|
457
|
+
def ccv():
|
458
|
+
"""Calculate an ClinGen-CVC-VIGG classification from submitted criteria."""
|
459
|
+
criteria = request.args.getlist("criterion")
|
460
|
+
classification = get_ccv(criteria)
|
461
|
+
|
462
|
+
ccv_bayesian = get_ccv_temperature(criteria)
|
463
|
+
ccv_conflicts = get_ccv_conflicts(criteria)
|
464
|
+
return jsonify({"classification": classification, "conflicts": ccv_conflicts, **ccv_bayesian})
|
465
|
+
|
466
|
+
|
356
467
|
@variant_bp.route(
|
357
468
|
"/<institute_id>/<case_name>/<variant_id>/<order>",
|
358
469
|
methods=["POST"],
|
@@ -17,6 +17,8 @@ from scout.constants import (
|
|
17
17
|
CANCER_EXPORT_HEADER,
|
18
18
|
CANCER_SPECIFIC_VARIANT_DISMISS_OPTIONS,
|
19
19
|
CANCER_TIER_OPTIONS,
|
20
|
+
CCV_COMPLETE_MAP,
|
21
|
+
CCV_MAP,
|
20
22
|
CHROMOSOMES,
|
21
23
|
CHROMOSOMES_38,
|
22
24
|
CLINSIG_MAP,
|
@@ -362,6 +364,7 @@ def get_manual_assessments(variant_obj):
|
|
362
364
|
## display manual input of interest: classified, commented, tagged, mosaicism or dismissed.
|
363
365
|
assessment_keywords = [
|
364
366
|
"acmg_classification",
|
367
|
+
"ccv_classification",
|
365
368
|
"manual_rank",
|
366
369
|
"cancer_tier",
|
367
370
|
"dismiss_variant",
|
@@ -402,6 +405,16 @@ def get_manual_assessments(variant_obj):
|
|
402
405
|
assessment["label"] = classification["short"]
|
403
406
|
assessment["display_class"] = classification["color"]
|
404
407
|
|
408
|
+
if assessment_type == "ccv_classification":
|
409
|
+
ccv_classification = variant_obj[assessment_type]
|
410
|
+
if isinstance(ccv_classification, int):
|
411
|
+
ccv_code = CCV_MAP[ccv_classification]
|
412
|
+
ccv_classification = CCV_COMPLETE_MAP[ccv_code]
|
413
|
+
|
414
|
+
assessment["title"] = "ClinGen-CGC-VIGG: {}".format(ccv_classification["label"])
|
415
|
+
assessment["label"] = ccv_classification["short"]
|
416
|
+
assessment["display_class"] = ccv_classification["color"]
|
417
|
+
|
405
418
|
if assessment_type == "dismiss_variant":
|
406
419
|
dismiss_variant_options = {
|
407
420
|
**DISMISS_VARIANT_OPTIONS,
|
@@ -918,6 +931,11 @@ def parse_variant(
|
|
918
931
|
acmg_code = ACMG_MAP[variant_obj["acmg_classification"]]
|
919
932
|
variant_obj["acmg_classification"] = ACMG_COMPLETE_MAP[acmg_code]
|
920
933
|
|
934
|
+
ccv_classification = variant_obj.get("ccv_classification")
|
935
|
+
if isinstance(ccv_classification, int):
|
936
|
+
ccv_code = CCV_MAP[variant_obj["ccv_classification"]]
|
937
|
+
variant_obj["ccv_classification"] = CCV_COMPLETE_MAP[ccv_code]
|
938
|
+
|
921
939
|
# convert length for SV variants
|
922
940
|
variant_length = variant_obj.get("length")
|
923
941
|
variant_obj["length"] = {100000000000: "inf", -1: "n.d."}.get(variant_length, variant_length)
|
@@ -1409,6 +1427,19 @@ def cancer_variants(store, institute_id, case_name, variants_query, variant_coun
|
|
1409
1427
|
secondary_gene = gene
|
1410
1428
|
variant_obj["second_rep_gene"] = secondary_gene
|
1411
1429
|
variant_obj["clinical_assessments"] = get_manual_assessments(variant_obj)
|
1430
|
+
|
1431
|
+
evaluations = []
|
1432
|
+
# Get previous ClinGen-CGC-VIGG evaluations of the variant from other cases
|
1433
|
+
for evaluation_obj in store.get_ccv_evaluations(variant_obj):
|
1434
|
+
if evaluation_obj["case_id"] == case_obj["_id"]:
|
1435
|
+
continue
|
1436
|
+
|
1437
|
+
ccv_classification = evaluation_obj["ccv_classification"]
|
1438
|
+
|
1439
|
+
evaluation_obj["ccv_classification"] = CCV_COMPLETE_MAP.get(ccv_classification)
|
1440
|
+
evaluations.append(evaluation_obj)
|
1441
|
+
variant_obj["ccv_evaluations"] = evaluations
|
1442
|
+
|
1412
1443
|
variants_list.append(variant_obj)
|
1413
1444
|
|
1414
1445
|
data = dict(
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
{% from "variants/components.html" import external_scripts, external_stylesheets, gene_cell, frequency_cell_general, observed_cell_general, variant_funct_anno_cell %}
|
4
4
|
{% from "variants/utils.html" import cancer_filters, cell_rank, pagination_footer, pagination_hidden_div, dismiss_variants_block, filter_form_footer, filter_script_main, update_stash_filter_button_status, callers_cell %}
|
5
|
-
{% from "variants/indicators.html" import pin_indicator, causative_badge, clinical_assessments_badge, comments_badge, dismissals_badge, evaluations_badge, group_assessments_badge, matching_manual_rank, other_tiered_variants, research_assessments_badge %}
|
5
|
+
{% from "variants/indicators.html" import pin_indicator, causative_badge, clinical_assessments_badge, comments_badge, dismissals_badge, evaluations_badge, ccv_evaluations_badge, group_assessments_badge, matching_manual_rank, other_tiered_variants, research_assessments_badge %}
|
6
6
|
|
7
7
|
{% block title %}
|
8
8
|
{{ variant_type|capitalize }} somatic variants
|
@@ -109,6 +109,7 @@
|
|
109
109
|
{{ comments_badge(case, institute, variant) }}
|
110
110
|
{{ causative_badge(variant, case) }}
|
111
111
|
{{ other_tiered_variants(variant) }}
|
112
|
+
{{ ccv_evaluations_badge(variant) }}
|
112
113
|
</td>
|
113
114
|
<td>{{ rank_cell(variant) }}</td>
|
114
115
|
<td>{{ cadd_cell(variant) }}</td>
|
@@ -161,94 +161,84 @@
|
|
161
161
|
|
162
162
|
{% macro gene_cell(variant, inherit_palette) %}
|
163
163
|
<div class="align-items-center">
|
164
|
-
|
165
|
-
|
164
|
+
|
165
|
+
{% macro gene_tooltip(gene) %}
|
166
|
+
<div>
|
166
167
|
<div>
|
168
|
+
<strong>{{ gene.hgnc_symbol }}</strong>: {{ gene.description }}
|
169
|
+
</div>
|
170
|
+
{% if gene.inheritance %}
|
167
171
|
<div>
|
168
|
-
<strong>
|
172
|
+
<strong>Models</strong>: {{ gene.inheritance|join(',') }}
|
169
173
|
</div>
|
170
|
-
|
171
|
-
|
172
|
-
<strong>Models</strong>: {{ variant.first_rep_gene.inheritance|join(',') }}
|
173
|
-
</div>
|
174
|
-
{% endif %}
|
175
|
-
{% if variant.first_rep_gene.phenotypes %}
|
174
|
+
{% endif %}
|
175
|
+
{% if gene.phenotypes %}
|
176
176
|
<div><strong>OMIM disease</strong>
|
177
|
-
{% for disease in
|
178
|
-
<div>
|
179
|
-
{{ disease.description }}
|
180
|
-
</div>
|
177
|
+
{% for disease in gene.phenotypes %}
|
178
|
+
<div>{{ disease.description }}</div>
|
181
179
|
{% endfor %}
|
182
180
|
</div>
|
183
181
|
{% endif %}
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
182
|
+
</div>
|
183
|
+
{% endmacro %}
|
184
|
+
|
185
|
+
{% macro gene_link(gene) %}
|
186
|
+
<a
|
187
|
+
data-bs-toggle="tooltip"
|
188
|
+
data-bs-html="true"
|
189
|
+
title="{{ gene_tooltip(gene) }}"
|
190
|
+
href="{{ url_for('genes.gene', hgnc_id=gene.hgnc_id) }}"
|
191
|
+
>
|
192
|
+
{{ gene.hgnc_symbol or gene.hgnc_id }}
|
194
193
|
</a>
|
194
|
+
{% endmacro %}
|
195
|
+
|
196
|
+
{% macro panel_badge(variant, gene_id=0) %}
|
197
|
+
|
198
|
+
{% set matching_panels = [] %}
|
199
|
+
{% for panel in variant.case_panels|rejectattr('removed')|list %}
|
200
|
+
{% if gene_id in panel.hgnc_ids %}
|
201
|
+
{% set _ = matching_panels.append(panel) %}
|
202
|
+
{% endif %}
|
203
|
+
{% endfor %}
|
195
204
|
|
196
|
-
{% set panel_count =
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
205
|
+
{% set panel_count = matching_panels|length %}
|
206
|
+
<a
|
207
|
+
class="badge bg-secondary text-white"
|
208
|
+
data-bs-toggle="popover"
|
209
|
+
data-bs-html="true"
|
210
|
+
data-bs-trigger="hover click"
|
211
|
+
title="Overlapping gene panels"
|
212
|
+
data-bs-content="{% for panel in matching_panels %}
|
213
|
+
{{ panel.panel_name|safe }}<br>
|
214
|
+
{% endfor %}"
|
215
|
+
>{{ panel_count }}</a>
|
216
|
+
{% endmacro %}
|
217
|
+
|
218
|
+
{% if variant.category in ["cancer", "sv_cancer"] %}
|
219
|
+
{% if variant.first_rep_gene %}
|
220
|
+
{{ gene_link(variant.first_rep_gene) }}
|
221
|
+
{% endif %}
|
222
|
+
{% if variant.secondary_gene %}
|
223
|
+
<span class="text-muted">
|
224
|
+
({{ variant.second_rep_gene.hgnc_symbol or variant.second_rep_gene.hgnc_id }})
|
225
|
+
</span>
|
226
|
+
{% endif %}
|
227
|
+
{% if variant.first_rep_gene or variant.second_rep_gene %}
|
228
|
+
{{ panel_badge(variant, variant.first_rep_gene.hgnc_id or variant.second_rep_gene.hgnc_id) }}
|
209
229
|
{% endif %}
|
210
230
|
{% else %}
|
211
|
-
{% set panel_count = variant.case_panels|rejectattr('removed')|list|count %}
|
212
231
|
{% for gene in variant.genes %}
|
213
232
|
<div>
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
<div>
|
221
|
-
<strong>Models</strong>: {{ gene.inheritance|join(',') }}
|
222
|
-
</div>
|
223
|
-
{% endif %}
|
224
|
-
{% if gene.phenotypes %}
|
225
|
-
<div><strong>OMIM disease</strong>
|
226
|
-
{% for disease in gene.phenotypes %}
|
227
|
-
<div>
|
228
|
-
{{ disease.description }}
|
229
|
-
</div>
|
230
|
-
{% endfor %}
|
231
|
-
</div>
|
232
|
-
{% endif %}
|
233
|
-
</div>"
|
234
|
-
href="{{ url_for('genes.gene', hgnc_id=gene.hgnc_id) }}">{{ gene.hgnc_symbol or gene.hgnc_id }}
|
235
|
-
{% for model in gene.inheritance %} {{ inheritance_badge(model,inherit_palette) }}{% endfor %}</a>
|
236
|
-
{% if panel_count > 0 %}
|
237
|
-
<a
|
238
|
-
class="badge bg-secondary text-white"
|
239
|
-
data-bs-toggle="popover"
|
240
|
-
data-bs-html="true"
|
241
|
-
data-bs-trigger="hover click"
|
242
|
-
data-bs-content="{% for panel in variant.case_panels|sort(attribute='panel_name',case_sensitive=False)|rejectattr('removed') %}
|
243
|
-
{{ panel.panel_name|safe }}<br>
|
244
|
-
{% endfor %}"
|
245
|
-
title="Overlapping gene panels">{{panel_count}}
|
246
|
-
</a>
|
247
|
-
{% endif %}
|
248
|
-
</div>
|
233
|
+
{{ gene_link(gene) }}
|
234
|
+
{% for model in gene.inheritance %}
|
235
|
+
{{ inheritance_badge(model, inherit_palette) }}
|
236
|
+
{% endfor %}
|
237
|
+
{{ panel_badge(variant, gene.hgnc_id) }}
|
238
|
+
</div>
|
249
239
|
{% endfor %}
|
250
|
-
|
251
240
|
{% endif %}
|
241
|
+
|
252
242
|
</div>
|
253
243
|
{% endmacro %}
|
254
244
|
|
@@ -74,6 +74,17 @@
|
|
74
74
|
{% endif %}
|
75
75
|
{% endmacro %}
|
76
76
|
|
77
|
+
{% macro ccv_evaluations_badge(variant) %}
|
78
|
+
{% if variant.ccv_evaluations %}
|
79
|
+
{% for evaluation in (variant.ccv_evaluations or []) %}
|
80
|
+
<span class="badge bg-secondary" style="margin-left:1px" data-bs-toggle="tooltip" data-bs-placement="right"
|
81
|
+
title="Previously classified as {{ evaluation.ccv_classification.label }}">
|
82
|
+
{{ evaluation.ccv_classification.short }}
|
83
|
+
</span>
|
84
|
+
{% endfor %}
|
85
|
+
{% endif %}
|
86
|
+
{% endmacro %}
|
87
|
+
|
77
88
|
{% macro dismissals_badge(variant) %}
|
78
89
|
{% if variant.dismissals %}
|
79
90
|
<span class="badge bg-secondary" style="margin-left:1px" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="top"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import logging
|
2
|
-
from typing import Optional
|
2
|
+
from typing import List, Optional
|
3
3
|
|
4
4
|
import requests
|
5
5
|
|
@@ -41,7 +41,7 @@ class PanelAppClient:
|
|
41
41
|
for type in panel.get("types", []):
|
42
42
|
self.panel_types.add(type["slug"])
|
43
43
|
|
44
|
-
def get_panel_ids(self, signed_off: bool) ->
|
44
|
+
def get_panel_ids(self, signed_off: bool) -> List[int]:
|
45
45
|
"""Returns a list of panel ids contained in a json document with gene panels data."""
|
46
46
|
|
47
47
|
def get_ids(json_panels):
|
@@ -1,3 +1,11 @@
|
|
1
|
+
const imgMaxWidth = window.innerWidth*0.7;
|
2
|
+
const imgMaxHeight = window.innerHeight*0.7;
|
3
|
+
|
4
|
+
function calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) {
|
5
|
+
let ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
|
6
|
+
return [srcWidth*ratio, srcHeight*ratio];
|
7
|
+
}
|
8
|
+
|
1
9
|
/**
|
2
10
|
* This function fetches an image from disk and displays it in a specified div
|
3
11
|
* @param {string} imageUrl - The URL of the image to fetch
|
@@ -9,15 +17,24 @@
|
|
9
17
|
.then(blob => {
|
10
18
|
const img = document.createElement("img");
|
11
19
|
img.src = URL.createObjectURL(blob);
|
20
|
+
if (width > imgMaxWidth || height > imgMaxHeight) {
|
21
|
+
const resized_pixels = calculateAspectRatioFit(width, height, imgMaxWidth, imgMaxHeight);
|
22
|
+
width = resized_pixels[0];
|
23
|
+
height = resized_pixels[1];
|
24
|
+
}
|
12
25
|
if (width) {
|
13
26
|
img.style.width = width+'px';
|
14
27
|
}
|
15
28
|
if (height) {
|
16
29
|
img.style.height = height+'px';
|
17
30
|
}
|
18
|
-
//
|
31
|
+
// Add the image to a link and add the link to the div
|
32
|
+
const link = document.createElement('a');
|
33
|
+
link.appendChild(img);
|
34
|
+
link.href = img.src;
|
35
|
+
link.setAttribute('target', "_blank");
|
19
36
|
const div = document.getElementById(divId);
|
20
|
-
div.appendChild(
|
37
|
+
div.appendChild(link);
|
21
38
|
})
|
22
39
|
.catch(error => console.error(error));
|
23
40
|
}
|
scout/utils/ccv.py
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# coding=UTF-8
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from scout.constants.ccv import CCV_COMPLETE_MAP, CCV_POTENTIAL_CONFLICTS
|
5
|
+
|
6
|
+
|
7
|
+
def get_ccv_points(ccv_terms: set) -> int:
|
8
|
+
"""
|
9
|
+
Use the algorithm described in Clingen-CGC-VIGG classification paper (Horak 2022)
|
10
|
+
Given a set of CCV evidence criteria terms
|
11
|
+
for each term,
|
12
|
+
check prefixes if no suffix match or stand-alone criteria match
|
13
|
+
|
14
|
+
O positive, SB negative.
|
15
|
+
VS 8 points, S 4, M 2, P 1.
|
16
|
+
|
17
|
+
If no terms return None
|
18
|
+
|
19
|
+
Args:
|
20
|
+
ccv_terms(set(str)): A collection of prediction terms
|
21
|
+
Returns:
|
22
|
+
points(int):"""
|
23
|
+
|
24
|
+
ovs_terms = []
|
25
|
+
os_terms = []
|
26
|
+
om_terms = []
|
27
|
+
op_terms = []
|
28
|
+
sbvs_terms = []
|
29
|
+
sbs_terms = []
|
30
|
+
sbm_terms = []
|
31
|
+
sbp_terms = []
|
32
|
+
|
33
|
+
prefix_map = {
|
34
|
+
"OVS": ovs_terms,
|
35
|
+
"OS": os_terms,
|
36
|
+
"OM": om_terms,
|
37
|
+
"OP": op_terms,
|
38
|
+
"SBVS": sbvs_terms,
|
39
|
+
"SBS": sbs_terms,
|
40
|
+
"SBM": sbm_terms,
|
41
|
+
"SBP": sbp_terms,
|
42
|
+
}
|
43
|
+
|
44
|
+
suffix_map = {
|
45
|
+
"_Strong": {"O": os_terms, "SB": sbs_terms},
|
46
|
+
"_Moderate": {"O": om_terms, "SB": sbm_terms},
|
47
|
+
"_Supporting": {"O": op_terms, "SB": sbp_terms},
|
48
|
+
}
|
49
|
+
|
50
|
+
for term in ccv_terms:
|
51
|
+
for suffix, prefix_dict in suffix_map.items():
|
52
|
+
if term.endswith(suffix):
|
53
|
+
for prefix, term_list in prefix_dict.items():
|
54
|
+
if term.startswith(prefix):
|
55
|
+
term_list.append(term)
|
56
|
+
break
|
57
|
+
else:
|
58
|
+
continue
|
59
|
+
break
|
60
|
+
else:
|
61
|
+
for prefix, term_list in prefix_map.items():
|
62
|
+
if term.startswith(prefix):
|
63
|
+
term_list.append(term)
|
64
|
+
break
|
65
|
+
points = (
|
66
|
+
8 * len(ovs_terms)
|
67
|
+
+ 4 * len(os_terms)
|
68
|
+
+ 2 * len(om_terms)
|
69
|
+
+ len(op_terms)
|
70
|
+
- 8 * len(sbvs_terms)
|
71
|
+
- 4 * len(sbs_terms)
|
72
|
+
- 2 * len(sbm_terms)
|
73
|
+
- len(sbp_terms)
|
74
|
+
)
|
75
|
+
return points
|
76
|
+
|
77
|
+
|
78
|
+
def get_ccv(ccv_terms: set) -> Optional[str]:
|
79
|
+
"""Use the algorithm described in Clingen-CGC-VIGG classification paper (Horak 2022)
|
80
|
+
|
81
|
+
If no terms return None
|
82
|
+
|
83
|
+
O >= 10
|
84
|
+
OP 6 <= p <= 9
|
85
|
+
VUS 0 <= p <= 5
|
86
|
+
LB -1 <= p <= -6
|
87
|
+
B <= -7
|
88
|
+
|
89
|
+
Args:
|
90
|
+
ccv_terms(set(str)): A collection of prediction terms
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
prediction(str): in ['uncertain_significance','benign','likely_benign',
|
94
|
+
'likely_oncogenic','oncogenic']
|
95
|
+
|
96
|
+
"""
|
97
|
+
if not ccv_terms:
|
98
|
+
return None
|
99
|
+
|
100
|
+
points = get_ccv_points(ccv_terms)
|
101
|
+
|
102
|
+
if points <= -7:
|
103
|
+
prediction = "benign"
|
104
|
+
elif points <= -1:
|
105
|
+
prediction = "likely_benign"
|
106
|
+
elif points <= 5:
|
107
|
+
prediction = "uncertain_significance"
|
108
|
+
elif points <= 9:
|
109
|
+
prediction = "likely_oncogenic"
|
110
|
+
elif points >= 10:
|
111
|
+
prediction = "oncogenic"
|
112
|
+
|
113
|
+
return prediction
|
114
|
+
|
115
|
+
|
116
|
+
def get_ccv_temperature(ccv_terms: set) -> Optional[dict]:
|
117
|
+
"""
|
118
|
+
Use the algorithm described in Clingen-CGC-VIGG classification paper (Horak 2022)
|
119
|
+
|
120
|
+
O >= 10
|
121
|
+
OP 6 <= p <= 9
|
122
|
+
VUS 0 <= p <= 5
|
123
|
+
LB -1 <= p <= -6
|
124
|
+
B <= -7
|
125
|
+
|
126
|
+
If no terms return None
|
127
|
+
|
128
|
+
Args:
|
129
|
+
ccv_terms(set(str)): A collection of prediction terms
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
dict:
|
133
|
+
temperature:
|
134
|
+
(points, temperature, point_classification)
|
135
|
+
|
136
|
+
"""
|
137
|
+
TEMPERATURE_STRINGS = {
|
138
|
+
-1: {"label": "B/LB", "color": "success", "icon": "fa-times"},
|
139
|
+
0: {"label": "Ice cold", "color": "info", "icon": "fa-icicles"},
|
140
|
+
1: {"label": "Cold", "color": "info", "icon": "fa-snowman"},
|
141
|
+
2: {"label": "Cold", "color": "info", "icon": "fa-snowflake"},
|
142
|
+
3: {"label": "Tepid", "color": "yellow", "icon": "fa-temperature-half"},
|
143
|
+
4: {"label": "Warm", "color": "orange", "icon": "fa-mug-hot"},
|
144
|
+
5: {"label": "Hot", "color": "red", "icon": "fa-pepper-hot"},
|
145
|
+
6: {"label": "LO/O", "color": "danger", "icon": "fa-stethoscope"},
|
146
|
+
}
|
147
|
+
|
148
|
+
if not ccv_terms:
|
149
|
+
points = 0
|
150
|
+
point_classification = "uncertain_significance"
|
151
|
+
return {
|
152
|
+
"points": points,
|
153
|
+
"temperature": TEMPERATURE_STRINGS[points].get("label"),
|
154
|
+
"temperature_class": TEMPERATURE_STRINGS[points].get("color"),
|
155
|
+
"temperature_icon": TEMPERATURE_STRINGS[points].get("icon"),
|
156
|
+
"point_classification": CCV_COMPLETE_MAP[point_classification].get("short"),
|
157
|
+
}
|
158
|
+
|
159
|
+
points = get_ccv_points(ccv_terms)
|
160
|
+
|
161
|
+
if points <= -7:
|
162
|
+
point_classification = "benign"
|
163
|
+
temperature_icon = TEMPERATURE_STRINGS[-1].get("icon")
|
164
|
+
elif points <= -1:
|
165
|
+
point_classification = "likely_benign"
|
166
|
+
temperature_icon = TEMPERATURE_STRINGS[-1].get("icon")
|
167
|
+
elif points <= 5:
|
168
|
+
point_classification = "uncertain_significance"
|
169
|
+
elif points <= 9:
|
170
|
+
point_classification = "likely_oncogenic"
|
171
|
+
temperature_icon = TEMPERATURE_STRINGS[6].get("icon")
|
172
|
+
elif points >= 10:
|
173
|
+
point_classification = "oncogenic"
|
174
|
+
temperature_icon = TEMPERATURE_STRINGS[6].get("icon")
|
175
|
+
|
176
|
+
temperature_class = CCV_COMPLETE_MAP[point_classification].get("color")
|
177
|
+
temperature = CCV_COMPLETE_MAP[point_classification].get("label")
|
178
|
+
|
179
|
+
if point_classification == "uncertain_significance":
|
180
|
+
temperature_class = TEMPERATURE_STRINGS[points].get("color")
|
181
|
+
temperature = TEMPERATURE_STRINGS[points].get("label")
|
182
|
+
temperature_icon = TEMPERATURE_STRINGS[points].get("icon")
|
183
|
+
|
184
|
+
return {
|
185
|
+
"points": points,
|
186
|
+
"temperature": temperature,
|
187
|
+
"temperature_class": temperature_class,
|
188
|
+
"temperature_icon": temperature_icon,
|
189
|
+
"point_classification": CCV_COMPLETE_MAP[point_classification].get("short"),
|
190
|
+
}
|
191
|
+
|
192
|
+
|
193
|
+
def get_ccv_conflicts(ccv_terms: set) -> list:
|
194
|
+
"""Check potential conflict paris, return list of reference strings."""
|
195
|
+
|
196
|
+
conflicts = []
|
197
|
+
for t1, t2, reference in CCV_POTENTIAL_CONFLICTS:
|
198
|
+
if t1 in ccv_terms and t2 in ccv_terms:
|
199
|
+
conflicts.append(reference)
|
200
|
+
|
201
|
+
return conflicts
|