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,22 +1,20 @@
1
1
  import logging
2
+ from datetime import datetime
3
+ from typing import Optional
2
4
 
3
- from flask import current_app, flash
5
+ import requests
6
+ from flask import Response, current_app, flash, redirect, session, url_for
7
+ from flask_login import login_user
4
8
 
5
- from scout.server.extensions import ldap_manager
6
-
7
- LOG = logging.getLogger(__name__)
9
+ from scout.server.extensions import ldap_manager, oauth_client, store
8
10
 
11
+ from .models import LoginUser
9
12
 
10
- def ldap_authorized(userid, password):
11
- """Log in a LDAP user
13
+ LOG = logging.getLogger(__name__)
12
14
 
13
- Args:
14
- userid(str): user id provided in ldap login form (usually and email)
15
- password(str): user passord provided in ldap loding form
16
15
 
17
- Returns:
18
- authorized(bool): True or False
19
- """
16
+ def ldap_authorized(userid: str, password: str) -> bool:
17
+ """Log in an LDAP user."""
20
18
  authorized = False
21
19
  try:
22
20
  authorized = ldap_manager.authenticate(
@@ -63,3 +61,105 @@ def users(store):
63
61
  return dict(
64
62
  users=sorted(user_objs, key=lambda user: -user["events"]),
65
63
  )
64
+
65
+
66
+ def user_has_consented(user_consent: Optional[str]) -> bool:
67
+ """Check if user has given consent for activity logging."""
68
+ if not current_app.config.get("USERS_ACTIVITY_LOG_PATH"):
69
+ return True
70
+
71
+ if user_consent is None and "consent_given" not in session:
72
+ flash(
73
+ "Logging user activity is a requirement for using this site and accessing your account. Without consent to activity logging, you will not be able to log in into Scout.",
74
+ "warning",
75
+ )
76
+ return False
77
+ return True
78
+
79
+
80
+ def ldap_login(ldap_user: Optional[str], ldap_password: Optional[str]) -> Optional[str]:
81
+ """Authenticate user via LDAP and return user ID if authorized."""
82
+
83
+ if not ldap_user or not ldap_password:
84
+ return None
85
+
86
+ authorized: bool = ldap_authorized(ldap_user, ldap_password)
87
+
88
+ if authorized:
89
+ return ldap_user
90
+
91
+ flash("User not authorized by LDAP server", "warning")
92
+
93
+
94
+ def google_login() -> Optional[Response]:
95
+ """Authenticate user via Google OIDC and redirect to the redirect URI. The name of this endpoint should be present on the Google login settings."""
96
+ if "email" in session:
97
+ return redirect(url_for("public.login")) # Redirect to the login route with session info
98
+
99
+ redirect_uri: str = url_for("login.authorized", _external=True)
100
+ try:
101
+ return oauth_client.google.authorize_redirect(redirect_uri)
102
+ except Exception:
103
+ flash("An error has occurred while logging in user using Google OAuth", "warning")
104
+ return None
105
+
106
+
107
+ def keycloak_login() -> Optional[Response]:
108
+ """Authenticate user via Keycloak OIDC and redirect to the redirect URI. The name of this endpoint should be present on the Keycloak login settings."""
109
+
110
+ redirect_uri: str = url_for("login.authorized", _external=True)
111
+ try:
112
+ return oauth_client.keycloak.authorize_redirect(redirect_uri)
113
+ except Exception:
114
+ flash("An error has occurred while logging in user using Keycloak", "warning")
115
+ return None
116
+
117
+
118
+ def database_login(user_mail: Optional[str]) -> Optional[str]:
119
+ """Authenticate user against the Scout database and return email if successful."""
120
+ return user_mail
121
+
122
+
123
+ def validate_and_login_user(user_mail: Optional[str], user_id: Optional[str]) -> Response:
124
+ """Validate user in Scout database and log them in."""
125
+ user_obj: Optional[dict] = store.user(email=user_mail, user_id=user_id)
126
+
127
+ if user_obj is None:
128
+ flash("User not found in Scout database", "warning")
129
+ session.pop("email", None)
130
+ return redirect(url_for("public.index"))
131
+
132
+ user_obj["accessed_at"] = datetime.now()
133
+
134
+ if session.get("name"): # Set name & locale if available (Google Auth)
135
+ user_obj["name"] = session.get("name")
136
+ user_obj["locale"] = session.get("locale")
137
+
138
+ store.update_user(user_obj)
139
+ return perform_flask_login(LoginUser(user_obj))
140
+
141
+
142
+ def perform_flask_login(user_dict: "LoginUser") -> Response:
143
+ """Login user using Flask-Login."""
144
+ if login_user(user_dict, remember=True):
145
+ flash(f"Welcome {user_dict.name}", "success")
146
+ return redirect(url_for("cases.index"))
147
+
148
+ flash("Sorry, you could not log in", "warning")
149
+ return redirect(url_for("public.index"))
150
+
151
+
152
+ def logout_oidc_user(session, provider: str):
153
+ """Log out a user from an OIDC login provider-"""
154
+ logout_url = current_app.config[provider].get("logout_url")
155
+ if not logout_url or not session.get("token_response"):
156
+ return
157
+ refresh_token = session["token_response"]["refresh_token"]
158
+ requests.post(
159
+ logout_url,
160
+ data={
161
+ "client_id": current_app.config[provider]["client_id"],
162
+ "client_secret": current_app.config[provider]["client_secret"],
163
+ "refresh_token": refresh_token,
164
+ },
165
+ )
@@ -1,9 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import logging
3
- from datetime import datetime
3
+ from typing import Optional
4
4
 
5
5
  from flask import (
6
6
  Blueprint,
7
+ Response,
7
8
  current_app,
8
9
  flash,
9
10
  redirect,
@@ -45,82 +46,67 @@ def load_user(user_id):
45
46
 
46
47
  @login_bp.route("/login", methods=["GET", "POST"])
47
48
  @public_endpoint
48
- def login():
49
+ def login() -> Response:
49
50
  """Login a user if they have access."""
50
51
 
51
- if current_app.config.get("USERS_ACTIVITY_LOG_PATH"):
52
- if request.form.get("consent_checkbox") is None and "consent_given" not in session:
53
- flash(
54
- "Logging user activity is a requirement for using this site and accessing your account. Without consent to activity logging, you will not be able to log in into Scout.",
55
- "warning",
56
- )
57
- return redirect(url_for("public.index"))
58
- session["consent_given"] = True
52
+ if controllers.user_has_consented(user_consent=request.form.get("consent_checkbox")) is False:
53
+ return redirect(url_for("public.index"))
59
54
 
60
- user_id = None
61
- user_mail = None
55
+ user_id: Optional[str] = None
56
+ user_mail: Optional[str] = None
62
57
 
63
58
  if current_app.config.get("LDAP_HOST", current_app.config.get("LDAP_SERVER")):
64
- ldap_authorized = controllers.ldap_authorized(
65
- request.form.get("ldap_user"), request.form.get("ldap_password")
59
+ user_id = controllers.ldap_login(
60
+ ldap_user=request.form.get("ldap_user"), ldap_password=request.form.get("ldap_password")
66
61
  )
67
- if ldap_authorized is True:
68
- user_id = request.form.get("ldap_user")
69
- else:
70
- flash("User not authorized by LDAP server", "warning")
62
+ if user_id is None:
71
63
  return redirect(url_for("public.index"))
72
64
 
73
65
  elif current_app.config.get("GOOGLE"):
74
66
  if session.get("email"):
75
67
  user_mail = session["email"]
76
- session.pop("email", None)
77
68
  else:
78
- redirect_uri = url_for(".authorized", _external=True)
79
- try:
80
- return oauth_client.google.authorize_redirect(redirect_uri)
81
- except Exception as ex:
82
- flash("An error has occurred while logging in user using Google OAuth")
83
-
84
- elif request.form.get("email"): # log in against Scout database
85
- user_mail = request.form["email"]
86
- LOG.info("Validating user %s email %s against Scout database", user_id, user_mail)
87
-
88
- user_obj = store.user(email=user_mail, user_id=user_id)
89
- if user_obj is None:
90
- flash("User not found in Scout database", "warning")
91
- return redirect(url_for("public.index"))
69
+ # Redirect to Google OAuth if not completed
70
+ return controllers.google_login()
71
+
72
+ elif current_app.config.get("KEYCLOAK"):
73
+ if session.get("email"):
74
+ user_mail = session["email"]
75
+ else:
76
+ return controllers.keycloak_login()
92
77
 
93
- user_obj["accessed_at"] = datetime.now()
94
- if session.get("name"): # These args come from google auth
95
- user_obj["name"] = session.get("name")
96
- user_obj["locale"] = session.get("locale")
97
- store.update_user(user_obj)
78
+ elif request.form.get("email"):
79
+ user_mail = controllers.database_login(user_mail=request.form.get("email"))
98
80
 
99
- user_dict = LoginUser(user_obj)
100
- return perform_login(user_dict)
81
+ return controllers.validate_and_login_user(user_mail=user_mail, user_id=user_id)
101
82
 
102
83
 
103
84
  @login_bp.route("/authorized")
104
85
  @public_endpoint
105
86
  def authorized():
106
- """Google auth callback function"""
107
-
108
- token = oauth_client.google.authorize_access_token()
109
- google_user = oauth_client.google.parse_id_token(token, None)
110
- session["email"] = google_user.get("email").lower()
111
- session["name"] = google_user.get("name")
112
- session["locale"] = google_user.get("locale")
87
+ """OIDC callback function."""
88
+ if current_app.config.get("GOOGLE"):
89
+ client = oauth_client.google
90
+ if current_app.config.get("KEYCLOAK"):
91
+ client = oauth_client.keycloak
92
+ token = client.authorize_access_token()
93
+ user = client.parse_id_token(token, None)
94
+
95
+ session["email"] = user.get("email").lower()
96
+ session["name"] = user.get("name")
97
+ session["locale"] = user.get("locale")
98
+ session["token_response"] = token
113
99
 
114
100
  return redirect(url_for(".login"))
115
101
 
116
102
 
117
103
  @login_bp.route("/logout")
118
104
  def logout():
119
- logout_user()
120
- session.pop("email", None)
121
- session.pop("name", None)
122
- session.pop("locale", None)
123
- session.pop("consent_given", None)
105
+ logout_user() # logs out user from scout
106
+ for provider in ["GOOGLE", "KEYCLOAK"]:
107
+ if current_app.config.get(provider):
108
+ controllers.logout_oidc_user(session, provider)
109
+ session.clear()
124
110
  flash("you logged out", "success")
125
111
  return redirect(url_for("public.index"))
126
112
 
@@ -130,11 +116,3 @@ def users():
130
116
  """Show all users in the system."""
131
117
  data = controllers.users(store)
132
118
  return render_template("login/users.html", **data)
133
-
134
-
135
- def perform_login(user_dict):
136
- if login_user(user_dict, remember=True):
137
- flash("you logged in as: {}".format(user_dict.name), "success")
138
- return redirect(url_for("cases.index"))
139
- flash("sorry, you could not log in", "warning")
140
- return redirect(url_for("public.index"))
@@ -0,0 +1 @@
1
+ from .views import mme_bp
@@ -0,0 +1,18 @@
1
+ from typing import List
2
+
3
+ from scout.server.extensions import store
4
+
5
+
6
+ def institute_mme_cases(institute_id: str) -> List[dict]:
7
+ """Retrieves all cases for a given institute with an active MatchMaker Exchange submission."""
8
+ institute_mme_events = store.institute_events_by_verb(
9
+ category="case", institute_id=institute_id, verb="mme_add"
10
+ )
11
+ unique_case_ids = set([event["case"] for event in institute_mme_events])
12
+ mme_cases = []
13
+ for case_id in unique_case_ids:
14
+ case_obj = store.case(case_id=case_id)
15
+ if not case_obj or not case_obj.get("mme_submission"):
16
+ continue
17
+ mme_cases.append(case_obj)
18
+ return mme_cases
@@ -0,0 +1,153 @@
1
+ {% extends "layout.html" %}
2
+ {% from "overview/institute_sidebar.html" import institute_actionbar %}
3
+ {% from "utils.html" import db_table_external_scripts, db_table_external_stylesheets %}
4
+
5
+ {% block title %}
6
+ {{ super() }} - Institutes
7
+ {% endblock %}
8
+
9
+ {% block css %}
10
+ {{ super() }}
11
+ {{ db_table_external_stylesheets() }}
12
+ {% endblock %}
13
+
14
+ {% block top_nav %}
15
+ {{ super() }}
16
+ <li class="nav-item">
17
+ <a class="nav-link" href="{{ url_for('cases.index') }}">Institutes</a>
18
+ </li>
19
+ <li class="nav-item">
20
+ <a class="nav-link" href="{{ url_for('overview.cases', institute_id=institute._id) }}">{{ institute.display_name }}</a>
21
+ </li>
22
+ <li class="nav-item active d-flex align-items-center">
23
+ <span class="navbar-text">Cases in MatchMaker Exchange</span>
24
+ </li>
25
+ {% endblock %}
26
+
27
+ {% block content_main %}
28
+ <div class="container-float">
29
+ <div class="row mt-3" id="body-row"> <!--sidebar and main container are on the same row-->
30
+ <div class="col">
31
+ {{ mme_submitted() }}
32
+ </div>
33
+ </div>
34
+ </div>
35
+ {% endblock %}
36
+
37
+ {% macro mme_submitted() %}
38
+ <table class="table table-bordered table-striped" id="mmeTable">
39
+ <thead>
40
+ <tr>
41
+ <th>Case Name</th>
42
+ <th>Synopsis</th>
43
+ <th>Status</th>
44
+ <th>Case Assignees</th>
45
+ <th>MME Submission Updated</th>
46
+ <th>MME Patient(s)</th>
47
+ <th>MME Phenotype Features</th>
48
+ <th>MME Genomic Features</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ {% for case in mme_cases %}
53
+ <tr>
54
+ <td>
55
+ <a class="me-2" href="{{ url_for('cases.case', institute_id=case.owner, case_name=case.display_name) }}" target="_blank" rel="noopener">
56
+ {{ case.display_name }}
57
+ </a>
58
+ </td>
59
+ <td class="synopsis-cell" style="max-width: 300px;">
60
+ {% set collapse_id = 'collapse-' + case._id %}
61
+ {% set max_chars = 300 %}
62
+
63
+ {% if case.synopsis|length > max_chars %}
64
+ <div>
65
+ {{ case.synopsis[:max_chars] }}…
66
+ </div>
67
+
68
+ <a
69
+ class="btn btn-link p-0"
70
+ data-bs-toggle="collapse"
71
+ href="#{{ collapse_id }}"
72
+ role="button"
73
+ aria-expanded="false"
74
+ aria-controls="{{ collapse_id }}"
75
+ >More…</a>
76
+
77
+ <div class="collapse mt-1" id="{{ collapse_id }}">
78
+ <div class="card card-body p-2">
79
+ {{ case.synopsis }}
80
+ </div>
81
+ </div>
82
+ {% else %}
83
+ {{ case.synopsis }}
84
+ {% endif %}
85
+ </td>
86
+ <td>{{ case.status }}</td>
87
+ <td>
88
+ {% for assignee in case.assignees %}
89
+ <div>{{ assignee }}</div>
90
+ {% endfor %}
91
+ </td>
92
+ <td>
93
+ {{ case.mme_submission.updated_at.strftime('%Y-%m-%d %H:%M') }}
94
+ </td>
95
+ <td>
96
+ {% for patient in case.mme_submission.patients %}
97
+ <div>
98
+ <strong>{{ patient.label }}</strong> ({{ patient.sex }})<br>
99
+ <small>Contact: {{ patient.contact.name }} - <a href="{{ patient.contact.href }}">{{ patient.contact.href }}</a></small>
100
+ </div>
101
+ {% endfor %}
102
+ </td>
103
+ <td>
104
+ {% for disorder in case.mme_submission.disorders %}
105
+ <div>{{ disorder.label }} ({{ disorder.id }})</div>
106
+ {% endfor %}
107
+ </td>
108
+ <td>
109
+ {% for patient in case.mme_submission.patients %}
110
+ {% for gf in patient.genomicFeatures %}
111
+ <div>
112
+ <strong>{{ gf.gene.id }}</strong><br>
113
+ {% if gf.variant %}
114
+ Chr{{ gf.variant.referenceName }}:
115
+ {{ gf.variant.start|human_longint|safe }}-{{ gf.variant.end|human_longint|safe }}<br>
116
+ {{ gf.variant.referenceBases }} → {{ gf.variant.alternateBases }}<br>
117
+ Assembly: {{ gf.variant.assembly }}<br>
118
+ Zygosity: {{ gf.zygosity }}
119
+ {% endif %}
120
+ </div>
121
+ <hr>
122
+ {% endfor %}
123
+ {% endfor %}
124
+ </td>
125
+ </tr>
126
+ {% endfor %}
127
+ </tbody>
128
+ </table>
129
+ {% endmacro %}
130
+
131
+
132
+ {% block scripts %}
133
+ {{ super() }}
134
+ {{ db_table_external_scripts() }}
135
+ <script>
136
+ $(document).ready(function () {
137
+ $('#mmeTable').DataTable({
138
+ paging: true,
139
+ searching: true,
140
+ ordering: true,
141
+ responsive: true,
142
+ language: {
143
+ search: "Search all fields:",
144
+ lengthMenu: "Show _MENU_ entries",
145
+ info: "Showing _START_ to _END_ of _TOTAL_ cases",
146
+ },
147
+ columnDefs: [
148
+ { orderable: false, targets: [3, 5, 6, 7] } // Disable sorting for multi-line complex columns
149
+ ]
150
+ });
151
+ });
152
+ </script>
153
+ {% endblock %}
@@ -0,0 +1,34 @@
1
+ import logging
2
+
3
+ from flask import (
4
+ Blueprint,
5
+ render_template,
6
+ url_for,
7
+ )
8
+
9
+ from scout.server.extensions import store
10
+ from scout.server.utils import institute_and_case
11
+
12
+ from . import controllers
13
+
14
+ LOG = logging.getLogger(__name__)
15
+
16
+ mme_bp = Blueprint(
17
+ "mme",
18
+ __name__,
19
+ template_folder="templates",
20
+ static_folder="static",
21
+ static_url_path="/mme/static",
22
+ )
23
+
24
+
25
+ @mme_bp.route("/<institute_id>/mme_submissions", methods=["GET"])
26
+ def mme_submissions(institute_id: str):
27
+ """Retrieve all cases for an institute with associated a MME submission."""
28
+
29
+ institute_obj = institute_and_case(store, institute_id)
30
+ data = {
31
+ "institute": institute_obj,
32
+ "mme_cases": controllers.institute_mme_cases(institute_id=institute_id),
33
+ }
34
+ return render_template("mme/mme_submissions.html", **data)
@@ -10,12 +10,25 @@
10
10
  <li class="nav-item">
11
11
  <a class="nav-link" href="{{ url_for('cases.index') }}">Institutes</a>
12
12
  </li>
13
- <li class="nav-item">
14
- <a class="nav-link" href="{{ url_for('panels.panels') }}">Gene Panels</a>
15
- </li>
16
- <li class="nav-item active d-flex align-items-center">
17
- <span class="navbar-text">{{ panel.display_name }} {{ panel.version }}</span>
18
- </li>
13
+ {% if case %}
14
+ <li class="nav-item">
15
+ <a class="nav-link" href="{{ url_for('overview.cases', institute_id=institute._id) }}">
16
+ {{ institute.display_name }}
17
+ </a>
18
+ </li>
19
+ <li class="nav-item d-flex align-items-center">
20
+ <a class="nav-link" href="{{ url_for('cases.case', institute_id=institute._id, case_name=case.display_name) }}">
21
+ {{ case.display_name }}
22
+ </a>
23
+ </li>
24
+ {% else %}
25
+ <li class="nav-item">
26
+ <a class="nav-link" href="{{ url_for('panels.panels') }}">Gene Panels</a>
27
+ </li>
28
+ {% endif %}
29
+ <li class="nav-item active d-flex align-items-center">
30
+ <span class="navbar-text">{{ panel.display_name }} {{ panel.version }}</span>
31
+ </li>
19
32
  {% endblock %}
20
33
 
21
34
  {% block content_main %}
@@ -56,7 +56,14 @@
56
56
  $('#phenotypes_table').DataTable( {
57
57
  paging: true,
58
58
  pageLength: 50,
59
- dom: 'fBrtip',
59
+ layout: {
60
+ topStart: 'buttons',
61
+ topEnd: {
62
+ search: {
63
+ placeholder: 'Filter...'
64
+ }
65
+ }
66
+ },
60
67
  buttons: [
61
68
  {
62
69
  extend: 'excelHtml5',
@@ -21,7 +21,11 @@
21
21
  </p>
22
22
  {% else %}
23
23
  <form class="row" method="POST" action="{{ url_for('login.login') }}">
24
- {% if config.GOOGLE %}
24
+ {% if config.KEYCLOAK %}
25
+ <button name="googleLogin" class="btn btn-primary btn-lg text-white" href="{{ url_for('login.login') }}" type="submit">
26
+ Login with Keycloak
27
+ </button>
28
+ {% elif config.GOOGLE %}
25
29
  <button name="googleLogin" class="btn btn-primary btn-lg text-white" href="{{ url_for('login.login') }}" type="submit">
26
30
  Login with Google
27
31
  </button>
@@ -367,11 +367,13 @@ def variant(
367
367
  if variant_id in case_clinvars:
368
368
  variant_obj["clinvar_clinsig"] = case_clinvars.get(variant_id)["clinsig"]
369
369
 
370
- overlapping_vars = []
370
+ overlapping_variants, overlapping_outliers = [], []
371
371
  if get_overlapping:
372
- for var in store.overlapping(variant_obj):
372
+ overlapping_variants, overlapping_outliers = map(list, store.hgnc_overlapping(variant_obj))
373
+
374
+ for var in overlapping_variants:
373
375
  var.update(predictions(var.get("genes", [])))
374
- overlapping_vars.append(var)
376
+
375
377
  variant_obj["end_chrom"] = variant_obj.get("end_chrom", variant_obj["chromosome"])
376
378
 
377
379
  dismiss_options = DISMISS_VARIANT_OPTIONS
@@ -395,7 +397,8 @@ def variant(
395
397
  "causatives": other_causatives,
396
398
  "managed_variant": managed_variant,
397
399
  "events": events,
398
- "overlapping_vars": overlapping_vars,
400
+ "overlapping_vars": overlapping_variants,
401
+ "overlapping_outliers": overlapping_outliers,
399
402
  "manual_rank_options": MANUAL_RANK_OPTIONS,
400
403
  "cancer_tier_options": CANCER_TIER_OPTIONS,
401
404
  "dismiss_variant_options": dismiss_options,
@@ -406,9 +409,6 @@ def variant(
406
409
  "inherit_palette": INHERITANCE_PALETTE,
407
410
  "igv_tracks": get_igv_tracks("38" if variant_obj["is_mitochondrial"] else genome_build),
408
411
  "has_rna_tracks": case_has_rna_tracks(case_obj),
409
- "gene_has_full_coverage": get_gene_has_full_coverage(
410
- institute_obj, case_obj, variant_obj, genome_build
411
- ),
412
412
  "gens_info": gens.connection_settings(genome_build),
413
413
  "evaluations": evaluations,
414
414
  "ccv_evaluations": ccv_evaluations,
@@ -416,16 +416,19 @@ def variant(
416
416
  }
417
417
 
418
418
 
419
- def get_gene_has_full_coverage(
420
- institute_obj, case_obj, variant_obj, genome_build
421
- ) -> Dict[int, bool]:
419
+ def get_gene_has_full_coverage(institute_obj, case_obj, variant_obj) -> Dict[int, bool]:
422
420
  """
423
421
  Query chanjo2, if configured and d4 files are available for this case,
424
422
  for coverage completeness on the genes touching this variant.
425
423
  """
424
+ case_has_chanjo2_coverage(case_obj)
426
425
  if not case_obj.get("chanjo2_coverage"):
427
426
  return {}
428
427
 
428
+ genome_build = str(case_obj.get("genome_build", "37"))
429
+ if genome_build not in ["37", "38"]:
430
+ genome_build = "37"
431
+
429
432
  gene_has_full_coverage: dict = {
430
433
  hgnc_id: chanjo2.get_gene_complete_coverage(
431
434
  hgnc_id=hgnc_id,
@@ -653,6 +656,12 @@ def variant_acmg(store: MongoAdapter, institute_id: str, case_name: str, variant
653
656
  store, variant_obj, institute_id, case_name
654
657
  )
655
658
 
659
+ genome_build = str(case_obj.get("genome_build", "37"))
660
+ if genome_build not in ["37", "38"]:
661
+ genome_build = "37"
662
+
663
+ add_gene_info(store, variant_obj, genome_build=genome_build)
664
+
656
665
  return dict(
657
666
  institute=institute_obj,
658
667
  case=case_obj,
@@ -79,7 +79,11 @@
79
79
  <div class="row">
80
80
  <select {{ 'disabled' if evaluation and edit is false}} id="modifier-{{ criterion_code }}" name="modifier-{{ criterion_code }}" class="form-control form-select">
81
81
  <option value="" {% if not modifier %}selected{% endif %}>Strength modifier...</option>
82
- {% for level in "Strong", "Moderate", "Supporting" %}
82
+ {% set sa_level = "Stand-alone" %}
83
+ {% if category == "pathogenicity" %}
84
+ {% set sa_level = "Very Strong" %}
85
+ {% endif %}
86
+ {% for level in sa_level, "Strong", "Moderate", "Supporting" %}
83
87
  {% if(level != evidence) %}
84
88
  <option id="{{ criterion_code }}-{{ level }}" value="{{ level }}" {% if modifier == level %}selected{% endif %}>{{ level }}</option>
85
89
  {% endif %}
@@ -101,6 +105,15 @@
101
105
  </div>
102
106
  {% endfor %}
103
107
  </div>
108
+ <!-- external links -->
109
+ <div class="card panel-default mt-3">
110
+ <div class="card-body">
111
+ Search ClinGen Criteria Specifications (CSPEC):
112
+ {% for gene in variant.genes %}
113
+ <a href="{{ gene.cspec_link }}" class="btn btn-secondary text-white" rel="noopener" referrerpolicy="no-referrer" target="_blank">{{ gene.common.hgnc_symbol if gene.common else gene.hgnc_id }}</a>
114
+ {% endfor %}
115
+ </div>
116
+ </div>
104
117
  <!-- classification preview in the footer-->
105
118
  <div class="mt-3 fixed-bottom bg-light border">
106
119
  <div class="row">
@@ -148,7 +161,7 @@
148
161
 
149
162
  function update_classification() {
150
163
  var criteria = $(':checked').map(function(idx, elem) {
151
- const modifiers = ["Strong", "Moderate", "Supporting"];
164
+ const modifiers = ["Stand-alone", "Very Strong", "Strong", "Moderate", "Supporting"];
152
165
  for (possible_modifier of modifiers) {
153
166
  var modifier_option;
154
167
  if(elem.value !== null && elem.value !== '') {