scout-browser 4.93.1__py3-none-any.whl → 4.95.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 (53) hide show
  1. scout/adapter/mongo/base.py +0 -0
  2. scout/adapter/mongo/hgnc.py +5 -1
  3. scout/adapter/mongo/managed_variant.py +4 -2
  4. scout/adapter/mongo/query.py +91 -54
  5. scout/adapter/mongo/variant.py +13 -8
  6. scout/build/panel.py +1 -1
  7. scout/commands/export/export_command.py +0 -0
  8. scout/commands/load/base.py +0 -0
  9. scout/commands/load/user.py +0 -0
  10. scout/commands/update/disease.py +0 -0
  11. scout/commands/update/genes.py +0 -0
  12. scout/commands/wipe_database.py +0 -0
  13. scout/constants/gene_tags.py +22 -12
  14. scout/demo/643594.research.mei.vcf.gz +0 -0
  15. scout/demo/643594.research.mei.vcf.gz.tbi +0 -0
  16. scout/load/panelapp.py +8 -12
  17. scout/parse/omim.py +5 -6
  18. scout/parse/panelapp.py +16 -42
  19. scout/parse/variant/compound.py +20 -21
  20. scout/parse/variant/gene.py +0 -0
  21. scout/parse/variant/genotype.py +0 -0
  22. scout/resources/custom_igv_tracks/mane.bb +0 -0
  23. scout/server/blueprints/cases/controllers.py +48 -0
  24. scout/server/blueprints/cases/templates/cases/case_report.html +17 -2
  25. scout/server/blueprints/cases/views.py +5 -5
  26. scout/server/blueprints/clinvar/controllers.py +4 -5
  27. scout/server/blueprints/institutes/controllers.py +129 -67
  28. scout/server/blueprints/institutes/forms.py +5 -2
  29. scout/server/blueprints/institutes/templates/overview/cases.html +6 -0
  30. scout/server/blueprints/institutes/templates/overview/utils.html +6 -5
  31. scout/server/blueprints/managed_variants/forms.py +17 -2
  32. scout/server/blueprints/managed_variants/templates/managed_variants/managed_variants.html +2 -2
  33. scout/server/blueprints/variant/templates/variant/components.html +27 -4
  34. scout/server/blueprints/variant/templates/variant/sv-variant.html +2 -2
  35. scout/server/blueprints/variant/templates/variant/tx_overview.html +3 -3
  36. scout/server/blueprints/variant/views.py +1 -2
  37. scout/server/blueprints/variants/forms.py +33 -5
  38. scout/server/blueprints/variants/templates/variants/cancer-sv-variants.html +4 -18
  39. scout/server/blueprints/variants/templates/variants/cancer-variants.html +2 -12
  40. scout/server/blueprints/variants/templates/variants/components.html +15 -1
  41. scout/server/blueprints/variants/templates/variants/sv-variants.html +2 -2
  42. scout/server/links.py +1 -1
  43. scout/utils/acmg.py +0 -1
  44. scout/utils/ccv.py +1 -9
  45. scout/utils/link.py +4 -3
  46. scout/utils/md5.py +0 -0
  47. {scout_browser-4.93.1.dist-info → scout_browser-4.95.0.dist-info}/METADATA +66 -45
  48. {scout_browser-4.93.1.dist-info → scout_browser-4.95.0.dist-info}/RECORD +41 -42
  49. {scout_browser-4.93.1.dist-info → scout_browser-4.95.0.dist-info}/WHEEL +1 -2
  50. scout/__version__.py +0 -1
  51. scout_browser-4.93.1.dist-info/top_level.txt +0 -1
  52. {scout_browser-4.93.1.dist-info → scout_browser-4.95.0.dist-info}/entry_points.txt +0 -0
  53. {scout_browser-4.93.1.dist-info → scout_browser-4.95.0.dist-info/licenses}/LICENSE +0 -0
@@ -668,6 +668,7 @@
668
668
  <th>Inheritance models</th>
669
669
  {% endif %}
670
670
  <th>ACMG classification</th>
671
+ <th>Bayesian classification</th>
671
672
  {% if cancer %}
672
673
  <th>ClinGen-CGC-VICC classification</th>
673
674
  {% endif %}
@@ -714,18 +715,32 @@
714
715
  {% endif %}
715
716
  <td>
716
717
  {% if variant.acmg_classification %}
