scout-browser 4.92__py3-none-any.whl → 4.94__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 (67) 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/query.py +88 -53
  5. scout/adapter/mongo/variant.py +6 -5
  6. scout/adapter/mongo/variant_events.py +45 -1
  7. scout/build/ccv.py +59 -0
  8. scout/commands/export/export_command.py +0 -0
  9. scout/commands/load/base.py +0 -0
  10. scout/commands/load/user.py +0 -0
  11. scout/commands/serve.py +2 -1
  12. scout/commands/update/disease.py +0 -0
  13. scout/commands/update/genes.py +0 -0
  14. scout/commands/wipe_database.py +0 -0
  15. scout/constants/__init__.py +2 -0
  16. scout/constants/case_tags.py +2 -0
  17. scout/constants/ccv.py +244 -0
  18. scout/demo/643594.config.yaml +2 -2
  19. scout/demo/images/custom_images/1300x1000.jpg +0 -0
  20. scout/models/ccv_evaluation.py +26 -0
  21. scout/models/variant/variant.py +1 -0
  22. scout/parse/variant/compound.py +0 -0
  23. scout/parse/variant/gene.py +0 -0
  24. scout/parse/variant/genotype.py +0 -0
  25. scout/resources/custom_igv_tracks/mane.bb +0 -0
  26. scout/server/blueprints/cases/controllers.py +21 -0
  27. scout/server/blueprints/cases/templates/cases/case_report.html +53 -0
  28. scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +2 -2
  29. scout/server/blueprints/cases/templates/cases/index.html +0 -2
  30. scout/server/blueprints/clinvar/controllers.py +4 -5
  31. scout/server/blueprints/institutes/controllers.py +129 -67
  32. scout/server/blueprints/institutes/forms.py +5 -2
  33. scout/server/blueprints/institutes/templates/overview/cases.html +6 -0
  34. scout/server/blueprints/institutes/templates/overview/causatives.html +1 -1
  35. scout/server/blueprints/institutes/templates/overview/utils.html +18 -6
  36. scout/server/blueprints/institutes/templates/overview/verified.html +1 -1
  37. scout/server/blueprints/institutes/views.py +4 -0
  38. scout/server/blueprints/panels/controllers.py +5 -6
  39. scout/server/blueprints/panels/templates/panels/panel.html +5 -5
  40. scout/server/blueprints/variant/controllers.py +148 -1
  41. scout/server/blueprints/variant/templates/variant/cancer-variant.html +1 -1
  42. scout/server/blueprints/variant/templates/variant/ccv.html +183 -0
  43. scout/server/blueprints/variant/templates/variant/components.html +61 -3
  44. scout/server/blueprints/variant/templates/variant/tx_overview.html +3 -3
  45. scout/server/blueprints/variant/templates/variant/variant.html +1 -1
  46. scout/server/blueprints/variant/templates/variant/variant_details.html +29 -11
  47. scout/server/blueprints/variant/utils.py +21 -1
  48. scout/server/blueprints/variant/views.py +114 -3
  49. scout/server/blueprints/variants/controllers.py +31 -0
  50. scout/server/blueprints/variants/templates/variants/cancer-sv-variants.html +4 -18
  51. scout/server/blueprints/variants/templates/variants/cancer-variants.html +4 -13
  52. scout/server/blueprints/variants/templates/variants/components.html +77 -73
  53. scout/server/blueprints/variants/templates/variants/indicators.html +11 -0
  54. scout/server/blueprints/variants/templates/variants/sv-variants.html +2 -2
  55. scout/server/links.py +1 -1
  56. scout/server/static/custom_images.js +19 -2
  57. scout/utils/acmg.py +0 -1
  58. scout/utils/ccv.py +201 -0
  59. scout/utils/md5.py +0 -0
  60. {scout_browser-4.92.dist-info → scout_browser-4.94.dist-info}/METADATA +67 -45
  61. {scout_browser-4.92.dist-info → scout_browser-4.94.dist-info}/RECORD +54 -49
  62. {scout_browser-4.92.dist-info → scout_browser-4.94.dist-info}/WHEEL +1 -2
  63. scout/__version__.py +0 -1
  64. scout/demo/images/custom_images/640x480_two.jpg +0 -0
  65. scout_browser-4.92.dist-info/top_level.txt +0 -1
  66. {scout_browser-4.92.dist-info → scout_browser-4.94.dist-info}/entry_points.txt +0 -0
  67. {scout_browser-4.92.dist-info → scout_browser-4.94.dist-info/licenses}/LICENSE +0 -0
