Skip to content

Commit 5d5e4b2

Browse files
authored
Merge pull request #5537 from broadinstitute/dev
Dev
2 parents 167a34e + 93b69a4 commit 5d5e4b2

114 files changed

Lines changed: 10118 additions & 1340 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,5 @@ jobs:
140140
working-directory: ./ui
141141
- run: npm run lint
142142
working-directory: ./ui
143-
- run: npm test
143+
- run: npm test -- --coverage
144144
working-directory: ./ui

CLAUDE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
seqr is a web-based rare disease genomics tool (Broad Institute): Django (Python) server + React (JS) client, backed by **postgres** (`seqrdb` + `reference_data_db`), **clickhouse** (variant search, read-only from the app), and optional **redis** cache. Dataset loading is done by an external pipeline-runner service (not in this repo). Elasticsearch and Hail-backend search are deprecated in favor of ClickHouse — don't build new features against ES.
8+
9+
## Commands
10+
11+
**Backend:** `pip install -r requirements-dev.txt -r requirements.txt`, `./manage.py migrate`, `./manage.py migrate --database=reference_data`, `./manage.py runserver`
12+
13+
Run all backend tests (matches CI): `./manage.py test -p '*_tests.py' reference_data clickhouse_search seqr matchmaker panelapp`
14+
Single test: `./manage.py test seqr.views.apis.family_api_tests.FamilyAPITest.test_update_family`
15+
`clickhouse_search` tests need a running ClickHouse + `CLICKHOUSE_READER_USER`/`CLICKHOUSE_WRITER_USER`/`CLICKHOUSE_SERVICE_HOSTNAME`. CI enforces `--fail-under=99` coverage.
16+
17+
**Frontend (`ui/`):** `npm install`, `npm run start` (dev server), `npm run build`, `npm test`, `npm run lint`
18+
Single test: `npx jest path/to/File.test.js`. Coverage threshold 40% lines/statements. Node 14 required.
19+
20+
## Architecture
21+
22+
- **`seqr/`** — core app (users, projects, families, samples, saved variants, permissions). `models.py` is the central data model. Function-based views (no DRF) in `views/apis/*.py`, one module per resource, each paired with `*_tests.py`, wired by name in `urls.py`. No service layer — logic lives in `views/apis`/`utils/`.
23+
- **`reference_data/`** — external gene/variant reference datasets (OMIM, gencode, HPO, PanelApp, etc.), lives in the separate `reference_data` Postgres DB via `ReferenceDataRouter`; refreshed by `update_all_reference_data`.
24+
- **`matchmaker/`** — GA4GH Matchmaker Exchange (MME) protocol (`views/external_api.py`) plus seqr's own MME UI endpoints (`views/matchmaker_api.py`).
25+
- **`panelapp/`** — Genomics England PanelApp gene panel integration.
26+
- **`clickhouse_search/`** — the variant search backend (sole supported implementation; ES only remains in old migrations). Custom Django DB backend (`backend/`), ClickHouse-side models (`models/`), query construction in `search.py`/`managers.py` called from `seqr`'s `variant_search_api.py`. Routed via `ClickHouseRouter`.
27+
- **`vlm/`** — separate standalone `aiohttp` service (own requirements, own Auth0 auth), not part of the Django URLconf, deployed independently.
28+
29+
**DB routing:** four connections — `default` (seqrdb), `reference_data`, `clickhouse`/`clickhouse_write` — via `DATABASE_ROUTERS`. No cross-DB joins at the DB level.
30+
31+
**Auth:** Google OAuth2 / Azure AD v2 via `social_django`, plus `django-guardian` object-level permissions.
32+
33+
### Frontend (`ui/`)
34+
35+
- `app.jsx` entry: Redux `Provider` + react-router-dom v5.
36+
- `pages/<Page>/` — one dir per route, each with `X.jsx`, `reducers.js`, `selectors.js`(+`.test.js`), `constants.js`, `fixtures.js`, `components/`.
37+
- `shared/` — cross-page components/utils. `redux/` — classic ducks pattern, `redux/utils/reducerFactories.js` for generic reducers.
38+
- Module aliases via `babel-plugin-module-resolver`: `shared -> ./shared/`, `pages -> ./pages/`.
39+
- Semantic UI React + `styled-components`. Codebase is mostly JS/JSX; **new files should be written in TypeScript**.
40+
- Jest + Enzyme, colocated `*.test.js`. Prefer deep-rendering connected components over shallow-rendering unconnected ones in tests.
41+
42+
43+
## Development Notes
44+
45+
Never delete a file that is not in source control without confirming first