717
- <span class="badge rounded-pill bg-{{variant.acmg_classification['color'] if variant.acmg_classification['color'] else 'secondary'}}" title="{{variant.acmg_classification['code']}}">{{variant.acmg_classification['code'] }}</span>
718
+ <span class="badge rounded-pill bg-{{variant.acmg_classification['color'] if variant.acmg_classification['color'] else 'secondary'}}" title="{{variant.acmg_classification['label']}}">{{variant.acmg_classification['label'] }}</span>
719
+ {% else %}
720
+ -
721
+ {% endif %}
722
+ <td>
723
+ {% if variant.bayesian_acmg %}
724
+ <span class="badge rounded-pill bg-{{variant.bayesian_acmg.temperature_class}}">Score {{variant.bayesian_acmg.points|string}} <span class='fa {{variant.bayesian_acmg.temperature_icon}}'></span> {{variant.bayesian_acmg.temperature}} ({{variant.bayesian_acmg.point_classification}})</span>
718
725
  {% else %}
719
726
  -
720
727
  {% endif %}
721
728
  </td>
729
+ </td>
722
730
  {% if cancer %}
723
731
  <td>
724
732
  {% if variant.ccv_classification %}
725
- <span class="badge rounded-pill bg-{{variant.ccv_classification['color'] if variant.ccv_classification['color'] else 'secondary'}}" title="{{variant.ccv_classification['code']}}">{{variant.ccv_classification['code'] }}</span>
733
+ <span class="badge rounded-pill bg-{{variant.ccv_classification['color'] if variant.ccv_classification['color'] else 'secondary'}}" title="{{variant.ccv_classification['label']}}">{{variant.ccv_classification['label'] }}</span>
726
734
  {% else %}
727
735
  -
728
736
  {% endif %}
737
+ {% if variant.bayesian_ccv %}
738
+ <span class="badge rounded-pill bg-{{variant.bayesian_ccv.temperature_class}}">Score {{variant.bayesian_ccv.points|string}} <span class='fa {{variant.bayesian_ccv.temperature_icon}}'></span>
739
+ {% if variant.bayesian_ccv.point_classification == "VUS" %}
740
+ {{variant.bayesian_ccv.temperature}}
741
+ {% endif %}
742
+ </span>
743
+ {% endif %}
729
744
  </td>
730
745
  {% endif %}
731
746
  </tr>
@@ -7,6 +7,7 @@ import shutil
7
7
  from ast import literal_eval
8
8
  from io import BytesIO
9
9
  from operator import itemgetter
10
+ from tempfile import NamedTemporaryFile, mkdtemp
10
11
  from typing import Generator, Optional, Union
11
12
 
12
13
  from cairosvg import svg2png
@@ -286,7 +287,9 @@ def pdf_case_report(institute_id, case_name):
286
287
  # Workaround to be able to print the case pedigree to pdf
287
288
  if case_obj.get("madeline_info") and case_obj.get("madeline_info") != "":
288
289
  try:
289
- write_to = os.path.join(cases_bp.static_folder, "madeline.png")
290
+ write_to = NamedTemporaryFile(
291
+ mode="a+", prefix=case_obj.get("_id"), suffix="madeline.png"
292
+ )
290
293
  svg2png(
291
294
  bytestring=case_obj["madeline_info"],
292
295
  write_to=write_to,
@@ -322,11 +325,8 @@ def mt_report(institute_id, case_name):
322
325
  institute_obj, case_obj = institute_and_case(store, institute_id, case_name)
323
326
 
324
327
  # create a temp folder to write excel files into
325
- temp_excel_dir = os.path.join(
326
- cases_bp.static_folder, "_".join([case_obj["display_name"], "mt_reports"])
327
- )
328
- os.makedirs(temp_excel_dir, exist_ok=True)
329
328
 
329
+ temp_excel_dir = mkdtemp(suffix="_".join([case_obj["display_name"], "mt_reports"]))
330
330
  if controllers.mt_excel_files(store, case_obj, temp_excel_dir):
331
331
  data = zip_dir_to_obj(temp_excel_dir)
332
332
 
@@ -399,11 +399,10 @@ def json_api_submission(submission_id):
399
399
  afile.flush()
400
400
  afile.seek(0)
401
401
 
402
- with NamedTemporaryFile(
403
- mode="a+", prefix="Variant", suffix=".csv"
404
- ) as variant_file, NamedTemporaryFile(
405
- mode="a+", prefix="CaseData", suffix=".csv"
406
- ) as casedata_file:
402
+ with (
403
+ NamedTemporaryFile(mode="a+", prefix="Variant", suffix=".csv") as variant_file,
404
+ NamedTemporaryFile(mode="a+", prefix="CaseData", suffix=".csv") as casedata_file,
405
+ ):
407
406
  # Write temp Variant CSV file
408
407
  _, variants_header, variants_lines = clinvar_submission_file(
409
408
  submission_id, "variant_data", "SUB000"
@@ -3,7 +3,7 @@ import datetime
3
3
  import logging
4
4
  from typing import Dict, List, Optional, Tuple
5
5
 
6
- from flask import Response, current_app, flash, url_for
6
+ from flask import Response, current_app, flash, request, url_for
7
7
  from flask_login import current_user
8
8
  from pymongo import ASCENDING, DESCENDING
9
9
  from pymongo.cursor import Cursor
@@ -11,10 +11,13 @@ from werkzeug.datastructures import Headers, MultiDict
11
11
 
12
12
  from scout.adapter.mongo.base import MongoAdapter
13
13
  from scout.constants import (
14
+ CANCER_PHENOTYPE_MAP,
14
15
  CASE_STATUSES,
15
16
  DATE_DAY_FORMATTER,
16
17
  ID_PROJECTION,
17
18
  PHENOTYPE_GROUPS,
19
+ PHENOTYPE_MAP,
20
+ SEX_MAP,
18
21
  VARIANTS_TARGET_FROM_CATEGORY,
19
22
  )
20
23
  from scout.server.blueprints.variant.utils import predictions, update_representative_gene
@@ -422,97 +425,129 @@ def _sort_cases(data, request, all_cases):
422
425
  return all_cases
423
426
 
424
427
 
425
- def cases(store, request, institute_id):
426
- """Preprocess case objects.
428
+ def export_case_samples(institute_id, filtered_cases) -> Response:
429
+ """Export to CSV file a list of samples from selected cases."""
430
+ EXPORT_HEADER = [
431
+ "Sample ID",
432
+ "Sample Name",
433
+ "Analysis",
434
+ "Affected status",
435
+ "Sex",
436
+ "Sex confirmed",
437
+ "Parenthood confirmed",
438
+ "Predicted ancestry",
439
+ "Tissue",
440
+ "Case Name",
441
+ "Case ID",
442
+ "Analysis date",
443
+ "Case Status",
444
+ "Case phenotypes",
445
+ "Research",
446
+ "Track",
447
+ "Default panels",
448
+ "Genome build",
449
+ "SNV/SV rank models",
450
+ ]
451
+ export_lines = []
452
+ export_lines.append("\t".join(EXPORT_HEADER)) # Use tab-separated values
453
+ for case in filtered_cases:
454
+ for individual in case.get("individuals", []):
455
+ export_line = [
456
+ individual["individual_id"],
457
+ individual["display_name"],
458
+ individual.get("analysis_type").upper(),
459
+ (
460
+ CANCER_PHENOTYPE_MAP[individual.get("phenotype", 0)]
461
+ if case.get("track") == "cancer"
462
+ else PHENOTYPE_MAP[individual.get("phenotype", 0)]
463
+ ),
464
+ SEX_MAP[individual.get("sex", 0)],
465
+ individual.get("confirmed_sex") or "-",
466
+ individual.get("confirmed_parent") or "-",
467
+ individual.get("predicted_ancestry") or "-",
468
+ individual.get("tissue_type") or "-",
469
+ case["display_name"],
470
+ case["_id"],
471
+ case["analysis_date"].strftime("%Y-%m-%d %H:%M:%S"),
472
+ case["status"],
473
+ ", ".join(hpo["phenotype_id"] for hpo in case.get("phenotype_terms", [])),
474
+ case.get("is_research"),
475
+ case.get("track"),
476
+ ", ".join(
477
+ panel["panel_name"] for panel in case.get("panels") if panel.get("is_default")
478
+ ),
479
+ case.get("genome_build"),
480
+ f"{case.get('rank_model_version', '-')}/{case.get('sv_rank_model_version', '-')}",
481
+ ]
482
+ export_lines.append("\t".join(str(item) for item in export_line))
427
483
 
428
- Add all the necessary information to display the 'cases' view
484
+ file_content = "\n".join(export_lines)
429
485
 
430
- Args:
431
- store(adapter.MongoAdapter)
432
- request(flask.request) request sent by browser to the api_institutes endpoint
433
- institute_id(str): An _id of an institute
486
+ return Response(
487
+ file_content,
488
+ mimetype="text/plain",
489
+ headers={
490
+ "Content-Disposition": f"attachment;filename={institute_id}_cases_{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}.txt"
491
+ },
492
+ )
434
493
 
435
- Returns:
436
- data(dict): includes the cases, how many there are and the limit.
437
- """
494
+
495
+ def cases(store: MongoAdapter, request: request, institute_id: str) -> dict:
496
+ """Preprocess case objects for the 'cases' view."""
438
497
  data = {}
498
+
499
+ # Initialize data (institute info, filters, and case counts)
439
500
  institute_obj = institute_and_case(store, institute_id)
440
501
  data["institute"] = institute_obj
441
-
442
- name_query = request.form
443
- limit = int(request.form.get("search_limit")) if request.form.get("search_limit") else 100
444
-
445
502
  data["form"] = CaseFilterForm(request.form)
503
+ data["status_ncases"] = store.nr_cases_by_status(institute_id=institute_id)
504
+ data["nr_cases"] = sum(data["status_ncases"].values())
505
+
506
+ # Fetch Sanger unevaluated and validated cases
507
+ sanger_ordered_not_validated = get_sanger_unevaluated(store, institute_id, current_user.email)
508
+ data["sanger_unevaluated"], data["sanger_validated_by_others"] = sanger_ordered_not_validated
446
509
 
510
+ # Projection for fetching cases
447
511
  ALL_CASES_PROJECTION = {
448
512
  "analysis_date": 1,
449
513
  "assignees": 1,
450
514
  "beacon": 1,
451
515
  "case_id": 1,
452
516
  "display_name": 1,
517
+ "genome_build": 1,
453
518
  "individuals": 1,
454
519
  "is_rerun": 1,
455
520
  "is_research": 1,
456
521
  "mme_submission": 1,
457
522
  "owner": 1,
458
523
  "panels": 1,
524
+ "phenotype_terms": 1,
525
+ "rank_model_version": 1,
459
526
  "status": 1,
527
+ "sv_rank_model_version": 1,
460
528
  "track": 1,
461
529
  "vcf_files": 1,
462
530
  }
463
531
 
464
- data["status_ncases"] = store.nr_cases_by_status(institute_id=institute_id)
465
- data["nr_cases"] = sum(data["status_ncases"].values())
466
-
467
- sanger_ordered_not_validated: Tuple[Dict[str, list]] = get_sanger_unevaluated(
468
- store, institute_id, current_user.email
469
- )
470
- data["sanger_unevaluated"]: Dict[str, list] = sanger_ordered_not_validated[0]
471
- data["sanger_validated_by_others"]: Dict[str, list] = sanger_ordered_not_validated[1]
472
-
473
- # local function to add info to case obj
474
- def populate_case_obj(case_obj):
475
- analysis_types = set(ind["analysis_type"] for ind in case_obj["individuals"])
476
- if len(analysis_types) > 1:
477
- LOG.debug("Set analysis types to {'mixed'}")
478
- analysis_types = set(["mixed"])
479
-
480
- case_obj["analysis_types"] = list(analysis_types)
481
- case_obj["assignees"] = [
482
- store.user(user_email) for user_email in case_obj.get("assignees", [])
483
- ]
484
- # Check if case was re-runned
485
- last_analysis_date = case_obj.get("analysis_date", datetime.datetime.now())
486
- all_analyses_dates = set()
487
- for analysis in case_obj.get("analyses", [{"date": last_analysis_date}]):
488
- all_analyses_dates.add(analysis.get("date", last_analysis_date))
489
-
490
- case_obj["is_rerun"] = len(all_analyses_dates) > 1 or last_analysis_date > max(
491
- all_analyses_dates
492
- )
493
-
494
- case_obj["clinvar_variants"] = store.case_to_clinVars(case_obj["_id"])
495
- case_obj["display_track"] = TRACKS[case_obj.get("track", "rare")]
496
- return case_obj
497
-
532
+ # Group cases by status
498
533
  case_groups = {status: [] for status in CASE_STATUSES}
534
+ nr_cases_showall_statuses = 0
535
+ status_show_all_cases = institute_obj.get("show_all_cases_status") or ["prioritized"]
499
536
 
500
- nr_cases_showall_statuses = (
501
- 0 # Nr of cases for the case statuses where all cases should be shown
502
- )
503
- # In institute settings, retrieve all case status categories for which all cases should be displayed
504
- status_show_all_cases: List[str] = institute_obj.get("show_all_cases_status") or ["prioritized"]
537
+ # Process cases for statuses that require all cases to be shown
505
538
  for status in status_show_all_cases:
506
539
  cases_in_status = store.cases_by_status(
507
540
  institute_id=institute_id, status=status, projection=ALL_CASES_PROJECTION
508
541
  )
509
542
  cases_in_status = _sort_cases(data, request, cases_in_status)
510
543
  for case_obj in cases_in_status:
511
- populate_case_obj(case_obj)
512
- nr_cases_showall_statuses += 1
544
+ populate_case_obj(case_obj, store)
513
545
  case_groups[status].append(case_obj)
546
+ nr_cases_showall_statuses += 1
514
547
 
515
- # Retrieve cases for the remaining status categories
548
+ # Fetch additional cases based on filters
549
+ name_query = request.form
550
+ limit = int(request.form.get("search_limit")) if request.form.get("search_limit") else 100
516
551
  all_cases = store.cases(
517
552
  collaborator=institute_id,
518
553
  name_query=name_query,
@@ -525,17 +560,20 @@ def cases(store, request, institute_id):
525
560
  )
526
561
  all_cases = _sort_cases(data, request, all_cases)
527
562
 
563
+ if request.form.get("export"):
564
+ return export_case_samples(institute_id, all_cases)
565
+
566
+ # Process additional cases for the remaining statuses
528
567
  nr_cases = 0
529
568
  for case_obj in all_cases:
530
- case_status = case_obj["status"]
531
- if case_status in status_show_all_cases:
532
- continue
533
- if nr_cases == limit:
534
- break
535
- populate_case_obj(case_obj)
536
- case_groups[case_status].append(case_obj)
537
- nr_cases += 1
538
-
569
+ if case_obj["status"] not in status_show_all_cases:
570
+ if nr_cases == limit:
571
+ break
572
+ populate_case_obj(case_obj, store)
573
+ case_groups[case_obj["status"]].append(case_obj)
574
+ nr_cases += 1
575
+
576
+ # Compile the final data
539
577
  data["cases"] = [(status, case_groups[status]) for status in CASE_STATUSES]
540
578
  data["found_cases"] = nr_cases + nr_cases_showall_statuses
541
579
  data["limit"] = limit
@@ -543,8 +581,32 @@ def cases(store, request, institute_id):
543
581
  return data
544
582
 
545
583
 
584
+ def populate_case_obj(case_obj: dict, store: MongoAdapter):
585
+ """Helper function to populate additional case information."""
586
+ analysis_types = set(ind["analysis_type"] for ind in case_obj["individuals"])
587
+ if len(analysis_types) > 1:
588
+ analysis_types = set(["mixed"])
589
+ case_obj["analysis_types"] = list(analysis_types)
590
+
591
+ case_obj["assignees"] = [store.user(user_email) for user_email in case_obj.get("assignees", [])]
592
+
593
+ last_analysis_date = case_obj.get("analysis_date", datetime.datetime.now())
594
+ all_analyses_dates = {
595
+ analysis.get("date", last_analysis_date)
596
+ for analysis in case_obj.get("analyses", [{"date": last_analysis_date}])
597
+ }
598
+ case_obj["is_rerun"] = len(all_analyses_dates) > 1 or last_analysis_date > max(
599
+ all_analyses_dates
600
+ )
601
+
602
+ case_obj["clinvar_variants"] = store.case_to_clinVars(case_obj["_id"])
603
+ case_obj["display_track"] = TRACKS.get(case_obj.get("track", "rare"))
604
+
605
+
546
606
  def _get_unevaluated_variants_for_case(
547
- case_obj: dict, var_ids_list: List[str], sanger_validated_by_user_by_case: Dict[str, List[str]]
607
+ case_obj: dict,
608
+ var_ids_list: List[str],
609
+ sanger_validated_by_user_by_case: Dict[str, List[str]],
548
610
  ) -> Tuple[Dict[str, list]]:
549
611
  """Returns the variants with Sanger ordered by a user that need validation or are validated by another user."""
550
612
 
@@ -73,7 +73,8 @@ class InstituteForm(FlaskForm):
73
73
  pheno_abbrev = StringField("Abbreviation", validators=[validators.Optional()])
74
74
 
75
75
  gene_panels = NonValidatingSelectMultipleField(
76
- "Gene panels available for variants filtering", validators=[validators.Optional()]
76
+ "Gene panels available for variants filtering",
77
+ validators=[validators.Optional()],
77
78
  )
78
79
 
79
80
  gene_panels_matching = NonValidatingSelectMultipleField(
@@ -144,7 +145,8 @@ class GeneVariantFiltersForm(FlaskForm):
144
145
  variant_type = SelectMultipleField(choices=[("clinical", "clinical"), ("research", "research")])
145
146
  category = SelectMultipleField(choices=CATEGORY_CHOICES)
146
147
  hgnc_symbols = TagListField(
147
- "HGNC Symbols (comma-separated, case sensitive)", validators=[validators.InputRequired()]
148
+ "HGNC Symbols (comma-separated, case sensitive)",
149
+ validators=[validators.InputRequired()],
148
150
  )
149
151
  rank_score = IntegerField(default=15)
150
152
  phenotype_terms = TagListField("HPO terms (comma-separated)")
@@ -181,3 +183,4 @@ class CaseFilterForm(FlaskForm):
181
183
  has_rna = BooleanField("Has RNA-seq data")
182
184
  validation_ordered = BooleanField("Validation pending")
183
185
  search = SubmitField(label="Search")
186
+ export = SubmitField(label="Filter and export")
@@ -249,6 +249,12 @@
249
249
  advSearchBlock.hide();
250
250
  }
251
251
 
252
+ function StopSpinner() {
253
+ // Avoid page spinner being stuck on file download
254
+ $(window).unbind('beforeunload');
255
+ return true;
256
+ }
257
+
252
258
  advBlockCheckbox.on('click', function() {
253
259
  if($(this).is(':checked')) {
254
260
  advSearchBlock.show();
@@ -9,7 +9,7 @@
9
9
  {% endif %}
10
10
  {% endfor %}
11
11
 
12
- <form action="{{ form_action }}" method="POST" accept-charset="utf-8">
12
+ <form action="{{ form_action }}" method="POST" accept-charset="utf-8" onsubmit="return StopSpinner()">
13
13
  {{ form.hidden_tag() }}
14
14
 
15
15
  <div class="row">
@@ -19,7 +19,7 @@
19
19
  {{ form.search_institute(class="form-control") }}
20
20
  </div>
21
21
  {% endif %}
22
- <div class="col-md-3 mb-3">
22
+ <div class="col-md-2 mb-3">
23
23
  {{ form.case.label(class="form-label") }}
24
24
  {{ form.case(class="form-control", placeholder="example:18201", pattern="[^\\\<\>\?\!\=\/]*", title="Characters \<>?!=/ are not allowed") }}
25
25
  </div>
@@ -27,9 +27,10 @@
27
27
  {{ form.search_limit.label(class="form-label") }}
28
28
  {{ form.search_limit(class="form-control") }}
29
29
  </div>
30
- <div class="btn-sm mb-2 col-md-2 mx-auto">
31
- {{ form.search(class="btn btn-lg btn-primary mt-4") }}
32
- <a href="{{ reset_action }}" class="btn btn-lg btn-secondary mt-4 text-white">Reset search</a>
30
+ <div class="btn-sm mb-2 col-md-3 mx-auto">
31
+ {{ form.search(class="btn btn-primary mt-4") }}
32
+ {{ form.export(class="btn btn-primary mt-4") }}
33
+ <a href="{{ reset_action }}" class="btn btn-secondary mt-4 text-white">Reset search</a>
33
34
  </div>
34
35
  <div class="col-md-2 mb-3 form-check align-self-center mt-3 ms-3" data-bs-toggle="tooltip" title="Using multiple search criteria will narrow down your results as returned cases will contain all your searched conditions.">
35
36
  {{ form.advanced_search(class="form-check-input") }}
@@ -10,6 +10,7 @@ from wtforms import (
10
10
  SubmitField,
11
11
  validators,
12
12
  )
13
+ from wtforms.widgets import NumberInput
13
14
 
14
15
  from scout.constants import CHROMOSOMES, SV_TYPES
15
16
 
@@ -24,8 +25,22 @@ CATEGORY_CHOICES = [
24
25
 
25
26
 
26
27
  class ManagedVariantForm(FlaskForm):
27
- position = IntegerField("Start position", [validators.Optional()])
28
- end = IntegerField("End position", [validators.Optional()])
28
+ position = IntegerField(
29
+ "Start position",
30
+ [
31
+ validators.Optional(),
32
+ validators.NumberRange(min=0, message="Start position must be 1 or greater."),
33
+ ],
34
+ widget=NumberInput(min=1),
35
+ )
36
+ end = IntegerField(
37
+ "End position",
38
+ [
39
+ validators.Optional(),
40
+ validators.NumberRange(min=0, message="End position must be 1 or greater."),
41
+ ],
42
+ widget=NumberInput(min=1),
43
+ )
29
44
  cytoband_start = SelectField("Cytoband start", choices=[])
30
45
  cytoband_end = SelectField("Cytoband end", choices=[])
31
46
  description = StringField(label="Description")
@@ -212,8 +212,8 @@
212
212
  <tbody>
213
213
  <tr>
214
214
  <td>{{ add_form.chromosome(class_="form-control") }}</td>
215
- <td><input type="number" name="position" id="position" class="form-control" required></td>
216
- <td><input type="number" name="end" id="end" class="form-control"></td>
215
+ <td><input type="number" name="position" id="position" class="form-control" required min="1"></td>
216
+ <td><input type="number" name="end" id="end" class="form-control" min="1"></td>
217
217
  <td><input type="text" name="reference" id="reference" class="form-control" required></td>
218
218
  <td><input type="text" name="alternative" id="alternative" class="form-control" required></td>
219
219
  <td>{{ add_form.category(class_="form-control", onchange="populateSubTypeSelect('add_variant')") }}</td>
@@ -38,9 +38,13 @@
38
38
  <li class="list-group-item {{ 'list-group-item-info' if current_variant }}">
39
39
  <div class="d-flex">
40
40
  <span>
41
- <a href="{{ url_for('variant.evaluation', evaluation_id=data._id) }}">
41
+ {% if variant.category in ["snv", "cancer_snv"] %}
42
+ <a href="{{ url_for('variant.evaluation', evaluation_id=data._id) }}">
43
+ {{ data.classification.label }}
44
+ </a>
45
+ {% else %}
42
46
  {{ data.classification.label }}
43
- </a>
47
+ {% endif %}
44
48
  <span class="badge bg-info">{{ data.classification.short }}</span>
45
49
  </span>
46
50
  <span>
@@ -133,7 +137,6 @@
133
137
  {% endmacro %}
134
138
 
135
139
  {% macro acmg_form(institute, case, variant, ACMG_OPTIONS, selected=None) %}
136
- <label class="mt-3" data-bs-toggle="tooltip" title="Richards et al 2015"><a href="https://www.acmg.net/docs/standards_guidelines_for_the_interpretation_of_sequence_variants.pdf" rel="noopener noreferrer" target="_blank" style="color: inherit; text-decoration: inherit;">ACMG classification</a></label>
137
140
  <form action="{{ url_for('variant.variant_update', institute_id=institute._id, case_name=case.display_name, variant_id=variant._id) }}" method="POST">
138
141
  <div class="d-flex justify-content-between">
139
142
  {% for option in ACMG_OPTIONS %}
@@ -168,7 +171,7 @@
168
171
  <form action="{{ url_for('variant.ccv_evaluation', evaluation_id=data._id) }}" method="POST" style="display: inline-block;">
169
172
  <button class="btn btn-xs btn-link" >Delete</button>
170
173
  </form>
171
- {% if data.ccv_criteria %}
174
+ {% if data.ccv_criteria %}
172
175
  <a class="btn btn-xs btn-link" href="{{ url_for('variant.ccv_evaluation', evaluation_id=data._id, edit=True) }}" data-bs-toggle="tooltip" title="Editing this classification will result in a new classification">Edit</a>
173
176
  {% endif %}
174
177
  {% endif %}
@@ -191,6 +194,25 @@
191
194
  </form>
192
195
  {% endmacro %}
193
196
 
197
+ {% macro panel_classify_sv(variant, institute, case, ACMG_OPTIONS, evaluations) %}
198
+ <div class="mt-3">
199
+ ACMG (INDEL) <a href="https://www.acmg.net/docs/standards_guidelines_for_the_interpretation_of_sequence_variants.pdf" rel="noopener noreferrer" target="_blank" style="text-decoration: inherit;" data-bs-toggle="tooltip" title="Richards et al 2015">classification</a>
200
+ </div>
201
+ <div>
202
+ {{ acmg_form(institute, case, variant, ACMG_OPTIONS, variant.acmg_classification.code if variant.acmg_classification) }}
203
+ </div>
204
+ <div class="mt-3">
205
+ ACMG SV classification <a href="https://pubmed.ncbi.nlm.nih.gov/31690835/" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">guidelines</a> & <a href="https://cnvcalc.clinicalgenome.org/cnvcalc/" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">guide</a>.
206
+ </div>
207
+ {% if evaluations %} <!-- scrollable previous ACMG evaluations div-->
208
+ <div class="list-group mt-3" style="max-height:200px;overflow-y: scroll;">
209
+ {% for evaluation in evaluations %}
210
+ {{ acmg_classification_item(variant, evaluation) }}
211
+ {% endfor %}
212
+ </div>
213
+ {% endif %}
214
+ {% endmacro %}
215
+
194
216
  {% macro panel_classify(variant, institute, case, ACMG_OPTIONS, CCV_OPTIONS, manual_rank_options, cancer_tier_options, dismiss_variant_options, mosaic_variant_options, evaluations, ccv_evaluations) %}
195
217
  <div class="card panel-default">
196
218
  <div class="panel-heading">Classify</div>
@@ -203,6 +225,7 @@
203
225
  {% if case.track != "cancer" %}
204
226
  {{ mosaic_variant_button(variant, institute, case, mosaic_variant_options) }}
205
227
  {% endif %}
228
+ ACMG <a href="https://www.acmg.net/docs/standards_guidelines_for_the_interpretation_of_sequence_variants.pdf" rel="noopener noreferrer" target="_blank" style="text-decoration: inherit;" data-bs-toggle="tooltip" title="Richards et al 2015">classification</a>
206
229
  {{ acmg_form(institute, case, variant, ACMG_OPTIONS, variant.acmg_classification.code if variant.acmg_classification) }}
207
230
  <div class="mt-3">
208
231
  <a href="{{ url_for('variant.variant_acmg', institute_id=institute._id, case_name=case.display_name, variant_id=variant._id) }}" class="btn btn-outline-secondary form-control">Classify</a>
@@ -1,7 +1,7 @@
1
1
  {% extends "layout.html" %}
2
2
  {% from "utils.html" import activity_panel, comments_panel, pedigree_panel %}
3
3
  {% from "variant/variant_details.html" import old_observations %}
4
- {% from "variant/components.html" import external_scripts, external_stylesheets, matching_variants, variant_scripts %}
4
+ {% from "variant/components.html" import external_scripts, external_stylesheets, matching_variants, panel_classify_sv, variant_scripts %}
5
5
  {% from "variant/utils.html" import causative_button, genes_panel, igv_track_selection, modal_causative, overlapping_panel, sv_alignments, pin_button, transcripts_panel, custom_annotations, gene_panels %}
6
6
  {% from "variant/rank_score_results.html" import rankscore_panel %}
7
7
  {% from "variant/gene_disease_relations.html" import orpha_omim_phenotypes %}
@@ -196,7 +196,7 @@
196
196
  {{ variant_tag_button(variant, institute, case, manual_rank_options) }}
197
197
  </div>
198
198
  <div>
199
- ACMG classification <a href="https://pubmed.ncbi.nlm.nih.gov/31690835/" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">guidelines</a> & <a href="https://cnvcalc.clinicalgenome.org/cnvcalc/" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">guide</a>.
199
+ {{ panel_classify_sv(variant, institute, case, ACMG_OPTIONS, evaluations) }}
200
200
  </div>
201
201
  </div>
202
202
  <div class="list-group mt-3">
@@ -76,15 +76,15 @@
76
76
  </td> <!-- end of ID col-->
77
77
 
78
78
  <td> <!-- HGVS Description col -->
79
- {% set hgvs_c = (tx.coding_sequence_name or '')|truncate(20, True) %}
79
+ {% set hgvs_c = (tx.coding_sequence_name or '')|truncate(30, True) %}
80
80
  {% if variant.chromosome in ["MT","M"] %}
81
81
  {% set mt_notation = "m." ~ variant.position ~ variant.reference ~ ">" ~ variant.alternative %}
82
- {{ mt_notation|truncate(20,True) }} <span class="text-muted">({{ hgvs_c }})</span>
82
+ {{ mt_notation|truncate(30,True) }} <span class="text-muted">({{ hgvs_c }})</span>
83
83
  {% else %}
84
84
  {{ hgvs_c }}
85
85
  {% endif %}
86
86
  <span class="text-muted float-end">
87
- {{ (tx.protein_sequence_name or '')|url_decode }}
87
+ {{ (tx.protein_sequence_name or '')|url_decode|truncate(30, True) }}
88
88
  </span>
89
89
  </td> <!-- end of HGVS Description col -->
90
90
 
@@ -374,7 +374,6 @@ def variant_update(institute_id, case_name, variant_id):
374
374
  @templated("variant/acmg.html")
375
375
  def evaluation(evaluation_id):
376
376
  """Show, edit or delete an ACMG evaluation."""
377
-
378
377
  evaluation_obj = store.get_evaluation(evaluation_id)
379
378
  if evaluation_obj is None:
380
379
  flash("Evaluation was not found in database", "warning")
@@ -392,7 +391,7 @@ def evaluation(evaluation_id):
392
391
  if check_reset_variant_classification(store, evaluation_obj, link):
393
392
  flash("Cleared ACMG classification.", "info")
394
393
 
395
- return redirect(link)
394
+ return redirect(request.referrer)
396
395
 
397
396
  return dict(
398
397
  evaluation=evaluation_obj,