scout-browser 4.98.0__py3-none-any.whl → 4.100.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 (91) hide show
  1. scout/adapter/mongo/case.py +30 -15
  2. scout/adapter/mongo/clinvar.py +23 -31
  3. scout/adapter/mongo/event.py +14 -4
  4. scout/adapter/mongo/institute.py +42 -55
  5. scout/adapter/mongo/omics_variant.py +14 -1
  6. scout/adapter/mongo/query.py +24 -1
  7. scout/adapter/mongo/variant.py +44 -22
  8. scout/adapter/mongo/variant_loader.py +169 -186
  9. scout/build/individual.py +5 -1
  10. scout/build/variant/variant.py +8 -0
  11. scout/commands/download/ensembl.py +18 -3
  12. scout/commands/load/research.py +2 -3
  13. scout/commands/update/individual.py +3 -0
  14. scout/commands/update/panelapp.py +15 -2
  15. scout/constants/__init__.py +6 -2
  16. scout/constants/clnsig.py +2 -0
  17. scout/constants/file_types.py +12 -0
  18. scout/constants/igv_tracks.py +9 -6
  19. scout/constants/indexes.py +5 -4
  20. scout/constants/panels.py +3 -0
  21. scout/constants/query_terms.py +1 -0
  22. scout/constants/variant_tags.py +6 -6
  23. scout/demo/643594.config.yaml +1 -0
  24. scout/load/panelapp.py +11 -5
  25. scout/models/case/case.py +1 -0
  26. scout/models/case/case_loading_models.py +7 -1
  27. scout/parse/ensembl.py +8 -3
  28. scout/parse/variant/clnsig.py +38 -0
  29. scout/parse/variant/genotype.py +4 -10
  30. scout/parse/variant/models.py +5 -11
  31. scout/parse/variant/rank_score.py +5 -13
  32. scout/parse/variant/variant.py +90 -111
  33. scout/server/app.py +39 -22
  34. scout/server/blueprints/alignviewers/controllers.py +29 -10
  35. scout/server/blueprints/alignviewers/templates/alignviewers/igv_viewer.html +51 -11
  36. scout/server/blueprints/cases/controllers.py +9 -3
  37. scout/server/blueprints/cases/templates/cases/case_report.html +25 -13
  38. scout/server/blueprints/cases/templates/cases/chanjo2_form.html +1 -1
  39. scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +1 -1
  40. scout/server/blueprints/cases/templates/cases/gene_panel.html +1 -1
  41. scout/server/blueprints/cases/templates/cases/utils.html +25 -6
  42. scout/server/blueprints/clinvar/controllers.py +34 -15
  43. scout/server/blueprints/clinvar/templates/clinvar/clinvar_submissions.html +34 -12
  44. scout/server/blueprints/clinvar/templates/clinvar/multistep_add_variant.html +14 -5
  45. scout/server/blueprints/clinvar/views.py +14 -2
  46. scout/server/blueprints/diagnoses/static/diagnoses.js +8 -1
  47. scout/server/blueprints/institutes/controllers.py +10 -2
  48. scout/server/blueprints/institutes/static/variants_list_scripts.js +9 -1
  49. scout/server/blueprints/institutes/templates/overview/institute_sidebar.html +9 -1
  50. scout/server/blueprints/login/controllers.py +112 -12
  51. scout/server/blueprints/login/views.py +38 -60
  52. scout/server/blueprints/mme/__init__.py +1 -0
  53. scout/server/blueprints/mme/controllers.py +18 -0
  54. scout/server/blueprints/mme/templates/mme/mme_submissions.html +153 -0
  55. scout/server/blueprints/mme/views.py +34 -0
  56. scout/server/blueprints/panels/templates/panels/panel.html +19 -6
  57. scout/server/blueprints/phenotypes/templates/phenotypes/hpo_terms.html +8 -1
  58. scout/server/blueprints/public/templates/public/index.html +5 -1
  59. scout/server/blueprints/variant/controllers.py +19 -10
  60. scout/server/blueprints/variant/templates/variant/acmg.html +15 -2
  61. scout/server/blueprints/variant/templates/variant/cancer-variant.html +1 -1
  62. scout/server/blueprints/variant/templates/variant/components.html +38 -16
  63. scout/server/blueprints/variant/templates/variant/sv-variant.html +2 -2
  64. scout/server/blueprints/variant/templates/variant/utils.html +23 -11
  65. scout/server/blueprints/variant/templates/variant/variant.html +42 -1
  66. scout/server/blueprints/variant/views.py +12 -0
  67. scout/server/blueprints/variants/controllers.py +20 -3
  68. scout/server/blueprints/variants/forms.py +8 -3
  69. scout/server/blueprints/variants/templates/variants/components.html +34 -0
  70. scout/server/blueprints/variants/templates/variants/indicators.html +11 -13
  71. scout/server/blueprints/variants/templates/variants/mei-variants.html +8 -6
  72. scout/server/blueprints/variants/templates/variants/sv-variants.html +9 -7
  73. scout/server/blueprints/variants/templates/variants/utils.html +35 -34
  74. scout/server/blueprints/variants/templates/variants/variants.html +4 -25
  75. scout/server/config.py +8 -0
  76. scout/server/extensions/bionano_extension.py +0 -1
  77. scout/server/extensions/chanjo2_extension.py +54 -13
  78. scout/server/links.py +15 -0
  79. scout/server/static/bs_styles.css +34 -6
  80. scout/server/templates/utils.html +9 -10
  81. scout/server/utils.py +40 -5
  82. scout/utils/acmg.py +25 -26
  83. scout/utils/ensembl_biomart_clients.py +2 -1
  84. scout/utils/ensembl_rest_clients.py +25 -32
  85. scout/utils/hgvs.py +1 -1
  86. scout/utils/scout_requests.py +1 -3
  87. {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/METADATA +10 -14
  88. {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/RECORD +91 -87
  89. {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/WHEEL +0 -0
  90. {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/entry_points.txt +0 -0
  91. {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  {% import "bootstrap/wtf.html" as wtf %}
2
2
  {% from "utils.html" import comments_table %}
3
+ {% from "cases/utils.html" import pretty_link_variant %}
3
4
  {% from "variants/indicators.html" import pin_indicator, causative_badge, clinical_assessments_badge, comments_badge, dismissals_badge, evaluations_badge, group_assessments_badge, matching_manual_rank, research_assessments_badge %}
4
5
 
5
6
  {% macro filter_script_main(cytobands) %}
@@ -192,9 +193,9 @@
192
193
  {% endif %}
193
194
  <td>
194
195
  {% if compound.not_loaded %}
195
- {{ compound.display_name }} <small>(not loaded)</small>
196
+ {{ compound.display_name|truncate(20, True) }} <small>(not loaded)</small>
196
197
  {% elif is_popover %}
197
- {{ compound.display_name }}
198
+ {{ compound.display_name|truncate(20, True) }}
198
199
  {% else %}
199
200
  <a href='{{ url_for("variant.variant",
200
201
  institute_id=institute._id,
@@ -217,7 +218,7 @@
217
218
  <td>
218
219
  {% for annotation in compound.functional_annotations %}
219
220
  {{ annotation }}<br>
220
- {% endfor %}
221
+ {% endfor %}x
221
222
  </td>
222
223
  </tr>
223
224
  {% endfor %}
@@ -225,30 +226,25 @@
225
226
  </table>
226
227
  {% endmacro %}
227
228
 
228
- {% macro svs_table(institute, case, overlapping) %}
229
+ {% macro overlapping_tooltip_table(institute, case, overlapping) %}
229
230
  <table class='table table-bordered table-hover table-condensed'>
230
231
  <thead>
231
232
  <tr>
232
- <th>Region</th>
233
+ <th>Pos</th>
233
234
  <th>Type</th>
234
235
  <th>Length</th>
235
236
  <th>Rank score</th>
236
237
  </tr>
237
238
  </thead>
238
239
  <tbody>
239
- {% for sv in overlapping %}
240
+ {% for variant in overlapping %}
240
241
  <tr>
241
242
  <td>
242
- <a href='{{ url_for("variant.sv_variant",
243
- institute_id=institute._id,
244
- case_name=case.display_name,
245
- variant_id=sv._id) }}'>
246
- {{ sv.chromosome }}{{ sv.cytoband_start }}
247
- </a>
243
+ {{ pretty_link_variant(variant, case) }}
248
244
  </td>
249
- <td class='text-end'>{{ sv.sub_category }}</td>
250
- <td class='text-end'>{{ sv.length if sv.length < 100000000000 else "-" }}</td>
251
- <td class='text-end'>{{ sv.rank_score }}</td>
245
+ <td class='text-end'>{{ variant.sub_category }}</td>
246
+ <td class='text-end'>{{ variant.length if variant.length and variant.length < 100000000000 else '-' }}</td>
247
+ <td class='text-end'>{{ variant.rank_score }}</td>
252
248
  </tr>
253
249
  {% endfor %}
254
250
  </tbody>
@@ -336,7 +332,7 @@
336
332
  {{ form.spidex_human(class="selectpicker", data_style="btn-secondary") }}
337
333
  </div>
338
334
  <div class="col-2">
339
- <span>{{ form.clinsig.label(class="control-label") }}</span>
335
+ <span>{{ form.clinsig.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Germline classification criteria.") }}</span>
340
336
  <span style="float:right;">{{ form.clinsig_exclude.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Exclude variants with clinical significance among the selected categories.") }} {{form.clinsig_exclude}}</span>
341
337
  {{ form.clinsig(class="selectpicker", data_style="btn-secondary") }}
342
338
  </div>
@@ -625,7 +621,7 @@
625
621
  </div>
626
622
  </div>
627
623
  <div class="col-2">
628
- {{ form.clinsig.label(class="control-label") }}
624
+ {{ form.clinsig.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Germline classification criteria.") }}
629
625
  {{ form.clinsig(class="selectpicker", data_style="btn-secondary") }}
630
626
  </div>
631
627
  <div class="col-1">
@@ -726,26 +722,17 @@
726
722
  {{ form.genetic_models(class="selectpicker", data_style="btn-secondary") }}
727
723
  </div>
728
724
  <div class="col-2">
729
- <span>{{ form.clinsig.label(class="control-label") }}</span>
725
+ <span>{{ form.clinsig.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Germline classification criteria.") }}</span>
730
726
  <span style="float:right;">{{ form.clinsig_exclude.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Exclude variants with clinical significance among the selected categories.") }} {{form.clinsig_exclude}}</span>
731
727
  {{ form.clinsig(class="selectpicker", data_style="btn-secondary") }}
732
728
  </div>
733
729
  <div class="col-2">
734
- <div class="form-check d-flex justify-content-start">
735
- {{ form.mvl_tag(class="form-check-input",type="checkbox") }}
736
- {{ form.mvl_tag.label(class="ms-2 form-check-label") }}
737
- </div>
738
- <div class="form-check d-flex justify-content-start">
739
- {{ form.clinvar_tag(class="form-check-input",type="checkbox") }}
740
- {{ form.clinvar_tag.label(class="ms-2 form-check-label") }}
741
- </div>
742
- <div class="form-check d-flex justify-content-start">
743
- {{ form.cosmic_tag(class="form-check-input",type="checkbox") }}
744
- {{ form.cosmic_tag.label(class="ms-2 form-check-label") }}
745
- </div>
730
+ <span>{{ form.clinsig_onc.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="ClinVar oncogenicity criteria will be applied alongside the other search criteria, not as an alternative. Oncogenicity annotations are still sparse in ClinVar, so relying solely on them could be too restrictive.") }}</span>
731
+ <span style="float:right;">{{ form.clinsig_onc_exclude.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Exclude variants with oncogenicity among the selected categories.") }} {{form.clinsig_onc_exclude}}</span>
732
+ {{ form.clinsig_onc(class="selectpicker", data_style="btn-secondary") }}
746
733
  </div>
747
734
  </div>
748
- <div class="row">
735
+ <div class="row mt-2">
749
736
  <div class="col-4">
750
737
  {{ form.hgnc_symbols.label(class="control-label") }}
751
738
  {{ form.hgnc_symbols(class="form-control") }}
@@ -766,14 +753,28 @@
766
753
  {{ form.alt_count.label(class="control-label") }}
767
754
  {{ form.alt_count(class="form-control") }}
768
755
  </div>
769
- <div class="col-2">
756
+ <div class="col-1">
770
757
  {{ form.tumor_frequency.label(class="control-label") }}
771
758
  {{ form.tumor_frequency(class="form-control", min="0", max=1) }}
772
759
  </div>
773
- <div class="col-2">
760
+ <div class="col-1">
774
761
  {{ form.control_frequency.label(class="control-label") }}
775
762
  {{ form.control_frequency(class="form-control", min="0", max=1) }}
776
763
  </div>
764
+ <div class="col-2">
765
+ <div class="form-check d-flex justify-content-start">
766
+ {{ form.mvl_tag(class="form-check-input",type="checkbox") }}
767
+ {{ form.mvl_tag.label(class="ms-2 form-check-label") }}
768
+ </div>
769
+ <div class="form-check d-flex justify-content-start">
770
+ {{ form.clinvar_tag(class="form-check-input",type="checkbox") }}
771
+ {{ form.clinvar_tag.label(class="ms-2 form-check-label") }}
772
+ </div>
773
+ <div class="form-check d-flex justify-content-start">
774
+ {{ form.cosmic_tag(class="form-check-input",type="checkbox") }}
775
+ {{ form.cosmic_tag.label(class="ms-2 form-check-label") }}
776
+ </div>
777
+ </div>
777
778
  </div>
778
779
  <div class="form" id="chromosome_search">
779
780
  <div class="row" style="margin-top:20px;">
@@ -865,7 +866,7 @@
865
866
  {{ form.genetic_models(class="selectpicker", data_style="btn-secondary") }}
866
867
  </div>
867
868
  <div class="col-2">
868
- {{ form.clinsig.label(class="control-label") }}
869
+ {{ form.clinsig.label(class="control-label", data_bs_toggle="tooltip", data_bs_placement="left", title="Germline classification criteria.") }}
869
870
  {{ form.clinsig(class="selectpicker", data_style="btn-secondary") }}
870
871
  </div>
871
872
  </div>
@@ -1,6 +1,6 @@
1
1
  {% extends "layout.html" %}
2
- {% from "variants/utils.html" import compounds_table, svs_table, snv_filters, cell_rank, pagination_footer, pagination_hidden_div, dismiss_variants_block, filter_form_footer, filter_script_main, update_stash_filter_button_status, mark_heteroplasmic_mt %}
3
- {% from "variants/components.html" import external_scripts, external_stylesheets, frequency_cell_general, gene_cell, observed_cell_general %}
2
+ {% from "variants/utils.html" import snv_filters, cell_rank, pagination_footer, pagination_hidden_div, dismiss_variants_block, filter_form_footer, filter_script_main, update_stash_filter_button_status, mark_heteroplasmic_mt %}
3
+ {% from "variants/components.html" import external_scripts, external_stylesheets, frequency_cell_general, gene_cell, observed_cell_general, overlapping_cell %}
4
4
 
5
5
  {% block title %}
6
6
  {{ super() }} - {{ institute.display_name }} - {{ case.display_name }} - {{ form.variant_type.data|capitalize }} variants
@@ -59,7 +59,7 @@
59
59
  <th style="width:5%" title="Rank score">Score</th>
60
60
  <th style="width:4%" title="Chromosome">Chr.</th>
61
61
  <th style="width:12%" title="HGNC symbols">Gene</th>
62
- <th style="width:6%" title="Pop Freq">Pop Freq</th>
62
+ <th style="width:6%" title="Population Frequency">Pop Freq</th>
63
63
  <th style="width:10%" title="Observed database matches">Observed</th>
64
64
  <th style="width:5%" title="CADD score">CADD</th>
65
65
  <th style="width:18%" title="Functional annotation">Function</th>
@@ -93,7 +93,7 @@
93
93
  {{ functional_annotation_cell(variant) }}
94
94
  </td>
95
95
  <td>{{ cell_models(variant) }}</td>
96
- <td>{{ overlapping_cell(variant) }}</td>
96
+ <td>{{ overlapping_cell(variant, institute, case) }}</td>
97
97
  </tr>
98
98
  {% else %}
99
99
  <tr>
@@ -159,27 +159,6 @@
159
159
  {% endif %}
160
160
  {% endmacro %}
161
161
 
162
- {% macro overlapping_cell(variant) %}
163
-
164
- {% if variant.compounds %}
165
- {% set ns=namespace(show_compounds=false) %}
166
- {% for compound in variant.compounds if not compound.is_dismissed %}
167
- {% set ns.show_compounds = true %}
168
- {% endfor %}
169
- {% if ns.show_compounds %}
170
- <a href="#" class="badge bg-primary text-white" data-bs-toggle="popover" data-bs-placement="left"
171
- data-bs-html="true" data-bs-trigger="hover click"
172
- data-bs-content="{{ compounds_table(institute, case, variant.compounds[:20], is_popover=true) }}">Compounds</a>
173
- {% endif %}
174
- {% endif %}
175
-
176
- {% if variant.overlapping %}
177
- <a href="#" class="badge bg-warning" data-bs-toggle="popover" data-bs-placement="left"
178
- data-bs-html="true" data-bs-trigger="hover click"
179
- data-bs-content="{{ svs_table(institute, case, variant.overlapping[:20]) }}">Overlapping SVs</a>
180
- {% endif %}
181
- {% endmacro %}
182
-
183
162
  {% block scripts %}
184
163
  {{ super() }}
185
164
  {{ external_scripts() }}
scout/server/config.py CHANGED
@@ -38,6 +38,14 @@ ACCREDITATION_BADGE = "swedac-1926-iso17025.png"
38
38
  # discovery_url="https://accounts.google.com/.well-known/openid-configuration",
39
39
  # )
40
40
 
41
+ # Parameters required for login with Keycloak
42
+ # KEYCLOAK = dict(
43
+ # client_id="<name_of_client>",
44
+ # client_secret="secret",
45
+ # discovery_url="http://localhost:8080/realms/<name_of_realm>/.well-known/openid-configuration",
46
+ # logout_url="http://localhost:8080/realms/<name_of_realm>/protocol/openid-connect/logout",
47
+ # )
48
+
41
49
  # Chanjo database connection string - used by chanjo report to create coverage reports
42
50
  # SQLALCHEMY_DATABASE_URI = "mysql+pymysql://test_user:test_passwordw@127.0.0.1:3306/chanjo"
43
51
 
@@ -7,7 +7,6 @@ For further development, the server has a Swagger-like demo interface at https:/
7
7
  which is useful for details, and for sniffing actual message content structure, required cookie variable names etc.
8
8
  """
9
9
 
10
- import json
11
10
  import logging
12
11
  from typing import Dict, Iterable, List, Optional, Tuple
13
12
 
@@ -1,11 +1,13 @@
1
1
  import logging
2
- from typing import Dict, List
2
+ from typing import Dict
3
3
 
4
4
  import requests
5
- from flask import current_app
5
+ from flask import Flask, current_app
6
+
7
+ from scout.server.utils import get_case_mito_chromosome
8
+ from scout.utils.scout_requests import get_request_json, post_request_json
6
9
 
7
10
  REF_CHROM = "14"
8
- MT_CHROM = "MT"
9
11
  LOG = logging.getLogger(__name__)
10
12
 
11
13
  CHANJO_BUILD_37 = "GRCh37"
@@ -15,14 +17,33 @@ CHANJO_BUILD_38 = "GRCh38"
15
17
  class Chanjo2Client:
16
18
  """Runs requests to chanjo2 and returns results in the expected format."""
17
19
 
18
- def mt_coverage_stats(self, individuals: dict) -> Dict[str, dict]:
20
+ def init_app(self, app: Flask):
21
+ """The chanjo2 client has nothing to init, just check chanjo2 heartbeat."""
22
+ self.get_status(app)
23
+
24
+ def get_status(self, app: Flask) -> dict:
25
+ """Check Chanjo2 heartbeat, LOG status and return status."""
26
+ chanjo2_heartbeat_url: str = app.config.get("CHANJO2_URL")
27
+ json_resp = get_request_json(chanjo2_heartbeat_url)
28
+
29
+ if not json_resp:
30
+ LOG.warning(
31
+ f"Chanjo2 is configured at {chanjo2_heartbeat_url} but does not return heartbeat."
32
+ )
33
+ return json_resp
34
+
35
+ LOG.info(f"{json_resp.get('content',{}).get('message')}")
36
+ return json_resp
37
+
38
+ def mt_coverage_stats(self, case_obj: dict) -> Dict[str, dict]:
19
39
  """Sends a POST requests to the chanjo2 coverage/d4/interval to collect stats for the MT case report."""
20
40
 
21
41
  chanjo2_chrom_cov_url: str = "/".join(
22
42
  [current_app.config.get("CHANJO2_URL"), "coverage/d4/interval/"]
23
43
  )
24
44
  coverage_stats = {}
25
- for ind in individuals:
45
+ case_mt_chrom = get_case_mito_chromosome(case_obj)
46
+ for ind in case_obj.get("individuals", []):
26
47
 
27
48
  if not ind.get("d4_file"):
28
49
  continue
@@ -30,14 +51,21 @@ class Chanjo2Client:
30
51
 
31
52
  # Get mean coverage over chr14
32
53
  chrom_cov_query["chromosome"] = REF_CHROM
33
- resp = requests.post(chanjo2_chrom_cov_url, json=chrom_cov_query)
34
- autosome_cov = resp.json().get("mean_coverage")
35
54
 
36
- # Get mean coverage over chrMT
37
- chrom_cov_query["chromosome"] = MT_CHROM
38
- resp = requests.post(chanjo2_chrom_cov_url, json=chrom_cov_query)
55
+ autosome_cov_json = post_request_json(chanjo2_chrom_cov_url, chrom_cov_query)
56
+ if autosome_cov_json.get("status_code") != 200:
57
+ raise ValueError(
58
+ f"Chanjo2 get autosome coverage failed: {autosome_cov_json.get('message')}"
59
+ )
60
+
61
+ autosome_cov = autosome_cov_json.get("content", {}).get("mean_coverage")
39
62
 
40
- mt_cov = resp.json().get("mean_coverage")
63
+ # Get mean coverage over chrMT
64
+ chrom_cov_query["chromosome"] = case_mt_chrom
65
+ mt_cov_json = post_request_json(chanjo2_chrom_cov_url, chrom_cov_query)
66
+ if mt_cov_json.get("status_code") != 200:
67
+ raise ValueError(f"Chanjo2 get MT coverage failed: {mt_cov_json.get('message')}")
68
+ mt_cov = mt_cov_json.get("content", {}).get("mean_coverage")
41
69
 
42
70
  coverage_info = dict(
43
71
  mt_coverage=mt_cov,
@@ -66,6 +94,8 @@ class Chanjo2Client:
66
94
  "interval_type": "genes",
67
95
  "samples": [],
68
96
  }
97
+ analysis_types = []
98
+
69
99
  for ind in individuals:
70
100
  if not ind.get("d4_file"):
71
101
  continue
@@ -73,10 +103,21 @@ class Chanjo2Client:
73
103
  gene_cov_query["samples"].append(
74
104
  {"coverage_file_path": ind["d4_file"], "name": ind["individual_id"]}
75
105
  )
106
+ analysis_types.append(ind.get("analysis_type"))
76
107
 
77
- resp = requests.post(chanjo2_gene_cov_url, json=gene_cov_query)
78
- gene_cov = resp.json()
108
+ if "wes" in analysis_types:
109
+ gene_cov_query["interval_type"] = "exons"
110
+ elif "wts" in analysis_types:
111
+ gene_cov_query["interval_type"] = "transcripts"
112
+
113
+ gene_cov_json = post_request_json(chanjo2_gene_cov_url, gene_cov_query)
114
+
115
+ if gene_cov_json.get("status_code") != 200:
116
+ raise ValueError(
117
+ f"Chanjo2 get complete coverage failed: {gene_cov_json.get('message')}"
118
+ )
79
119
 
120
+ gene_cov = gene_cov_json.get("content")
80
121
  full_coverage = bool(gene_cov)
81
122
  for sample in gene_cov.keys():
82
123
  if gene_cov[sample]["coverage_completeness_percent"] < 100:
scout/server/links.py CHANGED
@@ -54,6 +54,7 @@ def add_gene_links(
54
54
  gene_obj["string_link"] = string(ensembl_id)
55
55
  gene_obj["reactome_link"] = reactome(ensembl_id)
56
56
  gene_obj["clingen_link"] = clingen(hgnc_id)
57
+ gene_obj["cspec_link"] = clingen_cspec(hgnc_id)
57
58
  gene_obj["gencc_link"] = gencc(hgnc_id)
58
59
  gene_obj["expression_atlas_link"] = expression_atlas(ensembl_id)
59
60
  gene_obj["exac_link"] = exac(ensembl_id)
@@ -80,6 +81,7 @@ def add_gene_links(
80
81
  gene_obj["gnomad_str_link"] = gnomad_str_gene(hgnc_symbol)
81
82
  gene_obj["panelapp_link"] = panelapp_gene(hgnc_symbol)
82
83
  gene_obj["decipher_link"] = decipher_gene(hgnc_symbol)
84
+ gene_obj["cancer_hotspots_link"] = cancer_hotspots_gene(hgnc_symbol)
83
85
  if institute:
84
86
  gene_obj["alamut_link"] = alamut_gene_link(institute, gene_obj, build)
85
87
 
@@ -90,6 +92,12 @@ def decipher_gene(hgnc_symbol: str) -> Optional[str]:
90
92
  return f"https://www.deciphergenomics.org/gene/{hgnc_symbol}/overview/clinical-info"
91
93
 
92
94
 
95
+ def cancer_hotspots_gene(hgnc_symbol: str) -> Optional[str]:
96
+ """Create link to Decipher gene."""
97
+ if hgnc_symbol:
98
+ return f"https://www.cancerhotspots.org/#/home?search={hgnc_symbol}"
99
+
100
+
93
101
  def panelapp_gene(hgnc_symbol):
94
102
  link = "https://panelapp.genomicsengland.co.uk/panels/entities/{}"
95
103
 
@@ -250,6 +258,13 @@ def clingen(hgnc_id):
250
258
  return link.format(hgnc_id)
251
259
 
252
260
 
261
+ def clingen_cspec(hgnc_id):
262
+ link = "https://cspec.genome.network/cspec/ui/svi/?search=HGNC:{}"
263
+ if not hgnc_id:
264
+ return None
265
+ return link.format(hgnc_id)
266
+
267
+
253
268
  def gencc(hgnc_id):
254
269
  link = "https://search.thegencc.org/genes/HGNC:{}"
255
270
  if not hgnc_id:
@@ -371,6 +371,23 @@ body {
371
371
  }
372
372
  /* end of sidebar-related stuff */
373
373
 
374
+ /* A collapse icon for button use */
375
+ .btn[aria-expanded="false"] .collapse-button-icon::before {
376
+ content: " \f0da";
377
+ font-family: "Font Awesome 5 Free", ui-monospace;
378
+ font-weight: 900;
379
+ display: inline;
380
+ text-align: left;
381
+ }
382
+
383
+ .btn[aria-expanded="true"] .collapse-button-icon::before {
384
+ content: " \f0d7";
385
+ font-family: "Font Awesome 5 Free", ui-monospace;
386
+ font-weight: 900;
387
+ display: inline;
388
+ text-align: left;
389
+
390
+
374
391
  option {
375
392
  width: 100%;
376
393
  }
@@ -386,9 +403,20 @@ option {
386
403
  }
387
404
 
388
405
  .str-variants-reviewer-container {
389
- padding: 20px 5px;
390
- }
391
- .str-variants-reviewer-container svg {
392
- width: 100%;
393
- height: auto;
394
- }
406
+ padding: 20px 5px;
407
+ }
408
+
409
+ .str-variants-reviewer-container svg {
410
+ width: 100%;
411
+ height: auto;
412
+ }
413
+
414
+ .blink_me {
415
+ animation: blinker 2s linear infinite;
416
+ }
417
+
418
+ @keyframes blinker {
419
+ 50% {
420
+ opacity: 0;
421
+ }
422
+ }
@@ -234,16 +234,15 @@
234
234
  {% endmacro %}
235
235
 
236
236
  {% macro db_table_external_stylesheets() %}
237
- <link rel="stylesheet" href="https://cdn.datatables.net/1.12.0/css/dataTables.bootstrap5.min.css" integrity="sha512-r9doM4NDlA2Cr0R56ielwMfkzL8miUuqbwNV4G1detiqlX58aTINapm2g2CoASFYedwgn3ulX6xmcDmRS0bmzw==" crossorigin="anonymous">
238
- <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.2.3/css/buttons.dataTables.min.css" integrity="sha512-tnmsL7vm/5wo8jsDSW4033D6dW3xn8qyIDZTIssv/3YKyLU3pMg5IZB0k88RGmZPlyyUoK7EEghuaoXZIinDXA==" crossorigin="anonymous">
239
- <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.2.3/css/buttons.bootstrap5.min.css" integrity="sha512-VX0h3jcn1Oc0lFy0aRxuq6U/tTk/6Ryurar5O3Az2DHLA1mpURv2Zgzl5MJJ5weChE7rdlsiB/ka5rUstkTN8g==" crossorigin="anonymous">
240
- {% endmacro %}
237
+ <link rel="stylesheet" href="https://cdn.datatables.net/2.2.2/css/dataTables.bootstrap5.min.css" integrity="sha512-pVSTZJo4Kj/eLMUG1w+itkGx+scwF00G5dMb02FjgU9WwF7F/cpZvu1Bf1ojA3iAf8y94cltGnuPV9vwv3CgZw==" referrerpolicy="no-referrer" crossorigin="anonymous">
238
+ <link rel="stylesheet" href="https://cdn.datatables.net/buttons/3.2.2/css/buttons.bootstrap5.min.css" integrity="sha512-9Mi4C/wtfinscXIdOGZvqv0ZYvhBUOQCrwqmTZyiYuEgJQHWnXenkYQOC44I3kyh7uaOmz9eNxeMkC8xGBLOWg==" referrerpolicy="no-referrer" crossorigin="anonymous">
239
+ {% endmacro %}
241
240
 
242
241
  {% macro db_table_external_scripts() %}
243
- <script src="https://cdn.datatables.net/1.12.0/js/jquery.dataTables.min.js" integrity="sha512-fu0WiDG5xqtX2iWk7cp17Q9so54SC+5lk/z/glzwlKFdEOwGG6piUseP2Sik9hlvlmyOJ0lKXRSuv1ltdVk9Jg==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
244
- <script src="https://cdn.datatables.net/1.12.0/js/dataTables.bootstrap5.min.js" integrity="sha512-nfoMMJ2SPcUdaoGdaRVA1XZpBVyDGhKQ/DCedW2k93MTRphPVXgaDoYV1M/AJQLCiw/cl2Nbf9pbISGqIEQRmQ==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
245
- <script src="https://cdn.datatables.net/buttons/2.2.3/js/dataTables.buttons.min.js" integrity="sha512-QT3oEXamRhx0x+SRDcgisygyWze0UicgNLFM9Dj5QfJuu2TVyw7xRQfmB0g7Z5/TgCdYKNW15QumLBGWoPefYg==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
246
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.0/jszip.min.js" integrity="sha512-0zVs73boVJk7Mlo/06K7cX+GWqW4xv0waiKkfXPGEhQ3Ww3UznkFlywBKIx4XetI/StJwac4bUNoeBGJY3Yugw==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
247
- <script src="https://cdn.datatables.net/buttons/2.2.3/js/buttons.html5.js" integrity="sha512-BSdPiqLbAJOVF1Q+rleIJJIio/8Ihf1zXxfYMinSHMpKdRWX634Mi4Rt8LRYfn0k5nbSlFO7Y7klzNCG8tokFQ==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
248
- <script src="https://cdn.datatables.net/buttons/2.2.3/js/buttons.bootstrap5.js" integrity="sha512-ZA1gDCJI+Hul95Y1tSRi3D/GfWRRX11oJH+QqtjnpzJLBDmU9DsO2db5oL3Gn7LezkSL/XWCL5v04ZiCy/RpKQ==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
242
+ <script src="https://cdn.datatables.net/2.2.2/js/dataTables.min.js" integrity="sha512-iwuEh+XTZQNzwdjYrqsxyWJXyReKVclGX4D2uGYBGNIR6o0VBwIfeYQv+lQjxmye87K6C6mc7qrqJlCcaeZJhA==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
243
+ <script src="https://cdn.datatables.net/2.2.2/js/dataTables.bootstrap5.min.js" integrity="sha512-Cwi0jz7fz7mrX990DlJ1+rmiH/D9/rjfOoEex8C9qrPRDDqwMPdWV7pJFKzhM10gAAPlufZcWhfMuPN699Ej0w==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
244
+ <script src="https://cdn.datatables.net/buttons/3.2.2/js/dataTables.buttons.min.js" integrity="sha512-T/4qQRgBJ8Ev6Lk8M943pMIbCpgqtVf5XI77hxe3nQn0f9a0gn/WKXWMbuGv6cpjp9NM8Mc+3/3GnpW+3+TckA==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
245
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" integrity="sha512-XMVd28F1oH/O71fzwBnV7HucLxVwtxf26XV8P4wPk26EDxuGZ91N8bsOttmnomcCD3CS5ZMRL50H0GgOHvegtg==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
246
+ <script src="https://cdn.datatables.net/buttons/3.2.2/js/buttons.html5.min.js" integrity="sha512-I86DAquH004N60kPVNHp+9QkC5y61C5ODHj6ewhddXnewOJ31wyOJHNlNEr8NUR3xqtGozfMb557So1pR0uDCA==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
247
+ <script src="https://cdn.datatables.net/buttons/3.2.2/js/buttons.bootstrap5.min.js" integrity="sha512-rVAaUqNTbX7lADp767RxnIMvbROobK+gn7SpbbPc+qMUPotgAkSsRhCKF5ziUEquBsdGcv3eOaZK1fIbGwh5CA==" referrerpolicy="no-referrer" crossorigin="anonymous"></script>
249
248
  {% endmacro %}
scout/server/utils.py CHANGED
@@ -206,6 +206,19 @@ def user_institutes(store, login_user):
206
206
  return institutes
207
207
 
208
208
 
209
+ def get_case_genome_build(case_obj: dict) -> str:
210
+ """returns the genome build of a case, as a string."""
211
+ return "38" if "38" in case_obj.get("genome_build", "37") else "37"
212
+
213
+
214
+ def get_case_mito_chromosome(case_obj: dict) -> str:
215
+ """Returns MT or M, according to the genome build of a case."""
216
+ case_build = get_case_genome_build(case_obj)
217
+ if case_build == "38":
218
+ return "M"
219
+ return "MT"
220
+
221
+
209
222
  def case_has_mtdna_report(case_obj: dict):
210
223
  """Display mtDNA report on the case sidebar only for some specific cases."""
211
224
  if case_obj.get("track", "rare") == "cancer":
@@ -244,15 +257,23 @@ def case_has_chanjo2_coverage(case_obj: dict):
244
257
  def case_has_alignments(case_obj: dict):
245
258
  """Add info on bam/cram files availability to a case dictionary
246
259
 
260
+ This sets the availability of alignments for autosomal chromosomes.
261
+ If paraphase or de novo assembly alignments, this is also sufficient to set
262
+ alignment availability.
247
263
  Args:
248
264
  case_obj(scout.models.Case)
249
265
  """
250
- case_obj["bam_files"] = False # Availability of alignments for autosomal chromosomes
266
+ case_obj["bam_files"] = False
251
267
  for ind in case_obj.get("individuals"):
252
- bam_path = ind.get("bam_file")
253
- if bam_path and os.path.exists(bam_path):
254
- case_obj["bam_files"] = True
255
- return
268
+ for alignment_path_key in [
269
+ "bam_file",
270
+ "assembly_alignment_path",
271
+ "paraphase_alignment_path",
272
+ ]:
273
+ bam_path = ind.get(alignment_path_key)
274
+ if bam_path and os.path.exists(bam_path):
275
+ case_obj["bam_files"] = True
276
+ return
256
277
 
257
278
 
258
279
  def case_has_mt_alignments(case_obj: dict):
@@ -301,14 +322,28 @@ def case_append_alignments(case_obj: dict):
301
322
  """Deconvolute information about files to case_obj.
302
323
 
303
324
  This function prepares bam/cram files and their indexes for easy access in templates.
325
+
326
+ index is set only if it is expected as a separate key on the indiviudal: an
327
+ attempt at discovery is still made for files with index: None.
304
328
  """
305
329
  unwrap_settings = [
306
330
  {"path": "bam_file", "append_to": "bam_files", "index": "bai_files"},
331
+ {"path": "assembly_alignment_path", "append_to": "assembly_alignments", "index": None},
307
332
  {"path": "mt_bam", "append_to": "mt_bams", "index": "mt_bais"},
333
+ {
334
+ "path": "paraphase_alignment_path",
335
+ "append_to": "paraphase_alignments",
336
+ "index": None,
337
+ },
308
338
  {"path": "rhocall_bed", "append_to": "rhocall_beds", "index": None},
309
339
  {"path": "rhocall_wig", "append_to": "rhocall_wigs", "index": None},
310
340
  {"path": "upd_regions_bed", "append_to": "upd_regions_beds", "index": None},
311
341
  {"path": "upd_sites_bed", "append_to": "upd_sites_beds", "index": None},
342
+ {
343
+ "path": "minor_allele_frequency_wig",
344
+ "append_to": "minor_allele_frequency_wigs",
345
+ "index": None,
346
+ },
312
347
  {"path": "tiddit_coverage_wig", "append_to": "tiddit_coverage_wigs", "index": None},
313
348
  ]
314
349
 
scout/utils/acmg.py CHANGED
@@ -173,7 +173,7 @@ def get_acmg_criteria(acmg_terms: set) -> tuple:
173
173
  finally, check remaining prefixes if no suffix match or stand-alone criteria match
174
174
 
175
175
  Return a tuple with
176
- pvs: This variable indicates if Pathogenicity Very Strong exists
176
+ pvs_terms: This variable indicates if Pathogenicity Very Strong exists
177
177
  ps_terms: Collection of terms with Pathogenicity Strong
178
178
  pm_terms: Collection of terms with Pathogenicity moderate
179
179
  pp_terms: Collection of terms with Pathogenicity supporting
@@ -182,25 +182,29 @@ def get_acmg_criteria(acmg_terms: set) -> tuple:
182
182
  bp_terms: Collection of terms with supporting Benign evidence
183
183
  """
184
184
 
185
- pvs = False
185
+ pvs_terms = []
186
186
  ps_terms = []
187
187
  pm_terms = []
188
188
  pp_terms = []
189
189
 
190
- ba = False
190
+ ba_terms = []
191
191
  bs_terms = []
192
192
  bp_terms = []
193
193
 
194
194
  suffix_map = {
195
+ "_Stand-alone": {"B": ba_terms},
196
+ "_Very Strong": {"P": pvs_terms},
195
197
  "_Strong": {"P": ps_terms, "B": bs_terms},
196
198
  "_Moderate": {"P": pm_terms},
197
199
  "_Supporting": {"P": pp_terms, "B": bp_terms},
198
200
  }
199
201
 
200
202
  prefix_map = {
203
+ "PVS": pvs_terms,
201
204
  "PS": ps_terms,
202
205
  "PM": pm_terms,
203
206
  "PP": pp_terms,
207
+ "BA": ba_terms,
204
208
  "BS": bs_terms,
205
209
  "BP": bp_terms,
206
210
  }
@@ -216,17 +220,12 @@ def get_acmg_criteria(acmg_terms: set) -> tuple:
216
220
  continue
217
221
  break
218
222
  else:
219
- if term.startswith("PVS"):
220
- pvs = True
221
- elif term.startswith("BA"):
222
- ba = True
223
- else:
224
- for prefix, term_list in prefix_map.items():
225
- if term.startswith(prefix):
226
- term_list.append(term)
227
- break
223
+ for prefix, term_list in prefix_map.items():
224
+ if term.startswith(prefix):
225
+ term_list.append(term)
226
+ break
228
227
 
229
- return (pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms)
228
+ return (pvs_terms, ps_terms, pm_terms, pp_terms, ba_terms, bs_terms, bp_terms)
230
229
 
231
230
 
232
231
  def get_acmg(acmg_terms: set) -> Optional[str]:
@@ -316,19 +315,19 @@ def get_acmg_temperature(acmg_terms: set) -> Optional[dict]:
316
315
  if not acmg_terms:
317
316
  return {}
318
317
 
319
- (pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms) = get_acmg_criteria(acmg_terms)
320
-
321
- if ba:
322
- points = -8
323
- else:
324
- points = (
325
- 8 * pvs
326
- + 4 * len(ps_terms)
327
- + 2 * len(pm_terms)
328
- + len(pp_terms)
329
- - 4 * len(bs_terms)
330
- - len(bp_terms)
331
- )
318
+ (pvs_terms, ps_terms, pm_terms, pp_terms, ba_terms, bs_terms, bp_terms) = get_acmg_criteria(
319
+ acmg_terms
320
+ )
321
+
322
+ points = (
323
+ 8 * len(pvs_terms)
324
+ + 4 * len(ps_terms)
325
+ + 2 * len(pm_terms)
326
+ + len(pp_terms)
327
+ - 8 * len(ba_terms)
328
+ - 4 * len(bs_terms)
329
+ - len(bp_terms)
330
+ )
332
331
 
333
332
  if points <= -7:
334
333
  point_classification = "benign"