Skip to content

Commit

Permalink
Feature/ch/10184 support patient filters sent via postdata (#4635)
Browse files Browse the repository at this point in the history
* refactor StudyId extractor

* 🔨 Refactor Extraction code of StudyViewURL Query

* Update StudyViewPage to accept filterJson from PostData

* Remove debug statement

* Update localdb test

* Update postedQuery.spec.js

* Update to use correct json format of patientIdentifiers

* Remove only from test, and small clean up

* Update StudyViewPage to use lodash unescape to convert html entity

* Remove localdist from getUrl fn
  • Loading branch information
haynescd authored Jun 7, 2023
1 parent bf23302 commit e9a08df
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 135 deletions.
28 changes: 28 additions & 0 deletions end-to-end-test/local/specs/core/postedquery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var _ = require('lodash');
var {
useExternalFrontend,
waitForOncoprint,
getElementByTestHandle,
} = require('../../../shared/specUtils');

const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, '');
Expand Down Expand Up @@ -56,3 +57,30 @@ describe('posting query parameters (instead of GET) to query page', function() {
waitForOncoprint();
});
});

describe('Post Data for StudyView Filtering with filterJson via HTTP Post', () => {
it('Verify PatientIdentifier Filter via postData', () => {
const filterJsonQuery = {
filterJson:
'{"patientIdentifiers":[{"studyId":"lgg_ucsf_2014_test_generic_assay","patientId":"P01"}]}',
};

const NUMBER_OF_PATIENTS_AFTER_FILTER = 1;

goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}`, true);

postDataToUrl(
`${CBIOPORTAL_URL}/study/summary?id=lgg_ucsf_2014_test_generic_assay`,
filterJsonQuery
);

getElementByTestHandle('selected-patients').waitForExist({
timeout: 20000,
});

assert.equal(
getElementByTestHandle('selected-patients').getText(),
NUMBER_OF_PATIENTS_AFTER_FILTER
);
});
});
26 changes: 25 additions & 1 deletion end-to-end-test/shared/specUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,22 @@ function setDropdownOpen(
);
}

/**
* @param {string} url
* @returns {string} modifiedUrl
*/
function getUrl(url) {
if (!useExternalFrontend) {
console.log('Connecting to: ' + url);
} else {
const urlparam = 'localdev';
const prefix = url.indexOf('?') > 0 ? '&' : '?';
console.log('Connecting to: ' + `${url}${prefix}${urlparam}=true`);
url = `${url}${prefix}${urlparam}=true`;
}
return url;
}

function goToUrlAndSetLocalStorage(url, authenticated = false) {
const currentUrl = browser.getUrl();
const needToLogin =
Expand Down Expand Up @@ -554,12 +570,20 @@ function getOncoprintGroupHeaderOptionsElements(trackGroupIndex) {
};
}

/**
*
* @param {string} url
* @param {any} data
* @param {boolean} authenticated
*/
function postDataToUrl(url, data, authenticated = true) {
const currentUrl = browser.getUrl();
const needToLogin =
authenticated && (!currentUrl || !currentUrl.includes('http'));

url = getUrl(url);
browser.execute(
(url, data) => {
(/** @type {string} */ url, /** @type {any} */ data) => {
function formSubmit(url, params) {
// method="smart" means submit with GET iff the URL wouldn't be too long

Expand Down
27 changes: 27 additions & 0 deletions src/pages/studyView/StudyViewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { CustomChartData } from 'shared/api/session-service/sessionServiceModels
import { HelpWidget } from 'shared/components/HelpWidget/HelpWidget';
import { buildCBioPortalPageUrl } from 'shared/api/urls';
import StudyViewPageSettingsMenu from 'pages/studyView/menu/StudyViewPageSettingsMenu';
import QueryString from 'qs';

export interface IStudyViewPageProps {
routing: any;
Expand Down Expand Up @@ -191,6 +192,12 @@ export default class StudyViewPage extends React.Component<
}
}

// Overrite filterJson from URL with what is defined in postData
const postDataFilterJson = this.getFilterJsonFromPostData();
if (postDataFilterJson) {
newStudyViewFilter.filterJson = postDataFilterJson;
}

let updateStoreFromURLPromise = remoteData(() => Promise.resolve([]));
if (!_.isEqual(newStudyViewFilter, this.store.studyViewQueryFilter)) {
this.store.studyViewQueryFilter = newStudyViewFilter;
Expand Down Expand Up @@ -234,6 +241,26 @@ export default class StudyViewPage extends React.Component<
}, 500);
}

private getFilterJsonFromPostData(): string | undefined {
let filterJson: string | undefined;

const parsedFilterJson = _.unescape(
getBrowserWindow()?.postData?.filterJson
);

if (parsedFilterJson) {
try {
JSON.parse(parsedFilterJson);
filterJson = parsedFilterJson;
} catch (error) {
console.error(
`PostData.filterJson does not have valid JSON, error: ${error}`
);
}
}
return filterJson;
}

@autobind
private toolbarRef(ref: any) {
this.toolbar = ref;
Expand Down
156 changes: 22 additions & 134 deletions src/pages/studyView/StudyViewPageStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ import {
PatientIdentifier,
PatientIdentifierFilter,
} from 'shared/model/PatientIdentifierFilter';
import {
ClinicalAttributeQueryExtractor,
SharedGroupsAndCustomDataQueryExtractor,
StudyIdQueryExtractor,
StudyViewFilterQueryExtractor,
StudyViewQueryExtractor,
} from './StudyViewQueryExtractor';

export const STUDY_VIEW_FILTER_AUTOSUBMIT = 'study_view_filter_autosubmit';

Expand Down Expand Up @@ -2136,114 +2143,27 @@ export class StudyViewPageStore

@action
async updateStoreFromURL(query: StudyViewURLQuery): Promise<void> {
let studyIdsString: string = '';
let studyIds: string[] = [];
if (query.studyId) {
studyIdsString = query.studyId;
}
if (query.cancer_study_id) {
studyIdsString = query.cancer_study_id;
}
if (query.id) {
studyIdsString = query.id;
}
if (studyIdsString) {
studyIds = studyIdsString.trim().split(',');
if (!_.isEqual(studyIds, toJS(this.studyIds))) {
// update if different
this.studyIds = studyIds;
}
}
if (query.sharedGroups) {
this.sharedGroupSet = stringListToSet(
query.sharedGroups.trim().split(',')
);
// Open group comparison manager if there are shared groups in the url
this.showComparisonGroupUI = true;
}
if (query.sharedCustomData) {
this.sharedCustomChartSet = stringListToSet(
query.sharedCustomData.trim().split(',')
);
this.showCustomDataSelectionUI = true;
}
const queryExtractors: Array<StudyViewQueryExtractor<void>> = [
new StudyIdQueryExtractor(),
new SharedGroupsAndCustomDataQueryExtractor(),
];

// We do not support studyIds in the query filters
let filters: Partial<StudyViewFilter> = {};
const asyncQueryExtractors: Array<StudyViewQueryExtractor<
Promise<void>
>> = [];
if (query.filterJson) {
const parsedFilterJson = this.parseRawFilterJson(query.filterJson);
if (query.filterJson.includes('patientIdentifiers')) {
const sampleListIds = studyIds.map(s => s.concat('', '_all'));
const samples = await this.fetchSamplesWithSampleListIds(
sampleListIds
);
filters = this.getStudyViewFilterFromPatientIdentifierFilter(
parsedFilterJson as PatientIdentifierFilter,
samples
);
} else {
filters = parsedFilterJson as Partial<StudyViewFilter>;
}
this.updateStoreByFilters(filters);
asyncQueryExtractors.push(new StudyViewFilterQueryExtractor());
} else if (query.filterAttributeId && query.filterValues) {
const clinicalAttributes = _.uniqBy(
await defaultClient.fetchClinicalAttributesUsingPOST({
studyIds: studyIds,
}),
clinicalAttribute =>
`${clinicalAttribute.patientAttribute}-${clinicalAttribute.clinicalAttributeId}`
);

const matchedAttr = _.find(
clinicalAttributes,
(attr: ClinicalAttribute) =>
attr.clinicalAttributeId.toUpperCase() ===
query.filterAttributeId!.toUpperCase()
);
if (matchedAttr !== undefined) {
if (matchedAttr.datatype == DataType.NUMBER) {
filters.clinicalDataFilters = [
{
attributeId: matchedAttr.clinicalAttributeId,
values: query
.filterValues!.split(',')
.map(range => {
const convertResult = range.split('-');
return {
start: Number(convertResult[0]),
end: Number(convertResult[1]),
} as DataFilterValue;
}),
} as ClinicalDataFilter,
];
} else {
filters.clinicalDataFilters = [
{
attributeId: matchedAttr.clinicalAttributeId,
values: getClinicalEqualityFilterValuesByString(
query.filterValues
).map(value => ({ value })),
} as ClinicalDataFilter,
];
}
this.updateStoreByFilters(filters);
} else {
this.pageStatusMessages['unknownClinicalAttribute'] = {
message: `The clinical attribute ${query.filterAttributeId} is not available for this study`,
status: 'danger',
};
}
asyncQueryExtractors.push(new ClinicalAttributeQueryExtractor());
}
}

parseRawFilterJson(filterJson: string): any {
let rawJson;
try {
rawJson = JSON.parse(decodeURIComponent(filterJson));
} catch (e) {
console.error('FilterJson invalid Json: error: ', e);
for (const extractor of queryExtractors) {
extractor.accept(query, this);
}
return rawJson;

await Promise.all(
asyncQueryExtractors.map(ex => ex.accept(query, this))
);
}

fetchSamplesWithSampleListIds(sampleListIds: string[]) {
Expand All @@ -2255,38 +2175,6 @@ export class StudyViewPageStore
});
}

getStudyViewFilterFromPatientIdentifierFilter(
patientIdentifierFilter: PatientIdentifierFilter,
samples: Sample[]
): Partial<StudyViewFilter> {
const filters: Partial<StudyViewFilter> = {};
const sampleIdentifiers = this.convertPatientIdentifiersToSampleIdentifiers(
patientIdentifierFilter.patientIdentifiers,
samples
);
if (sampleIdentifiers.length > 0) {
filters.sampleIdentifiers = sampleIdentifiers;
}
return filters;
}

convertPatientIdentifiersToSampleIdentifiers(
patientIdentifiers: Array<PatientIdentifier>,
samples: Sample[]
): SampleIdentifier[] {
const patientIdentifiersMap = new Map<string, PatientIdentifier>(
patientIdentifiers.map(p => [p.studyId.concat('_', p.patientId), p])
);
return samples
.filter(s =>
patientIdentifiersMap.has(s.studyId.concat('_', s.patientId))
)
.map(s => ({
sampleId: s.sampleId,
studyId: s.studyId,
}));
}

@computed
get initialFilters(): StudyViewFilter {
let initialFilter = {} as StudyViewFilter;
Expand Down
Loading

0 comments on commit e9a08df

Please sign in to comment.