@@ -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>
@@ -10,6 +10,8 @@ from scout.constants import (
10
10
  ACMG_COMPLETE_MAP,
11
11
  ACMG_MAP,
12
12
  CALLERS,
13
+ CCV_COMPLETE_MAP,
14
+ CCV_MAP,
13
15
  INHERITANCE_PALETTE,
14
16
  VERBS_ICONS_MAP,
15
17
  VERBS_MAP,
@@ -72,6 +74,7 @@ def verified(institute_id):
72
74
  return dict(
73
75
  acmg_map={key: ACMG_COMPLETE_MAP[value] for key, value in ACMG_MAP.items()},
74
76
  callers=CALLERS,
77
+ ccv_map={key: CCV_COMPLETE_MAP[value] for key, value in CCV_MAP.items()},
75
78
  inherit_palette=INHERITANCE_PALETTE,
76
79
  institute=institute_obj,
77
80
  verified=verified_vars,
@@ -87,6 +90,7 @@ def causatives(institute_id):
87
90
  acmg_map={key: ACMG_COMPLETE_MAP[value] for key, value in ACMG_MAP.items()},
88
91
  callers=CALLERS,
89
92
  causatives=controllers.causatives(institute_obj, request),
93
+ ccv_map={key: CCV_COMPLETE_MAP[value] for key, value in CCV_MAP.items()},
90
94
  institute=institute_obj,
91
95
  inherit_palette=INHERITANCE_PALETTE,
92
96
  )
@@ -160,7 +160,6 @@ def update_panel(store, panel_name, csv_lines, option):
160
160
  Returns:
161
161
  panel_obj(dict)
162
162
  """
163
- new_genes = []
164
163
  panel_obj = store.gene_panel(panel_name)
165
164
  if panel_obj is None:
166
165
  return None
@@ -202,11 +201,11 @@ def update_panel(store, panel_name, csv_lines, option):
202
201
  )
203
202
 
204
203
  info_data = {
205
- "disease_associated_transcripts": new_gene["transcripts"],
206
- "reduced_penetrance": new_gene["reduced_penetrance"],
207
- "mosaicism": new_gene["mosaicism"],
208
- "inheritance_models": new_gene["inheritance_models"],
209
- "database_entry_version": new_gene["database_entry_version"],
204
+ "disease_associated_transcripts": new_gene.get("transcripts", []),
205
+ "reduced_penetrance": new_gene.get("reduced_penetrance", ""),
206
+ "mosaicism": new_gene.get("mosaicism", ""),
207
+ "inheritance_models": new_gene.get("inheritance_models", []),
208
+ "database_entry_version": new_gene.get("database_entry_version", ""),
210
209
  }
211
210
  if (
212
211
  option == "replace"
@@ -223,15 +223,15 @@
223
223
  <span data-bs-toggle="tooltip" data-bs-placement="right" data-bs-html="true" title="
224
224
  <div class='text-left'>
225
225
  <strong>Transcripts</strong>:
226
- {{ gene.info.disease_associated_transcripts|join(',') }} <br>
226
+ {{ gene.info.disease_associated_transcripts|join(',') if gene.info.disease_associated_transcripts }} <br>
227
227
  <strong>Reduced penetrance</strong>:
228
- {{ 'yes' if gene.info.reduced_penetrance else 'no' }} <br>
228
+ {{ gene.info.reduced_penetrance if gene.info.reduced_penetrance }} <br>
229
229
  <strong>Mosaicism</strong>:
230
- {{ 'yes' if gene.info.mosaicism else 'no' }} <br>
230
+ {{ gene.info.mosaicism if gene.info.mosaicism }} <br>
231
231
  <strong>Inheritance models</strong>:
232
- {{ gene.info.inheritance_models|join(',') }} <br>
232
+ {{ gene.info.inheritance_models|join(',') if gene.info.inheritance_models }} <br>
233
233
  <strong>Entry version</strong>:
234
- {{ gene.info.database_entry_version }} <br>
234
+ {{ gene.info.database_entry_version if gene.info.database_entry_version }} <br>
235
235
  </div>
236
236
  ">
237
237
  {{ gene.symbol }}
@@ -16,6 +16,10 @@ from scout.constants import (
16
16
  CANCER_SPECIFIC_VARIANT_DISMISS_OPTIONS,
17
17
  CANCER_TIER_OPTIONS,
18
18
  CASE_TAGS,
19
+ CCV_COMPLETE_MAP,
20
+ CCV_CRITERIA,
21
+ CCV_MAP,
22
+ CCV_OPTIONS,
19
23
  DISMISS_VARIANT_OPTIONS,
20
24
  IGV_TRACKS,
21
25
  INHERITANCE_PALETTE,
@@ -41,6 +45,7 @@ from scout.server.utils import (
41
45
  from .utils import (
42
46
  add_gene_info,
43
47
  associate_variant_genes_with_case_panels,
48
+ ccv_evaluation,
44
49
  clinsig_human,
45
50
  default_panels,
46
51
  end_position,
@@ -194,9 +199,11 @@ def variant(
194
199
  'cancer_tier_options': CANCER_TIER_OPTIONS,
195
200
  'dismiss_variant_options': DISMISS_VARIANT_OPTIONS,
196
201
  'ACMG_OPTIONS': ACMG_OPTIONS,
202
+ 'CCV_OPTIONS': CCV_OPTIONS,
197
203
  'igv_tracks': IGV_TRACKS,
198
204
  'gens_info': <dict>,
199
205
  'evaluations': <list(evaluations)>,
206
+ 'ccv_evaluations': <list(evaluations)>,
200
207
  'rank_score_results': <list(rank_score_results)>,
201
208
  }
202
209
 
@@ -340,6 +347,17 @@ def variant(
340
347
  evaluation(store, evaluation_obj)
341
348
  evaluations.append(evaluation_obj)
342
349
 
350
+ # Prepare classification information for visualisation
351
+ ccv_classification = variant_obj.get("ccv_classification")
352
+ if isinstance(ccv_classification, int):
353
+ ccv_code = CCV_MAP[variant_obj["ccv_classification"]]
354
+ variant_obj["ccv_classification"] = CCV_COMPLETE_MAP[ccv_code]
355
+
356
+ ccv_evaluations = []
357
+ for evaluation_obj in store.get_ccv_evaluations(variant_obj):
358
+ ccv_evaluation(store, evaluation_obj)
359
+ ccv_evaluations.append(evaluation_obj)
360
+
343
361
  case_clinvars = store.case_to_clinVars(case_obj.get("display_name"))
344
362
 
345
363
  if variant_id in case_clinvars:
@@ -379,12 +397,14 @@ def variant(
379
397
  "dismiss_variant_options": dismiss_options,
380
398
  "mosaic_variant_options": MOSAICISM_OPTIONS,
381
399
  "ACMG_OPTIONS": ACMG_OPTIONS,
400
+ "CCV_OPTIONS": CCV_OPTIONS,
382
401
  "case_tag_options": CASE_TAGS,
383
402
  "inherit_palette": INHERITANCE_PALETTE,
384
403
  "igv_tracks": get_igv_tracks("38" if variant_obj["is_mitochondrial"] else genome_build),
385
404
  "has_rna_tracks": case_has_rna_tracks(case_obj),
386
405
  "gens_info": gens.connection_settings(genome_build),
387
406
  "evaluations": evaluations,
407
+ "ccv_evaluations": ccv_evaluations,
388
408
  "rank_score_results": variant_rank_scores(store, case_obj, variant_obj),
389
409
  }
390
410
 
@@ -442,7 +462,7 @@ def get_loqusdb_obs_cases(
442
462
  obs_cases = []
443
463
  user_institutes_ids = set([inst["_id"] for inst in user_institutes(store, current_user)])
444
464
  for i, case_id in enumerate(obs_families):
445
- if len(obs_cases) == 10:
465
+ if len(obs_cases) == 50:
446
466
  break
447
467
  if case_id == variant_obj["case_id"]:
448
468
  continue
@@ -708,3 +728,130 @@ def variant_acmg_post(
708
728
  criteria=criteria,
709
729
  )
710
730
  return classification
731
+
732
+
733
+ def variant_ccv(store: MongoAdapter, institute_id: str, case_name: str, variant_id: str):
734
+ """Collect data relevant for rendering ClinGen-CCV-VIGG classification form.
735
+
736
+ Args:
737
+ store(scout.adapter.MongoAdapter)
738
+ institute_id(str): institute_obj['_id']
739
+ case_name(str): case_obj['display_name']
740
+ variant_id(str): variant_obj['document_id']
741
+
742
+ Returns:
743
+ data(dict): Things for the template
744
+ """
745
+ variant_obj = store.variant(variant_id)
746
+
747
+ if not variant_obj:
748
+ return abort(404)
749
+
750
+ institute_obj, case_obj = variant_institute_and_case(
751
+ store, variant_obj, institute_id, case_name
752
+ )
753
+
754
+ return dict(
755
+ institute=institute_obj,
756
+ case=case_obj,
757
+ variant=variant_obj,
758
+ CRITERIA=CCV_CRITERIA,
759
+ CCV_OPTIONS=CCV_OPTIONS,
760
+ )
761
+
762
+
763
+ def check_reset_variant_ccv_classification(
764
+ store: MongoAdapter, evaluation_obj: dict, link: str
765
+ ) -> bool:
766
+ """Check if this was the last ClinGen-CGC-VIGG evaluation left on the variant.
767
+ If there is still a classification we want to remove the classification.
768
+
769
+ Args:
770
+ stores(cout.adapter.MongoAdapter)
771
+ evaluation_obj(dict): ClinGen-CGC-VIGG evaluation object
772
+ link(str): link for event
773
+
774
+ Returns: reset(bool) - True if classification reset was attempted
775
+
776
+ """
777
+
778
+ if list(store.get_ccv_evaluations_case_specific(evaluation_obj["variant_specific"])):
779
+ return False
780
+
781
+ variant_obj = store.variant(document_id=evaluation_obj["variant_specific"])
782
+
783
+ if not variant_obj:
784
+ return abort(404)
785
+
786
+ ccv_classification = variant_obj.get("ccv_classification")
787
+
788
+ if not isinstance(ccv_classification, int):
789
+ return False
790
+
791
+ institute_obj, case_obj = variant_institute_and_case(
792
+ store,
793
+ variant_obj,
794
+ evaluation_obj["institute"]["_id"],
795
+ evaluation_obj["case"]["display_name"],
796
+ )
797
+ user_obj = store.user(current_user.email)
798
+
799
+ new_ccv = None
800
+ store.submit_ccv_evaluation(
801
+ variant_obj=variant_obj,
802
+ user_obj=user_obj,
803
+ institute_obj=institute_obj,
804
+ case_obj=case_obj,
805
+ link=link,
806
+ classification=new_ccv,
807
+ )
808
+ return True
809
+
810
+
811
+ def variant_ccv_post(
812
+ store: MongoAdapter,
813
+ institute_id: str,
814
+ case_name: str,
815
+ variant_id: str,
816
+ user_email: str,
817
+ criteria: list,
818
+ ) -> dict:
819
+ """Calculate an ClinGen-CGC-VICC classification based on a list of criteria.
820
+
821
+ Args:
822
+ store(scout.adapter.MongoAdapter)
823
+ institute_id(str): institute_obj['_id']
824
+ case_name(str): case_obj['display_name']
825
+ variant_id(str): variant_obj['document_id']
826
+ user_mail(str)
827
+ criteria()
828
+
829
+ Returns:
830
+ data(dict): Things for the template
831
+
832
+ """
833
+ variant_obj = store.variant(variant_id)
834
+
835
+ if not variant_obj:
836
+ return abort(404)
837
+
838
+ institute_obj, case_obj = variant_institute_and_case(
839
+ store, variant_obj, institute_id, case_name
840
+ )
841
+
842
+ user_obj = store.user(user_email)
843
+ variant_link = url_for(
844
+ "variant.variant",
845
+ institute_id=institute_id,
846
+ case_name=case_name,
847
+ variant_id=variant_id,
848
+ )
849
+ classification = store.submit_ccv_evaluation(
850
+ institute_obj=institute_obj,
851
+ case_obj=case_obj,
852
+ variant_obj=variant_obj,
853
+ user_obj=user_obj,
854
+ link=variant_link,
855
+ criteria=criteria,
856
+ )
857
+ return classification