Skip to content

Commit

Permalink
Merge pull request #3502 from broadinstitute/multiple-variant-tag-fil…
Browse files Browse the repository at this point in the history
…tering

Multiple variant tags filtering
  • Loading branch information
hanars authored Sep 7, 2023
2 parents 7f9e733 + 91d3959 commit 48143e0
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 70 deletions.
7 changes: 5 additions & 2 deletions seqr/views/apis/summary_data_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,11 @@ def saved_variants_page(request, tag):
if tag == 'ALL':
saved_variant_models = SavedVariant.objects.exclude(varianttag=None)
else:
tag_type = VariantTagType.objects.get(name=tag, project__isnull=True)
saved_variant_models = SavedVariant.objects.filter(varianttag__variant_tag_type=tag_type)
tags = tag.split(';')
tag_types = VariantTagType.objects.filter(name__in=tags, project__isnull=True)
saved_variant_models = SavedVariant.objects.all()
for tt in tag_types:
saved_variant_models = saved_variant_models.filter(varianttag__variant_tag_type=tt).distinct()

saved_variant_models = saved_variant_models.filter(family__project__guid__in=get_project_guids_user_can_view(request.user))

Expand Down
10 changes: 9 additions & 1 deletion seqr/views/apis/summary_data_api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ def test_saved_variants_page(self):
expected_variant_guids.add('SV0000002_1248367227_r0390_100')
self.assertSetEqual(set(response.json()['savedVariantsByGuid'].keys()), expected_variant_guids)

multi_tag_url = reverse(saved_variants_page, args=['Review;Tier 1 - Novel gene and phenotype'])
response = self.client.get('{}?gene=ENSG00000135953'.format(multi_tag_url))
self.assertEqual(response.status_code, 200)
self.assertSetEqual(set(response.json()['savedVariantsByGuid'].keys()), {'SV0000001_2103343353_r0390_100'})

def test_hpo_summary_data(self):
url = reverse(hpo_summary_data, args=['HP:0002011'])
self.check_require_login(url)
Expand Down Expand Up @@ -238,7 +243,7 @@ def assert_has_expected_calls(self, users, skip_group_call_idxs=None):
self.mock_get_groups.assert_has_calls(group_calls)
self.mock_get_ws_acl.assert_not_called()
self.mock_get_group_members.assert_not_called()
self.mock_get_ws_access_level.assert_not_called()


# Test for permissions from AnVIL only
class AnvilSummaryDataAPITest(AnvilAuthenticationTestCase, SummaryDataAPITest):
Expand All @@ -249,9 +254,12 @@ class AnvilSummaryDataAPITest(AnvilAuthenticationTestCase, SummaryDataAPITest):
def test_mme_details(self, *args):
super(AnvilSummaryDataAPITest, self).test_mme_details(*args)
assert_has_expected_calls(self, [self.no_access_user, self.manager_user, self.analyst_user])
self.mock_get_ws_access_level.assert_not_called()

def test_saved_variants_page(self):
super(AnvilSummaryDataAPITest, self).test_saved_variants_page()
assert_has_expected_calls(self, [
self.no_access_user, self.manager_user, self.manager_user, self.analyst_user, self.analyst_user
], skip_group_call_idxs=[2])
self.mock_get_ws_access_level.assert_called_with(
self.analyst_user, 'my-seqr-billing', 'anvil-1kg project nåme with uniçøde')
21 changes: 12 additions & 9 deletions ui/pages/Project/components/SavedVariants.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
VARIANT_PER_PAGE_FIELD,
EXCLUDED_TAG_NAME,
REVIEW_TAG_NAME,
DISCOVERY_CATEGORY_NAME,
SHOW_ALL,
} from 'shared/utils/constants'
import UpdateButton from 'shared/components/buttons/UpdateButton'
import { LargeMultiselect, Dropdown } from 'shared/components/form/Inputs'
Expand All @@ -24,7 +26,7 @@ import { TAG_FORM_FIELD } from '../constants'
import { loadSavedVariants, updateSavedVariantTable } from '../reducers'
import {
getCurrentProject, getProjectTagTypeOptions, getTaggedVariantsByFamily, getProjectVariantSavedByOptions,
getSavedVariantTagTypeCounts, getSavedVariantTagTypeCountsByFamily,
getSavedVariantTagTypeCounts, getSavedVariantTagTypeCountsByFamily, getSavedVariantTableState,
} from '../selectors'
import VariantTagTypeBar, { getSavedVariantsLinkPath } from './VariantTagTypeBar'
import SelectSavedVariantsTable, { TAG_COLUMN, VARIANT_POS_COLUMN, GENES_COLUMN } from './SelectSavedVariantsTable'
Expand All @@ -37,8 +39,6 @@ const LabelLink = styled(Link)`
}
`

