scout-browser 4.90.1__py3-none-any.whl → 4.91__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 (62) hide show
  1. scout/__version__.py +1 -1
  2. scout/adapter/mongo/case.py +27 -38
  3. scout/commands/export/variant.py +14 -4
  4. scout/commands/load/panel.py +2 -1
  5. scout/commands/update/panelapp.py +11 -3
  6. scout/commands/view/case.py +2 -2
  7. scout/constants/__init__.py +1 -2
  8. scout/constants/acmg.py +15 -15
  9. scout/constants/case_tags.py +0 -46
  10. scout/constants/clnsig.py +2 -1
  11. scout/constants/gene_tags.py +0 -6
  12. scout/constants/panels.py +16 -0
  13. scout/constants/variants_export.py +2 -0
  14. scout/demo/__init__.py +4 -1
  15. scout/demo/panelapp_panel.json +463 -0
  16. scout/demo/panelapp_panels_reduced.json +37 -0
  17. scout/load/panel.py +3 -142
  18. scout/load/panelapp.py +138 -0
  19. scout/models/case/case_loading_models.py +5 -4
  20. scout/parse/panel.py +3 -117
  21. scout/parse/panelapp.py +112 -0
  22. scout/parse/variant/clnsig.py +26 -21
  23. scout/parse/variant/genotype.py +6 -5
  24. scout/server/blueprints/alignviewers/controllers.py +7 -5
  25. scout/server/blueprints/alignviewers/templates/alignviewers/utils.html +1 -1
  26. scout/server/blueprints/cases/templates/cases/case_sma.html +49 -42
  27. scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +27 -12
  28. scout/server/blueprints/cases/views.py +18 -7
  29. scout/server/blueprints/clinvar/templates/clinvar/clinvar_submissions.html +7 -7
  30. scout/server/blueprints/clinvar/templates/clinvar/multistep_add_variant.html +2 -2
  31. scout/server/blueprints/dashboard/controllers.py +128 -165
  32. scout/server/blueprints/dashboard/forms.py +3 -13
  33. scout/server/blueprints/dashboard/templates/dashboard/dashboard_general.html +17 -22
  34. scout/server/blueprints/institutes/forms.py +1 -2
  35. scout/server/blueprints/institutes/templates/overview/cases.html +2 -133
  36. scout/server/blueprints/institutes/templates/overview/utils.html +135 -0
  37. scout/server/blueprints/omics_variants/templates/omics_variants/outliers.html +5 -0
  38. scout/server/blueprints/panels/templates/panels/panel.html +5 -1
  39. scout/server/blueprints/panels/templates/panels/panel_pdf_simple.html +5 -1
  40. scout/server/blueprints/variant/controllers.py +6 -1
  41. scout/server/blueprints/variant/templates/variant/buttons.html +11 -10
  42. scout/server/blueprints/variant/templates/variant/components.html +63 -44
  43. scout/server/blueprints/variant/templates/variant/str-variant-reviewer.html +1 -1
  44. scout/server/blueprints/variant/templates/variant/utils.html +38 -10
  45. scout/server/blueprints/variant/templates/variant/variant.html +1 -1
  46. scout/server/blueprints/variants/controllers.py +9 -4
  47. scout/server/blueprints/variants/templates/variants/cancer-sv-variants.html +9 -5
  48. scout/server/blueprints/variants/templates/variants/cancer-variants.html +6 -17
  49. scout/server/blueprints/variants/templates/variants/str-variants.html +2 -2
  50. scout/server/blueprints/variants/templates/variants/sv-variants.html +8 -1
  51. scout/server/blueprints/variants/templates/variants/utils.html +14 -0
  52. scout/server/extensions/__init__.py +2 -0
  53. scout/server/extensions/panelapp_extension.py +75 -0
  54. scout/server/links.py +19 -1
  55. scout/server/utils.py +25 -33
  56. {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/METADATA +1 -1
  57. {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/RECORD +61 -57
  58. {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/WHEEL +1 -1
  59. scout/demo/panelapp_test_panel.json +0 -79
  60. {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/LICENSE +0 -0
  61. {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/entry_points.txt +0 -0
  62. {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/top_level.txt +0 -0
@@ -197,22 +197,37 @@
197
197
  <span class="menu-collapsed">Genome build {{ case.genome_build }}</span>
198
198
  </div>
199
199
  </div>
200
- <div href="#" class="bg-dark list-group-item d-inline-block text-white">
201
- {% if case.bam_files %}
200
+ <div href="#" class="bg-dark list-group-item d-inline-block text-white"
201
+ {% if not case.bam_files %} data-bs-toggle="tooltip" title="Alignment file(s) missing" {% endif %}>
202
202
  <form action="{{url_for('alignviewers.igv', institute_id=case['owner'], case_name=case['display_name']) }}" target="_blank" rel="noopener">
203
- <button role="submit" class="btn btn-xs form-control btn-secondary">I<span class="menu-collapsed">GV viewer</span></button>
203
+ <button type="submit" class="btn btn-xs form-control btn-secondary"
204
+ {% if not case.bam_files %}
205
+ disabled="disabled"><span class="fa fa-times-circle fa-fw me-1"></span>
206
+ {% else %}><span class="fa fa-magnifying-glass fa-fw me-1"></span>
207
+ {% endif %}
208
+ <span class="menu-collapsed">IGV g</span>D<span class="menu-collapsed">NA</span>
209
+ </button>
204
210
  </form>
205
- {% else %}
206
- <span class="fa fa-times-circle fa-fw me-3"></span><span class="text-muted menu-collapsed">Alignment missing</span>
207
- {% endif %}
208
- </div>
209
- {% if has_rna_tracks %}
210
- <div href="#" class="bg-dark list-group-item d-inline-block text-white">
211
- <form action="{{url_for('alignviewers.sashimi_igv', institute_id=case['owner'], case_name=case['display_name']) }}" target="_blank" rel="noopener">
212
- <button role="submit" data-bs-toggle="tooltip" data-bs-placement="top" title="Available in build GRCh{{ case.rna_genome_build or '38' }}" class="btn btn-xs form-control btn-secondary">R<span class="menu-collapsed">NA splicing</span></button>
211
+ </div>
212
+ <div href="#" class="bg-dark list-group-item d-inline-block text-white"
213
+ {% if not case.mt_bams %} data-bs-toggle="tooltip" title="Alignment file(s) missing" {% endif %}>
214
+ <form action="{{url_for('alignviewers.igv', institute_id=case['owner'], case_name=case['display_name'], chrom='M', start=1, stop=16569) }}" target="_blank" rel="noopener">
215
+ <button type="submit" class="btn btn-xs form-control btn-secondary"
216
+ {% if not case.mt_bams %}
217
+ disabled="disabled"><span class="fa fa-times-circle fa-fw me-1"></span>
218
+ {% else %}
219
+ ><span class="fa fa-magnifying-glass fa-fw me-1"></span>
220
+ {% endif %}
221
+ <span class="menu-collapsed">IGV </span>m<span class="menu-collapsed">tDNA</span></button>
213
222
  </form>
214
223
  </div>
215
- {% endif %}
224
+ {% if has_rna_tracks %}
225
+ <div href="#" class="bg-dark list-group-item d-inline-block text-white">
226
+ <form action="{{url_for('alignviewers.sashimi_igv', institute_id=case['owner'], case_name=case['display_name']) }}" target="_blank" rel="noopener">
227
+ <button type="submit" class="btn btn-xs form-control btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top" title="Available in build GRCh{{ case.rna_genome_build or '38' }}"><span class="fa fa-magnifying-glass fa-fw me-1"></span><span class="menu-collapsed">IGV </span>R<span class="menu-collapsed">NA</span></button>
228
+ </form>
229
+ </div>
230
+ {% endif %}
216
231
  {% endmacro %}
217
232
 
218
233
  {% macro rank_model(case) %}
@@ -25,6 +25,8 @@ from flask import (
25
25
  url_for,
26
26
  )
27
27
  from flask_login import current_user
28
+ from pymongo.errors import OperationFailure
29
+ from werkzeug.datastructures import ImmutableMultiDict
28
30
 
29
31
  from scout.constants import DATE_DAY_FORMATTER
30
32
  from scout.server.blueprints.variants.controllers import activate_case
@@ -619,7 +621,13 @@ def caselist(institute_id):
619
621
  if query is None:
620
622
  return abort(500)
621
623
 
622
- matching_cases = store.cases(owner=institute_id, name_query="case:" + query)
624
+ name_query = ImmutableMultiDict(
625
+ {
626
+ "case": query,
627
+ }
628
+ )
629
+
630
+ matching_cases = store.cases(owner=institute_id, name_query=name_query)
623
631
  display_names = []
624
632
  if institute_id: # called from case page, where institute_id is provided
625
633
  display_names = sorted([case["display_name"] for case in matching_cases])
@@ -643,13 +651,16 @@ def hpoterms():
643
651
  query = request.args.get("query")
644
652
  if query is None:
645
653
  return abort(500)
646
- terms = sorted(store.hpo_terms(query=query), key=itemgetter("hpo_number"))
647
- json_terms = [
648
- {"name": "{} | {}".format(term["_id"], term["description"]), "id": term["_id"]}
649
- for term in terms[:7]
650
- ]
654
+ try:
655
+ terms = sorted(store.hpo_terms(query=query), key=itemgetter("hpo_number"))
656
+ json_terms = [
657
+ {"name": "{} | {}".format(term["_id"], term["description"]), "id": term["_id"]}
658
+ for term in terms[:7]
659
+ ]
651
660
 
652
- return jsonify(json_terms)
661
+ return jsonify(json_terms)
662
+ except OperationFailure as of:
663
+ return jsonify({"error": of._message})
653
664
 
654
665
 
655
666
  @cases_bp.route("/api/v1/disease-terms")
@@ -101,8 +101,8 @@
101
101
  {% endif %}
102
102
  {% if subm_obj.status == 'submitted' %} <!--Submission status query -->
103
103
  <form id="statusApi_{{subm_obj._id}}" action="{{ url_for('clinvar.clinvar_submission_status', submission_id=subm_obj.clinvar_subm_id) }}" method="POST">
104
- {{ status_modal() }}
105
- <td style="width: 20%"><button type="button" class="btn btn-sm btn-secondary form-control" name="status_enquiry" value="api_status" data-bs-toggle="modal" data-bs-target="#statusModal">Submission status enquiry</button></td>
104
+ {{ status_modal(subm_obj._id) }}
105
+ <td style="width: 20%"><button type="button" class="btn btn-sm btn-secondary form-control" name="status_enquiry" value="api_status" data-bs-toggle="modal" data-bs-target="#statusModal_{{subm_obj._id}}">Submission status enquiry</button></td>
106
106
  </form>
107
107
  {% endif %}
108
108
  </tr>
@@ -156,7 +156,7 @@
156
156
  <td><a href="{{ url_for('cases.case', institute_id=institute._id, case_name=subm_obj.cases[subm_variant.case_id]) }}" target="_blank" rel="noopener">{{subm_obj.cases[subm_variant.case_id]}}</a></td>
157
157
  <td>{{subm_variant.ref_seq or '-'}}</td>
158
158
  <td>{{subm_variant.gene_symbol or '-'}}</td>
159
- <td>{{subm_variant.hgvs|truncate(25,true,'..') or '-'}}</td>
159
+ <td>{{subm_variant.hgvs|truncate(100,true,'..') or '-'}}</td>
160
160
  <td>{{subm_variant.clinsig}}</td>
161
161
  <td>{{subm_variant.added_by or "N/A"}}</td>
162
162
  <form id="delete_variant_{{subm_variant._id}}" action="{{ url_for('clinvar.clinvar_delete_object', submission=subm_obj._id, object_type='variant_data') }}" method="POST">
@@ -170,7 +170,7 @@
170
170
  <ul class="list-group">
171
171
  {% for key, value in variant_header_fields.items() %}
172
172
  {% if subm_variant[key]%}
173
- <li class="list-group-item">{{ value }}: <strong>{{ subm_variant[key]|replace(subm_variant[key][75:], '..') if subm_variant[key]|length > 75 else subm_variant[key]}}</strong></li>
173
+ <li class="list-group-item">{{ value }}: <strong>{{ subm_variant[key]}}</strong></li>
174
174
  {% endif %}
175
175
  {% endfor %}
176
176
  </ul>
@@ -214,7 +214,7 @@
214
214
  </form>
215
215
  <td><button id="{{case._id}}" type="button" class="btn btn-primary btn-xs cd_btn"><span class="fa fa-search-plus" aria-hidden="true"></span></button></td>
216
216
  <td><a href="{{ url_for('cases.case', institute_id=institute._id, case_name=subm_obj.cases[case.case_id]) }}" target="_blank" rel="noopener">{{subm_obj.cases[case.case_id]}}</a></td>
217
- <td>{{var_key_name[case.linking_id]|truncate(25,true,'..')}}</td>
217
+ <td>{{var_key_name[case.linking_id]|truncate(100,true,'..')}}</td>
218
218
  <td>{{case.allele_origin}}</td>
219
219
  <form id="delete_casedata_{{case._id}}" action="{{ url_for('clinvar.clinvar_delete_object', submission=subm_obj._id, object_type='case_data') }}" method="POST">
220
220
  <td><button type="submit" name="delete_object" value="{{case._id}}" class="btn btn-danger btn-xs"><span class="fa fa-trash" aria-hidden="true"></span></button></td>
@@ -246,8 +246,8 @@
246
246
  </div>
247
247
  {% endmacro %}
248
248
 
249
- {% macro status_modal() %}
250
- <div class="modal fade" id="statusModal" tabindex="-1">
249
+ {% macro status_modal(subm_id) %}
250
+ <div class="modal fade" id="statusModal_{{subm_id}}" tabindex="-1">
251
251
  <div class="modal-dialog">
252
252
  <div class="modal-content">
253
253
  <div class="modal-header">
@@ -390,10 +390,10 @@ function validateConditions(){
390
390
  return false;
391
391
  }
392
392
 
393
- // Make sure that provided conditions are numbers, except when condition type is MeSH
393
+ // Make sure that provided conditions are numbers, except when condition type is MeSH or MedGen
394
394
  var selectedConditionsValues = $('#condition_tags').val();
395
395
  for (let i = 0; i < selectedConditionsValues.length; i++) {
396
- if (isNaN(selectedConditionsValues[i]) && $('#condition_type').val() != 'MeSH'){
396
+ if (isNaN(selectedConditionsValues[i]) && !['Mesh', 'MedGen'].includes($('#condition_type').val())){
397
397
  alert(`Condition ID "${selectedConditionsValues[i]}" has an invalid format.`);
398
398
  return false;
399
399
  }
@@ -1,9 +1,13 @@
1
1
  import logging
2
+ from typing import Dict, List, Set
2
3
 
3
4
  from flask import flash, redirect, request, url_for
4
5
  from flask_login import current_user
6
+ from werkzeug.datastructures.structures import ImmutableMultiDict
7
+ from werkzeug.local import LocalProxy
5
8
 
6
- from scout.constants import CASE_SEARCH_TERMS, CASE_TAGS
9
+ from scout.adapter import MongoAdapter
10
+ from scout.constants import CASE_TAGS
7
11
  from scout.server.extensions import store
8
12
  from scout.server.utils import user_institutes
9
13
 
@@ -22,7 +26,7 @@ def institute_select_choices():
22
26
  # Collect only institutes available to the user
23
27
  institute_objs = user_institutes(store, current_user)
24
28
  for inst in institute_objs:
25
- institute_choices.append((inst["_id"], inst["display_name"]))
29
+ institute_choices.append((inst["_id"], f'{inst["display_name"]} ({inst["_id"]})'))
26
30
  return institute_choices
27
31
 
28
32
 
@@ -33,116 +37,90 @@ def dashboard_form(request_form=None):
33
37
  return form
34
38
 
35
39
 
36
- def compose_slice_query(search_type, search_term):
37
- """Extract a filter query given a form search term and search type
40
+ def populate_dashboard_data(request: LocalProxy) -> dict:
41
+ """Collect data to display on the dashboard page"""
38
42
 
39
- Args:
40
- search_type(str): example -> "case:"
41
- search_term(str): example -> "17867"
42
-
43
- Returns:
44
- slice_query(str): example case:17867
45
- """
46
- slice_query = None
47
- if search_term and search_type:
48
- slice_query = "".join([search_type, search_term])
49
-
50
- return slice_query
51
-
52
-
53
- def populate_dashboard_data(request):
54
- """Prepate data display object to be returned to the view
55
-
56
- Args:
57
- request(flask.rquest): request received by the view
58
-
59
- Returns:
60
- data(dict): data to be diplayed in the template
61
- """
62
- data = {"dashboard_form": dashboard_form(request.form), "search_terms": CASE_SEARCH_TERMS}
43
+ data = {"dashboard_form": dashboard_form(request.form)}
63
44
  if request.method == "GET":
64
45
  return data
65
46
 
66
- allowed_insititutes = [inst[0] for inst in institute_select_choices()]
47
+ allowed_institutes = [inst[0] for inst in institute_select_choices()]
67
48
 
68
49
  institute_id = request.form.get(
69
- "search_institute", allowed_insititutes[0]
50
+ "search_institute", allowed_institutes[0]
70
51
  ) # GET request has no institute, select the first option of the select
71
52
 
72
- if institute_id and institute_id not in allowed_insititutes:
53
+ if institute_id and institute_id not in allowed_institutes:
73
54
  flash("Your user is not allowed to visualize this data", "warning")
74
55
  redirect(url_for("dashboard.index"))
75
56
 
76
57
  if institute_id == "All":
77
58
  institute_id = None
78
59
 
79
- search_term = request.form["search_term"].strip() if request.form.get("search_term") else ""
80
- slice_query = compose_slice_query(request.form.get("search_type"), search_term)
81
- get_dashboard_info(store, data, institute_id, slice_query)
60
+ get_dashboard_info(adapter=store, data=data, institute_id=institute_id, cases_form=request.form)
82
61
  return data
83
62
 
84
63
 
85
- def get_dashboard_info(adapter, data={}, institute_id=None, slice_query=None):
86
- """Append case data stats to data display object
87
- Args:
88
- adapter(adapter.MongoAdapter)
89
- data(dict): data dictionary to be passed to template
90
- institute_id(str): institute id
91
- slice_query(str): example case:55888
64
+ def get_dashboard_info(
65
+ adapter: MongoAdapter, data: dict = None, institute_id: str = None, cases_form=None
66
+ ) -> dict:
67
+ """Append case data stats to data display object"""
92
68
 
93
- Returns:
94
- data(dict): data to be diplayed in the template
95
- """
96
- # If a slice_query is present then numbers in "General statistics" and "Case statistics" will
97
- # reflect the data available for the query
98
- general_sliced_info = get_general_case_info(
99
- adapter, institute_id=institute_id, slice_query=slice_query
69
+ if not data:
70
+ data = {}
71
+
72
+ # Filter data using eventual filter provided in cases filters form
73
+ filtered_cases_info = get_general_case_info(
74
+ adapter=adapter, institute_id=institute_id, cases_form=cases_form
100
75
  )
101
76
 
102
- total_sliced_cases = general_sliced_info["total_cases"]
103
- data["total_cases"] = total_sliced_cases
77
+ total_filtered_cases = filtered_cases_info["total_cases"]
78
+ data["total_cases"] = total_filtered_cases
104
79
 
105
- if total_sliced_cases == 0:
80
+ if total_filtered_cases == 0:
106
81
  return data
107
82
 
108
83
  data["pedigree"] = []
109
- for ped_info in general_sliced_info["pedigree"].values():
110
- ped_info["percent"] = ped_info["count"] / total_sliced_cases
84
+ for ped_info in filtered_cases_info["pedigree"].values():
85
+ ped_info["percent"] = ped_info["count"] / total_filtered_cases
111
86
  data["pedigree"].append(ped_info)
112
87
 
113
88
  data["cases"] = get_case_groups(
114
- adapter, total_sliced_cases, institute_id=institute_id, slice_query=slice_query
89
+ adapter=adapter,
90
+ total_cases=total_filtered_cases,
91
+ institute_id=institute_id,
92
+ name_query=cases_form,
115
93
  )
116
94
 
117
95
  data["analysis_types"] = get_analysis_types(
118
- adapter, total_sliced_cases, institute_id=institute_id, slice_query=slice_query
96
+ adapter=adapter, institute_id=institute_id, name_query=cases_form
119
97
  )
120
98
 
121
99
  overview = [
122
100
  {
123
101
  "title": "Phenotype terms",
124
- "count": general_sliced_info["phenotype_cases"],
125
- "percent": general_sliced_info["phenotype_cases"] / total_sliced_cases,
102
+ "count": filtered_cases_info["phenotype_cases"],
103
+ "percent": filtered_cases_info["phenotype_cases"] / total_filtered_cases,
126
104
  },
127
105
  {
128
106
  "title": "Causative variants",
129
- "count": general_sliced_info["causative_cases"],
130
- "percent": general_sliced_info["causative_cases"] / total_sliced_cases,
107
+ "count": filtered_cases_info["causative_cases"],
108
+ "percent": filtered_cases_info["causative_cases"] / total_filtered_cases,
131
109
  },
132
110
  {
133
111
  "title": "Pinned variants",
134
- "count": general_sliced_info["pinned_cases"],
135
- "percent": general_sliced_info["pinned_cases"] / total_sliced_cases,
112
+ "count": filtered_cases_info["pinned_cases"],
113
+ "percent": filtered_cases_info["pinned_cases"] / total_filtered_cases,
136
114
  },
137
115
  {
138
116
  "title": "Cohort tag",
139
- "count": general_sliced_info["cohort_cases"],
140
- "percent": general_sliced_info["cohort_cases"] / total_sliced_cases,
117
+ "count": filtered_cases_info["cohort_cases"],
118
+ "percent": filtered_cases_info["cohort_cases"] / total_filtered_cases,
141
119
  },
142
120
  {
143
121
  "title": "Case status tag",
144
- "count": general_sliced_info["tagged_cases"],
145
- "percent": general_sliced_info["tagged_cases"] / total_sliced_cases,
122
+ "count": filtered_cases_info["tagged_cases"],
123
+ "percent": filtered_cases_info["tagged_cases"] / total_filtered_cases,
146
124
  },
147
125
  ]
148
126
 
@@ -150,8 +128,8 @@ def get_dashboard_info(adapter, data={}, institute_id=None, slice_query=None):
150
128
  overview.append(
151
129
  {
152
130
  "title": CASE_TAGS[stats_tag]["label"] + " status tag",
153
- "count": general_sliced_info[stats_tag + "_cases"],
154
- "percent": general_sliced_info[stats_tag + "_cases"] / total_sliced_cases,
131
+ "count": filtered_cases_info[stats_tag + "_cases"],
132
+ "percent": filtered_cases_info[stats_tag + "_cases"] / total_filtered_cases,
155
133
  }
156
134
  )
157
135
 
@@ -160,26 +138,10 @@ def get_dashboard_info(adapter, data={}, institute_id=None, slice_query=None):
160
138
  return data
161
139
 
162
140
 
163
- def get_general_case_info(adapter, institute_id=None, slice_query=None):
164
- """Return general information about cases
165
-
166
- Count e.g. cases with each kind of case tag, as well as cases that have phenotype, causatives or pinned variants
167
- marked or are part of cohorts.
168
-
169
- Gather pedigree information - single, duo, trio or many individuals in case.
170
-
171
- Args:
172
- adapter(adapter.MongoAdapter)
173
- institute_id(str)
174
- slice_query(str): Query to filter cases to obtain statistics for.
175
-
176
- Returns:
177
- general(dict)
178
- """
179
- general = {}
180
-
181
- # Potentially sensitive slice queries are assumed allowed if we have got this far
182
- name_query = slice_query
141
+ def get_general_case_info(
142
+ adapter, institute_id: str = None, cases_form: ImmutableMultiDict = None
143
+ ) -> dict:
144
+ """Return general information about cases."""
183
145
 
184
146
  CASE_GENERAL_INFO_PROJECTION = {
185
147
  "phenotype_terms": 1,
@@ -189,77 +151,88 @@ def get_general_case_info(adapter, institute_id=None, slice_query=None):
189
151
  "individuals": 1,
190
152
  "tags": 1,
191
153
  }
154
+
192
155
  cases = adapter.cases(
193
- owner=institute_id, name_query=name_query, projection=CASE_GENERAL_INFO_PROJECTION
156
+ owner=institute_id, name_query=cases_form, projection=CASE_GENERAL_INFO_PROJECTION
194
157
  )
195
158
 
196
- case_counter_keys = [
197
- "phenotype",
198
- "causative",
199
- "pinned",
200
- "cohort",
201
- "tagged",
202
- ]
203
- case_counter_keys.extend(CASE_TAGS.keys())
159
+ # Initialize counters and structures
160
+ case_counter_keys = ["phenotype", "causative", "pinned", "cohort", "tagged"] + list(
161
+ CASE_TAGS.keys()
162
+ )
163
+ case_counter = initialize_case_counter(case_counter_keys)
164
+ pedigree = initialize_pedigree()
204
165
 
205
- case_counter = {}
206
- for counter in case_counter_keys:
207
- case_counter[counter] = 0
166
+ case_ids: Set[str] = set()
167
+ total_cases = 0
208
168
 
209
- pedigree = {
210
- 1: {"title": "Single", "count": 0},
211
- 2: {"title": "Duo", "count": 0},
212
- 3: {"title": "Trio", "count": 0},
213
- "many": {"title": "Many", "count": 0},
169
+ # Process each case
170
+ for total_cases, case in enumerate(cases, 1):
171
+ case_ids.add(case["_id"])
172
+ update_case_counters(case, case_counter, case_counter_keys)
173
+ update_pedigree(case, pedigree)
174
+
175
+ # Prepare general info dictionary
176
+ general = {
177
+ "total_cases": total_cases,
178
+ "pedigree": pedigree,
179
+ "case_ids": case_ids,
180
+ **{f"{key}_cases": count for key, count in case_counter.items()},
214
181
  }
215
182
 
216
- case_ids = set()
183
+ return general
217
184
 
218
- total_cases = 0
219
- for total_cases, case in enumerate(cases, 1):
220
- case_ids.add(case["_id"])
221
- if case.get("phenotype_terms"):
222
- case_counter["phenotype"] += 1
223
- if case.get("causatives"):
224
- case_counter["causative"] += 1
225
- if case.get("suspects"):
226
- case_counter["pinned"] += 1
227
- if case.get("cohorts"):
228
- case_counter["cohort"] += 1
229
- if case.get("tags"):
230
- case_counter["tagged"] += 1
231
- case_tags = case.get("tags")
232
- for tag in case_tags:
233
- case_counter[tag] += 1
234
185
 
235
- nr_individuals = len(case.get("individuals", []))
236
- if nr_individuals == 0:
237
- continue
238
- if nr_individuals > 3:
239
- pedigree["many"]["count"] += 1
240
- else:
241
- pedigree[nr_individuals]["count"] += 1
186
+ def initialize_case_counter(case_counter_keys) -> Dict[str, int]:
187
+ """Initialize the case counter dictionary with the given keys set to zero."""
188
+ return {key: 0 for key in case_counter_keys}
242
189
 
243
- general["total_cases"] = total_cases
244
- general["pedigree"] = pedigree
245
- general["case_ids"] = case_ids
246
- for counter in case_counter_keys:
247
- general[counter + "_cases"] = case_counter[counter]
248
190
 
249
- return general
191
+ def initialize_pedigree() -> Dict:
192
+ """Initialize the pedigree structure with counts set to zero."""
193
+ return {
194
+ 1: {"title": "Single", "count": 0},
195
+ 2: {"title": "Duo", "count": 0},
196
+ 3: {"title": "Trio", "count": 0},
197
+ "many": {"title": "Many", "count": 0},
198
+ }
250
199
 
251
200
 
252
- def get_case_groups(adapter, total_cases, institute_id=None, slice_query=None):
253
- """Return the information about case groups
201
+ def update_case_counters(case: dict, case_counter: Dict[str, int], case_counter_keys):
202
+ """Update case counter based on the case's attributes."""
203
+ if case.get("phenotype_terms"):
204
+ case_counter["phenotype"] += 1
205
+ if case.get("causatives"):
206
+ case_counter["causative"] += 1
207
+ if case.get("suspects"):
208
+ case_counter["pinned"] += 1
209
+ if case.get("cohorts"):
210
+ case_counter["cohort"] += 1
211
+ if case.get("tags"):
212
+ case_counter["tagged"] += 1
213
+ for tag in case.get("tags", []):
214
+ if tag in case_counter_keys: # Ensure tag is in the predefined keys
215
+ case_counter[tag] += 1
254
216
 
255
- Args:
256
- store(adapter.MongoAdapter)
257
- total_cases(int): Total number of cases
258
- slice_query(str): Query to filter cases to obtain statistics for.
259
217
 
260
- Returns:
261
- cases(dict):
262
- """
218
+ def update_pedigree(case: dict, pedigree: Dict):
219
+ """Update pedigree information based on the number of individuals in the case."""
220
+ nr_individuals = len(case.get("individuals", []))
221
+ if nr_individuals == 0:
222
+ return
223
+ if nr_individuals > 3:
224
+ pedigree["many"]["count"] += 1
225
+ else:
226
+ pedigree[nr_individuals]["count"] += 1
227
+
228
+
229
+ def get_case_groups(
230
+ adapter: MongoAdapter,
231
+ total_cases: int,
232
+ institute_id: str = None,
233
+ name_query: ImmutableMultiDict = None,
234
+ ) -> List[dict]:
235
+ """Return the information about case groups"""
263
236
  # Create a group with all cases in the database
264
237
  cases = [{"status": "all", "count": total_cases, "percent": 1}]
265
238
  # Group the cases based on their status
@@ -267,12 +240,12 @@ def get_case_groups(adapter, total_cases, institute_id=None, slice_query=None):
267
240
  group = {"$group": {"_id": "$status", "count": {"$sum": 1}}}
268
241
 
269
242
  subquery = {}
270
- if institute_id and slice_query:
271
- subquery = adapter.cases(owner=institute_id, name_query=slice_query, yield_query=True)
243
+ if institute_id and name_query:
244
+ subquery = adapter.cases(owner=institute_id, name_query=name_query, yield_query=True)
272
245
  elif institute_id:
273
246
  subquery = adapter.cases(owner=institute_id, yield_query=True)
274
- elif slice_query:
275
- subquery = adapter.cases(name_query=slice_query, yield_query=True)
247
+ elif name_query:
248
+ subquery = adapter.cases(name_query=name_query, yield_query=True)
276
249
 
277
250
  query = {"$match": subquery} if subquery else {}
278
251
 
@@ -294,28 +267,18 @@ def get_case_groups(adapter, total_cases, institute_id=None, slice_query=None):
294
267
  return cases
295
268
 
296
269
 
297
- def get_analysis_types(adapter, total_cases, institute_id=None, slice_query=None):
298
- """Return information about analysis types.
299
- Group cases based on analysis type for the individuals.
300
- Args:
301
- adapter(adapter.MongoAdapter)
302
- total_cases(int): Total number of cases
303
- institute_id(str)
304
- slice_query(str): Query to filter cases to obtain statistics for.
305
- Returns:
306
- analysis_types array of hashes with name: analysis_type(str), count: count(int)
307
-
308
- """
309
- # Group cases based on analysis type of the individuals
310
- query = {}
270
+ def get_analysis_types(
271
+ adapter: MongoAdapter, institute_id: str = None, name_query: ImmutableMultiDict = None
272
+ ) -> List[dict]:
273
+ """Group cases based on analysis type of the individuals."""
311
274
 
312
275
  subquery = {}
313
- if institute_id and slice_query:
314
- subquery = adapter.cases(owner=institute_id, name_query=slice_query, yield_query=True)
276
+ if institute_id and name_query:
277
+ subquery = adapter.cases(owner=institute_id, name_query=name_query, yield_query=True)
315
278
  elif institute_id:
316
279
  subquery = adapter.cases(owner=institute_id, yield_query=True)
317
- elif slice_query:
318
- subquery = adapter.cases(name_query=slice_query, yield_query=True)
280
+ elif name_query:
281
+ subquery = adapter.cases(name_query=name_query, yield_query=True)
319
282
 
320
283
  query = {"$match": subquery}
321
284
 
@@ -1,12 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
- from flask_wtf import FlaskForm
3
2
  from wtforms import SelectField, StringField, SubmitField, validators
4
3
 
5
- from scout.constants import CASE_SEARCH_TERMS
6
-
7
- CASE_SEARCH_KEY = [("", "")] + [
8
- (value["prefix"], value["label"]) for key, value in CASE_SEARCH_TERMS.items()
9
- ]
4
+ from scout.server.blueprints.institutes.forms import CaseFilterForm
10
5
 
11
6
 
12
7
  class NonValidatingSelectField(SelectField):
@@ -16,12 +11,7 @@ class NonValidatingSelectField(SelectField):
16
11
  pass
17
12
 
18
13
 
19
- class DashboardFilterForm(FlaskForm):
20
- """Takes care of cases filtering in cases page"""
14
+ class DashboardFilterForm(CaseFilterForm):
15
+ """Takes care of cases filtering on dashboard page"""
21
16
 
22
- search_type = NonValidatingSelectField(
23
- "Search by", [validators.Optional()], choices=CASE_SEARCH_KEY
24
- )
25
17
  search_institute = NonValidatingSelectField("Institute", choices=[])
26
- search_term = StringField("Search term")
27
- search = SubmitField(label="Search")