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.
- scout/adapter/mongo/case.py +30 -15
- scout/adapter/mongo/clinvar.py +23 -31
- scout/adapter/mongo/event.py +14 -4
- scout/adapter/mongo/institute.py +42 -55
- scout/adapter/mongo/omics_variant.py +14 -1
- scout/adapter/mongo/query.py +24 -1
- scout/adapter/mongo/variant.py +44 -22
- scout/adapter/mongo/variant_loader.py +169 -186
- scout/build/individual.py +5 -1
- scout/build/variant/variant.py +8 -0
- scout/commands/download/ensembl.py +18 -3
- scout/commands/load/research.py +2 -3
- scout/commands/update/individual.py +3 -0
- scout/commands/update/panelapp.py +15 -2
- scout/constants/__init__.py +6 -2
- scout/constants/clnsig.py +2 -0
- scout/constants/file_types.py +12 -0
- scout/constants/igv_tracks.py +9 -6
- scout/constants/indexes.py +5 -4
- scout/constants/panels.py +3 -0
- scout/constants/query_terms.py +1 -0
- scout/constants/variant_tags.py +6 -6
- scout/demo/643594.config.yaml +1 -0
- scout/load/panelapp.py +11 -5
- scout/models/case/case.py +1 -0
- scout/models/case/case_loading_models.py +7 -1
- scout/parse/ensembl.py +8 -3
- scout/parse/variant/clnsig.py +38 -0
- scout/parse/variant/genotype.py +4 -10
- scout/parse/variant/models.py +5 -11
- scout/parse/variant/rank_score.py +5 -13
- scout/parse/variant/variant.py +90 -111
- scout/server/app.py +39 -22
- scout/server/blueprints/alignviewers/controllers.py +29 -10
- scout/server/blueprints/alignviewers/templates/alignviewers/igv_viewer.html +51 -11
- scout/server/blueprints/cases/controllers.py +9 -3
- scout/server/blueprints/cases/templates/cases/case_report.html +25 -13
- scout/server/blueprints/cases/templates/cases/chanjo2_form.html +1 -1
- scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +1 -1
- scout/server/blueprints/cases/templates/cases/gene_panel.html +1 -1
- scout/server/blueprints/cases/templates/cases/utils.html +25 -6
- scout/server/blueprints/clinvar/controllers.py +34 -15
- scout/server/blueprints/clinvar/templates/clinvar/clinvar_submissions.html +34 -12
- scout/server/blueprints/clinvar/templates/clinvar/multistep_add_variant.html +14 -5
- scout/server/blueprints/clinvar/views.py +14 -2
- scout/server/blueprints/diagnoses/static/diagnoses.js +8 -1
- scout/server/blueprints/institutes/controllers.py +10 -2
- scout/server/blueprints/institutes/static/variants_list_scripts.js +9 -1
- scout/server/blueprints/institutes/templates/overview/institute_sidebar.html +9 -1
- scout/server/blueprints/login/controllers.py +112 -12
- scout/server/blueprints/login/views.py +38 -60
- scout/server/blueprints/mme/__init__.py +1 -0
- scout/server/blueprints/mme/controllers.py +18 -0
- scout/server/blueprints/mme/templates/mme/mme_submissions.html +153 -0
- scout/server/blueprints/mme/views.py +34 -0
- scout/server/blueprints/panels/templates/panels/panel.html +19 -6
- scout/server/blueprints/phenotypes/templates/phenotypes/hpo_terms.html +8 -1
- scout/server/blueprints/public/templates/public/index.html +5 -1
- scout/server/blueprints/variant/controllers.py +19 -10
- scout/server/blueprints/variant/templates/variant/acmg.html +15 -2
- scout/server/blueprints/variant/templates/variant/cancer-variant.html +1 -1
- scout/server/blueprints/variant/templates/variant/components.html +38 -16
- scout/server/blueprints/variant/templates/variant/sv-variant.html +2 -2
- scout/server/blueprints/variant/templates/variant/utils.html +23 -11
- scout/server/blueprints/variant/templates/variant/variant.html +42 -1
- scout/server/blueprints/variant/views.py +12 -0
- scout/server/blueprints/variants/controllers.py +20 -3
- scout/server/blueprints/variants/forms.py +8 -3
- scout/server/blueprints/variants/templates/variants/components.html +34 -0
- scout/server/blueprints/variants/templates/variants/indicators.html +11 -13
- scout/server/blueprints/variants/templates/variants/mei-variants.html +8 -6
- scout/server/blueprints/variants/templates/variants/sv-variants.html +9 -7
- scout/server/blueprints/variants/templates/variants/utils.html +35 -34
- scout/server/blueprints/variants/templates/variants/variants.html +4 -25
- scout/server/config.py +8 -0
- scout/server/extensions/bionano_extension.py +0 -1
- scout/server/extensions/chanjo2_extension.py +54 -13
- scout/server/links.py +15 -0
- scout/server/static/bs_styles.css +34 -6
- scout/server/templates/utils.html +9 -10
- scout/server/utils.py +40 -5
- scout/utils/acmg.py +25 -26
- scout/utils/ensembl_biomart_clients.py +2 -1
- scout/utils/ensembl_rest_clients.py +25 -32
- scout/utils/hgvs.py +1 -1
- scout/utils/scout_requests.py +1 -3
- {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/METADATA +10 -14
- {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/RECORD +91 -87
- {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/WHEEL +0 -0
- {scout_browser-4.98.0.dist-info → scout_browser-4.100.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
-
|
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
|
-
|
18
|
-
|
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
|
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
|
52
|
-
|
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
|
-
|
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
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
94
|
-
|
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
|
-
|
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
|
-
"""
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
session.
|
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
|
-
|
14
|
-
<
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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 %}
|
@@ -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.
|
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
|
-
|
370
|
+
overlapping_variants, overlapping_outliers = [], []
|
371
371
|
if get_overlapping:
|
372
|
-
|
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
|
-
|
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":
|
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
|
-
{%
|
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 !== '') {
|