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.
Files changed (82) hide show
  1. scout/adapter/mongo/base.py +3 -0
  2. scout/adapter/mongo/case.py +27 -2
  3. scout/adapter/mongo/ccv.py +131 -0
  4. scout/adapter/mongo/hgnc.py +5 -1
  5. scout/adapter/mongo/managed_variant.py +4 -2
  6. scout/adapter/mongo/query.py +91 -54
  7. scout/adapter/mongo/variant.py +17 -11
  8. scout/adapter/mongo/variant_events.py +45 -1
  9. scout/build/ccv.py +59 -0
  10. scout/build/panel.py +1 -1
  11. scout/commands/export/export_command.py +0 -0
  12. scout/commands/load/base.py +0 -0
  13. scout/commands/load/user.py +0 -0
  14. scout/commands/serve.py +2 -1
  15. scout/commands/update/disease.py +0 -0
  16. scout/commands/update/genes.py +0 -0
  17. scout/commands/wipe_database.py +0 -0
  18. scout/constants/__init__.py +2 -0
  19. scout/constants/case_tags.py +2 -0
  20. scout/constants/ccv.py +244 -0
  21. scout/constants/gene_tags.py +22 -12
  22. scout/demo/643594.config.yaml +2 -2
  23. scout/demo/643594.research.mei.vcf.gz +0 -0
  24. scout/demo/643594.research.mei.vcf.gz.tbi +0 -0
  25. scout/demo/images/custom_images/1300x1000.jpg +0 -0
  26. scout/load/panelapp.py +8 -12
  27. scout/models/ccv_evaluation.py +26 -0
  28. scout/models/variant/variant.py +1 -0
  29. scout/parse/omim.py +5 -6
  30. scout/parse/panelapp.py +16 -42
  31. scout/parse/variant/compound.py +20 -21
  32. scout/parse/variant/gene.py +0 -0
  33. scout/parse/variant/genotype.py +0 -0
  34. scout/resources/custom_igv_tracks/mane.bb +0 -0
  35. scout/server/blueprints/cases/controllers.py +48 -0
  36. scout/server/blueprints/cases/templates/cases/case_report.html +61 -1
  37. scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +2 -2
  38. scout/server/blueprints/cases/templates/cases/index.html +0 -2
  39. scout/server/blueprints/cases/views.py +5 -5
  40. scout/server/blueprints/clinvar/controllers.py +4 -5
  41. scout/server/blueprints/institutes/controllers.py +129 -67
  42. scout/server/blueprints/institutes/forms.py +5 -2
  43. scout/server/blueprints/institutes/templates/overview/cases.html +6 -0
  44. scout/server/blueprints/institutes/templates/overview/causatives.html +1 -1
  45. scout/server/blueprints/institutes/templates/overview/utils.html +18 -6
  46. scout/server/blueprints/institutes/templates/overview/verified.html +1 -1
  47. scout/server/blueprints/institutes/views.py +4 -0
  48. scout/server/blueprints/managed_variants/forms.py +17 -2
  49. scout/server/blueprints/managed_variants/templates/managed_variants/managed_variants.html +2 -2
  50. scout/server/blueprints/panels/controllers.py +5 -6
  51. scout/server/blueprints/panels/templates/panels/panel.html +5 -5
  52. scout/server/blueprints/variant/controllers.py +148 -1
  53. scout/server/blueprints/variant/templates/variant/cancer-variant.html +1 -1
  54. scout/server/blueprints/variant/templates/variant/ccv.html +183 -0
  55. scout/server/blueprints/variant/templates/variant/components.html +86 -5
  56. scout/server/blueprints/variant/templates/variant/sv-variant.html +2 -2
  57. scout/server/blueprints/variant/templates/variant/tx_overview.html +3 -3
  58. scout/server/blueprints/variant/templates/variant/variant.html +1 -1
  59. scout/server/blueprints/variant/templates/variant/variant_details.html +29 -11
  60. scout/server/blueprints/variant/utils.py +21 -1
  61. scout/server/blueprints/variant/views.py +115 -5
  62. scout/server/blueprints/variants/controllers.py +31 -0
  63. scout/server/blueprints/variants/forms.py +33 -5
  64. scout/server/blueprints/variants/templates/variants/cancer-sv-variants.html +4 -18
  65. scout/server/blueprints/variants/templates/variants/cancer-variants.html +4 -13
  66. scout/server/blueprints/variants/templates/variants/components.html +77 -73
  67. scout/server/blueprints/variants/templates/variants/indicators.html +11 -0
  68. scout/server/blueprints/variants/templates/variants/sv-variants.html +2 -2
  69. scout/server/links.py +1 -1
  70. scout/server/static/custom_images.js +19 -2
  71. scout/utils/acmg.py +0 -1
  72. scout/utils/ccv.py +193 -0
  73. scout/utils/link.py +4 -3
  74. scout/utils/md5.py +0 -0
  75. {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/METADATA +67 -45
  76. {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/RECORD +70 -65
  77. {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/WHEEL +1 -2
  78. scout/__version__.py +0 -1
  79. scout/demo/images/custom_images/640x480_two.jpg +0 -0
  80. scout_browser-4.92.dist-info/top_level.txt +0 -1
  81. {scout_browser-4.92.dist-info → scout_browser-4.95.0.dist-info}/entry_points.txt +0 -0
  82. {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['code']}}">{{variant.acmg_classification['code'] }}</span>
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 = os.path.join(cases_bp.static_folder, "madeline.png")
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 NamedTemporaryFile(
403
- mode="a+", prefix="Variant", suffix=".csv"
404
- ) as variant_file, NamedTemporaryFile(
405
- mode="a+", prefix="CaseData", suffix=".csv"
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 cases(store, request, institute_id):
426
- """Preprocess case objects.
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
- Add all the necessary information to display the 'cases' view
484
+ file_content = "\n".join(export_lines)
429
485
 
430
- Args:
431
- store(adapter.MongoAdapter)
432
- request(flask.request) request sent by browser to the api_institutes endpoint
433
- institute_id(str): An _id of an institute
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
- Returns:
436
- data(dict): includes the cases, how many there are and the limit.
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
- data["status_ncases"] = store.nr_cases_by_status(institute_id=institute_id)
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
- nr_cases_showall_statuses = (
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
- # Retrieve cases for the remaining status categories
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
- case_status = case_obj["status"]
531
- if case_status in status_show_all_cases:
532
- continue
533
- if nr_cases == limit:
534
- break
535
- populate_case_obj(case_obj)
536
- case_groups[case_status].append(case_obj)
537
- nr_cases += 1
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, var_ids_list: List[str], sanger_validated_by_user_by_case: Dict[str, List[str]]
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", validators=[validators.Optional()]
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)", validators=[validators.InputRequired()]
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-3 mb-3">
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-2 mx-auto">
31
- {{ form.search(class="btn btn-lg btn-primary mt-4") }}
32
- <a href="{{ reset_action }}" class="btn btn-lg btn-secondary mt-4 text-white">Reset search</a>
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>