scout-browser 4.92__py3-none-any.whl → 4.95.0__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/adapter/mongo/base.py +3 -0
- scout/adapter/mongo/case.py +27 -2
- scout/adapter/mongo/ccv.py +131 -0
- scout/adapter/mongo/hgnc.py +5 -1
- scout/adapter/mongo/managed_variant.py +4 -2
- scout/adapter/mongo/query.py +91 -54
- scout/adapter/mongo/variant.py +17 -11
- scout/adapter/mongo/variant_events.py +45 -1
- scout/build/ccv.py +59 -0
- scout/build/panel.py +1 -1
- scout/commands/export/export_command.py +0 -0
- scout/commands/load/base.py +0 -0
- scout/commands/load/user.py +0 -0
- scout/commands/serve.py +2 -1
- scout/commands/update/disease.py +0 -0
- scout/commands/update/genes.py +0 -0
- scout/commands/wipe_database.py +0 -0
- scout/constants/__init__.py +2 -0
- scout/constants/case_tags.py +2 -0
- scout/constants/ccv.py +244 -0
- scout/constants/gene_tags.py +22 -12
- scout/demo/643594.config.yaml +2 -2
- scout/demo/643594.research.mei.vcf.gz +0 -0
- scout/demo/643594.research.mei.vcf.gz.tbi +0 -0
- scout/demo/images/custom_images/1300x1000.jpg +0 -0
- scout/load/panelapp.py +8 -12
- scout/models/ccv_evaluation.py +26 -0
- scout/models/variant/variant.py +1 -0
- scout/parse/omim.py +5 -6
- scout/parse/panelapp.py +16 -42
- scout/parse/variant/compound.py +20 -21
- scout/parse/variant/gene.py +0 -0
- scout/parse/variant/genotype.py +0 -0
- scout/resources/custom_igv_tracks/mane.bb +0 -0
- scout/server/blueprints/cases/controllers.py +48 -0
- scout/server/blueprints/cases/templates/cases/case_report.html +61 -1
- scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +2 -2
- scout/server/blueprints/cases/templates/cases/index.html +0 -2
- scout/server/blueprints/cases/views.py +5 -5
- scout/server/blueprints/clinvar/controllers.py +4 -5
- scout/server/blueprints/institutes/controllers.py +129 -67
- scout/server/blueprints/institutes/forms.py +5 -2
- scout/server/blueprints/institutes/templates/overview/cases.html +6 -0
- scout/server/blueprints/institutes/templates/overview/causatives.html +1 -1
- scout/server/blueprints/institutes/templates/overview/utils.html +18 -6
- scout/server/blueprints/institutes/templates/overview/verified.html +1 -1
- scout/server/blueprints/institutes/views.py +4 -0
- scout/server/blueprints/managed_variants/forms.py +17 -2
- scout/server/blueprints/managed_variants/templates/managed_variants/managed_variants.html +2 -2
- 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 +86 -5
- scout/server/blueprints/variant/templates/variant/sv-variant.html +2 -2
- scout/server/blueprints/variant/templates/variant/tx_overview.html +3 -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 +115 -5
- scout/server/blueprints/variants/controllers.py +31 -0
- scout/server/blueprints/variants/forms.py +33 -5
- scout/server/blueprints/variants/templates/variants/cancer-sv-variants.html +4 -18
- scout/server/blueprints/variants/templates/variants/cancer-variants.html +4 -13
- scout/server/blueprints/variants/templates/variants/components.html +77 -73
- scout/server/blueprints/variants/templates/variants/indicators.html +11 -0
- scout/server/blueprints/variants/templates/variants/sv-variants.html +2 -2
- scout/server/links.py +1 -1
- scout/server/static/custom_images.js +19 -2
- scout/utils/acmg.py +0 -1
- scout/utils/ccv.py +193 -0
- scout/utils/link.py +4 -3
- scout/utils/md5.py +0 -0
- {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/METADATA +67 -45
- {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/RECORD +70 -65
- {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/WHEEL +1 -2
- scout/__version__.py +0 -1
- scout/demo/images/custom_images/640x480_two.jpg +0 -0
- scout_browser-4.92.dist-info/top_level.txt +0 -1
- {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/entry_points.txt +0 -0
- {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info/licenses}/LICENSE +0 -0
@@ -62,6 +62,8 @@ from scout.server.utils import (
|
|
62
62
|
case_has_rna_tracks,
|
63
63
|
institute_and_case,
|
64
64
|
)
|
65
|
+
from scout.utils.acmg import get_acmg_temperature
|
66
|
+
from scout.utils.ccv import get_ccv_temperature
|
65
67
|
|
66
68
|
LOG = logging.getLogger(__name__)
|
67
69
|
|
@@ -611,6 +613,46 @@ def check_outdated_gene_panel(panel_obj, latest_panel):
|
|
611
613
|
return extra_genes, missing_genes
|
612
614
|
|
613
615
|
|
616
|
+
def add_bayesian_acmg_classification(variant_obj: dict):
|
617
|
+
"""Append info to display the ACMG VUS Bayesian score / temperature.
|
618
|
+
Criteria have a term and a modifier field on the db document
|
619
|
+
that are joined together in a string to conform to a regular
|
620
|
+
ACMG term format. A set of such terms are passed on for evaluation
|
621
|
+
to the same function as the ACMG classification form uses.
|
622
|
+
"""
|
623
|
+
variant_acmg_classifications = list(
|
624
|
+
store.get_evaluations_case_specific(document_id=variant_obj["_id"])
|
625
|
+
)
|
626
|
+
if variant_acmg_classifications:
|
627
|
+
terms = set()
|
628
|
+
for criterium in variant_acmg_classifications[0].get("criteria", []):
|
629
|
+
term = criterium.get("term")
|
630
|
+
if criterium.get("modifier"):
|
631
|
+
term += f"_{criterium.get('modifier')}"
|
632
|
+
terms.add(term)
|
633
|
+
variant_obj["bayesian_acmg"] = get_acmg_temperature(terms)
|
634
|
+
|
635
|
+
|
636
|
+
def add_bayesian_ccv_classification(variant_obj: dict):
|
637
|
+
"""Append info to display the CCV VUS Bayesian score / temperature.
|
638
|
+
Criteria have a term and a modifier field on the db document
|
639
|
+
that are joined together in a string to conform to a regular
|
640
|
+
CCV term format. A set of such terms are passed on for evaluation
|
641
|
+
to the same function as the CCV classification form uses.
|
642
|
+
"""
|
643
|
+
variant_ccv_classifications = list(
|
644
|
+
store.get_ccv_evaluations_case_specific(document_id=variant_obj["_id"])
|
645
|
+
)
|
646
|
+
if variant_ccv_classifications:
|
647
|
+
terms = set()
|
648
|
+
for criterium in variant_ccv_classifications[0].get("ccv_criteria", []):
|
649
|
+
term = criterium.get("term")
|
650
|
+
if criterium.get("modifier"):
|
651
|
+
term += f"_{criterium.get('modifier')}"
|
652
|
+
terms.add(term)
|
653
|
+
variant_obj["bayesian_ccv"] = get_ccv_temperature(terms)
|
654
|
+
|
655
|
+
|
614
656
|
def case_report_variants(store: MongoAdapter, case_obj: dict, institute_obj: dict, data: dict):
|
615
657
|
"""Gather evaluated variants info to include in case report."""
|
616
658
|
|
@@ -624,6 +666,8 @@ def case_report_variants(store: MongoAdapter, case_obj: dict, institute_obj: dic
|
|
624
666
|
continue
|
625
667
|
if case_key == "partial_causatives":
|
626
668
|
var_obj["phenotypes"] = case_obj["partial_causatives"][var_id]
|
669
|
+
add_bayesian_acmg_classification(var_obj)
|
670
|
+
add_bayesian_ccv_classification(var_obj)
|
627
671
|
evaluated_variants_by_type[eval_category].append(
|
628
672
|
_get_decorated_var(var_obj=var_obj, institute_obj=institute_obj, case_obj=case_obj)
|
629
673
|
)
|
@@ -663,6 +707,10 @@ def _append_evaluated_variant_by_type(
|
|
663
707
|
"""
|
664
708
|
for eval_category, variant_key in CASE_REPORT_VARIANT_TYPES.items():
|
665
709
|
if variant_key in var_obj and var_obj[variant_key] is not None:
|
710
|
+
|
711
|
+
add_bayesian_acmg_classification(var_obj)
|
712
|
+
add_bayesian_ccv_classification(var_obj)
|
713
|
+
|
666
714
|
evaluated_variants_by_type[eval_category].append(
|
667
715
|
_get_decorated_var(var_obj=var_obj, institute_obj=institute_obj, case_obj=case_obj)
|
668
716
|
)
|
@@ -40,6 +40,11 @@
|
|
40
40
|
<li class="nav-item">
|
41
41
|
<a class="nav-link link-secondary" style="text-decoration: none !important;" href="#acmg_variants">ACMG-classified variants</a>
|
42
42
|
</li>
|
43
|
+
{% if cancer %}
|
44
|
+
<li class="nav-item">
|
45
|
+
<a class="nav-link link-secondary" style="text-decoration: none !important;" href="#ccv_variants">ClinGen-CGC-VICC-classified variants</a>
|
46
|
+
</li>
|
47
|
+
{% endif %}
|
43
48
|
<li class="nav-item">
|
44
49
|
<a class="nav-link link-secondary" style="text-decoration: none !important;" href="#manual_ranked_variants">Manual ranked</a>
|
45
50
|
</li>
|
@@ -64,6 +69,7 @@
|
|
64
69
|
{{ causatives_panel()}}
|
65
70
|
{{ pinned_panel() }}
|
66
71
|
{{ classified_panel() }}
|
72
|
+
{% if cancer %}{{ ccv_classified_panel() }}{% endif %}
|
67
73
|
{{ tagged_panel() }}
|
68
74
|
{{ commented_panel() }}
|
69
75
|
{{ dismissed_panel() }}
|
@@ -392,6 +398,33 @@
|
|
392
398
|
</div>
|
393
399
|
{% endmacro %}
|
394
400
|
|
401
|
+
{% macro ccv_classified_panel() %}
|
402
|
+
<div class="card border-warning mb-3" style="border-width: 5px; display: block;">
|
403
|
+
<div class="card-header bg-warning text-dark" style="border-top-left-radius: 0; border-top-right-radius: 0;">
|
404
|
+
<a id="ccv_variants"><strong>Other ClinGen-CGC-VICC-classified Variants</strong></a>
|
405
|
+
</div>
|
406
|
+
<div class="card-body">
|
407
|
+
{% set duplicated_variants = [] %}
|
408
|
+
{% if variants.ccv_classified_detailed %}
|
409
|
+
{% for variant in variants.ccv_classified_detailed|sort(attribute='variant_rank') %}
|
410
|
+
{% if variant['_id'] not in printed_vars %}
|
411
|
+
{% do printed_vars.append(variant['_id']) %}
|
412
|
+
{{ variant_content(variant, loop.index) }}
|
413
|
+
<br>
|
414
|
+
{% else %}
|
415
|
+
{% do duplicated_variants.append(variant['_id']) %}
|
416
|
+
{% endif %}
|
417
|
+
{% endfor %}
|
418
|
+
{% else %}
|
419
|
+
No ClinGen-CGC-VICC-classified variants available for this case
|
420
|
+
{% endif %}
|
421
|
+
{% if variants.ccv_classified_detailed and duplicated_variants|length == variants.ccv_classified_detailed|length %}
|
422
|
+
All ClinGen-CGC-VICC-classified variants are already described in the previous views
|
423
|
+
{% endif %}
|
424
|
+
</div>
|
425
|
+
</div>
|
426
|
+
{% endmacro %}
|
427
|
+
|
395
428
|
{% macro tagged_panel() %}
|
396
429
|
<div class="card border-warning mb-3" style="border-width: 5px; display: block;">
|
397
430
|
<div class="card-header bg-warning text-dark" style="border-top-left-radius: 0; border-top-right-radius: 0;">
|
@@ -635,6 +668,10 @@
|
|
635
668
|
<th>Inheritance models</th>
|
636
669
|
{% endif %}
|
637
670
|
<th>ACMG classification</th>
|
671
|
+
<th>Bayesian classification</th>
|
672
|
+
{% if cancer %}
|
673
|
+
<th>ClinGen-CGC-VICC classification</th>
|
674
|
+
{% endif %}
|
638
675
|
</tr>
|
639
676
|
</thead>
|
640
677
|
<tbody>
|
@@ -678,11 +715,34 @@
|
|
678
715
|
{% endif %}
|
679
716
|
<td>
|
680
717
|
{% if variant.acmg_classification %}
|
681
|
-
<span class="badge rounded-pill bg-{{variant.acmg_classification['color'] if variant.acmg_classification['color'] else 'secondary'}}" title="{{variant.acmg_classification['
|
718
|
+
<span class="badge rounded-pill bg-{{variant.acmg_classification['color'] if variant.acmg_classification['color'] else 'secondary'}}" title="{{variant.acmg_classification['label']}}">{{variant.acmg_classification['label'] }}</span>
|
682
719
|
{% else %}
|
683
720
|
-
|
684
721
|
{% endif %}
|
722
|
+
<td>
|
723
|
+
{% if variant.bayesian_acmg %}
|
724
|
+
<span class="badge rounded-pill bg-{{variant.bayesian_acmg.temperature_class}}">Score {{variant.bayesian_acmg.points|string}} <span class='fa {{variant.bayesian_acmg.temperature_icon}}'></span> {{variant.bayesian_acmg.temperature}} ({{variant.bayesian_acmg.point_classification}})</span>
|
725
|
+
{% else %}
|
726
|
+
-
|
727
|
+
{% endif %}
|
728
|
+
</td>
|
685
729
|
</td>
|
730
|
+
{% if cancer %}
|
731
|
+
<td>
|
732
|
+
{% if variant.ccv_classification %}
|
733
|
+
<span class="badge rounded-pill bg-{{variant.ccv_classification['color'] if variant.ccv_classification['color'] else 'secondary'}}" title="{{variant.ccv_classification['label']}}">{{variant.ccv_classification['label'] }}</span>
|
734
|
+
{% else %}
|
735
|
+
-
|
736
|
+
{% endif %}
|
737
|
+
{% if variant.bayesian_ccv %}
|
738
|
+
<span class="badge rounded-pill bg-{{variant.bayesian_ccv.temperature_class}}">Score {{variant.bayesian_ccv.points|string}} <span class='fa {{variant.bayesian_ccv.temperature_icon}}'></span>
|
739
|
+
{% if variant.bayesian_ccv.point_classification == "VUS" %}
|
740
|
+
{{variant.bayesian_ccv.temperature}}
|
741
|
+
{% endif %}
|
742
|
+
</span>
|
743
|
+
{% endif %}
|
744
|
+
</td>
|
745
|
+
{% endif %}
|
686
746
|
</tr>
|
687
747
|
</tbody>
|
688
748
|
</table>
|
@@ -372,7 +372,7 @@
|
|
372
372
|
<select class="form-control form-control-sm" name="collaborator">
|
373
373
|
<option selected disabled value="">Select institute</option>
|
374
374
|
{% for collab_id, collab_name in collaborators %}
|
375
|
-
<option value="{{ collab_id }}">{{ collab_name }}</option>
|
375
|
+
<option value="{{ collab_id }}">{{ collab_name }} ({{ collab_id }})</option>
|
376
376
|
{% endfor %}
|
377
377
|
</select>
|
378
378
|
<span class="input-group-btn">
|
@@ -389,7 +389,7 @@
|
|
389
389
|
<select class="form-control form-control-sm" name="collaborator">
|
390
390
|
<option>Institute</option>
|
391
391
|
{% for collab_id, collab_name in case.o_collaborators %}
|
392
|
-
<option value="{{ collab_id }}">{{ collab_name }}</option>
|
392
|
+
<option value="{{ collab_id }}">{{ collab_name }} ({{ collab_id }})</option>
|
393
393
|
{% endfor %}
|
394
394
|
</select>
|
395
395
|
<div class="input-group-btn">
|
@@ -25,9 +25,7 @@
|
|
25
25
|
<a href="{{ url_for('overview.cases', institute_id=institute._id) }}">
|
26
26
|
{{ institute.display_name }}
|
27
27
|
</a>
|
28
|
-
{% if current_user.is_admin %}
|
29
28
|
<span class="text-muted">({{ institute._id }})</span>
|
30
|
-
{% endif %}
|
31
29
|
<span class="badge bg-secondary rounded-pill float-end">{{ case_count }} cases </span>
|
32
30
|
</li>
|
33
31
|
{% endfor %}
|
@@ -7,6 +7,7 @@ import shutil
|
|
7
7
|
from ast import literal_eval
|
8
8
|
from io import BytesIO
|
9
9
|
from operator import itemgetter
|
10
|
+
from tempfile import NamedTemporaryFile, mkdtemp
|
10
11
|
from typing import Generator, Optional, Union
|
11
12
|
|
12
13
|
from cairosvg import svg2png
|
@@ -286,7 +287,9 @@ def pdf_case_report(institute_id, case_name):
|
|
286
287
|
# Workaround to be able to print the case pedigree to pdf
|
287
288
|
if case_obj.get("madeline_info") and case_obj.get("madeline_info") != "":
|
288
289
|
try:
|
289
|
-
write_to =
|
290
|
+
write_to = NamedTemporaryFile(
|
291
|
+
mode="a+", prefix=case_obj.get("_id"), suffix="madeline.png"
|
292
|
+
)
|
290
293
|
svg2png(
|
291
294
|
bytestring=case_obj["madeline_info"],
|
292
295
|
write_to=write_to,
|
@@ -322,11 +325,8 @@ def mt_report(institute_id, case_name):
|
|
322
325
|
institute_obj, case_obj = institute_and_case(store, institute_id, case_name)
|
323
326
|
|
324
327
|
# create a temp folder to write excel files into
|
325
|
-
temp_excel_dir = os.path.join(
|
326
|
-
cases_bp.static_folder, "_".join([case_obj["display_name"], "mt_reports"])
|
327
|
-
)
|
328
|
-
os.makedirs(temp_excel_dir, exist_ok=True)
|
329
328
|
|
329
|
+
temp_excel_dir = mkdtemp(suffix="_".join([case_obj["display_name"], "mt_reports"]))
|
330
330
|
if controllers.mt_excel_files(store, case_obj, temp_excel_dir):
|
331
331
|
data = zip_dir_to_obj(temp_excel_dir)
|
332
332
|
|
@@ -399,11 +399,10 @@ def json_api_submission(submission_id):
|
|
399
399
|
afile.flush()
|
400
400
|
afile.seek(0)
|
401
401
|
|
402
|
-
with
|
403
|
-
mode="a+", prefix="Variant", suffix=".csv"
|
404
|
-
|
405
|
-
|
406
|
-
) as casedata_file:
|
402
|
+
with (
|
403
|
+
NamedTemporaryFile(mode="a+", prefix="Variant", suffix=".csv") as variant_file,
|
404
|
+
NamedTemporaryFile(mode="a+", prefix="CaseData", suffix=".csv") as casedata_file,
|
405
|
+
):
|
407
406
|
# Write temp Variant CSV file
|
408
407
|
_, variants_header, variants_lines = clinvar_submission_file(
|
409
408
|
submission_id, "variant_data", "SUB000"
|
@@ -3,7 +3,7 @@ import datetime
|
|
3
3
|
import logging
|
4
4
|
from typing import Dict, List, Optional, Tuple
|
5
5
|
|
6
|
-
from flask import Response, current_app, flash, url_for
|
6
|
+
from flask import Response, current_app, flash, request, url_for
|
7
7
|
from flask_login import current_user
|
8
8
|
from pymongo import ASCENDING, DESCENDING
|
9
9
|
from pymongo.cursor import Cursor
|
@@ -11,10 +11,13 @@ from werkzeug.datastructures import Headers, MultiDict
|
|
11
11
|
|
12
12
|
from scout.adapter.mongo.base import MongoAdapter
|
13
13
|
from scout.constants import (
|
14
|
+
CANCER_PHENOTYPE_MAP,
|
14
15
|
CASE_STATUSES,
|
15
16
|
DATE_DAY_FORMATTER,
|
16
17
|
ID_PROJECTION,
|
17
18
|
PHENOTYPE_GROUPS,
|
19
|
+
PHENOTYPE_MAP,
|
20
|
+
SEX_MAP,
|
18
21
|
VARIANTS_TARGET_FROM_CATEGORY,
|
19
22
|
)
|
20
23
|
from scout.server.blueprints.variant.utils import predictions, update_representative_gene
|
@@ -422,97 +425,129 @@ def _sort_cases(data, request, all_cases):
|
|
422
425
|
return all_cases
|
423
426
|
|
424
427
|
|
425
|
-
def
|
426
|
-
"""
|
428
|
+
def export_case_samples(institute_id, filtered_cases) -> Response:
|
429
|
+
"""Export to CSV file a list of samples from selected cases."""
|
430
|
+
EXPORT_HEADER = [
|
431
|
+
"Sample ID",
|
432
|
+
"Sample Name",
|
433
|
+
"Analysis",
|
434
|
+
"Affected status",
|
435
|
+
"Sex",
|
436
|
+
"Sex confirmed",
|
437
|
+
"Parenthood confirmed",
|
438
|
+
"Predicted ancestry",
|
439
|
+
"Tissue",
|
440
|
+
"Case Name",
|
441
|
+
"Case ID",
|
442
|
+
"Analysis date",
|
443
|
+
"Case Status",
|
444
|
+
"Case phenotypes",
|
445
|
+
"Research",
|
446
|
+
"Track",
|
447
|
+
"Default panels",
|
448
|
+
"Genome build",
|
449
|
+
"SNV/SV rank models",
|
450
|
+
]
|
451
|
+
export_lines = []
|
452
|
+
export_lines.append("\t".join(EXPORT_HEADER)) # Use tab-separated values
|
453
|
+
for case in filtered_cases:
|
454
|
+
for individual in case.get("individuals", []):
|
455
|
+
export_line = [
|
456
|
+
individual["individual_id"],
|
457
|
+
individual["display_name"],
|
458
|
+
individual.get("analysis_type").upper(),
|
459
|
+
(
|
460
|
+
CANCER_PHENOTYPE_MAP[individual.get("phenotype", 0)]
|
461
|
+
if case.get("track") == "cancer"
|
462
|
+
else PHENOTYPE_MAP[individual.get("phenotype", 0)]
|
463
|
+
),
|
464
|
+
SEX_MAP[individual.get("sex", 0)],
|
465
|
+
individual.get("confirmed_sex") or "-",
|
466
|
+
individual.get("confirmed_parent") or "-",
|
467
|
+
individual.get("predicted_ancestry") or "-",
|
468
|
+
individual.get("tissue_type") or "-",
|
469
|
+
case["display_name"],
|
470
|
+
case["_id"],
|
471
|
+
case["analysis_date"].strftime("%Y-%m-%d %H:%M:%S"),
|
472
|
+
case["status"],
|
473
|
+
", ".join(hpo["phenotype_id"] for hpo in case.get("phenotype_terms", [])),
|
474
|
+
case.get("is_research"),
|
475
|
+
case.get("track"),
|
476
|
+
", ".join(
|
477
|
+
panel["panel_name"] for panel in case.get("panels") if panel.get("is_default")
|
478
|
+
),
|
479
|
+
case.get("genome_build"),
|
480
|
+
f"{case.get('rank_model_version', '-')}/{case.get('sv_rank_model_version', '-')}",
|
481
|
+
]
|
482
|
+
export_lines.append("\t".join(str(item) for item in export_line))
|
427
483
|
|
428
|
-
|
484
|
+
file_content = "\n".join(export_lines)
|
429
485
|
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
486
|
+
return Response(
|
487
|
+
file_content,
|
488
|
+
mimetype="text/plain",
|
489
|
+
headers={
|
490
|
+
"Content-Disposition": f"attachment;filename={institute_id}_cases_{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}.txt"
|
491
|
+
},
|
492
|
+
)
|
434
493
|
|
435
|
-
|
436
|
-
|
437
|
-
"""
|
494
|
+
|
495
|
+
def cases(store: MongoAdapter, request: request, institute_id: str) -> dict:
|
496
|
+
"""Preprocess case objects for the 'cases' view."""
|
438
497
|
data = {}
|
498
|
+
|
499
|
+
# Initialize data (institute info, filters, and case counts)
|
439
500
|
institute_obj = institute_and_case(store, institute_id)
|
440
501
|
data["institute"] = institute_obj
|
441
|
-
|
442
|
-
name_query = request.form
|
443
|
-
limit = int(request.form.get("search_limit")) if request.form.get("search_limit") else 100
|
444
|
-
|
445
502
|
data["form"] = CaseFilterForm(request.form)
|
503
|
+
data["status_ncases"] = store.nr_cases_by_status(institute_id=institute_id)
|
504
|
+
data["nr_cases"] = sum(data["status_ncases"].values())
|
505
|
+
|
506
|
+
# Fetch Sanger unevaluated and validated cases
|
507
|
+
sanger_ordered_not_validated = get_sanger_unevaluated(store, institute_id, current_user.email)
|
508
|
+
data["sanger_unevaluated"], data["sanger_validated_by_others"] = sanger_ordered_not_validated
|
446
509
|
|
510
|
+
# Projection for fetching cases
|
447
511
|
ALL_CASES_PROJECTION = {
|
448
512
|
"analysis_date": 1,
|
449
513
|
"assignees": 1,
|
450
514
|
"beacon": 1,
|
451
515
|
"case_id": 1,
|
452
516
|
"display_name": 1,
|
517
|
+
"genome_build": 1,
|
453
518
|
"individuals": 1,
|
454
519
|
"is_rerun": 1,
|
455
520
|
"is_research": 1,
|
456
521
|
"mme_submission": 1,
|
457
522
|
"owner": 1,
|
458
523
|
"panels": 1,
|
524
|
+
"phenotype_terms": 1,
|
525
|
+
"rank_model_version": 1,
|
459
526
|
"status": 1,
|
527
|
+
"sv_rank_model_version": 1,
|
460
528
|
"track": 1,
|
461
529
|
"vcf_files": 1,
|
462
530
|
}
|
463
531
|
|
464
|
-
|
465
|
-
data["nr_cases"] = sum(data["status_ncases"].values())
|
466
|
-
|
467
|
-
sanger_ordered_not_validated: Tuple[Dict[str, list]] = get_sanger_unevaluated(
|
468
|
-
store, institute_id, current_user.email
|
469
|
-
)
|
470
|
-
data["sanger_unevaluated"]: Dict[str, list] = sanger_ordered_not_validated[0]
|
471
|
-
data["sanger_validated_by_others"]: Dict[str, list] = sanger_ordered_not_validated[1]
|
472
|
-
|
473
|
-
# local function to add info to case obj
|
474
|
-
def populate_case_obj(case_obj):
|
475
|
-
analysis_types = set(ind["analysis_type"] for ind in case_obj["individuals"])
|
476
|
-
if len(analysis_types) > 1:
|
477
|
-
LOG.debug("Set analysis types to {'mixed'}")
|
478
|
-
analysis_types = set(["mixed"])
|
479
|
-
|
480
|
-
case_obj["analysis_types"] = list(analysis_types)
|
481
|
-
case_obj["assignees"] = [
|
482
|
-
store.user(user_email) for user_email in case_obj.get("assignees", [])
|
483
|
-
]
|
484
|
-
# Check if case was re-runned
|
485
|
-
last_analysis_date = case_obj.get("analysis_date", datetime.datetime.now())
|
486
|
-
all_analyses_dates = set()
|
487
|
-
for analysis in case_obj.get("analyses", [{"date": last_analysis_date}]):
|
488
|
-
all_analyses_dates.add(analysis.get("date", last_analysis_date))
|
489
|
-
|
490
|
-
case_obj["is_rerun"] = len(all_analyses_dates) > 1 or last_analysis_date > max(
|
491
|
-
all_analyses_dates
|
492
|
-
)
|
493
|
-
|
494
|
-
case_obj["clinvar_variants"] = store.case_to_clinVars(case_obj["_id"])
|
495
|
-
case_obj["display_track"] = TRACKS[case_obj.get("track", "rare")]
|
496
|
-
return case_obj
|
497
|
-
|
532
|
+
# Group cases by status
|
498
533
|
case_groups = {status: [] for status in CASE_STATUSES}
|
534
|
+
nr_cases_showall_statuses = 0
|
535
|
+
status_show_all_cases = institute_obj.get("show_all_cases_status") or ["prioritized"]
|
499
536
|
|
500
|
-
|
501
|
-
0 # Nr of cases for the case statuses where all cases should be shown
|
502
|
-
)
|
503
|
-
# In institute settings, retrieve all case status categories for which all cases should be displayed
|
504
|
-
status_show_all_cases: List[str] = institute_obj.get("show_all_cases_status") or ["prioritized"]
|
537
|
+
# Process cases for statuses that require all cases to be shown
|
505
538
|
for status in status_show_all_cases:
|
506
539
|
cases_in_status = store.cases_by_status(
|
507
540
|
institute_id=institute_id, status=status, projection=ALL_CASES_PROJECTION
|
508
541
|
)
|
509
542
|
cases_in_status = _sort_cases(data, request, cases_in_status)
|
510
543
|
for case_obj in cases_in_status:
|
511
|
-
populate_case_obj(case_obj)
|
512
|
-
nr_cases_showall_statuses += 1
|
544
|
+
populate_case_obj(case_obj, store)
|
513
545
|
case_groups[status].append(case_obj)
|
546
|
+
nr_cases_showall_statuses += 1
|
514
547
|
|
515
|
-
#
|
548
|
+
# Fetch additional cases based on filters
|
549
|
+
name_query = request.form
|
550
|
+
limit = int(request.form.get("search_limit")) if request.form.get("search_limit") else 100
|
516
551
|
all_cases = store.cases(
|
517
552
|
collaborator=institute_id,
|
518
553
|
name_query=name_query,
|
@@ -525,17 +560,20 @@ def cases(store, request, institute_id):
|
|
525
560
|
)
|
526
561
|
all_cases = _sort_cases(data, request, all_cases)
|
527
562
|
|
563
|
+
if request.form.get("export"):
|
564
|
+
return export_case_samples(institute_id, all_cases)
|
565
|
+
|
566
|
+
# Process additional cases for the remaining statuses
|
528
567
|
nr_cases = 0
|
529
568
|
for case_obj in all_cases:
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
569
|
+
if case_obj["status"] not in status_show_all_cases:
|
570
|
+
if nr_cases == limit:
|
571
|
+
break
|
572
|
+
populate_case_obj(case_obj, store)
|
573
|
+
case_groups[case_obj["status"]].append(case_obj)
|
574
|
+
nr_cases += 1
|
575
|
+
|
576
|
+
# Compile the final data
|
539
577
|
data["cases"] = [(status, case_groups[status]) for status in CASE_STATUSES]
|
540
578
|
data["found_cases"] = nr_cases + nr_cases_showall_statuses
|
541
579
|
data["limit"] = limit
|
@@ -543,8 +581,32 @@ def cases(store, request, institute_id):
|
|
543
581
|
return data
|
544
582
|
|
545
583
|
|
584
|
+
def populate_case_obj(case_obj: dict, store: MongoAdapter):
|
585
|
+
"""Helper function to populate additional case information."""
|
586
|
+
analysis_types = set(ind["analysis_type"] for ind in case_obj["individuals"])
|
587
|
+
if len(analysis_types) > 1:
|
588
|
+
analysis_types = set(["mixed"])
|
589
|
+
case_obj["analysis_types"] = list(analysis_types)
|
590
|
+
|
591
|
+
case_obj["assignees"] = [store.user(user_email) for user_email in case_obj.get("assignees", [])]
|
592
|
+
|
593
|
+
last_analysis_date = case_obj.get("analysis_date", datetime.datetime.now())
|
594
|
+
all_analyses_dates = {
|
595
|
+
analysis.get("date", last_analysis_date)
|
596
|
+
for analysis in case_obj.get("analyses", [{"date": last_analysis_date}])
|
597
|
+
}
|
598
|
+
case_obj["is_rerun"] = len(all_analyses_dates) > 1 or last_analysis_date > max(
|
599
|
+
all_analyses_dates
|
600
|
+
)
|
601
|
+
|
602
|
+
case_obj["clinvar_variants"] = store.case_to_clinVars(case_obj["_id"])
|
603
|
+
case_obj["display_track"] = TRACKS.get(case_obj.get("track", "rare"))
|
604
|
+
|
605
|
+
|
546
606
|
def _get_unevaluated_variants_for_case(
|
547
|
-
case_obj: dict,
|
607
|
+
case_obj: dict,
|
608
|
+
var_ids_list: List[str],
|
609
|
+
sanger_validated_by_user_by_case: Dict[str, List[str]],
|
548
610
|
) -> Tuple[Dict[str, list]]:
|
549
611
|
"""Returns the variants with Sanger ordered by a user that need validation or are validated by another user."""
|
550
612
|
|
@@ -73,7 +73,8 @@ class InstituteForm(FlaskForm):
|
|
73
73
|
pheno_abbrev = StringField("Abbreviation", validators=[validators.Optional()])
|
74
74
|
|
75
75
|
gene_panels = NonValidatingSelectMultipleField(
|
76
|
-
"Gene panels available for variants filtering",
|
76
|
+
"Gene panels available for variants filtering",
|
77
|
+
validators=[validators.Optional()],
|
77
78
|
)
|
78
79
|
|
79
80
|
gene_panels_matching = NonValidatingSelectMultipleField(
|
@@ -144,7 +145,8 @@ class GeneVariantFiltersForm(FlaskForm):
|
|
144
145
|
variant_type = SelectMultipleField(choices=[("clinical", "clinical"), ("research", "research")])
|
145
146
|
category = SelectMultipleField(choices=CATEGORY_CHOICES)
|
146
147
|
hgnc_symbols = TagListField(
|
147
|
-
"HGNC Symbols (comma-separated, case sensitive)",
|
148
|
+
"HGNC Symbols (comma-separated, case sensitive)",
|
149
|
+
validators=[validators.InputRequired()],
|
148
150
|
)
|
149
151
|
rank_score = IntegerField(default=15)
|
150
152
|
phenotype_terms = TagListField("HPO terms (comma-separated)")
|
@@ -181,3 +183,4 @@ class CaseFilterForm(FlaskForm):
|
|
181
183
|
has_rna = BooleanField("Has RNA-seq data")
|
182
184
|
validation_ordered = BooleanField("Validation pending")
|
183
185
|
search = SubmitField(label="Search")
|
186
|
+
export = SubmitField(label="Filter and export")
|
@@ -249,6 +249,12 @@
|
|
249
249
|
advSearchBlock.hide();
|
250
250
|
}
|
251
251
|
|
252
|
+
function StopSpinner() {
|
253
|
+
// Avoid page spinner being stuck on file download
|
254
|
+
$(window).unbind('beforeunload');
|
255
|
+
return true;
|
256
|
+
}
|
257
|
+
|
252
258
|
advBlockCheckbox.on('click', function() {
|
253
259
|
if($(this).is(':checked')) {
|
254
260
|
advSearchBlock.show();
|
@@ -29,7 +29,7 @@
|
|
29
29
|
<div class="container-float">
|
30
30
|
<div class="row" id="body-row"> <!--sidebar and main container are on the same row-->
|
31
31
|
<div class="col-12">
|
32
|
-
{{ variant_list_content(institute, causatives, acmg_map, callers, inherit_palette) }}
|
32
|
+
{{ variant_list_content(institute, causatives, acmg_map, ccv_map, callers, inherit_palette) }}
|
33
33
|
</div>
|
34
34
|
</div> <!-- end of div id body-row -->
|
35
35
|
</div>
|
@@ -9,7 +9,7 @@
|
|
9
9
|
{% endif %}
|
10
10
|
{% endfor %}
|
11
11
|
|
12
|
-
<form action="{{ form_action }}" method="POST" accept-charset="utf-8">
|
12
|
+
<form action="{{ form_action }}" method="POST" accept-charset="utf-8" onsubmit="return StopSpinner()">
|
13
13
|
{{ form.hidden_tag() }}
|
14
14
|
|
15
15
|
<div class="row">
|
@@ -19,7 +19,7 @@
|
|
19
19
|
{{ form.search_institute(class="form-control") }}
|
20
20
|
</div>
|
21
21
|
{% endif %}
|
22
|
-
<div class="col-md-
|
22
|
+
<div class="col-md-2 mb-3">
|
23
23
|
{{ form.case.label(class="form-label") }}
|
24
24
|
{{ form.case(class="form-control", placeholder="example:18201", pattern="[^\\\<\>\?\!\=\/]*", title="Characters \<>?!=/ are not allowed") }}
|
25
25
|
</div>
|
@@ -27,9 +27,10 @@
|
|
27
27
|
{{ form.search_limit.label(class="form-label") }}
|
28
28
|
{{ form.search_limit(class="form-control") }}
|
29
29
|
</div>
|
30
|
-
<div class="btn-sm mb-2 col-md-
|
31
|
-
{{ form.search(class="btn btn-
|
32
|
-
|
30
|
+
<div class="btn-sm mb-2 col-md-3 mx-auto">
|
31
|
+
{{ form.search(class="btn btn-primary mt-4") }}
|
32
|
+
{{ form.export(class="btn btn-primary mt-4") }}
|
33
|
+
<a href="{{ reset_action }}" class="btn btn-secondary mt-4 text-white">Reset search</a>
|
33
34
|
</div>
|
34
35
|
<div class="col-md-2 mb-3 form-check align-self-center mt-3 ms-3" data-bs-toggle="tooltip" title="Using multiple search criteria will narrow down your results as returned cases will contain all your searched conditions.">
|
35
36
|
{{ form.advanced_search(class="form-check-input") }}
|
@@ -446,7 +447,7 @@
|
|
446
447
|
{% endmacro %}
|
447
448
|
|
448
449
|
|
449
|
-
{% macro variant_list_content(institute, variants, acmg_map, callers, inherit_palette) %}
|
450
|
+
{% macro variant_list_content(institute, variants, acmg_map, ccv_map, callers, inherit_palette) %}
|
450
451
|
<div class="card mt-3">
|
451
452
|
<div class="card-body overflow-auto">
|
452
453
|
<table id="variants_table" class="table display table-sm">
|
@@ -469,6 +470,7 @@
|
|
469
470
|
<th data-bs-toggle='tooltip' data-bs-container='body' title="ref/alt-GQ">Zygosity</th>
|
470
471
|
<th>Inheritance</th>
|
471
472
|
<th>ACMG</th>
|
473
|
+
<th>ClinGen-CGC-VICC</th>
|
472
474
|
<th>Case</th>
|
473
475
|
<th>Analysis type</th>
|
474
476
|
<th>Validated status</th>
|
@@ -608,6 +610,16 @@
|
|
608
610
|
{% endif %}
|
609
611
|
</a>
|
610
612
|
</td>
|
613
|
+
<td><!-- Clingen-CGC-VIGG -->
|
614
|
+
<a href="#" data-bs-toggle="tooltip" title="Clingen-CGC-VIGG classification assigned by Scout users"
|
615
|
+
style="text-decoration: none; color: #000;">
|
616
|
+
{% if 'ccv_classification' in variant %}
|
617
|
+
<span class="badge bg-{{ccv_map[variant.ccv_classification].color}}">{{ccv_map[variant.ccv_classification].short}}</span>
|
618
|
+
{% else %}
|
619
|
+
-
|
620
|
+
{% endif %}
|
621
|
+
</a>
|
622
|
+
</td>
|
611
623
|
<td><!-- Case -->
|
612
624
|
<a href="{{ url_for('cases.case',
|
613
625
|
institute_id=institute._id,
|
@@ -58,7 +58,7 @@
|
|
58
58
|
{{validated_chart()}}
|
59
59
|
</div>
|
60
60
|
<div class="row">
|
61
|
-
<div class="col-12">{{ variant_list_content(institute, verified, acmg_map, callers, inherit_palette) }}</div>
|
61
|
+
<div class="col-12">{{ variant_list_content(institute, verified, acmg_map, ccv_map, callers, inherit_palette) }}</div>
|
62
62
|
</div>
|
63
63
|
</div>
|
64
64
|
</div>
|