clickhouse_search/all_search_tests.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,17 +367,24 @@ def test_multi_project_search(self):
367367
)
368368

369369
def test_all_project_search(self):
370-
request_body = {'allGenomeProjectFamilies': '38'}
371370
self._assert_expected_search_error(
372-
'No data available for genome version "GRCh38"', request_body=request_body, check_login=self.check_require_login,
371+
'No data available for genome version "GRCh37"', request_body={'allGenomeProjectFamilies': '37'},
372+
check_login=self.check_require_login,
373+
)
374+
375+
request_body = {'allGenomeProjectFamilies': '38'}
376+
additional_response = {'familiesByGuid': {'F000001_1': mock.ANY}}
377+
self._assert_expected_search(
378+
[FAMILY_3_VARIANT, FAMILY_1_VARIANT], project_families=[
379+
{'projectGuid': 'R0001_1kg', 'familyGuids': ['F000001_1', 'F000003_3', 'F000005_5']},
380+
], request_body=request_body, additional_response=additional_response,
373381
)
374382

375383
self.login_collaborator()
376384
project_families = [{'projectGuid': 'R0001_1kg', 'familyGuids': [
377385
'F000001_1', 'F000002_2', 'F000003_3', 'F000004_4', 'F000005_5', 'F000006_6', 'F000007_7', 'F000008_8',
378386
'F000009_9', 'F000010_10', 'F000013_13',
379387
]}]
380-
additional_response = {'familiesByGuid': {'F000001_1': mock.ANY}}
381388
self._assert_expected_search(
382389
[VARIANT1, VARIANT2, MULTI_FAMILY_VARIANT, VARIANT4, GCNV_VARIANT1, GCNV_VARIANT2,
383390
GCNV_VARIANT3, GCNV_VARIANT4, FAMILY_1_VARIANT, MITO_VARIANT1, MITO_VARIANT2, MITO_VARIANT3],
@@ -1613,7 +1620,7 @@ def _cached_lookup_variant(self, variant, expected_individuals=None):
16131620
def test_get_single_variant(self):
16141621
url_template = (reverse(query_single_variant_handler, args=['variant_id']) + '?familyGuid={}').replace('variant_id', '{}')
16151622
url = url_template.format('21-3343353-GAGA-G', 'F000001_1')
1616-
self.check_collaborator_login(url)
1623+
self.check_require_login(url)
16171624

16181625
response = self.client.get(url)
16191626
self.assertEqual(response.status_code, 200)
@@ -1630,7 +1637,12 @@ def test_get_single_variant(self):
16301637
]},
16311638
})
16321639

1633-
response = self.client.get(url_template.format(VARIANT_IDS[1], 'F000002_2'))
1640+
family_2_url = url_template.format(VARIANT_IDS[1], 'F000002_2')
1641+
response = self.client.get(family_2_url)
1642+
self.assertEqual(response.status_code, 403)
1643+
1644+
self.login_collaborator()
1645+
response = self.client.get(family_2_url)
16341646
self.assertEqual(response.status_code, 400)
16351647
self.assertDictEqual(response.json(), {'error': 'Variant 1-91511686-TCA-G not found'})
16361648

@@ -1652,8 +1664,8 @@ def test_get_single_variant(self):
16521664
self.assertEqual(response.status_code, 200)
16531665
self.assertDictEqual(response.json()['variantsById'], {'7-143270172-A-G': GRCH37_VARIANT})
16541666

1655-
self.assertTrue(all(call.args[0].startswith('projects__') for call in self.mock_redis.get.mock_calls))
1656-
self.assertTrue(all(call.args[0].startswith('projects__') for call in self.mock_redis.set.mock_calls))
1667+
self.assertTrue(all(call.args[0].startswith('project_analysis_groups__') for call in self.mock_redis.get.mock_calls))
1668+
self.assertTrue(all(call.args[0].startswith('project_analysis_groups__') for call in self.mock_redis.set.mock_calls))
16571669

16581670
def test_frequency_filter(self):
16591671
sv_callset_filter = {'sv_callset': {'af': 0.05}}
@@ -2389,6 +2401,12 @@ def test_gene_variant_lookup(self):
23892401
check_login=self.check_require_login,
23902402
)
23912403

