diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e4e7c1d4..d264ec69bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # _seqr_ Changes ## dev +* Migrate "Submit to Clinvar" to generic report flag for Variant Notes (REQUIRES DB MIGRATION) ## 10/28/24 * Update RNA Tissue Type choices (REQUIRES DB MIGRATION) diff --git a/seqr/fixtures/1kg_project.json b/seqr/fixtures/1kg_project.json index 3b737b363b..03f05701b7 100644 --- a/seqr/fixtures/1kg_project.json +++ b/seqr/fixtures/1kg_project.json @@ -2177,7 +2177,7 @@ "last_modified_date": "2018-02-23T17:32:23.054Z", "saved_variants": [2], "note": "test n\u00f8te", - "submit_to_clinvar": false, + "report": false, "search_hash": null } }, @@ -2191,7 +2191,7 @@ "last_modified_date": "2019-02-23T17:32:23.054Z", "saved_variants": [2], "note": "a later note", - "submit_to_clinvar": false, + "report": false, "search_hash": null } }, diff --git a/seqr/fixtures/report_variants.json b/seqr/fixtures/report_variants.json index 06ed7b2d11..fb5e2e4ad7 100644 --- a/seqr/fixtures/report_variants.json +++ b/seqr/fixtures/report_variants.json @@ -143,6 +143,19 @@ "search_hash": null } }, +{ + "model": "seqr.variantnote", + "pk": 123, + "fields": { + "guid": "VN0000123_prefix_19107_DEL_r0", + "created_date": "2019-05-15T14:51:58.410Z", + "created_by": null, + "last_modified_date": "2019-02-23T17:32:23.054Z", + "saved_variants": [7], + "note": "Phasing incorrect in input VCF", + "report": true + } +}, { "model": "seqr.variantfunctionaldata", "pk": 29, diff --git a/seqr/migrations/0078_rename_submit_to_clinvar_variantnote_report.py b/seqr/migrations/0078_rename_submit_to_clinvar_variantnote_report.py new file mode 100644 index 0000000000..d9071e0269 --- /dev/null +++ b/seqr/migrations/0078_rename_submit_to_clinvar_variantnote_report.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-11-19 15:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seqr', '0077_alter_rnasample_tissue_type'), + ] + + operations = [ + migrations.RenameField( + model_name='variantnote', + old_name='submit_to_clinvar', + new_name='report', + ), + ] diff --git a/seqr/models.py b/seqr/models.py index 5461deec8c..1dd9135acc 100644 --- a/seqr/models.py +++ b/seqr/models.py @@ -903,7 +903,7 @@ class Meta: class VariantNote(ModelWithGUID): saved_variants = models.ManyToManyField('SavedVariant') note = models.TextField() - submit_to_clinvar = models.BooleanField(default=False) + report = models.BooleanField(default=False) # these are for context search_hash = models.CharField(max_length=50, null=True) @@ -915,7 +915,7 @@ def __unicode__(self): GUID_PREFIX = 'VN' class Meta: - json_fields = ['guid', 'note', 'submit_to_clinvar', 'last_modified_date', 'created_by'] + json_fields = ['guid', 'note', 'report', 'last_modified_date', 'created_by'] class VariantFunctionalData(ModelWithGUID): diff --git a/seqr/views/apis/report_api_tests.py b/seqr/views/apis/report_api_tests.py index 0ccb56bd20..db3dbb36f0 100644 --- a/seqr/views/apis/report_api_tests.py +++ b/seqr/views/apis/report_api_tests.py @@ -636,8 +636,8 @@ ], [ 'Broad_NA20889_1_249045487_DEL', 'Broad_NA20889', '', 'SV', 'GRCh37', '1', '249045487', '', '', '', 'OR4G11P', '', '', '', 'Heterozygous', '', 'unknown', 'Broad_NA20889_1_248367227', '', 'Candidate', - 'Immunodeficiency 38', 'OMIM:616126', 'Autosomal recessive', 'Full', '', '', 'SR-ES', '', 'DEL', '', - '249045898', '1', 'DEL:chr1:249045123-249045456', '', + 'Immunodeficiency 38', 'OMIM:616126', 'Autosomal recessive', 'Full', '', '', 'SR-ES', + 'Phasing incorrect in input VCF', 'DEL', '', '249045898', '1', 'DEL:chr1:249045123-249045456', '', ], ] @@ -1428,6 +1428,7 @@ def test_variant_metadata(self): 'gene_id': None, 'gene_known_for_phenotype': 'Candidate', 'genetic_findings_id': 'NA20889_1_249045487_DEL', + 'notes': 'Phasing incorrect in input VCF', 'participant_id': 'NA20889', 'pos': 249045487, 'projectGuid': 'R0003_test', diff --git a/seqr/views/apis/saved_variant_api.py b/seqr/views/apis/saved_variant_api.py index 18740ba3b9..af850a667c 100644 --- a/seqr/views/apis/saved_variant_api.py +++ b/seqr/views/apis/saved_variant_api.py @@ -116,7 +116,7 @@ def create_variant_note_handler(request, variant_guids): def _create_variant_note(saved_variants, note_json, user): note = create_model_from_json(VariantNote, { 'note': note_json.get('note'), - 'submit_to_clinvar': note_json.get('submitToClinvar') or False, + 'report': note_json.get('report') or False, 'search_hash': note_json.get('searchHash'), }, user) note.saved_variants.set(saved_variants) diff --git a/seqr/views/apis/saved_variant_api_tests.py b/seqr/views/apis/saved_variant_api_tests.py index 1d33fc37c9..53baaa50e6 100644 --- a/seqr/views/apis/saved_variant_api_tests.py +++ b/seqr/views/apis/saved_variant_api_tests.py @@ -518,19 +518,19 @@ def test_create_update_and_delete_variant_note(self): # send valid request to create variant_note response = self.client.post(create_variant_note_url, content_type='application/json', data=json.dumps( - {'note': 'new_variant_note', 'submitToClinvar': True, 'familyGuid': 'F000001_1'} + {'note': 'new_variant_note', 'report': True, 'familyGuid': 'F000001_1'} )) self.assertEqual(response.status_code, 200) new_note_guid = response.json()['savedVariantsByGuid'][VARIANT_GUID]['noteGuids'][0] new_note_response = response.json()['variantNotesByGuid'][new_note_guid] self.assertEqual(new_note_response['note'], 'new_variant_note') - self.assertEqual(new_note_response['submitToClinvar'], True) + self.assertEqual(new_note_response['report'], True) new_variant_note = VariantNote.objects.filter(guid=new_note_guid).first() self.assertIsNotNone(new_variant_note) self.assertEqual(new_variant_note.note, new_note_response['note']) - self.assertEqual(new_variant_note.submit_to_clinvar, new_note_response['submitToClinvar']) + self.assertEqual(new_variant_note.report, new_note_response['report']) # save variant_note as gene_note response = self.client.post(create_variant_note_url, content_type='application/json', data=json.dumps( @@ -568,18 +568,18 @@ def test_create_update_and_delete_variant_note(self): # update the variant_note update_variant_note_url = reverse(update_variant_note_handler, args=[VARIANT_GUID, new_note_guid]) response = self.client.post(update_variant_note_url, content_type='application/json', data=json.dumps( - {'note': 'updated_variant_note', 'submitToClinvar': False})) + {'note': 'updated_variant_note', 'report': False})) self.assertEqual(response.status_code, 200) updated_note_response = response.json()['variantNotesByGuid'][new_note_guid] self.assertEqual(updated_note_response['note'], 'updated_variant_note') - self.assertEqual(updated_note_response['submitToClinvar'], False) + self.assertEqual(updated_note_response['report'], False) updated_variant_note = VariantNote.objects.filter(guid=updated_note_response['noteGuid']).first() self.assertIsNotNone(updated_variant_note) self.assertEqual(updated_variant_note.note, updated_note_response['note']) - self.assertEqual(updated_variant_note.submit_to_clinvar, updated_note_response['submitToClinvar']) + self.assertEqual(updated_variant_note.report, updated_note_response['report']) # delete the variant_note delete_variant_note_url = reverse(delete_variant_note_handler, args=[VARIANT_GUID, updated_variant_note.guid]) @@ -603,7 +603,7 @@ def test_create_partially_saved_compound_het_variant_note(self): 'tagGuids': ['VT1708633_2103343353_r0390_100', 'VT1726961_2103343353_r0390_100'], 'noteGuids': []}, ], 'note': 'one_saved_one_not_saved_compount_hets_note', - 'submitToClinvar': True, + 'report': True, 'familyGuid': 'F000001_1', } response = self.client.post(create_saved_variant_url, content_type='application/json', data=json.dumps(request_body)) @@ -643,20 +643,20 @@ def test_create_update_and_delete_compound_hets_variant_note(self): invalid_comp_hets_variant_note_url = reverse( create_variant_note_handler, args=['not_variant,{}'.format(COMPOUND_HET_1_GUID)]) response = self.client.post(invalid_comp_hets_variant_note_url, content_type='application/json', data=json.dumps( - {'note': 'new_compound_hets_variant_note', 'submitToClinvar': True, 'familyGuid': 'F000001_1'} + {'note': 'new_compound_hets_variant_note', 'report': True, 'familyGuid': 'F000001_1'} )) self.assertEqual(response.status_code, 400) self.assertDictEqual(response.json(), {'error': 'Unable to find the following variant(s): not_variant'}) response = self.client.post(create_comp_hets_variant_note_url, content_type='application/json', data=json.dumps( - {'note': 'new_compound_hets_variant_note', 'submitToClinvar': True, 'familyGuid': 'F000001_1'} + {'note': 'new_compound_hets_variant_note', 'report': True, 'familyGuid': 'F000001_1'} )) self.assertEqual(response.status_code, 200) response_json = response.json() for note in response.json()['variantNotesByGuid'].values(): self.assertEqual(note['note'], 'new_compound_hets_variant_note') - self.assertEqual(note['submitToClinvar'], True) + self.assertEqual(note['report'], True) self.assertEqual( response_json['savedVariantsByGuid'][COMPOUND_HET_1_GUID]['noteGuids'][0], @@ -666,24 +666,24 @@ def test_create_update_and_delete_compound_hets_variant_note(self): new_variant_note = VariantNote.objects.get(guid=new_note_guid) self.assertEqual(new_variant_note.note, response_json['variantNotesByGuid'][new_note_guid]['note']) self.assertEqual( - new_variant_note.submit_to_clinvar, response_json['variantNotesByGuid'][new_note_guid]['submitToClinvar'] + new_variant_note.report, response_json['variantNotesByGuid'][new_note_guid]['report'] ) # update the variants_note for both compound hets update_variant_note_url = reverse(update_variant_note_handler, args=[','.join([COMPOUND_HET_1_GUID, COMPOUND_HET_2_GUID]), new_note_guid]) response = self.client.post(update_variant_note_url, content_type='application/json', data=json.dumps( - {'note': 'updated_variant_note', 'submitToClinvar': False})) + {'note': 'updated_variant_note', 'report': False})) self.assertEqual(response.status_code, 200) updated_note_response = response.json()['variantNotesByGuid'][new_note_guid] self.assertEqual(updated_note_response['note'], 'updated_variant_note') - self.assertEqual(updated_note_response['submitToClinvar'], False) + self.assertEqual(updated_note_response['report'], False) updated_variant_note = VariantNote.objects.get(guid=new_note_guid) self.assertEqual(updated_variant_note.note, updated_note_response['note']) - self.assertEqual(updated_variant_note.submit_to_clinvar, updated_note_response['submitToClinvar']) + self.assertEqual(updated_variant_note.report, updated_note_response['report']) # save variant_note as gene_note for both compound hets response = self.client.post( diff --git a/seqr/views/apis/summary_data_api_tests.py b/seqr/views/apis/summary_data_api_tests.py index 15df14d46c..199b4bd08b 100644 --- a/seqr/views/apis/summary_data_api_tests.py +++ b/seqr/views/apis/summary_data_api_tests.py @@ -108,7 +108,7 @@ 'chrom_end-1': None, 'pos_end-1': None, 'notes-1': '', - 'notes-2': '', + 'notes-2': 'Phasing incorrect in input VCF', 'phenotype_contribution-1': 'Partial', 'phenotype_contribution-2': 'Full', 'partial_contribution_explained-1': 'HP:0000501|HP:0000365', diff --git a/seqr/views/utils/anvil_metadata_utils.py b/seqr/views/utils/anvil_metadata_utils.py index 8874a017eb..2a9e287c00 100644 --- a/seqr/views/utils/anvil_metadata_utils.py +++ b/seqr/views/utils/anvil_metadata_utils.py @@ -327,6 +327,7 @@ def _get_parsed_saved_discovery_variants_by_family( annotations = dict( tags=ArrayAgg('varianttag__variant_tag_type__name', distinct=True), + notes=ArrayAgg('variantnote__note', distinct=True, filter=Q(variantnote__report=True)), partial_hpo_terms=ArrayAgg('variantfunctionaldata__metadata', distinct=True, filter=Q(variantfunctionaldata__functional_data_tag='Partial Phenotype Contribution')), validated_name=ArrayAgg('variantfunctionaldata__metadata', distinct=True, filter=Q(variantfunctionaldata__functional_data_tag='Validated Name')), ) @@ -364,6 +365,7 @@ def _get_parsed_saved_discovery_variants_by_family( 'gene_known_for_phenotype': 'Known' if 'Known gene for phenotype' in variant.tags else 'Candidate', 'phenotype_contribution': phenotype_contribution, 'partial_contribution_explained': partial_hpo_terms.replace(', ', '|'), + 'notes': variant.notes, 'sv_type': sv_type, 'sv_name': (variant_json.get('svName') or '{svType}:chr{chrom}:{pos}-{end}'.format(**variant_json)) if sv_type else None, 'variant_type': variant_type, @@ -527,7 +529,7 @@ def _get_genetic_findings_rows(rows: list[dict], individual: Individual, family_ del row['genotypes'] gene_variants = variants_by_gene[row[GENE_COLUMN]] - notes = [] + notes = row['notes'] or [] if len(gene_variants) > 2: discovery_notes = _get_discovery_notes(row, gene_variants, omit_parent_mnvs) if discovery_notes is None: diff --git a/seqr/views/utils/test_utils.py b/seqr/views/utils/test_utils.py index 660d3d1482..7901559819 100644 --- a/seqr/views/utils/test_utils.py +++ b/seqr/views/utils/test_utils.py @@ -815,7 +815,7 @@ def _get_list_param(call, param): 'tagGuid', 'name', 'category', 'color', 'searchHash', 'metadata', 'lastModifiedDate', 'createdBy', 'variantGuids', } -VARIANT_NOTE_FIELDS = {'noteGuid', 'note', 'submitToClinvar', 'lastModifiedDate', 'createdBy', 'variantGuids'} +VARIANT_NOTE_FIELDS = {'noteGuid', 'note', 'report', 'lastModifiedDate', 'createdBy', 'variantGuids'} FUNCTIONAL_FIELDS = { 'tagGuid', 'name', 'color', 'metadata', 'metadataTitle', 'lastModifiedDate', 'createdBy', 'variantGuids', diff --git a/ui/shared/components/panel/variants/FamilyVariantTags.jsx b/ui/shared/components/panel/variants/FamilyVariantTags.jsx index 40b4b62ee7..9adede7048 100644 --- a/ui/shared/components/panel/variants/FamilyVariantTags.jsx +++ b/ui/shared/components/panel/variants/FamilyVariantTags.jsx @@ -14,6 +14,7 @@ import { getVariantId, getMmeSubmissionsByGuid, getGenesById, + getUser, } from 'redux/selectors' import { DISCOVERY_CATEGORY_NAME, MME_TAG_NAME, GREGOR_FINDING_TAG_NAME } from 'shared/utils/constants' import { snakecaseToTitlecase } from 'shared/utils/stringUtils' @@ -32,32 +33,23 @@ const TagTitle = styled.span` color: #999; ` -const RedItal = styled.i` - color: red; -` - const NO_DISPLAY = { display: 'none' } const SHORTCUT_TAGS = ['Review', 'Excluded'] const VARIANT_NOTE_FIELDS = [{ - name: 'submitToClinvar', - label: ( - - ), - component: BooleanCheckbox, - style: { paddingTop: '2em' }, -}, -{ name: 'saveAsGeneNote', label: 'Add to public gene notes', component: BooleanCheckbox, }] +const ANALYST_VARIANT_NOTE_FIELDS = [{ + name: 'report', + label: 'Include in report notes', + component: BooleanCheckbox, +}, ...VARIANT_NOTE_FIELDS, +] + const DEPRECATED_MME_TAG = 'seqr MME (old)' const AIP_TAG_TYPE = 'AIP' const NO_EDIT_TAG_TYPES = [AIP_TAG_TYPE, GREGOR_FINDING_TAG_NAME] @@ -258,7 +250,7 @@ MatchmakerLabel.propTypes = { const FamilyVariantTags = React.memo(({ variant, variantTagNotes, family, projectTagTypes, projectFunctionalTagTypes, dispatchUpdateVariantNote, dispatchUpdateFamilyVariantTags, dispatchUpdateFamilyVariantFunctionalTags, isCompoundHet, variantId, - linkToSavedVariants, mmeSubmissionsByGuid, genesById, + linkToSavedVariants, mmeSubmissionsByGuid, genesById, user, }) => ( family ? ( @@ -337,7 +329,7 @@ const FamilyVariantTags = React.memo(({ initialValues={variantTagNotes} modalId={family.familyGuid} modalTitle={`Variant Note for Family ${family.displayName}`} - additionalEditFields={VARIANT_NOTE_FIELDS} + additionalEditFields={user.isAnalyst ? ANALYST_VARIANT_NOTE_FIELDS : VARIANT_NOTE_FIELDS} defaultId={variantId} idField="variantGuids" isEditable @@ -367,6 +359,7 @@ FamilyVariantTags.propTypes = { dispatchUpdateFamilyVariantFunctionalTags: PropTypes.func.isRequired, mmeSubmissionsByGuid: PropTypes.object, genesById: PropTypes.object, + user: PropTypes.object, } FamilyVariantTags.defaultProps = { @@ -386,6 +379,7 @@ const mapStateToProps = (state, ownProps) => { variantTagNotes: ((getVariantTagNotesByFamilyVariants(state) || {})[ownProps.familyGuid] || {})[variantId], mmeSubmissionsByGuid: getMmeSubmissionsByGuid(state), genesById: getGenesById(state), + user: getUser(state), } }