Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ch/10184 support patient filters sent via postdata #4635

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