diff --git a/.gitignore b/.gitignore index 880596013..472a1dec2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ /js/assets/fonts /js/assets/images js/config-local.js +js/config-gis.js js/version.js package-lock.json -.vscode \ No newline at end of file +.vscode +.DS_Store diff --git a/js/pages/cohort-definitions/Validation/QuestionSet.js b/js/pages/cohort-definitions/Validation/QuestionSet.js new file mode 100644 index 000000000..089f7cf3f --- /dev/null +++ b/js/pages/cohort-definitions/Validation/QuestionSet.js @@ -0,0 +1,75 @@ +define(['knockout', 'services/Validation', './QuestionSetForm'], function (ko, ValidationService, QuestionSetForm) { + function QuestionSet(id, cohortName, qSetId, qSetName, qSetQuestions, mode) { + const self = this; + self.qsetName = ko.observable(qSetName); + self.setId = ko.observable(qSetId); + self.setQuestions = ko.observableArray(qSetQuestions); + self.mode = ko.observable(mode); + self.questionItems = ko.observableArray([]); + + self.questionSetForm = new QuestionSetForm(id, cohortName); + + self.goBack = function (parent) { + setTimeout(() => { + parent.valTabMode(parent.default_view); + // if you don't do this, ko complains. + }, 1000); + + + }; + + self.resetValues = function (id, cohortName, qSetId, qSetName, qSetQuestions, mode) { + self.qsetName(qSetName); + self.setId(qSetId); + self.setQuestions(qSetQuestions); + self.mode(mode); + self.showSelectedQset(); + }; + + self.submitQsetForm = function (sn) { + return self.questionSetForm.createQuestionSet(sn); + }; + + self.initialize = function () { + self.questionSetForm.initialize(); + }; + + + self.showSelectedQset = function () { + self.questionItems([]); + for (let i = 0; i < self.setQuestions().length; i++) { + let qs = {}; + let qsAnswers = []; + let cq = self.setQuestions()[i]; + let qnum = "Question " + (i + 1) + ': '; + + if (cq !== undefined) { + qs['text'] = qnum + cq.text; + qs['type'] = 'Question Type: ' + cq.type; + qs['caseQ'] = 'Case Question: ' + cq.caseQuestion; + qs['req'] = 'Required: ' + cq.required; + for (let j = 0; j < cq.answers.length; j++) { + if (cq.type !== 'TEXTAREA') { + qsAnswers.push(cq.answers[j].text); + } else { + qsAnswers.push("None"); + } + } + } else { + qs['text'] = qnum + ''; + qs['type'] = 'Question Type: ' + ''; + qs['caseQ'] = 'Case Question: ' + ''; + qs['req'] = 'Required: ' + ''; + } + qs['answers'] = qsAnswers; + self.questionItems.push(qs); + } + }; + + + } + + QuestionSet.prototype.constructor = QuestionSet; + return QuestionSet; + +}); \ No newline at end of file diff --git a/js/pages/cohort-definitions/Validation/QuestionSetForm.js b/js/pages/cohort-definitions/Validation/QuestionSetForm.js new file mode 100644 index 000000000..c7a62cae6 --- /dev/null +++ b/js/pages/cohort-definitions/Validation/QuestionSetForm.js @@ -0,0 +1,174 @@ +define(['knockout', 'services/Validation'], function (ko, ValidationService) { + function QuestionSetForm(id, cohortName) { + const self = this; + self.questions = ko.observableArray([]); + self.questionSetName = ko.observable(); + self.questionTypes = ko.observableArray(['Text', 'Radio Button', 'Checkbox', 'Numeric', 'Date']); + self.bools = ko.observableArray(['false', 'true']); + self.caseBools = ko.observableArray(['false', 'true']); + self.errorMessage = ko.observable(); + + + function Answer() { + const self = this; + self.text = ko.observable(); + self.value = ''; + // self.helpText = ko.observable() + } + + function Question() { + const self = this; + self.text = ko.observable(''); + // self.helpText = ko.observable(''); + self.type = ko.observable(''); + self.caseQuestion = ko.observable(''); + self.required = ko.observable(''); + self.answers = ko.observableArray([]); + + self.addAnswer = function () { + if ((self.type() !== 'Text' && self.type() !== 'Numeric' && self.type() !== 'Date') && self.type() !== undefined) { + self.answers.push(new Answer()); + } + }; + self.removeAnswer = function (item) { + self.answers.remove(item); + }; + + self.questionTypeChanged = function (obj, evt) { + if (obj !== 'Text' && obj !== 'Numeric' && obj !== 'Date') { + if (self.answers().length === 0) { + self.addAnswer(); + } + } + }; + } + + self.addQuestion = function () { + self.questions.push(new Question()); + }; + + self.initialize = function () { + self.addQuestion(); + }; + + + self.removeQuestion = function (item) { + self.questions.remove(item); + }; + + self.createQuestionSet = function (sampleSourceKey) { + if (self.errorMessage() != null) { + self.errorMessage(''); + } + + //check that qset name is not undefined + if (self.questionSetName() === undefined) { + self.errorMessage('Please enter question set name.'); + return null; + } + + if (self.questions().length === 0) { + self.errorMessage("Please include at least one question"); + return null; + } + + let numCaseQuestions = 0; + let currentAnswer = null; + let i; + let j = 0; + for (i = 0; i < self.questions().length; i++) { + + let currentQuestion = self.questions()[i]; + let numAnswers = self.questions()[i].answers().length; + let currentQuestionType = self.questions()[i].type(); + + + //check that questions are not null or unselected + if (currentQuestion.text() == null || + // currentQuestion.helpText() == null || + currentQuestion.type() == null || + currentQuestion.caseQuestion() == null || + currentQuestion.required() == null) { + self.errorMessage('Please do not leave any question field blank or unselected.'); + break; + } + + const multipleAnswerType = (currentQuestionType !== 'Text' && currentQuestionType !== 'Numeric' && currentQuestionType !== 'Date'); + //enforce that all have answers unless text + if (numAnswers === 0 && multipleAnswerType) { + self.errorMessage('Checkbox and radio button questions must have answers'); + break; + } + + //check that answers don't have null values + for (j = 0; j < self.questions()[i].answers().length; j++) { + currentAnswer = self.questions()[i].answers()[j]; + if (currentAnswer.text() == null && // || currentAnswer.helpText() == null) && + multipleAnswerType) { + self.errorMessage('Please do not leave answers blank.'); + break; + } + } + + + //check that only one question is a case question + if (currentQuestion.caseQuestion() === 'true') { + if (++numCaseQuestions > 1) { + self.errorMessage('Only one question can be a case question.'); + break; + } + } + + } + + if (self.errorMessage()) { + return null; + } + + for (i = 0; i < self.questions().length; i++) { + if (self.questions()[i].type() === 'Text') { + self.questions()[i].answers.push(new Answer()); + self.questions()[i].answers()[0].text = ''; + self.questions()[i].type = 'TEXTAREA'; + } else if (self.questions()[i].type() === 'Numeric') { + self.questions()[i].answers.push(new Answer()); + self.questions()[i].answers()[0].text = ''; + self.questions()[i].type = 'NUMERIC'; + } else if (self.questions()[i].type() === 'Date') { + self.questions()[i].answers.push(new Answer()); + self.questions()[i].answers()[0].text = ''; + self.questions()[i].type = 'DATE'; + } else if (self.questions()[i].type() === "Checkbox") { + self.questions()[i].type = 'MULTI_SELECT'; + for (j = 0; j < self.questions()[i].answers().length; j++) { + currentAnswer = self.questions()[i].answers()[j]; + currentAnswer.value = currentAnswer.text(); + } + } else if (self.questions()[i].type() === "Radio Button") { + self.questions()[i].type = 'SINGLE_SELECT'; + for (j = 0; j < self.questions()[i].answers().length; j++) { + currentAnswer = self.questions()[i].answers()[j]; + currentAnswer.value = j; + } + } + } + + const data = { + cohortName: cohortName, + cohortSource: sampleSourceKey, + cohortId: id, + name: self.questionSetName(), + questions: ko.toJS(self.questions()) + }; + return ValidationService.submitQuestionSet(JSON.stringify(data)); + + + }; + + self.initialize(); + } + + QuestionSetForm.prototype.constructor = QuestionSetForm; + return QuestionSetForm; + +}); \ No newline at end of file diff --git a/js/pages/cohort-definitions/Validation/ValidationTool.js b/js/pages/cohort-definitions/Validation/ValidationTool.js new file mode 100644 index 000000000..251377b6d --- /dev/null +++ b/js/pages/cohort-definitions/Validation/ValidationTool.js @@ -0,0 +1,534 @@ +define(['knockout', 'services/Validation', 'services/Annotation', './QuestionSet', 'utils/CsvUtils', 'services/Sample'], + function (ko, ValidationService, annotationService, QuestionSet, CsvUtils, sampleService) { + function ValidationTool(id, cohortName, sourceStatus, reportSource, sampleSource) { + const self = this; + const DEFAULT_VALIDATION_VIEW = 'show_qsets'; + const NEW_QUESTION_SET_VIEW = 'new_qset'; + const SELECTED_QUESTION_SET_VIEW = 'selected_qset'; + + self.default_view = DEFAULT_VALIDATION_VIEW; + + self.cohortId = id; + self.valTabMode = ko.observable(DEFAULT_VALIDATION_VIEW); + self.showCreateValidationSet = ko.observable(false); + self.errorMessage = ko.observable(''); + + if (!sampleSource) { + let k = Object.keys(sourceStatus); + if (k.length === 1) { + sampleSource = k[0]; + } + } + + if (!sampleSource && reportSource) { + sampleSource = reportSource; + } + + self.sampleSourceKey = ko.observable(sampleSource); + self.sampleSize = ko.observable(); + self.sampleName = ko.observable(); + self.samples = ko.observableArray([]); + self.display_sample = ko.observableArray([]); + + self.clickedSet = ko.observable(); + + self.validationAnnotationSetsLoading = ko.observable(false); + self.validationAnnotationSets = ko.observableArray([]); + self.rawAnnotationSets = ko.observableArray([]); + + self.annotationStudySets = ko.observableArray([]); + self.annotationStudiesLoading = ko.observable(false); + + self.studyResultsModalShown = ko.observable(false); + self.studyResultsLoading = ko.observable(false); + self.studySetResults = ko.observableArray([]); + self.selectResult = ko.observable(); + + self.questionSet = new QuestionSet(id, cohortName, null, null, [], 'NEW'); + + self.annotationSampleLoading = ko.observable(false); + self.annotationSampleLinkShown = ko.observable(false); + self.isSampleLinking = ko.observable(false); + self.sampleSets = ko.observableArray([]); + + self.validationAnnotationSetCols = [ + { + title: 'ID', + data: 'id' + }, + { + title: 'Name', + data: 'name' + }, + { + title: '# Questions', + data: 'q_num' + }, + { + title: 'Actions', + sortable: false, + render: function () { + return ` `; + } + } + ]; + + self.annotationStudySetCols = [ + { + title: 'Set ID', + data: 'questionSetId' + }, + { + title: 'Set Name', + data: 'questionSetName' + }, + { + title: 'Sample ID', + data: 'cohortSampleId' + }, + { + title: 'Sample Name', + data: 'cohortSampleName' + }, + { + title: 'Actions', + sortable: false, + render: function () { + return ` `; + } + } + + ]; + self.studySetResultCols = [ + { + title: 'Cohort ID', + data: 'cohortId' + }, + { + title: 'Cohort Name', + data: 'cohortName' + }, + { + title: 'Source', + data: 'dataSourceId' + }, + { + title: 'Sample ID', + data: 'cohortSampleId' + }, + { + title: 'Sample Name', + data: 'cohortSampleName' + }, + { + title: 'Set ID', + data: 'questionSetId' + }, + { + title: 'Set Name', + data: 'questionSetName' + }, + { + title: 'Patient', + data: 'patientId' + }, + { + title: 'Question', + data: 'questionText' + }, + { + title: 'Answer Value', + data: 'answerValue' + }, + { + title: 'Answer Text', + data: 'answerText' + }, + { + title: 'Case Question', + data: 'caseStatus' + } + ]; + + self.sampleCols = [ + { + title: 'ID', + data: 'id' + }, + { + title: 'Sample Name', + data: 'name' + }, + { + title: 'Sample Size', + data: 'size', + }, + { + title: 'Actions', + sortable: false, + render: function () { + return ``; + } + } + ]; + + self.filterSet = ko.computed(function () { + if (!self.clickedSet()) { + return undefined; + } else { + return ko.utils.arrayFilter(self.rawAnnotationSets(), function (s) { + return s.id === self.clickedSet(); + }); + } + }); + + self.getSampleList = function (cohortDefinitionId, sourceKey) { + this.annotationSampleLoading(true); + this.annotationSampleLinkShown(true); + sampleService.getSampleList({cohortDefinitionId, sourceKey}) + .then(res => { + if (res.generationStatus !== "COMPLETE") { + alert('Cohort should be generated before creating samples'); + return; + } + const sampleListData = res.samples; + this.sampleSets(sampleListData); + }) + .catch(error => { + console.error(error); + alert('Error when fetching sample list, please try again later'); + }) + .finally(() => { + this.annotationSampleLoading(false); + }); + }; + + + self.onSampleRowClick = function (d, e) { + const sampleId = d['id']; + const sourceKey = self.sampleSourceKey(); + const cohortDefinitionId = self.cohortId; + const setId = self.clickedSet(); + if (e.target.className === "btn btn-success btn-sm sample-study-launch-btn") { + self.isSampleLinking(true); + const btn = e.target; + const status = btn.nextSibling; + status.textContent = "Retrieving study information..."; + btn.disabled = true; + annotationService.getSetsBySampleId(sampleId) + .then((items) => { + return items.includes(setId); + }) + .then((linked) => { + if (linked) { + status.textContent = "Launching study..."; + annotationService.getAnnotationsBySampleIdSetId(sampleId, setId) + .then((items) => { + if (items.length > 0) { + window.location = `#/profiles/${sourceKey}/${items[0].subjectId}/${cohortDefinitionId}/${sampleId}/${setId}`; + location.reload(); + } else { + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + } + }) + .catch(error => { + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + }) + .finally(() => { + self.isSampleLinking(false); + self.annotationSampleLinkShown(false); + }); + } else { + status.textContent = "Creating study..."; + annotationService.linkAnnotationToSamples({ + 'sampleId': sampleId, + 'cohortDefinitionId': cohortDefinitionId, + 'sourceKey': sourceKey, + 'annotationSetId': setId + }) + .then(res => { + status.textContent = "Launching Study..."; + annotationService.getAnnotationsBySampleIdSetId(sampleId, setId) + .then((items) => { + if (items.length > 0) { + window.location = `#/profiles/${sourceKey}/${items[0].subjectId}/${cohortDefinitionId}/${sampleId}/${setId}`; + location.reload(); + } else { + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + } + }) + .catch(error => { + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + }) + .finally(() => { + self.isSampleLinking(false); + self.annotationSampleLinkShown(false); + }); + }) + .catch(error => { + console.error(error); + status.textContent = "Error creating study!"; + btn.disabled = false; + alert('Error linking sample to annotations. Please try again later.'); + }) + .finally(() => { + self.isSampleLinking(false); + self.annotationSampleLinkShown(false); + }); + } + }); + } + }; + + self.onValidationAnnotationListClick = function (d, e) { + self.clickedSet(d.id); + const items = self.filterSet(); + if (items !== undefined && items.length > 0) { + let result = items[0]; + self.selectResult(result); + if (e.target.className === 'btn btn-success btn-sm annotation-set-view-btn') { + self.valTabMode(SELECTED_QUESTION_SET_VIEW); + } else if (e.target.className === 'btn btn-danger btn-sm annotation-set-delete-btn') { + + annotationService.deleteQuestionSet(d.id) + .then(res => { + return res.data; + }) + .then(res => { + if (res && res.statusCodeValue !== 200) { + alert(res.body); + } else { + self.loadAnnotationSets(); + } + }) + .catch((err) => { + console.error(err); + alert('Unable to delete Question Set because it is in use by a Study.'); + }); + } else if (e.target.className === 'btn btn-primary btn-sm annotation-set-samples-btn') { + self.getSampleList(self.cohortId, self.sampleSourceKey()); + } + } + + }; + + self.onAnnotationStudyListClick = function (d, e) { + const questionSetId = d.questionSetId; + const cohortSampleId = d.cohortSampleId; + + + if (e.target.className === "btn btn-success btn-sm annotation-study-view-btn") { + self.studyResultsLoading(true); + self.studyResultsModalShown(true); + const superTable = annotationService.getSuperTable(questionSetId, cohortSampleId) + .catch(() => { + console.error('Error when loading super table results, please try again later'); + }); + if (superTable !== undefined) { + superTable.then(res => { + const mapped = res.map((r) => { + r['dataSourceId'] = r['dataSourceKey']; + r['cohortSampleId'] = cohortSampleId; + r['questionSetId'] = questionSetId; + return r; + }); + self.studySetResults(mapped); + + }).finally(() => { + self.studyResultsLoading(false); + }); + } + } else if (e.target.className === "btn btn-primary btn-sm annotation-study-launch-btn") { + annotationService.getAnnotationsBySampleIdSetId(cohortSampleId, questionSetId) + .then((items) => { + if (items.length > 0) { + let item = items[0]; + let source = self.sampleSourceKey(); + if (!source) { + alert('Please select a source from the Samples tab'); + } else { + + window.open(`#/profiles/${source}/${item.subjectId}/${self.cohortId}/${cohortSampleId}/${questionSetId}`); + } + + } + }); + + } + + }; + + self.loadAnnotationSets = function () { + self.validationAnnotationSetsLoading(true); + const annotationSet = annotationService.getAnnotationSets(self.cohortId) + .catch(() => { + console.error('Error when refreshing annotation sets, please try again later'); + }); + if (annotationSet !== undefined) { + annotationSet.then(res => { + self.rawAnnotationSets(res); + const transformAnnotationSets = res.map(el => ({ + id: el.id, + q_num: el.questions.length, + name: el.name + })); + self.validationAnnotationSets(transformAnnotationSets); + }).finally(() => { + self.validationAnnotationSetsLoading(false); + }); + } + }; + + self.loadStudySets = function () { + + self.annotationStudiesLoading(true); + const studySet = annotationService.getStudySets(self.cohortId) + .catch(() => { + console.error('Error when refreshing study sets, please try again later'); + }); + if (studySet !== undefined) { + studySet.then(res => { + self.annotationStudySets(res); + }).finally(() => { + self.annotationStudiesLoading(false); + }); + } + }; + + self.valTabMode.subscribe(function (mode) { + if (mode === DEFAULT_VALIDATION_VIEW) { + self.loadAnnotationSets(); + self.loadStudySets(); + } else if (mode === SELECTED_QUESTION_SET_VIEW) { + let result = self.selectResult(); + self.questionSet.resetValues(id, cohortName, result.id, result.name, result.questions, 'VIEW'); + + } + }); + + self.addQuestionSet = function () { + self.questionSet.resetValues(id, cohortName, null, null, [], 'NEW'); + self.valTabMode(NEW_QUESTION_SET_VIEW); + }; + + + self.submitQsetForm = function () { + const submitted = self.questionSet.submitQsetForm(self.sampleSourceKey()); + if (submitted !== null) { + setTimeout(() => { + submitted.then(res => { + self.valTabMode(DEFAULT_VALIDATION_VIEW); + }); + // if you don't do this, ko complains. + }, 500); + } + }; + + self.newValSample = function () { + if (self.sampleName().length >= 32) { + self.errorMessage('Name must be less than 32 characters'); + } else { + self.errorMessage(''); + ValidationService.createValidationSet(self.sampleSourceKey(), self.sampleSize(), self.sampleName(), id, self.getSamples); + } + }; + + self.fixSamples = function (index) { + if (self.display_sample().length > 0) { + self.display_sample.removeAll(); + } + for (let i = 0; i < self.samples()[index()].sample().length; i++) { + self.display_sample.push(self.samples()[index()].sample()[i]); + } + }; + + self.getSamples = function () { + ValidationService.getSamples(self.sampleSourceKey(), id).then(function (data) { + let samples = []; + for (let i = 0; i < data.length; i++) { + let row = {'name': null, 'size': null, 'annotated': null}; + row['name'] = data[i][0]; + row['size'] = data[i][1]; + row['annotated'] = data[i][2]; + let name = ''; + if (row['name'].indexOf(' ') >= 0) { + name = row['name'].split(" ").join('_'); + } else { + name = row['name']; + } + row['url'] = window.location.origin + '/#/profiles/' + self.sampleSourceKey() + '/' + data[i][3] + '/' + id + '/' + name; + let sample = ko.observableArray([]); + for (let j = 0; j < data[i][4].length; j++) { + sample.push({ + 'id': data[i][4][j][0], + 'ann': data[i][4][j][1], + "url": window.location.origin + '/#/profiles/' + self.sampleSourceKey() + '/' + data[i][4][j][0] + '/' + id + '/' + name + }); + } + row['sample'] = sample; + samples.push(row); + } + self.samples(samples); + }); + }; + + self.exportAnnotations = function (sampleName) { + ValidationService.exportAnnotation(self.sampleSourceKey(), id, sampleName).then(function (data) { + let questions = data[0][2][0].questions; + let subjects = data.slice(1); + let rows = []; + for (let i = 0; i < subjects.length; i++) { + for (let j = 0; j < questions.length; j++) { + let row = { + 'Source': self.sampleSourceKey(), + "Cohort ID": id, + 'Question Set Name': self.questionSet.qsetName(), + 'Validation Set Name': sampleName, + 'Patient ID': subjects[i][0], + 'Question Type': questions[j].type, + 'Case Question': questions[j].caseQuestion, + "Required": questions[j].required, + "Question Text": questions[j].text + }; + const answer = subjects[i][1][j]; + if (answer.length === 0) { + row['Answer'] = null; + } else { + if (answer === 1) { + row['Answer'] = true; + } else if (answer === 0) { + row['Answer'] = false; + } else if (answer.length > 0) { + row['Answer'] = answer.toString(); + } else { + row['Answer'] = answer; + } + + } + rows.push(row); + } + } + CsvUtils.saveAsCsv(rows); + }); + }; + + self.loadAnnotationSets(); + self.loadStudySets(); + } + + ValidationTool.prototype.constructor = ValidationTool; + return ValidationTool; + + }); diff --git a/js/pages/cohort-definitions/cohort-definition-manager.css b/js/pages/cohort-definitions/cohort-definition-manager.css index db5777dc3..71cb68760 100644 --- a/js/pages/cohort-definitions/cohort-definition-manager.css +++ b/js/pages/cohort-definitions/cohort-definition-manager.css @@ -87,4 +87,46 @@ color: #265a88; cursor: pointer; } - \ No newline at end of file + +.sample-list.fa-check-square { + color: #009688; + cursor: pointer; +} + +.validation-section { + padding-top: 5px; + padding-bottom: 5px; + font-size: 12px; +} + +.validation-label { + font-size: 16px; +} + +.validation-row { + border: 1px solid #ccc; + margin: 5px; + padding: 9px 3px; +} + +.validation-sub-header { + padding-left: 10px; +} + +.annotation-link-status { + margin: 5pt; + vertical-align: middle; + padding-left: 10px; +} + +.annotation-view-bullet { + list-style-type: none; +} + +.annotation-view-header { + padding-left: 15px; +} + +.sample-status { + padding: 10px; +} \ No newline at end of file diff --git a/js/pages/cohort-definitions/cohort-definition-manager.html b/js/pages/cohort-definitions/cohort-definition-manager.html index a003d2065..127968065 100644 --- a/js/pages/cohort-definitions/cohort-definition-manager.html +++ b/js/pages/cohort-definitions/cohort-definition-manager.html @@ -12,34 +12,34 @@
+ data-bind="placeholder: ko.i18n('const.newEntityNames.cohortDefinition', 'New Cohort Definition'), enabled: canEdit(), textInput: currentCohortDefinition().name, css: { emptyInput: !isNameFilled() }" />
+ data-bind="title: ko.i18n('common.tags', 'Tags'), visible: canEdit() && !previewVersion(), click: () => isTagsModalShown(!isTagsModalShown()), css: { disabled: isProcessing() }"> @@ -82,6 +82,9 @@ data-bind="visible: !previewVersion(), css: { active: $component.tabMode() == 'reporting' }, click: function() { $component.selectTab('reporting'); }"> +
  • + +
  • @@ -319,30 +322,30 @@

    + data-bind="text: ko.i18n('cohortDefinitions.cohortDefinitionManager.panels.reportSelections', 'Report Selections')">
    @@ -350,13 +353,13 @@

    + data-bind="css : { invalid: $component.reportSourceKey()==undefined }, options: $component.cdmSources(), optionsValue:'sourceKey', optionsText:'sourceName', value:$component.reportSourceKey, optionsCaption: ko.i18n('dataSources.selectASource', 'Select a Source')">

    + data-bind="visible: showReportNameDropdown,disable: $component.loadingReport, attr:{disabled: $component.loadingReport}, css : {invalid: $component.reportReportName()==undefined }, options:reportingAvailableReports, optionsCaption:$component.reportOptionCaption, optionsText:'name', optionsValue:'name', value:$component.reportReportName">

    @@ -392,7 +395,7 @@

    + data-bind="visible: (!$component.reportingSourceStatusLoading() && $component.reportingState()=='generating_reports')">
    @@ -400,24 +403,24 @@

    - - - - + + + + - - - - - - + + + + + +

    + data-bind="visible: (!$component.reportingSourceStatusLoading() && $component.reportingState()=='report_unavailable')">
    @@ -428,7 +431,7 @@

    + data-bind="visible: (!$component.reportingSourceStatusLoading() && $component.reportingState()=='unknown_cohort_report_state')">
    @@ -444,7 +447,7 @@

    + data-bind="visible: (!$component.reportingSourceStatusLoading() && $component.createReportJobFailed)">
    @@ -471,10 +474,10 @@

    + data-bind="visible:$root.router.currentView() == 'loading' || $component.loadingReport()"> + params="{reportSourceKey: reportSourceKey, loadingReport: loadingReport, reportReportName: reportReportName, reportCohortDefinitionId: reportCohortDefinitionId, reportTriggerRun: reportTriggerRun, showSelectionArea: false} ">

    @@ -492,37 +495,37 @@

    - -
    -
    -
    -
    -
    -
    -
    - - -
    +
    -
    - -
    - -
    - + -
    - -
    - + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + + + +
    +
    +
    +

    Question Sets

    +
    + + +
    +
    +
    +
    +

    Annotation Studies

    +
    + + +
    +
    +
    +
    +
    + +
    +
    +

    +
      +
    • +
      +
      +
      +
      Question Info
      +
        +
      • +
      • +
      • +
      + +
      +
      +
      Answer Options
      +
        +
      • +
      +
      + +
      + +
    • +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
      +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + + +
      +
      +
        +
        +
      • +
        + +
        +
        + + + + +
        +
      • + +
        +
      +
    +
    +
    +
    +
    + +
    +
    + +
    +
    - - -
    -
    -
    -
    +
    - +
    + + + +
    +
    + -
    -
    - - - - -
    -
    - - - -
    + +
    + + + + +
    +
    + + + +
    -
    -
    +
    -
    - -
    - -
    -
    - -
    -
    - -
    +
    + +
    + +
    +
    + +
    +
    +
    - - - - - - - +
    + + + + + + + this.sampleName().trim() === ""); this.patientCountError=ko.pureComputed(() => !(this.patientCount() > 0)); // this works because null == 0 this.isAgeRange =ko.pureComputed(() => ['between','notBetween'].includes(this.sampleAgeType())); this.firstAgeError = ko.pureComputed(() => this.firstAge() != null && this.firstAge() < 0); @@ -257,6 +263,21 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', //validation input value + this.selectedSampleId = ko.observable(''); + this.selectedSampleName = ko.observable(''); + this.sampleDataLoading = ko.observable(false); + this.sampleData =ko.observableArray([]); + this.sampleList = ko.observableArray(); + + // validation samples + + this.annotationSets = ko.observableArray([]); + this.annotationLinkShown = ko.observable(false); + this.annotationSetsLoading = ko.observable(false); + this.isAnnotationSetLinking = ko.observable(false); + this.isAnnotationSetLoading = ko.observable(false); + this.annotationSampleId = ko.observable(''); + //sample list this.sampleListCols = [ { @@ -290,10 +311,12 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', { title: ko.i18n('columns.action', 'Action'), sortable: false, - render: function() {return ` `} + render: function() { + return ` ` + } } - ] - this.sampleList = ko.observableArray() + ]; + // Sample data table this.sampleCols = [ @@ -314,17 +337,37 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', } ]; - this.selectedSampleId = ko.observable(''); - this.selectedSampleName = ko.observable(''); - this.sampleDataLoading = ko.observable(false); - this.sampleData =ko.observableArray([]); + this.annotationSetCols = [ + { + title: 'ID', + data: 'id' + }, + { + title: 'Name', + data: 'name' + }, + { + title: '# Questions', + data: 'q_num' + }, + { + title: 'Actions', + sortable: false, + data: 'id', + render: (d) => { + return ``; - this.isCohortGenerated = ko.pureComputed(() => { - const sourceInfo = this.cohortDefinitionSourceInfo().find(d => d.sourceKey == this.sampleSourceKey()); - if (sourceInfo&&this.getStatusMessage(sourceInfo) == 'COMPLETE') { - return true; + + } } - return false; + ]; + + // end validation samples + + this.isCohortGenerated = ko.pureComputed(() => { + const sourceInfo = this.cohortDefinitionSourceInfo().find(d => d.sourceKey === this.sampleSourceKey()); + return !!(sourceInfo && this.getStatusMessage(sourceInfo) === 'COMPLETE'); + }); //end of sample states @@ -333,6 +376,15 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', return sharedState.sources().filter((source) => commonUtils.hasCDM(source) && authApi.hasSourceAccess(source.sourceKey)); }); + + this.validationTool = ko.pureComputed(() => { + if (this.currentCohortDefinition()) { + console.log('init validation tool'); + return new ValidationTool(this.currentCohortDefinition().id(), this.currentCohortDefinition().name(), + this.sourceAnalysesStatus, this.reportSourceKey(), this.sampleSourceKey()); + } + }); + this.cohortDefinitionCaption = ko.pureComputed(() => { if (this.currentCohortDefinition()) { if (this.currentCohortDefinition().id() === 0 || this.currentCohortDefinition().id() === null) { @@ -372,7 +424,7 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', } if (this.currentCohortDefinition() && (this.currentCohortDefinition() - .id() != 0)) { + .id() !== 0)) { return authApi.isPermittedUpdateCohort(this.currentCohortDefinition() .id()) || !config.userAuthenticationEnabled; } else { @@ -1782,13 +1834,27 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', .finally(() => { this.sampleDataLoading(false); }); + } else if (e.target.className === 'sample-list fa fa-check-square') { + this.annotationLinkShown(true); + this.annotationSetsLoading(true); + this.annotationSampleId(sampleId); + + this.loadAnnotationSets(cohortDefinitionId) + .then(res => { + this.showAnnotationDataTable(res); + }) + .finally(() => { + this.annotationSetsLoading(false); + }); + + } else { this.fetchSampleData({sampleId, sourceKey, cohortDefinitionId}); } } fetchSampleData({sampleId, sourceKey, cohortDefinitionId}) { - this.sampleDataLoading(true) + this.sampleDataLoading(true); sampleService.getSample({ cohortDefinitionId, sourceKey, sampleId }) .then(res=>{ this.selectedSampleId(sampleId); @@ -1805,6 +1871,13 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', }) } + loadAnnotationSets(cohortDefinitionId) { + return annotationService.getAnnotationSets(cohortDefinitionId) + .catch(() => { + alert('Error when refreshing annotation sets, please try again later'); + }) + } + refreshSample({sampleId, sourceKey, cohortDefinitionId}) { return sampleService.refreshSample({cohortDefinitionId, sourceKey, sampleId}) .catch(() => { @@ -1819,6 +1892,100 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', window.open(`#/profiles/${sourceKey}/${d.personId}/${cohortDefinitionId}/${sampleId}`); } + onAnnotationListClick(d, e) { + const sampleId = this.annotationSampleId(); + const cohortDefinitionId= this.currentCohortDefinition().id(); + const sourceKey=this.sampleSourceKey(); + + if (e.target.className === 'btn btn-primary btn-sm annotation-link-btn') { + this.isAnnotationSetLinking(true); + const btn = e.target; + const status = btn.nextSibling; + status.textContent = "Retrieving study information..."; + btn.disabled = true; + annotationService.getSetsBySampleId(sampleId) + .then((items) => { + return items.includes(d['id']); + }) + .then((linked) => { + if (linked) { + status.textContent = "Launching study..."; + annotationService.getAnnotationsBySampleIdSetId(sampleId, d.id) + .then((items) => { + if (items.length > 0) { + window.location = `#/profiles/${sourceKey}/${items[0].subjectId}/${cohortDefinitionId}/${sampleId}/${d.id}`; + location.reload(); + } else { + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + } + }) + .catch(error=>{ + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + }) + .finally(() => { + this.isAnnotationSetLinking(false); + this.annotationLinkShown(false); + }) + } + else { + status.textContent = "Creating study..."; + annotationService.linkAnnotationToSamples({ + 'sampleId': sampleId, + 'cohortDefinitionId': cohortDefinitionId, + 'sourceKey': sourceKey, + 'annotationSetId': d.id + }) + .then(res => { + status.textContent = "Launching Study..."; + console.log('posted annotation sample link'); + console.log(res); + annotationService.getAnnotationsBySampleIdSetId(sampleId, d.id) + .then((items) => { + if (items.length > 0) { + window.location = `#/profiles/${sourceKey}/${items[0].subjectId}/${cohortDefinitionId}/${sampleId}/${d.id}`; + location.reload(); + } else { + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + } + }) + .catch(error=>{ + console.error(error); + status.textContent = "Error launching study!"; + btn.disabled = false; + alert('Error launching sample to annotations. Please try again later.'); + }) + .finally(() => { + this.isAnnotationSetLinking(false); + this.annotationLinkShown(false); + }) + }) + .catch(error=>{ + console.error(error); + status.textContent = "Error creating study!"; + btn.disabled = false; + alert('Error linking sample to annotations. Please try again later.'); + }) + .finally(() => { + this.isAnnotationSetLinking(false); + this.annotationLinkShown(false); + }); + } + }); + + + + } + } + showSampleDataTable(sample) { const transformedSampleData = sample.map(el => ({ personId: el.personId, @@ -1828,6 +1995,15 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html', this.sampleData(transformedSampleData); } + + showAnnotationDataTable(annotationSet) { + const transformAnnotationSets = annotationSet.map(el => ({ + id: el.id, + q_num: el.questions.length, + name: el.name + })); + this.annotationSets(transformAnnotationSets) + } } return commonUtils.build('cohort-definition-manager', CohortDefinitionManager, view); diff --git a/js/pages/profiles/annotation/annotation.less b/js/pages/profiles/annotation/annotation.less new file mode 100644 index 000000000..59398dffd --- /dev/null +++ b/js/pages/profiles/annotation/annotation.less @@ -0,0 +1,82 @@ +.annotation-widget { + width: 300px; + float: right; + margin-right: -300px; + transition: margin 700ms; + background-color: #fafafa; + position: relative; + border-left: 1px solid #e0e0e0; + height: 100%; + &.open { + margin-right: 0px; + } + .annotation-toggle { + position: absolute; + left: 0px; + transform: rotate(270deg); + left: -45px; + top: 66px; + text-transform: uppercase; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + &:focus { + outline: 0; + } + } + .back-link { + margin-top: 8px; + } + .annotation-view { + padding: 10px 20px; + .annotation-next-prev { + margin-bottom: 15px; + + .nav-subject-id { + margin-left: 5px; + font-size: 10px; + color: #616161; + text-decoration: underline; + } + } + .annotation-progress { + margin-bottom: 10px; + .progress { + height: 6px; + } + .progress-word { + font-size: 10px; + color: #616161; + margin-top: 3px; + } + } + .questions { + color: #424242; + padding-left: 0px; + list-style-position: inside; + .question { + margin-bottom: 12px; + .question-text { + display: inline; + } + .answers { + margin-top: 6px; + .answer-multi {} + .answer-single {} + .answer-text {} + } + } + } + .annotation-loading { + text-align: center; + margin: 20px 0px; + font-size: 14px; + color: #616161; + .annotation-loading-icon { + margin-right: 5px; + } + .annotation-loading-text { + + } + } + } + } \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Annotation.js b/js/pages/profiles/annotation/view-models/Annotation.js new file mode 100644 index 000000000..b6e521441 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Annotation.js @@ -0,0 +1,138 @@ +define(['knockout', './Set', './Result', 'services/Annotation'], function (ko, Set, Result, annotationService) { + + function Annotation(set, subjectId, cohortId, sourceKey, rawResults, annotationId, annotationView, sampleName, questionSetId) { + const self = this; + self.set = new Set(set); + self.setId = set.id; + self.annotationId = ko.observable(annotationId); + self.subjectId = subjectId; + self.cohortId = cohortId; + self.sourceKey = sourceKey; + self.sampleName = sampleName; + self.nav = annotationView.navigation(); + self.annotationSaving = ko.observable(false); + + self.rawToForm = function (rawResults) { + if (!rawResults) { + return []; + } + return rawResults.sort((a, b) => b.questionId - a.questionId).sort().reduce((accumulator, current) => { + const length = accumulator.length; + if (length === 0 || accumulator[length - 1].questionId !== current.questionId) { + if (current.type === 'MULTI_SELECT') { + current.value = [current.value]; + } + accumulator.push(current); + } else { + accumulator[length - 1].value.push(current.value); + } + return accumulator; + }, []); + }; + + self.formToRaw = function (massagedResults, questions) { + return Object.keys(massagedResults).reduce((accumulator, current) => { + switch (massagedResults[current].type) { + case 'MULTI_SELECT': { + massagedResults[current].value.forEach(value => { + var answerId = _.find(_.find(questions, {id: massagedResults[current].questionId}).answers, {value: value}).id; + var result = { + ...massagedResults[current], + value: value, + answerId: answerId, + annotationId: self.annotationId, + setId: self.setId + }; + accumulator.push(result); + }); + return accumulator; + } + case 'SINGLE_SELECT': { + if (!massagedResults[current].value) { + return accumulator; + } + const answerId = _.find(_.find(questions, {id: massagedResults[current].questionId}).answers, {value: massagedResults[current].value}).id; + const result = { + ...massagedResults[current], + answerId: answerId, + annotationId: self.annotationId, + setId: self.setId + }; + accumulator.push(result); + return accumulator; + } + default: { + const answerId = _.find(questions, {id: massagedResults[current].questionId}).answers[0].id; + const result = { + ...massagedResults[current], + answerId: answerId, + annotationId: self.annotationId, + setId: self.setId + }; + accumulator.push(result); + return accumulator; + } + } + }, []); + }; + + let massagedResults = self.rawToForm(rawResults); + + self.results = ko.toJS(self.set.questions).reduce((accumulator, current) => { + accumulator['question_' + current.id] = new Result(_.find(massagedResults, {questionId: current.id}) || { + questionId: current.id, + type: current.type, + required: current.required + }); + return accumulator; + }, {}); + + self.createOrUpdate = function (annotation) { + self.annotationSaving(true); + const errors = Object.keys(annotation.results).reduce((accumulator, key) => { + const result = annotation.results[key]; + if (!result.validate(result.value()) && result.required()) { + result.valid(false); + return { + count: accumulator.count + 1 + }; + } + return accumulator; + }, {count: 0}); + + if (errors.count > 0) { + self.annotationSaving(false); + return; + } + + const {results, set} = ko.toJS(annotation); + + const payload = { + id: annotationId, + subjectId: subjectId, + cohortId: cohortId, + setId: set.id, + cohortSampleId: sampleName, + results: [ + ...self.formToRaw(results, set.questions) + ] + }; + annotationService.createOrUpdateAnnotation(payload) + .then((annotation) => { + this.annotationId(annotation.id); + if (sampleName.indexOf(' ') >= 0) { + sampleName = sampleName.split(" ").join('_'); + } + self.annotationSaving(false); + //window.location = `#/profiles/${sourceKey}/${ko.toJS(annotationView).navigation.nextSubjectId}/${cohortId}/${sampleName}`; + self.nav.nextLink(); + }).catch((error) => { + self.annotationSaving(false); + }); + }; + } + + Annotation.prototype.constructor = Annotation; + + return Annotation; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/AnnotationView.js b/js/pages/profiles/annotation/view-models/AnnotationView.js new file mode 100644 index 000000000..7b6977f53 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/AnnotationView.js @@ -0,0 +1,31 @@ +define(['knockout', './Content', './Navigation', './SetSelect'], function (ko, Content, Navigation, SetSelect) { + + function AnnotationView(sets, cohortId, personId, sourceKey, sampleName, questionSetId) { + const self = this; + self.content = ko.observable(); + self.navigation = ko.observable(); + + self.loadedContent = ko.observable(false); + self.loadedNavigation = ko.observable(false); + self.loadedSelect = ko.observable(false); + + self.loaded = ko.computed(() => { + return self.loadedContent(); + }); + + self.initContent = function (set, cohortId, personId, sourceKey, sampleName) { + self.content(new Content(set, cohortId, personId, sourceKey, self, sampleName, questionSetId)); + self.loadedContent(true); + }; + self.initNavigation = function (sampleName, cohortId, personId, sourceKey) { + self.navigation(new Navigation(sampleName, cohortId, personId, sourceKey, questionSetId)); + self.loadedNavigation(true); + }; + self.setSelect = new SetSelect(sets, self, cohortId, personId, sourceKey, sampleName, questionSetId); + self.loadedSelect(true); + } + + AnnotationView.prototype.constructor = AnnotationView; + + return AnnotationView; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/AnnotationWidget.js b/js/pages/profiles/annotation/view-models/AnnotationWidget.js new file mode 100644 index 000000000..373cfce2a --- /dev/null +++ b/js/pages/profiles/annotation/view-models/AnnotationWidget.js @@ -0,0 +1,38 @@ +define(['knockout', './AnnotationView', './SettingsView', 'services/Annotation'], function (ko, AnnotationView, SettingsView, annotationService) { + + function AnnotationWidget(cohortId, personId, sourceKey, sampleName, questionSetId) { + const self = this; + let cachedOption = localStorage.getItem('annotationToggleState'); + if (cachedOption === undefined) { + cachedOption = 'open'; + } + self.currentView = ko.observable("annotationView"); + self.annotationToggleState = ko.observable('open'); + self.isVisible = ko.observable(false); + + self.toggleAnnotationPanel = function () { + const nextAnnotationToggleState = this.annotationToggleState() === 'open' ? '' : 'open'; + this.annotationToggleState(nextAnnotationToggleState); + localStorage.setItem('annotationToggleState', nextAnnotationToggleState); + }; + + self.initialize = function (cohortId, personId, sourceKey, sampleName, settingsView, questionSetId) { + annotationService.getAnnotationSets(cohortId, sourceKey) + .then((sets) => { + if (sets.length === 0 || !sets) { + self.isVisible(false); + } else { + self.annotationView = new AnnotationView(sets, cohortId, personId, sourceKey, sampleName, + questionSetId); + self.isVisible(true); + } + }); + }; + + self.settingsView = new SettingsView(self); + self.initialize(cohortId, personId, sourceKey, sampleName, self.settingsView, questionSetId); + } + + AnnotationWidget.prototype.constructor = AnnotationWidget; + return AnnotationWidget; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Answer.js b/js/pages/profiles/annotation/view-models/Answer.js new file mode 100644 index 000000000..8cf1189f0 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Answer.js @@ -0,0 +1,13 @@ +define(['knockout'], function (ko) { + + function Answer(answer) { + const self = this; + self.id = ko.observable(answer.id); + self.text = ko.observable(answer.text); + self.value = ko.observable(answer.value); + } + + Answer.prototype.constructor = Answer; + + return Answer; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Content.js b/js/pages/profiles/annotation/view-models/Content.js new file mode 100644 index 000000000..dde264b0a --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Content.js @@ -0,0 +1,39 @@ +define(['knockout', './Annotation', 'services/Annotation'], function (ko, Annotation, annotationService) { + + function Content(set, cohortId, personId, sourceKey, annotationView, sampleName, questionSetId) { + const self = this; + self.annotation = null; + self.annotationLoaded = ko.observable(false); + self.initialize = function (set, cohortId, personId, sourceKey) { + let setId = questionSetId; + if (!setId) { + setId = set.id; + } + annotationService.getAnnotationBySampleIdbySubjectIdBySetId(setId, sampleName, personId, sourceKey) + .then((annotation) => { + if (!annotation) { + return {}; + } + return annotation; + }).then((annotation) => { + let {id} = annotation; + if (id) { + annotationService.getStudyResults(id) + .then((results) => { + self.annotation = new Annotation(set, personId, cohortId, sourceKey, results, id, annotationView, sampleName, setId); + self.annotationLoaded(true); + }); + } else { + self.annotation = new Annotation(set, personId, cohortId, sourceKey, [], null, annotationView, sampleName, setId); + self.annotationLoaded(true); + } + }); + }; + + self.initialize(set, cohortId, personId, sourceKey); + } + + Content.prototype.constructor = Content; + + return Content; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Navigation.js b/js/pages/profiles/annotation/view-models/Navigation.js new file mode 100644 index 000000000..6238045f1 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Navigation.js @@ -0,0 +1,85 @@ +define(['knockout', './Navigation', 'services/Annotation'], function (ko, Navigation, annotationService) { + + function Navigation(sampleName, cohortId, personId, sourceKey, questionSetId) { + const self = this; + self.prevSubjectId = ko.observable(); + self.nextSubjectId = ko.observable(); + self.nextUnannotatedSubjectId = ko.observable(); + self.numProfileSamples = ko.observable(); + self.numAnnotations = ko.observable(); + self.navigationLoaded = ko.observable(false); + self.sampleId = ko.observable(); + self.sourceKey = ko.observable(sourceKey); + self.cohortId = ko.observable(cohortId); + self.personId = ko.observable(personId); + self.questionSetId = ko.observable(questionSetId); + + this.prevLink = function () { + window.location = `#/profiles/${sourceKey}/${self.prevSubjectId()}/${cohortId}/${sampleName}/${questionSetId}`; + location.reload(); + }; + this.nextLink = function () { + if (sampleName.indexOf(' ') >= 0) { + sampleName = sampleName.split(" ").join('_'); + } + window.location = `#/profiles/${sourceKey}/${self.nextSubjectId()}/${cohortId}/${sampleName}/${questionSetId}`; + location.reload(); + }; + + + this.completionPercent = ko.computed(function () { + return Math.ceil((self.numAnnotations() / self.numProfileSamples()) * 100); + }); + + self.initialize = function (sampleName, cohortId, personId, sourceKey) { + annotationService.getAnnotationNavigation(sampleName, cohortId, personId, sourceKey) + .then((navigation) => { + if (sampleName.indexOf(' ') >= 0) { + sampleName = sampleName.split(" ").join('_'); + } + self.sampleId(sampleName); + const samples = navigation.elements; + + let val = -1; + for (let i = 0; i < samples.length; i++) { + let item = samples[i]; + if (item.personId === personId) { + val = i; + break; + } + } + if (val !== -1) { + let lastSubjectId = null; + let nextSubjectId = null; + if ((val - 1) < 0) { + lastSubjectId = samples[samples.length - 1].personId; + } else { + lastSubjectId = samples[val - 1].personId; + } + if ((val + 1) > (samples.length - 1)) { + nextSubjectId = samples[0].personId; + + } else { + nextSubjectId = samples[val + 1].personId; + } + + self.prevSubjectId(lastSubjectId); + self.nextSubjectId(nextSubjectId); + + self.numProfileSamples(samples.length); + self.navigationLoaded(true); + + self.numAnnotations(0); + self.nextUnannotatedSubjectId(nextSubjectId); + + } + }); + }; + + self.initialize(sampleName, cohortId, personId, sourceKey); + } + + Navigation.prototype.constructor = Navigation; + + return Navigation; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Question.js b/js/pages/profiles/annotation/view-models/Question.js new file mode 100644 index 000000000..1d2255f32 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Question.js @@ -0,0 +1,21 @@ +define(['knockout', './Answer'], function (ko, Answer) { + + function Question(question) { + + const self = this; + self.id = ko.observable(question.id); + self.text = ko.observable(question.text); + self.type = ko.observable(question.type); + self.required = ko.observable(question.required); + self.answers = ko.observableArray(); + + for (let i = 0; i < question.answers.length; i++) { + let answer = new Answer(question.answers[i]); + self.answers.push(answer); + } + } + + Question.prototype.constructor = Question; + + return Question; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Result.js b/js/pages/profiles/annotation/view-models/Result.js new file mode 100644 index 000000000..bb7351a2f --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Result.js @@ -0,0 +1,35 @@ +define(['knockout'], function (ko) { + + function Result(result, set) { + const self = this; + self.questionId = ko.observable(); + + if (result.type === 'MULTI_SELECT') { + self.value = ko.observableArray(); + self.value(result.value ? result.value : []); + } else { + self.value = ko.observable(); + self.value(result.value ? result.value : ''); + } + + self.questionId(result.questionId); + self.answerId = result.answerId; + self.type = result.type; + self.setId = result.setId; + + self.validate = function (value) { + return !!((value && !Array.isArray(value)) || (Array.isArray(value) && value.length > 0)); + }; + + self.required = ko.observable(result.required); + self.valid = ko.observable(true); + + self.value.subscribe(function (newValue) { + self.valid(self.validate(newValue)); + }); + } + + Result.prototype.constructor = Result; + + return Result; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/Set.js b/js/pages/profiles/annotation/view-models/Set.js new file mode 100644 index 000000000..7d2e214b7 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/Set.js @@ -0,0 +1,22 @@ +define(['knockout', './Question'], function (ko, Question) { + + function Set(set) { + + const self = this; + self.id = ko.observable(); + self.questions = ko.observableArray(); + + self.id(set.id); + + set.questions.sort((a, b) => a.id - b.id); + + for (let i = 0; i < set.questions.length; i++) { + let question = new Question(set.questions[i]); + self.questions.push(question); + } + } + + Set.prototype.constructor = Set; + + return Set; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/SetSelect.js b/js/pages/profiles/annotation/view-models/SetSelect.js new file mode 100644 index 000000000..8ad3c0b83 --- /dev/null +++ b/js/pages/profiles/annotation/view-models/SetSelect.js @@ -0,0 +1,43 @@ +define(['knockout'], function (ko) { + + function SetSelect(sets, annotationView, cohortId, personId, sourceKey, sampleName, questionSetId) { + const self = this; + self.currentSet = ko.observable({}); + self.currentSetId = ko.observable(questionSetId); + self.currentSet.subscribe((set) => { + self.currentSetId(set.id); + localStorage.setItem('currentSetId', set.id); + annotationView.initContent(set, cohortId, personId, sourceKey, sampleName); + annotationView.initNavigation(sampleName, cohortId, personId, sourceKey); + }); + self.sets = ko.observableArray(sets); + + + self.initialize = function (sets) { + + let currentSetId = localStorage.getItem('currentSetId'); + if (questionSetId) { + currentSetId = questionSetId; + } + + if (currentSetId && currentSetId.length) { + const foundSet = sets().filter((val) => { + return val.id === parseInt(currentSetId); + }); + if (foundSet.length > 0) { + self.currentSet(foundSet[0]); + } else { + self.currentSet(sets()[0]); + } + } else { + self.currentSet(sets()[0]); + } + }; + + self.initialize(self.sets); + } + + SetSelect.prototype.constructor = SetSelect; + + return SetSelect; +}); \ No newline at end of file diff --git a/js/pages/profiles/annotation/view-models/SettingsView.js b/js/pages/profiles/annotation/view-models/SettingsView.js new file mode 100644 index 000000000..df37442bb --- /dev/null +++ b/js/pages/profiles/annotation/view-models/SettingsView.js @@ -0,0 +1,14 @@ +define(['knockout'], function (ko) { + + function SettingsView(annotationWidget) { + const self = this; + + self.save = function () { + annotationWidget.currentView("annotationView"); + }; + } + + SettingsView.prototype.constructor = SettingsView; + + return SettingsView; +}); \ No newline at end of file diff --git a/js/pages/profiles/const.js b/js/pages/profiles/const.js index 73be315fe..8dcc9ce28 100644 --- a/js/pages/profiles/const.js +++ b/js/pages/profiles/const.js @@ -4,6 +4,8 @@ define( const paths = { source: sourceKey => `#/profiles/${sourceKey}`, person: (sourceKey, personId) => `#/profiles/${sourceKey}/${personId}`, + sample: (sourceKey, personId, cohortDefinitionId, sampleId) => `#/profiles/${sourceKey}/${personId}/${cohortDefinitionId}/${sampleId}`, + study: (sourceKey, personId, cohortDefinitionId, sampleId, questionSetId) => `#/profiles/${sourceKey}/${personId}/${cohortDefinitionId}/${sampleId}/${questionSetId}`, }; return { diff --git a/js/pages/profiles/profile-manager.html b/js/pages/profiles/profile-manager.html index 84bf28f5b..313d5a58b 100644 --- a/js/pages/profiles/profile-manager.html +++ b/js/pages/profiles/profile-manager.html @@ -50,7 +50,7 @@
    -
    +
    + +
    +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
      +
    1. +

      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
    2. +
    +
    + + +
    +
    +
    +
    + Loading content... +
    +
    +
    +
    +
    +
    + +
    +
    + Settings View +
    +
    +
    +
    +
    +
    diff --git a/js/pages/profiles/profile-manager.js b/js/pages/profiles/profile-manager.js index b3fc9f4dc..58852b0a5 100644 --- a/js/pages/profiles/profile-manager.js +++ b/js/pages/profiles/profile-manager.js @@ -21,11 +21,13 @@ define([ 'lodash', 'crossfilter', 'assets/ohdsi.util', + './annotation/view-models/AnnotationWidget', 'd3-tip', 'databindings', 'faceted-datatable', 'extensions/bindings/profileChart', 'less!./profile-manager.less', + 'less!./annotation/annotation.less', 'components/heading', 'components/ac-access-denied' ], @@ -51,6 +53,7 @@ define([ _, crossfilter, util, + AnnotationWidget ) { var reduceToRecs = [ // crossfilter group reduce functions where group val @@ -63,6 +66,7 @@ define([ class ProfileManager extends AutoBind(Page) { constructor(params) { super(params); + this.url = window.location this.sharedState = sharedState; this.aspectRatio = ko.observable(); this.config = config; @@ -71,6 +75,10 @@ define([ this.sourceKey = ko.observable(router.routerParams().sourceKey); this.personId = ko.observable(router.routerParams().personId); + + this.sampleName = ko.observable(router.routerParams().cohortSampleId); + this.questionSetId = ko.observable(router.routerParams().questionSetId); + this.personRecords = ko.observableArray(); this.cohortDefinitionId = ko.observable(router.routerParams().cohortDefinitionId); @@ -337,6 +345,22 @@ define([ if (this.personId()) { this.loadPerson(); } + // BEGIN ANNOTATION + if (this.cohortDefinitionId()) { + this.annotationWidget = new AnnotationWidget(this.cohortDefinitionId(), this.personId(), this.sourceKey(), this.sampleName(), this.questionSetId()); + } + + this.isAnnotationToggleVisible = ko.computed(() => { + if (!this.annotationWidget) { + this.annotationWidget = new AnnotationWidget(this.cohortDefinitionId(), this.personId(), this.sourceKey(), this.sampleName(), this.questionSetId()); + } + + if (this.annotationWidget) { + return this.annotationWidget.isVisible() && this.annotationWidget.annotationToggleState() === 'open'; + } + return false; + }); + // END ANNOTATION this.plugins = pluginRegistry.findByType(globalConstants.pluginTypes.PROFILE_WIDGET); } diff --git a/js/pages/profiles/profile-manager.less b/js/pages/profiles/profile-manager.less index b944339b3..3f453e122 100644 --- a/js/pages/profiles/profile-manager.less +++ b/js/pages/profiles/profile-manager.less @@ -1,4 +1,7 @@ .profile-manager { + height: auto; + overflow: hidden; + display: flex; &__chart { display: inline-block; width: 100%; diff --git a/js/pages/profiles/routes.js b/js/pages/profiles/routes.js index 688807b09..dd0cee2d2 100644 --- a/js/pages/profiles/routes.js +++ b/js/pages/profiles/routes.js @@ -10,6 +10,8 @@ define( params.sourceKey = (path[0] || null); params.personId = (path[1] || null); params.cohortDefinitionId = (path[2] || null); + params.cohortSampleId = (path[3] || null); + params.questionSetId = (path[4] || null); router.setCurrentView('profile-manager', params); }); diff --git a/js/services/Annotation.js b/js/services/Annotation.js new file mode 100644 index 000000000..4b50135d8 --- /dev/null +++ b/js/services/Annotation.js @@ -0,0 +1,170 @@ +define(function (require, exports) { + + const httpService = require('services/http'); + const config = require('appConfig'); + const authApi = require('services/AuthAPI'); + + const deleteQuestionSet = function (questionSetId) { + + return httpService.doGet(`${config.webAPIRoot}annotation/deleteSet/${questionSetId}`); + }; + + const getAnnotationSets = function (cohort) { + const data = { + cohortId: cohort || 0, + }; + + const response = httpService.doGet(`${config.webAPIRoot}annotation/sets`, data).then(({data}) => data); + response.catch((er) => { + console.error('Can\'t find annotation sets'); + }); + + return response; + }; + + const getStudySets = function (cohort) { + const cohortId = cohort || 0; + + const response = httpService.doGet(`${config.webAPIRoot}annotation/getsets?cohortId=${cohortId}`).then(({data}) => data); + response.catch((er) => { + console.error('Can\'t find study sets'); + }); + + return response; + }; + + const getSuperTable = function (qSetId, sampleId) { + const response = httpService.doGet(`${config.webAPIRoot}annotation/results/completeResults?questionSetId=${qSetId}&cohortSampleId=${sampleId}`).then(({data}) => data); + response.catch((er) => { + console.error('Can\'t find super table'); + }); + + return response; + }; + + const getStudyResults = function (annotationId) { + const response = httpService.doGet(`${config.webAPIRoot}annotation/results/${annotationId}`).then(({data}) => data); + response.catch((er) => { + console.error('Can\'t find study results'); + }); + + return response; + }; + + const getAnnotationByCohortIdbySubjectIdBySetId = function (set, cohort, subject, sourceKey) { + const data = { + cohortId: cohort || 0, + subjectId: subject || 0, + setId: set || 0 + }; + + + const response = httpService.doGet(`${config.webAPIRoot}annotation`, data).then(({data}) => data[0]); + response.catch((er) => { + console.error('Can\'t find annotations'); + }); + + + return response; + }; + + const getAnnotationBySampleIdbySubjectIdBySetId = function (set, sample, subject, sourceKey) { + const data = { + sampleId: sample || 0, + subjectId: subject || 0, + setId: set || 0 + }; + + const response = httpService.doGet(`${config.webAPIRoot}annotation?setId=${data.setId}&cohortSampleId=${data.sampleId}&subjectId=${data.subjectId}`) + .then(({data}) => data[0]); + response.catch((er) => { + console.error('Can\'t find annotations'); + }); + + + return response; + }; + + const getSamplesBySetId = function (setId) { + return httpService.doGet(`${config.webAPIRoot}annotation?setId=${setId}`) + .then((resp) => { + const samples = new Set(); + resp.data.forEach((x, i) => { + samples.add(x.cohortSampleId); + }); + return Array.from(samples); + }) + .catch(er => { + console.error("unable to load samples"); + }); + }; + + const getSetsBySampleId = function (sampleId) { + return httpService.doGet(`${config.webAPIRoot}annotation?cohortSampleId=${sampleId}`) + .then((resp) => { + const sets = new Set(); + resp.data.forEach((x, i) => { + sets.add(x.questionSetId); + }); + return Array.from(sets); + }) + .catch(er => { + console.error("unable to load sets"); + }); + }; + + const getAnnotationsBySampleIdSetId = function (sampleId, setId) { + return httpService.doGet(`${config.webAPIRoot}annotation?cohortSampleId=${sampleId}&setId=${setId}`) + .then((res) => { + return res.data; + }) + .catch(er => { + console.error("unable to load sets"); + }); + }; + + + const getAnnotationNavigation = function (sampleName, cohort, subject, source) { + const data = { + cohortId: cohort || 0, + subjectId: subject || 0, + sourceKey: source || '', + sampleName: sampleName || '' + }; + const response = httpService.doGet(`${config.webAPIRoot}cohortsample/${data.cohortId}/${data.sampleName}/${data.sampleName}`, {}).then(({data}) => data); + response.catch((er) => { + console.error('Can\'t find annotation navigation'); + }); + return response; + }; + + const createOrUpdateAnnotation = function (data) { + return httpService.doPost(`${config.webAPIRoot}annotation`, data).then(({annotation}) => data); + }; + + const linkAnnotationToSamples = function (data) { + const response = httpService.doPost(`${config.webAPIRoot}annotation/sample`, data).then(({link}) => data); + response.catch((er) => { + console.error('Unable to link annotation to sample'); + }); + + return response; + }; + + + return { + deleteQuestionSet, + getAnnotationSets, + getSetsBySampleId, + getSamplesBySetId, + getAnnotationsBySampleIdSetId, + getStudySets, + getSuperTable, + getStudyResults, + getAnnotationByCohortIdbySubjectIdBySetId, + getAnnotationBySampleIdbySubjectIdBySetId, + getAnnotationNavigation, + createOrUpdateAnnotation, + linkAnnotationToSamples + }; +}); \ No newline at end of file diff --git a/js/services/Validation.js b/js/services/Validation.js new file mode 100644 index 000000000..0af02a863 --- /dev/null +++ b/js/services/Validation.js @@ -0,0 +1,80 @@ +define(function (require, exports) { + + const httpService = require('services/http'); + const config = require('appConfig'); + const authApi = require('services/AuthAPI'); + const $ = require('jquery'); + + function createValidationSet(sk, ss, name, id, getSamples) { + var url = `${config.webAPIRoot}cohortSample/${sk}/${id}?size=${ss}&name=${name}`; + + var promise = $.ajax({ + url: url, + method: 'POST', + context: this, + contentType: 'application/json', + success: function (data) { + console.log('here') + getSamples() + return null; + }, + error: function (error) { + console.log("Error: " + error); + } + }); + + return promise; + } + function submitQuestionSet(data) { + var url = `${config.webAPIRoot}annotations/sets`; + return $.ajax({ + url: url, + data: data, + method: 'POST', + context: this, + contentType: 'application/json', + success: function (data) { + return null; + }, + error: function (error) { + console.log("Error: " + error); + } + }); + } + + const getSamples = function(sk, id) { + const response = httpService.doGet(`${config.webAPIRoot}cohortSample/${sk}/${id}`).then(({ data }) => data); + response.catch((er) => { + console.error('Unable to get Validation Sets'); + }); + return response; + }; + + const exportAnnotation = function(sk, cohortId, sampleName) { + if (sampleName.indexOf(' ') >=0) { + var sampleName = sampleName.split(" ").join('_'); + } + const response = httpService.doGet(`${config.webAPIRoot}annotations/csvData?sourceKey=${sk}&cohortID=${cohortId}&sampleName=${sampleName}`).then(({data}) => data); + response.catch((er) => { + console.error('Unable to download CSV') + }); + return response; + }; + + const getQsets = function(id) { + const response = httpService.doGet( `${config.webAPIRoot}annotations/sets?cohortId=${id}`).then(({ data }) => data); + response.catch((er) => { + console.error('Unable to get Question Sets'); + }); + return response; + }; + + API = { + createValidationSet, + getSamples, + exportAnnotation, + getQsets, + submitQuestionSet, + } + return API; + }); \ No newline at end of file