2404+
locus = {'rawItems': 'ENSG00000097046'}
2405+
self._assert_expected_search(
2406+
[], request_body={'allGenomeProjectFamilies': '37', 'includeNoAccessProjects': True},
2407+
locus=locus, project_families=[], response_search={'no_access_project_genome_version': '37'},
2408+
)
2409+
23922410
annotations = {
23932411
'missense': ['missense_variant'],
23942412
'other': ['non_coding_transcript_exon_variant'],
@@ -2398,30 +2416,30 @@ def test_gene_variant_lookup(self):
23982416
'gnomad_genomes': {'af': 0.003},
23992417
'gnomad_exomes': {'af': 0.003},
24002418
}
2401-
locus = {'rawItems': 'ENSG00000097046'}
24022419
response_search = {'no_access_project_genome_version': '38'}
24032420
variant4 = {**VARIANT4, 'selectedMainTranscriptId': 'ENST00000350997', 'numFamilies': 3}
24042421
del variant4['familyGuids']
24052422
del variant4['genotypes']
2423+
project_families = [
2424+
{'projectGuid': 'R0001_1kg', 'familyGuids': ['F000001_1', 'F000003_3', 'F000005_5']},
2425+
]
24062426
self._assert_expected_search(
24072427
[variant4], request_body=request_body, response_search=response_search,
24082428
cached_variant_fields=[{'selectedTranscript': CACHED_CONSEQUENCES_BY_KEY[4][1]}],
2409-
annotations=annotations, freqs=freqs, locus=locus, project_families=[], export_data=[
2429+
annotations=annotations, freqs=freqs, locus=locus, project_families=project_families, export_data=[
24102430
EXPORT_DATA[0][:27], EXPORT_DATA[4][:24] + ['3 Families', '', ''],
24112431
], gene_counts={'ENSG00000097046': {'total': 1, 'families': {}}},
24122432
)
24132433

24142434
freqs = {'callset': freqs['callset']}
2415-
variant3 = {**VARIANT3, 'selectedMainTranscriptId': 'ENST00000497611', 'numFamilies': 4}
2416-
del variant3['familyGuids']
2417-
del variant3['genotypes']
2435+
variant3 = {**FAMILY_3_VARIANT, 'selectedMainTranscriptId': 'ENST00000497611'}
24182436
self._assert_expected_search(
24192437
[variant3, variant4], request_body=request_body, response_search=response_search,
24202438
cached_variant_fields=[
24212439
{'selectedTranscript': CACHED_CONSEQUENCES_BY_KEY[3][3]},
24222440
{'selectedTranscript': CACHED_CONSEQUENCES_BY_KEY[4][1]},
24232441
],
2424-
annotations=annotations, freqs=freqs, locus=locus, project_families=[],
2442+
annotations=annotations, freqs=freqs, locus=locus, project_families=project_families,
24252443
)
24262444

24272445
self._assert_expected_search_error(
@@ -2430,22 +2448,22 @@ def test_gene_variant_lookup(self):
24302448
)
24312449

24322450
self._assert_expected_search(
2433-
[], request_body=request_body, response_search=response_search, project_families=[],
2451+
[], request_body=request_body, response_search=response_search, project_families=project_families,
24342452
annotations=annotations, freqs=freqs, locus=locus, inheritance_mode='homozygous_recessive',
24352453
)
24362454

2437-
variant3['numFamilies'] = 1
24382455
variant4['numFamilies'] = 1
2456+
self.maxDiff = None
24392457
self._assert_expected_search(
24402458
[variant3, variant4], request_body=request_body, response_search=response_search,
24412459
cached_variant_fields=[
24422460
{'selectedTranscript': CACHED_CONSEQUENCES_BY_KEY[3][3]},
24432461
{'selectedTranscript': CACHED_CONSEQUENCES_BY_KEY[4][1]},
24442462
],
2445-
annotations=annotations, freqs=freqs, locus=locus, inheritance_mode='de_novo', project_families=[],
2463+
annotations=annotations, freqs=freqs, locus=locus, inheritance_mode='de_novo', project_families=project_families,
24462464
gene_counts={
2447-
'ENSG00000097046': {'total': 2, 'families': {}},
2448-
'ENSG00000177000': {'total': 1, 'families': {}},
2465+
'ENSG00000097046': {'total': 2, 'families': {'F000003_3': 1}},
2466+
'ENSG00000177000': {'total': 1, 'families': {'F000003_3': 1}},
24492467
},
24502468
)
24512469

clickhouse_search/managers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,12 +1067,10 @@ def _filter_project_families(self, entries, sample_data):
10671067
def _sample_family_q(cls, sample_type, families):
10681068
return Q(family_guid__in=families)
10691069

1070-
def _search_call_data(self, entries, sample_data, inheritance_mode=None, inheritance_filter=None, qualityFilter=None, pathogenicity=None, exclude_projects=None, annotate_carriers=False, annotate_hom_alts=False, **kwargs):
1070+
def _search_call_data(self, entries, sample_data, inheritance_mode=None, inheritance_filter=None, qualityFilter=None, pathogenicity=None, annotate_carriers=False, annotate_hom_alts=False, **kwargs):
10711071
multi_sample_type_families = None
10721072
if sample_data:
10731073
entries, multi_sample_type_families = self._filter_project_families(entries, sample_data)
1074-
elif exclude_projects:
1075-
entries = entries.exclude(project_guid__in=exclude_projects)
10761074

10771075
inheritance_q = None
10781076
quality_q = None

clickhouse_search/search.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,32 +64,35 @@ def get_clickhouse_variants(families, user, genome_version=None, sort=None, samp
6464
except InvalidDatasetTypeException:
6565
continue
6666

67+
sample_data = sample_data_by_dataset_type[dataset_type]
68+
exclude_dt_keys = (exclude_keys or {}).get(dataset_type)
69+
dataset_results = []
70+
71+
if sample_data:
72+
logger.info(f'Loading {dataset_type} data for {sample_data["num_families"]} families', user)
73+
74+
if inheritance_mode != COMPOUND_HET:
75+
dataset_results += _get_search_results(
76+
entry_qs, variants_qs, sample_data, inheritance_mode=inheritance_mode, exclude_keys=exclude_dt_keys, **search, **parsed_filters,
77+
)
78+
79+
run_x_linked_male_search = has_x_linked and not (inheritance_mode == X_LINKED_RECESSIVE and sample_data.get('samples'))
80+
if run_x_linked_male_search:
81+
dataset_results += _get_x_linked_male_search_results(
82+
entry_qs, variants_qs, dataset_type, user, sample_data, exclude_keys=exclude_dt_keys,
83+
**search, **parsed_filters,
84+
)
85+
6786
if dataset_type == Dataset.DATASET_TYPE_VARIANT_CALLS and no_access_project_genome_version:
6887
results += _get_no_access_search_results(
6988
entry_qs, variants_qs, has_comp_het, user, **search, **parsed_filters,
70-
exclude_projects=sample_data_by_dataset_type[dataset_type].get('project_guids'), inheritance_mode=inheritance_mode,
89+
exclude_keys=[r['key'] for r in dataset_results] + (exclude_dt_keys or []), inheritance_mode=inheritance_mode,
7190
)
7291
searched_dataset_types.add(dataset_type)
7392

74-
sample_data = sample_data_by_dataset_type[dataset_type]
7593
if not sample_data:
7694
continue
7795

78-
logger.info(f'Loading {dataset_type} data for {sample_data["num_families"]} families', user)
79-
80-
dataset_results = []
81-
if inheritance_mode != COMPOUND_HET:
82-
dataset_results += _get_search_results(
83-
entry_qs, variants_qs, sample_data, inheritance_mode=inheritance_mode, exclude_keys=(exclude_keys or {}).get(dataset_type), **search, **parsed_filters,
84-
)
85-
86-
run_x_linked_male_search = has_x_linked and not (inheritance_mode == X_LINKED_RECESSIVE and sample_data.get('samples'))
87-
if run_x_linked_male_search:
88-
dataset_results += _get_x_linked_male_search_results(
89-
entry_qs, variants_qs, dataset_type, user, sample_data, exclude_keys=(exclude_keys or {}).get(dataset_type),
90-
**search, **parsed_filters,
91-
)
92-
9396
if has_comp_het:
9497
dataset_results += _get_data_type_comp_het_results_queryset(
9598
entry_qs, variants_qs, sample_data, user, parsed_filters, **search,

matchmaker/views/external_api_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_mme_metrics_proxy(self):
4242
self.assertEqual(response.status_code, 200)
4343
self.assertDictEqual(response.json(), {
4444
'metrics': {
45-
'numberOfCases': 4,
45+
'numberOfCases': 5,
4646
'numberOfSubmitters': 2,
4747
'numberOfUniqueGenes': 3,
4848
'numberOfUniqueFeatures': 4,

requirements-dev.txt

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,81 @@
11
#
2-
# This file is autogenerated by pip-compile with Python 3.9
2+
# This file is autogenerated by pip-compile with Python 3.11
33
# by the following command:
44
#
55
# pip-compile requirements-dev.in
66
#
7-
asgiref==3.6.0
7+
asgiref==3.12.1
88
# via
99
# -c requirements.txt
1010
# django
11-
build==0.10.0
11+
build==1.5.0
1212
# via pip-tools
13-
certifi==2024.7.4
13+
certifi==2026.6.17
1414
# via
1515
# -c requirements.txt
1616
# requests
17-
charset-normalizer==3.0.1
17+
charset-normalizer==3.4.9
1818
# via
1919
# -c requirements.txt
2020
# requests
21-
click==8.1.3
21+
click==8.4.2
2222
# via pip-tools
2323
coverage==6.5.0
2424
# via -r requirements-dev.in
25-
django==4.2.27
25+
django==4.2.30
2626
# via
2727
# -c requirements.txt
2828
# django-appconf
29+
# django-compressor
2930
# django-debug-toolbar
30-
django-appconf==1.0.5
31+
django-appconf==1.2.0
3132
# via django-compressor
32-
django-compressor==4.3.1
33+
django-compressor==4.6.0
3334
# via -r requirements-dev.in
34-
django-debug-toolbar==3.2.4
35+
django-debug-toolbar==6.3.0
3536
# via -r requirements-dev.in
36-
idna==3.7
37+
idna==3.18
3738
# via
3839
# -c requirements.txt
3940
# requests
40-
mock==5.0.1
41+
mock==5.2.0
4142
# via -r requirements-dev.in
42-
packaging==24.0
43+
packaging==26.2
4344
# via
4445
# -c requirements.txt
4546
# build
46-
pip-tools==6.12.2
47+
# wheel
48+
pip-tools==7.5.3
4749
# via -r requirements-dev.in
48-
pyproject-hooks==1.0.0
49-
# via build
50-
rcssmin==1.1.1
50+
pyproject-hooks==1.2.0
51+
# via
52+
# build
53+
# pip-tools
54+
pyyaml==6.0.3
55+
# via responses
56+
rcssmin==1.2.2
5157
# via django-compressor
52-
requests==2.32.4
58+
requests==2.34.2
5359
# via
5460
# -c requirements.txt
5561
# responses
56-
responses==0.22.0
62+
responses==0.26.2
5763
# via -r requirements-dev.in
58-
rjsmin==1.2.1
64+
rjsmin==1.2.5
5965
# via django-compressor
60-
sqlparse==0.5.4
66+
sqlparse==0.5.5
6167
# via
6268
# -c requirements.txt
6369
# django
6470
# django-debug-toolbar
65-
toml==0.10.2
66-
# via responses
67-
tomli==2.0.1
68-
# via
69-
# build
70-
# pyproject-hooks
71-
types-toml==0.10.8.5
72-
# via responses
73-
urllib3==1.26.19
71+
urllib3==2.7.0
7472
# via
7573
# -c requirements.txt
7674
# requests
7775
# responses
7876
urllib3-mock==0.3.3
7977
# via -r requirements-dev.in
80-
wheel==0.46.2
78+
wheel==0.47.0
8179
# via pip-tools
8280

8381
# The following packages are considered to be unsafe in a requirements file:

requirements.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Django>=4.2,<4.3 # core server-side framework
22
django-anymail # for sending emails using cloud-based mail service providers
33
django-clickhouse-backend
4-
django-csp # for setting CSP headers
4+
django-csp>=3.7,<4.0 # for setting CSP headers
55
django-guardian # object-level permissions for database records. Behind a major version due to missing Python 2 support
66
django-hijack # allows admins to login as other user
77
django-notifications-hq # notification app
8-
django-cors-headers # allows CORS requests for client-side development
8+
django-cors-headers>=3.13,<4.0 # allows CORS requests for client-side development
99
django-storages[google] # alternative GCS storage backend for the django media_root
1010
social-auth-app-django>5.0.0 # the package for Django to authenticate users with social medieas
1111
social-auth-core # the Python social authentication package. Required by social-auth-app-django

0 commit comments

Comments
 (0)