const ALL_FILTER = 'ALL'

const mapSavedByInputStateToProps = state => ({
options: getProjectVariantSavedByOptions(state),
})
Expand Down Expand Up @@ -134,6 +134,7 @@ class BaseProjectSavedVariants extends React.PureComponent {
tagTypeCounts: PropTypes.object,
updateTableField: PropTypes.func,
loadProjectSavedVariants: PropTypes.func,
categoryFilter: PropTypes.string,
}

getUpdateTagUrl = (newTag) => {
Expand All @@ -147,7 +148,7 @@ class BaseProjectSavedVariants extends React.PureComponent {
return getSavedVariantsLinkPath({
projectGuid: project.projectGuid,
analysisGroupGuid: match.params.analysisGroupGuid,
tag: !isCategory && newTag !== ALL_FILTER && newTag,
tag: !isCategory && newTag !== SHOW_ALL && newTag,
familyGuid: match.params.familyGuid,
})
}
Expand Down Expand Up @@ -191,7 +192,7 @@ class BaseProjectSavedVariants extends React.PureComponent {
})
return acc
}, [{
value: ALL_FILTER,
value: SHOW_ALL,
text: 'All Saved',
content: (
<LabelLink
Expand Down Expand Up @@ -234,14 +235,15 @@ class BaseProjectSavedVariants extends React.PureComponent {
}

render() {
const { project, analysisGroup, loadProjectSavedVariants, ...props } = this.props
const { familyGuid } = props.match.params
const { project, analysisGroup, loadProjectSavedVariants, categoryFilter, ...props } = this.props
const { familyGuid, tag, variantGuid } = props.match.params
const appliedTagCategoryFilter = tag || (variantGuid ? null : (categoryFilter || SHOW_ALL))

return (
<SavedVariants
tagOptions={this.tagOptions()}
filters={NON_DISCOVERY_FILTER_FIELDS}
discoveryFilters={FILTER_FIELDS}
filters={appliedTagCategoryFilter === DISCOVERY_CATEGORY_NAME ? FILTER_FIELDS : NON_DISCOVERY_FILTER_FIELDS}
selectedTag={appliedTagCategoryFilter}
additionalFilter={
(project.canEdit && familyGuid) ? <LinkSavedVariants familyGuid={familyGuid} {...props} /> : null
}
Expand All @@ -262,6 +264,7 @@ const mapStateToProps = (state, ownProps) => ({
tagTypeCounts: ownProps.match.params.familyGuid ?
getSavedVariantTagTypeCountsByFamily(state)[ownProps.match.params.familyGuid] :
getSavedVariantTagTypeCounts(state, ownProps),
categoryFilter: getSavedVariantTableState(state)?.categoryFilter,
})

const mapDispatchToProps = dispatch => ({
Expand Down
2 changes: 1 addition & 1 deletion ui/pages/Project/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const loadSavedVariants = ({ familyGuids, variantGuid, tag }) => (dispatc
// Do not load if already loaded
let expectedFamilyGuids
if (variantGuid) {
if (state.savedVariantsByGuid[variantGuid]) {
if (variantGuid.split(',').every(g => state.savedVariantsByGuid[g])) {
return
}
url = `${url}/${variantGuid}`
Expand Down
1 change: 1 addition & 0 deletions ui/pages/Project/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const getMmeSubmissionsLoading = state => state.mmeSubmissionsLoading.isL
export const getSamplesLoading = state => state.samplesLoading.isLoading
export const getTagTypesLoading = state => state.tagTypesLoading.isLoading
export const getFamilyTagTypeCounts = state => state.familyTagTypeCounts
export const getSavedVariantTableState = state => state.savedVariantTableState
const getFamiliesTableFiltersByProject = state => state.familyTableFilterState

export const getCurrentProject = createSelector(
Expand Down
12 changes: 4 additions & 8 deletions ui/pages/SummaryData/components/SavedVariants.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,10 @@ const TAG_OPTIONS = [
label: { empty: true, circular: true, style: { backgroundColor: 'white' } },
}))

TAG_OPTIONS.push({
value: SHOW_ALL,
text: 'All',
key: 'all',
label: { empty: true, circular: true, style: { backgroundColor: 'white' } },
})

const PAGE_URL = '/summary_data/saved_variants'

const getUpdateTagUrl =
(selectedTag, match) => `${PAGE_URL}/${selectedTag}${match.params.gene ? `/${match.params.gene}` : ''}`
(selectedTag, match) => `${PAGE_URL}/${(selectedTag || []).join(';') || SHOW_ALL}${match.params.gene ? `/${match.params.gene}` : ''}`

const getGeneHref = tag => selectedGene => `${PAGE_URL}/${tag || SHOW_ALL}/${selectedGene.key}`

Expand All @@ -82,6 +75,9 @@ const BaseSavedVariants = React.memo(({ loadVariants, geneDetail, ...props }) =>
filters={FILTER_FIELDS}
getUpdateTagUrl={getUpdateTagUrl}
loadVariants={loadVariants}
summaryFullWidth
multiple
selectedTag={tag && tag.split(';').filter(t => t !== SHOW_ALL)}
additionalFilter={
<StyledForm inline>
<Form.Field
Expand Down
6 changes: 3 additions & 3 deletions ui/pages/SummaryData/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export const loadSuccessStory = successStoryTypes => (dispatch) => {

export const loadSavedVariants = ({ tag, gene = '' }) => (dispatch, getState) => {
// Do not load if already loaded
if (tag) {
if (getState().savedVariantTags[tag]) {
if (tag && tag !== SHOW_ALL) {
const loadedTags = getState().savedVariantTags
if (loadedTags[tag] || tag.split(';').some(t => loadedTags[t])) {
return
}
} else if (!gene) {
Expand Down Expand Up @@ -86,7 +87,6 @@ export const reducers = {
savedVariantTags: createSingleObjectReducer(RECEIVE_SAVED_VARIANT_TAGS),
externalAnalysisUploadStats: createSingleValueReducer(RECEIVE_EXTERNAL_ANALYSIS_UPLOAD_STATS, {}),
allProjectSavedVariantTableState: createSingleObjectReducer(UPDATE_ALL_PROJECT_SAVED_VARIANT_TABLE_STATE, {
categoryFilter: SHOW_ALL,
sort: SORT_BY_XPOS,
page: 1,
recordsPerPage: 25,
Expand Down
28 changes: 12 additions & 16 deletions ui/shared/components/panel/variants/SavedVariants.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { Grid, Dropdown, Message } from 'semantic-ui-react'
import styled from 'styled-components'

import { getSavedVariantsIsLoading, getSavedVariantsLoadingError } from 'redux/selectors'
import {
DISCOVERY_CATEGORY_NAME,
VARIANT_PAGINATION_FIELD,
} from 'shared/utils/constants'
import { VARIANT_PAGINATION_FIELD } from 'shared/utils/constants'

import ExportTableButton from '../../buttons/ExportTableButton'
import StateChangeForm from '../../form/StateChangeForm'
Expand All @@ -22,7 +19,6 @@ import {
getVisibleSortedSavedVariants,
} from './selectors'

const ALL_FILTER = 'ALL'
const MAX_FILTERS = 4

const ControlsRow = styled(Grid.Row)`
Expand All @@ -43,8 +39,8 @@ class SavedVariants extends React.PureComponent {
match: PropTypes.object,
history: PropTypes.object,
tagOptions: PropTypes.arrayOf(PropTypes.object),
selectedTag: PropTypes.any, // eslint-disable-line react/forbid-prop-types
filters: PropTypes.arrayOf(PropTypes.object),
discoveryFilters: PropTypes.arrayOf(PropTypes.object),
loading: PropTypes.bool,
error: PropTypes.string,
variantsToDisplay: PropTypes.arrayOf(PropTypes.any),
Expand All @@ -59,6 +55,8 @@ class SavedVariants extends React.PureComponent {
loadVariants: PropTypes.func,
additionalFilter: PropTypes.node,
tableSummaryComponent: PropTypes.elementType,
multiple: PropTypes.bool,
summaryFullWidth: PropTypes.bool,
}

state = { showAllFilters: false }
Expand All @@ -74,17 +72,14 @@ class SavedVariants extends React.PureComponent {

render() {
const {
match, tableState, filters, discoveryFilters, totalPages, variantsToDisplay, totalVariantsCount, firstRecordIndex,
match, tableState, filters, totalPages, variantsToDisplay, totalVariantsCount, firstRecordIndex,
tableSummaryComponent, loading, filteredVariantsCount, tagOptions, additionalFilter, updateTableField,
variantExportConfig, loadVariants, error,
variantExportConfig, loadVariants, error, multiple, summaryFullWidth, selectedTag,
} = this.props
const { showAllFilters } = this.state
const { familyGuid, variantGuid, tag } = match.params

const appliedTagCategoryFilter = tag || (variantGuid ? null : (tableState.categoryFilter || ALL_FILTER))
const { familyGuid, variantGuid } = match.params

let shownFilters = (discoveryFilters && appliedTagCategoryFilter === DISCOVERY_CATEGORY_NAME) ?
discoveryFilters : filters
let shownFilters = filters
const hasHiddenFilters = !showAllFilters && shownFilters.length > MAX_FILTERS
if (hasHiddenFilters) {
shownFilters = shownFilters.slice(0, MAX_FILTERS)
Expand All @@ -107,18 +102,19 @@ class SavedVariants extends React.PureComponent {
})}
{!loading && (
<ControlsRow>
<Grid.Column width={4}>
<Grid.Column width={summaryFullWidth ? 16 : 4}>
{`Showing ${shownSummary} ${filteredVariantsCount} `}
<Dropdown
inline
multiple={multiple}
options={tagOptions}
value={appliedTagCategoryFilter}
value={selectedTag}
onChange={this.navigateToTag}
/>
{` variants ${allShown ? '' : `(${totalVariantsCount} total)`}`}

</Grid.Column>
<Grid.Column width={12} floated="right" textAlign="right">
<Grid.Column width={summaryFullWidth ? 16 : 12} floated="right" textAlign="right">
{additionalFilter}
{!variantGuid && (
<StateChangeForm
Expand Down
97 changes: 67 additions & 30 deletions ui/shared/components/panel/variants/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,78 @@ const matchingVariants = (variants, matchFunc) => variants.filter(o => (Array.is
// sorts manual variants to top of list, as manual variants are missing all populations
const sortCompHet = (a, b) => (a.populations ? 1 : 0) - (b.populations ? 1 : 0)

export const getPairedSelectedSavedVariants = createSelector(
getSavedVariantsByGuid,
const getProjectSavedVariantsSelection = createSelector(
(state, props) => props.match.params,
getFamiliesByGuid,
getAnalysisGroupsByGuid,
(state, props) => (props.project || {}).projectGuid,
state => state.currentProjectGuid,
getVariantTagsByGuid,
getVariantNotesByGuid,
(savedVariants, { tag, gene, familyGuid, analysisGroupGuid, variantGuid }, familiesByGuid, analysisGroupsByGuid,
projectGuid, tagsByGuid, notesByGuid) => {
let variants = Object.values(savedVariants)
if (variantGuid) {
variants = variants.filter(o => variantGuid.split(',').includes(o.variantGuid))
return variants.length > 1 ? [variants] : variants
({ tag, familyGuid, analysisGroupGuid, variantGuid }, familiesByGuid, analysisGroupsByGuid,
projectGuid, tagsByGuid) => {
if (!projectGuid) {
return null
}

if (analysisGroupGuid && analysisGroupsByGuid[analysisGroupGuid]) {
let variantFilter
if (variantGuid) {
variantFilter = o => variantGuid.split(',').includes(o.variantGuid)
} else if (analysisGroupGuid && analysisGroupsByGuid[analysisGroupGuid]) {
const analysisGroupFamilyGuids = analysisGroupsByGuid[analysisGroupGuid].familyGuids
variants = variants.filter(o => o.familyGuids.some(fg => analysisGroupFamilyGuids.includes(fg)))
variantFilter = o => o.familyGuids.some(fg => analysisGroupFamilyGuids.includes(fg))
} else if (familyGuid) {
variants = variants.filter(o => o.familyGuids.includes(familyGuid))
} else if (projectGuid) {
variants = variants.filter(o => o.familyGuids.some(fg => familiesByGuid[fg].projectGuid === projectGuid))
variantFilter = o => o.familyGuids.includes(familyGuid)
} else {
variantFilter = o => o.familyGuids.some(fg => familiesByGuid[fg].projectGuid === projectGuid)
}

const pairedFilters = []
if (tag === NOTE_TAG_NAME) {
pairedFilters.push(({ noteGuids }) => noteGuids.length)
} else if (tag === MME_TAG_NAME) {
pairedFilters.push(({ mmeSubmissions = [] }) => mmeSubmissions.length)
} else if (tag && tag !== SHOW_ALL) {
pairedFilters.push(({ tagGuids }) => tagGuids.some(tagGuid => tagsByGuid[tagGuid].name === tag))
} else if (!(familyGuid || analysisGroupGuid)) {
pairedFilters.push(({ tagGuids }) => tagGuids.length)
}

return [variantFilter, pairedFilters]
},
)

const getSummaryDataSavedVariantsSelection = createSelector(
(state, props) => props.match.params,
state => state.currentProjectGuid,
getVariantTagsByGuid,
({ tag, gene }, projectGuid, tagsByGuid) => {
if (projectGuid) {
return null
}
const pairedFilters = []
if (gene) {
pairedFilters.push(({ transcripts }) => gene in (transcripts || {}))
} if (tag && tag !== SHOW_ALL) {
const tags = tag.split(';')
pairedFilters.push(({ tagGuids }) => tags.every(t => tagGuids.some(tagGuid => tagsByGuid[tagGuid].name === t)))
}

const variantFilter = tag || gene ? null : () => false
return [variantFilter, pairedFilters]
},
)

export const getPairedSelectedSavedVariants = createSelector(
getProjectSavedVariantsSelection,
getSummaryDataSavedVariantsSelection,
getSavedVariantsByGuid,
getVariantTagsByGuid,
getVariantNotesByGuid,
(projectVariants, summaryDataVariants, savedVariants, tagsByGuid, notesByGuid) => {
const [variantFilter, pairedFilters] = projectVariants || summaryDataVariants

let variants = Object.values(savedVariants)
if (variantFilter) {
variants = variants.filter(variantFilter)
}

const selectedVariantsByGuid = variants.reduce((acc, variant) => ({ ...acc, [variant.variantGuid]: variant }), {})
Expand Down Expand Up @@ -130,21 +179,9 @@ export const getPairedSelectedSavedVariants = createSelector(
return acc
}, [])

if (tag === NOTE_TAG_NAME) {
pairedVariants = matchingVariants(pairedVariants, ({ noteGuids }) => noteGuids.length)
} else if (tag === MME_TAG_NAME) {
pairedVariants = matchingVariants(pairedVariants, ({ mmeSubmissions = [] }) => mmeSubmissions.length)
} else if (tag && tag !== SHOW_ALL) {
pairedVariants = matchingVariants(
pairedVariants, ({ tagGuids }) => tagGuids.some(tagGuid => tagsByGuid[tagGuid].name === tag),
)
} else if (!(familyGuid || analysisGroupGuid)) {
pairedVariants = matchingVariants(pairedVariants, ({ tagGuids }) => tagGuids.length)
}

if (gene) {
pairedVariants = matchingVariants(pairedVariants, ({ transcripts }) => gene in (transcripts || {}))
}
pairedFilters.forEach((pairedFilter) => {
pairedVariants = matchingVariants(pairedVariants, pairedFilter)
})

return pairedVariants
},
Expand Down
Loading

0 comments on commit 48143e0

Please sign in to comment.