diff --git a/app/client/views/patients/patient.coffee b/app/client/views/patients/patient.coffee index f4f3474..8f9d32d 100644 --- a/app/client/views/patients/patient.coffee +++ b/app/client/views/patients/patient.coffee @@ -1,143 +1,143 @@ AutoForm.hooks editPatientForm: onSubmit: (insertDoc, updateDoc, currentDoc) -> self = @ Meteor.call "updatePatients", [Session.get("selectedPatientId")], updateDoc, (error) -> if error? throwError error self.done() false Template.patient.created = -> self = @ @autorun -> Session.set 'patientSubscriptionsReady', false patient = Patients.findOne Session.get("selectedPatientId") if patient? self.subscribe("studyCompositesForPatient", patient._id) self.subscribe("visitsCompositeForPatient", patient._id, onReady: -> Session.set 'patientSubscriptionsReady', true) Template.patient.rendered = -> @$('[data-toggle=tooltip]').tooltip() Template.patient.helpers designs: -> ds = "" designs = @studyDesigns().forEach (d) -> ds += d.title+', ' ds.slice(0, -2) numVisits: -> visits = 0 @studyDesigns().forEach (d) -> visits += d.visits.length if d? visits template: -> selectedDesignVisitId = Session.get 'selectedDesignVisitId' if selectedDesignVisitId? "patientVisit" else "patientVisits" templateData: -> patient: @ formDoc: -> Patients.findOne Session.get("selectedPatientId") editPatientSchema: -> caseManagers = Meteor.users.find( roles: "caseManager" ).map (t) -> label: getUserDescription(t) value: t._id designs = StudyDesigns.find( studyId: @_id ).map (d) -> label: d.title value: d._id langs = isoLangs.map (l) -> label: "#{l.name} (#{l.nativeName})" value: l.code schema = primaryLanguage: label: 'Primary Language' type: String optional: true autoform: options: langs secondaryLanguage: label: 'Secondary Language' type: String optional: true autoform: options: langs hrid: label: "HRID" type: String optional: true max: 8 if Roles.userIsInRole(Meteor.userId(), 'admin') schema = _.extend schema, - caseManagerId: - label: "Case Manager" - type: String + caseManagerIds: + label: "Case Managers" + type: [String] optional: true autoform: - type: "select" + type: "select-checkbox" options: caseManagers new SimpleSchema(schema) Template.patient.events "click .id, click .hrid": (evt) -> selectPatientId(@_id, true) "click button.delete": (evt) -> patientId = @_id swal { title: 'Are you sure?' text: 'Do you really want to delete this patient? A log entry will be created.' type: 'warning' showCancelButton: true confirmButtonText: 'Yes' closeOnConfirm: false }, -> Meteor.call "removePatient", patientId, (error) -> if error? if error.reason is "patientHasData" swal { title: 'Attention!' text: """The patient you are about to remove has data entries. Please consider using the exclude checkbox instead. If you really want to proceed removing the patient please state a reason. A log entry will be created.""" type: 'input' showCancelButton: true confirmButtonText: 'Yes' inputPlaceholder: "Please state a reason." closeOnConfirm: false }, (confirmedWithReason)-> if confirmedWithReason is false #cancel swal.close() else if !confirmedWithReason? or confirmedWithReason.length is 0 swal.showInputError("You need to state a reason!") else Meteor.call "removePatient", patientId, confirmedWithReason, (error2) -> if error2? throwError error2 else swal.close() $('#studiesSelect').selectpicker('deselectAll') Session.set 'selectedDesignVisitId', null Session.set 'selectedPatientId', null Session.set 'selectedStudyIds', null return else throwError error else swal.close() $('#studiesSelect').selectpicker('deselectAll') Session.set 'selectedDesignVisitId', null Session.set 'selectedPatientId', null Session.set 'selectedStudyIds', null return return false diff --git a/app/client/views/patients/patients.coffee b/app/client/views/patients/patients.coffee index c8d818c..2739889 100644 --- a/app/client/views/patients/patients.coffee +++ b/app/client/views/patients/patients.coffee @@ -1,459 +1,459 @@ _refreshSelectsContentsTrigger = new ReactiveVar(false) AutoForm.hooks addPatientsForm: onSubmit: (insertDoc, updateDoc, currentDoc) -> self = @ update = '$set': studyDesignIds: [updateDoc['$set']['design']] Meteor.call 'createPatient', insertDoc.study, (error, patientId) -> if error? throwError error else Meteor.call "updatePatients", [patientId], update, (error) -> if error? throwError error self.done() false Template.patients.destroyed = -> $(window).off('hashchange') Template.patients.rendered = -> @subscribe("studies", onReady: -> refreshSelectValues()) @subscribe("patients", onReady: -> refreshSelectValues()) @subscribe("caseManagers", onReady: -> refreshSelectValues()) @subscribe("studyDesigns", onReady: -> refreshSelectValues()) @$('.selectpicker').each -> $(@).selectpicker() #actionsBox: true #only works with multiple now, nobody knows why #liveSearch: true $(window).on('hashchange', hashchange) @autorun -> Session.get('selectedStudyIds') #Session.get('selectedStudyDesignIds') Session.get('selectedPatientId') Session.get('selectedDesignVisitId') Session.get('selectedQuestionnaireWizzard') refreshSelectValues() refreshUrlParams() Template.patients.helpers studies: -> Studies.find({}, {sort: title: 1}) designs: -> selectedStudyIds = Session.get 'selectedStudyIds' find = {} if selectedStudyIds? find.studyId = {$in: selectedStudyIds} StudyDesigns.find(find, {sort: index: 1}).map (design) -> design.study = Studies.findOne(design.studyId) design patients: -> _refreshSelectsContentsTrigger.get() selectedStudyIds = Session.get 'selectedStudyIds' #selectedStudyDesignIds = Session.get 'selectedStudyDesignIds' selectedPatientId = Session.get 'selectedPatientId' find = {} if selectedStudyIds? and selectedStudyIds.length > 0 find.studyId = {$in: selectedStudyIds} #if selectedStudyDesignIds? and selectedStudyDesignIds.length > 0 # find.studyDesignIds = {$in: selectedStudyDesignIds} if !find.studyId? and !find.studyDesignId? and selectedPatientId? find._id = selectedPatientId Patients.find(find, {sort: {studyId: 1}}) visits: -> selectedStudyIds = Session.get 'selectedStudyIds' #selectedStudyDesignIds = Session.get 'selectedStudyDesignIds' selectedPatientId = Session.get 'selectedPatientId' find = {} if selectedStudyIds? find.studyId = {$in: selectedStudyIds} #if selectedStudyDesignIds? # find._id = {$in: selectedStudyDesignIds} if selectedPatientId? studyDesignIds = Patients.findOne(selectedPatientId).studyDesignIds find._id = {$in: studyDesignIds} visits = [] StudyDesigns.find(find, sort: {title: 1}).forEach (design) -> study = Studies.findOne(design.studyId) design.visits.forEach (v) -> v.design = design v.study = study visits.push v visits questionnaires: -> selectedPatientId = Session.get 'selectedPatientId' selectedDesignVisitId = Session.get 'selectedDesignVisitId' p = Patients.findOne selectedPatientId if p? questionnaireIds = [] visit = null StudyDesigns.find( _id: $in: p.studyDesignIds ).forEach (d) -> v = _.find d.visits, (visit) -> visit._id is selectedDesignVisitId if v? and v.length > 0 visit = v[0] if visit? return Questionnaires.find _id: {$in: v.questionnaireIds} , sort: title: 1 return singlePatient: -> Session.get('selectedPatientId')? singleVisit: -> Session.get('selectedPatientId')? and Session.get('selectedDesignVisitId')? patientsTableCursor: -> find = {} selectedStudyIds = Session.get 'selectedStudyIds' if selectedStudyIds? find.studyId = {$in: selectedStudyIds} #selectedStudyDesignIds = Session.get 'selectedStudyDesignIds' #if selectedStudyDesignIds? # find.studyDesignIds = {$in: selectedStudyDesignIds} #selectedPatientId = Session.get 'selectedPatientId' #if selectedPatientId? # find._id = selectedPatientId Patients.find(find) patientsRTS: -> useFontAwesome: true rowsPerPage: 100 showFilter: false fields: [ key: 'id', label: "ID" , key: 'hrid', label: "hrid" , key: 'studyId', label: "Study", sortOrder: 0 fn: (v,o) -> study = o.study() return study.title if study? , key: 'studyDesignIds', label: "Designs" fn: (v,o) -> ds = "" designs = o.studyDesigns().forEach (d) -> ds += d.title+', ' return ds.slice(0, -2) , - key: 'caseManagerId', label: "Case Manager" + key: 'caseManagerIds', label: "Case Managers" fn: (v,o) -> - caseManager = o.caseManager() - if caseManager? - getUserDescription caseManager + caseManagers = o.caseManagers() + if caseManagers? + caseManagers.map((x) -> getUserDescription(x)).join(', ') else "" , key: 'languages', label: "Languages" fn: (v,o) -> o.languages() , key: '', label: "no. sheduled visits" fn: (v,o) -> visits = 0 design = o.studyDesigns().forEach (d) -> visits += d.visits.length if d? return visits , key: "createdAt", label: 'created', sortByValue: true fn: (v,o)-> fullDate(v) ] selectedPatient: -> Patients.findOne _id: Session.get('selectedPatientId') addPatientsSchema: -> if Roles.userIsInRole(Meteor.userId(), 'admin') patientsFilter = {} else - patientsFilter = {'caseManagerId': Meteor.userId()} + patientsFilter = {'caseManagerIds': [Meteor.userId()]} patients = Patients.find(patientsFilter, {sort: {studyId: 1}}) studies = Studies.find( {'_id': {$in: Array.from(new Set(patients.map((x) -> x.studyId)))}} ) schema = study: label: "Study" type: String optional: false autoform: type: "select" options: studies.map((x) -> {'label': x.title, 'value': x._id}) design: label: "Design" type: String optional: false autoform: type: "select" options: () -> StudyDesigns.find( {'studyId': AutoForm.getFieldValue('study')} ).map((x) -> {'label': x.title, 'value': x._id}) new SimpleSchema(schema) Template.patients.events "click #patientsTable table tr": (evt) -> return if !@_id #header selectPatientId(@_id) return "change #studiesSelect": (evt) -> ids = $('#studiesSelect').val() if !ids? or ids.indexOf('deselect') > -1 $('#studiesSelect').selectpicker('deselectAll') ids = null Session.set 'selectedDesignVisitId', null Session.set 'selectedPatientId', null Session.set 'selectedStudyIds', ids return #"change #designsSelect": (evt) -> # ids = $('#designsSelect').val() # if ids.indexOf('deselect') > -1 # $('#designsSelect').selectpicker('deselectAll') # ids = null # Session.set 'selectedDesignVisitId', null # Session.set 'selectedPatientId', null # Session.set 'selectedStudyDesignIds', ids # return "change #patientSelect": (evt) -> id = $('#patientSelect').val() ids = null if id is 'deselect' if id is 'deselect' $('#patientSelect').selectpicker('deselectAll') id = null selectPatientId(id) return "change #visitSelect": (evt) -> id = $('#visitSelect').val() if id is 'deselect' $('#visitSelect').selectpicker('deselectAll') id = null Session.set 'selectedDesignVisitId', id return "change #questionnaireSelect": (evt) -> id = $('#questionnaireSelect').val() if id is 'deselect' $('#questionnaireSelect').selectpicker('deselectAll') id = null Session.set 'selectedQuestionnaireId', id return @selectPatientId = (id, clearVisitsSelection) -> if id? patient = Patients.findOne id if patient.studyDesignIds? # we can use the first designId here because all designs # must be in the same studyDesign studyDesign = StudyDesigns.findOne patient.studyDesignIds[0] if studyDesign? selectedStudyIds = Session.get('selectedStudyIds') or [] selectedStudyIds.push studyDesign.studyId selectedStudyIds = _.unique selectedStudyIds Session.set 'selectedStudyIds', selectedStudyIds #check if selectedDesignVisitId is posible for patient selectedDesignVisitId = Session.get 'selectedDesignVisitId' if selectedDesignVisitId? sd = StudyDesigns.findOne _id: $in: patient.studyDesignIds 'visits._id': selectedDesignVisitId if !sd #selectedDesignVisitId is from another design, which this patient isn't part of Session.set 'selectedDesignVisitId', null #selectedStudyDesignIds = Session.get('selectedStudyDesignIds') or [] #StudyDesigns.find(_id: $in: patient.studyDesignIds).forEach (d) -> # selectedStudyDesignIds.push d._id #selectedStudyDesignIds = _.unique selectedStudyDesignIds #Session.set 'selectedStudyDesignIds', selectedStudyDesignIds else Session.set 'selectedStudyIds', null #Session.set 'selectedStudyDesignIds', null if clearVisitsSelection Session.set 'selectedDesignVisitId', null Session.set 'selectedPatientId', id return refreshSelectValues = -> _refreshSelectsContentsTrigger.set(_refreshSelectsContentsTrigger.get()) Meteor.setTimeout -> $('#studiesSelect').selectpicker('val', Session.get('selectedStudyIds') or null) #$('#designsSelect').selectpicker('val', Session.get('selectedStudyDesignIds') or null) $('#patientSelect').selectpicker('val', Session.get('selectedPatientId') or null) $('#visitSelect').selectpicker('val', Session.get('selectedDesignVisitId') or null) $('#questionnaireSelect').selectpicker('val', Session.get('selectedQuestionnaireId') or null) , 100 _hashChangedInternally = false refreshUrlParams = -> newHash = studyIds: Session.get('selectedStudyIds') #designIds: Session.get('selectedStudyDesignIds') patientId: Session.get('selectedPatientId') visitId: Session.get('selectedDesignVisitId') questionnaireWizzard: Session.get('selectedQuestionnaireWizzard') #doesn't work because underscore is too old #hash = _.pick hash, (value, key, object) -> value? hash = {} Object.keys(newHash).forEach (key) -> value = newHash[key] hash[key] = value if value? if Object.keys(hash).length > 0 window.location.hash = JSON.stringify hash else window.location.hash = "" return hashchange = -> hash = window.location.hash if hash? and hash.length > 1 error = null #TODO fix decodeURI in safari try hash = decodeURI(hash) catch e console.log "decodeURI error:" console.log e error = e return if error? hash = JSON.parse hash.slice(1) Session.set 'selectedStudyIds', hash.studyIds #Session.set 'selectedStudyDesignIds', hash.designIds Session.set 'selectedPatientId', hash.patientId Session.set 'selectedDesignVisitId', hash.visitId qw = Session.get 'selectedQuestionnaireWizzard' if hash.questionnaireWizzard? if !qw? __showQuestionnaireWizzard hash.questionnaireWizzard else if qw? __closeQuestionnaireWizzard() else Session.set 'selectedStudyIds', null #Session.set 'selectedStudyDesignIds', null Session.set 'selectedPatientId', null Session.set 'selectedDesignVisitId', null if Session.get('selectedQuestionnaireWizzard')? __closeQuestionnaireWizzard() return ################################################ #selects rendering studyOptionTimeout = null refreshStudiesSelect = -> if studyOptionTimeout? Meteor.clearTimeout studyOptionTimeout studyOptionTimeout = Meteor.setTimeout((-> $('#studiesSelect').selectpicker 'refresh' studyOptionTimeout = false return ), 50) return Template.studyOption.rendered = -> refreshStudiesSelect() Template.studyOption.destroyed = -> refreshStudiesSelect() #designOptionTimeout = null #refreshDesignsSelect = -> # if designOptionTimeout? # Meteor.clearTimeout designOptionTimeout # designOptionTimeout = Meteor.setTimeout((-> # $('#designsSelect').selectpicker 'refresh' # designOptionTimeout = false # return # ), 50) # return #Template.designOption.rendered = -> # refreshDesignsSelect() #Template.designOption.destroyed = -> # refreshDesignsSelect() patientOptionTimeout = null refreshPatientsSelect = -> if patientOptionTimeout? Meteor.clearTimeout patientOptionTimeout patientOptionTimeout = Meteor.setTimeout((-> $('#patientSelect').selectpicker 'refresh' patientOptionTimeout = false return ), 50) return Template.patientOption.rendered = -> refreshPatientsSelect() Template.patientOption.destroyed = -> refreshPatientsSelect() Template.patientOption.helpers studyDesignTitles: -> titles = "" @studyDesigns().forEach (d) -> titles +=d.title+', ' titles.slice(0, -2) visitOptionTimeout = null refreshVisitsSelect = -> if visitOptionTimeout? Meteor.clearTimeout visitOptionTimeout visitOptionTimeout = Meteor.setTimeout((-> $('#visitSelect').selectpicker 'refresh' visitOptionTimeout = false return ), 50) return Template.visitOption.rendered = -> refreshVisitsSelect() Template.visitOption.destroyed = -> refreshVisitsSelect() questionnaireOptionTimeout = null refreshQuestionnaireSelect = -> if questionnaireOptionTimeout? Meteor.clearTimeout questionnaireOptionTimeout questionnaireOptionTimeout = Meteor.setTimeout((-> $('#questionnaireSelect').selectpicker 'refresh' questionnaireOptionTimeout = false return ), 50) return Template.questionnaireOption.rendered = -> refreshQuestionnaireSelect() Template.questionnaireOption.destroyed = -> refreshQuestionnaireSelect() Template.visitSelect.rendered = -> @$('.selectpicker').each -> $(@).selectpicker() Template.visitSelect.destroyed = -> @$('.selectpicker').each -> $(@).selectpicker('destroy') Template.questionnaireSelect.rendered = -> @$('.selectpicker').each -> $(@).selectpicker() Template.questionnaireSelect.destroyed = -> @$('.selectpicker').each -> $(@).selectpicker('destroy') ################################################ diff --git a/app/client/views/studies/edit_study_patients.coffee b/app/client/views/studies/edit_study_patients.coffee index 43ff235..9147af4 100644 --- a/app/client/views/studies/edit_study_patients.coffee +++ b/app/client/views/studies/edit_study_patients.coffee @@ -1,254 +1,254 @@ AutoForm.hooks editSessionPatientsForm: onSubmit: (insertDoc, updateDoc, currentDoc) -> self = @ ids = Session.get 'editingPatientIds' Meteor.call "updatePatients", Session.get('editingPatientIds'), updateDoc, (error) -> self.done() throwError error if error? false Template.editStudyPatients.rendered = -> Session.set('editingPatientIds', null) Template.editStudyPatients.helpers patients: -> Patients.find studyId: @_id patientsRTS: -> useFontAwesome: true, rowsPerPage: 100, showFilter: true, fields: [ { key: 'id', label: "", sortable: false, fn: (v, o) -> new Spacebars.SafeString("") }, { key: 'id', label: "ID" }, { key: 'hrid', label: "HRID" }, { key: 'studyDesignIds', label: "Designs", fn: (v,o) -> ds = "" designs = o.studyDesigns().forEach (d) -> ds += d.title+', ' return ds.slice(0, -2) }, - { key: 'caseManagerId', label: "Case Manager", fn: (v,o) -> getUserDescription(o.caseManager()) }, + { key: 'caseManagerIds', label: "Case Managers", fn: (v,o) -> o.caseManagers().map((x) -> getUserDescription(x)).join(', ') }, { key: 'languages', label: "Languages", fn: (v,o) -> o.languages() }, { key: 'isExcluded', label: "excluded", tmpl: Template.studyPatientsTableExcluded } { key: "createdAt", label: 'created', sortByValue: true, sortOrder: 0, sortDirection: 'descending', fn: (v,o)->fullDate(v) }, { key: 'buttons', label: '', tmpl: Template.studyPatientsTableButtons } ] editingPanelTitle: -> ids = Session.get('editingPatientIds') return "Patient editor" if !ids? or ids.length is 0 if ids.length > 1 "edit #{ids.length} patients" else p = Patients.findOne _id: ids[0] "edit patient: #{p.id}" numPatientsEditing: -> ids = Session.get('editingPatientIds') return ids.length if ids? false formDoc: -> ids = Session.get('editingPatientIds') if !ids? or ids.length is 0 or ids.length > 1 null else Patients.findOne _id: ids[0] editSessionPatientsSchema: -> caseManagers = Meteor.users.find( roles: "caseManager" ).map (t) -> label: getUserDescription(t) value: t._id designs = StudyDesigns.find( studyId: @_id ).map (d) -> label: d.title value: d._id langs = isoLangs.map (l) -> label: "#{l.name} (#{l.nativeName})" value: l.code schema = - caseManagerId: + caseManagerIds: label: "Case Manager" - type: String + type: [String] optional: true autoform: - type: "select" + type: "select-checkbox" options: caseManagers studyDesignIds: label: "Designs" type: [String] optional: true autoform: type: "select-checkbox" options: designs primaryLanguage: label: 'Primary Language' type: String optional: true autoform: options: langs secondaryLanguage: label: 'Secondary Language' type: String optional: true autoform: options: langs ids = Session.get('editingPatientIds') if ids? and ids.length is 1 schema = _.extend schema, hrid: label: "HRID" type: String optional: true max: 8 new SimpleSchema(schema) Template.editStudyPatients.events "click #createPatient": (evt) -> Meteor.call "createPatient", @_id, (error, patientId) -> if error? throwError error else editingPatientIds = Session.get("editingPatientIds") or [] editingPatientIds.push patientId $("input[data-id=#{patientId}]").prop 'checked', true Session.set "editingPatientIds", _.uniq editingPatientIds return "click .reactive-table tr": (evt) -> return if !@_id #header editingPatientIds = Session.get("editingPatientIds") or [] checkbox = $(evt.target).parent().find('input') if evt.target.type is "checkbox" if (index = editingPatientIds.indexOf(@_id)) > -1 editingPatientIds.splice(index, 1) checkbox.prop('checked', false) else editingPatientIds.push @_id checkbox.prop('checked', true) Session.set "editingPatientIds", _.uniq editingPatientIds else #click on row $('.editStudyPatients table').find('input').prop 'checked', false checkbox.prop('checked', true) Session.set "editingPatientIds", [@_id] return "click button.show": (evt) -> selectPatientId(@_id) Router.go "patients" "click button.remove": (evt) -> patientId = @_id swal { title: 'Are you sure?' text: 'Do you really want to delete this patient? A log entry will be created.' type: 'warning' showCancelButton: true confirmButtonText: 'Yes' closeOnConfirm: false }, -> Meteor.call "removePatient", patientId, (error) -> if error? if error.reason is "patientHasData" swal { title: 'Attention!' text: """The patient you are about to remove has data entries. Please consider using the exclude checkbox instead. If you really want to proceed removing the patient please state a reason. A log entry will be created.""" type: 'input' showCancelButton: true confirmButtonText: 'Yes' inputPlaceholder: "Please state a reason." closeOnConfirm: false }, (confirmedWithReason)-> if confirmedWithReason is false #cancel swal.close() else if !confirmedWithReason? or confirmedWithReason.length is 0 swal.showInputError("You need to state a reason!") else Meteor.call "removePatient", patientId, confirmedWithReason, (error2) -> if error2? throwError error2 else swal.close() return else throwError error else swal.close() return return false "click button.exclude": (evt) -> patientId = @_id swal { title: 'Exclude patient' text: 'If you really want to exclude this patient, type a reason and choose Yes.' type: 'input' showCancelButton: true confirmButtonText: 'Yes' inputPlaceholder: "Reason for exclusion." closeOnConfirm: false }, (reason) -> if reason is false #cancel swal.close() return if !reason? or reason.length is 0 swal.showInputError("You need to state a reason!") return false Meteor.call "excludePatient", patientId, reason, (error) -> if error? throwError error else swal("the patient has been excluded.") return true return false "click button.include": (evt) -> patientId = @_id swal { title: 'Include patient' text: 'If you really want to include this patient again, type a reason and choose Yes.' type: 'input' showCancelButton: true confirmButtonText: 'Yes' inputPlaceholder: "Reason for inclusion." closeOnConfirm: false }, (reason) -> if reason is false #cancel swal.close() return if !reason? or reason.length is 0 swal.showInputError("You need to state a reason!") return false Meteor.call "includePatient", patientId, reason, (error) -> if error? throwError error else swal("the patient has been included.") return true return false Template.studyPatientsTableExcluded.rendered = -> tmpl = @ @autorun -> Template.currentData() tmpl.$('[data-toggle=tooltip]').tooltip() return Template.studyPatientsTableExcluded.helpers lastExcludeInclude: -> l = @excludesIncludes.length if l > 0 @excludesIncludes[l-1] else null diff --git a/app/lib/check_roles.coffee b/app/lib/check_roles.coffee index bcbee72..b996375 100644 --- a/app/lib/check_roles.coffee +++ b/app/lib/check_roles.coffee @@ -1,32 +1,32 @@ @checkIfAdmin = -> user = Meteor.user() throw new Meteor.Error(401, "You need to login") unless user throw new Meteor.Error(401, "You need to be admin") unless Roles.userIsInRole(user, 'admin') @canAddPatient = (studyId) -> user = Meteor.user() throw new Meteor.Error(401, "You need to login") unless user authorized = false if Roles.userIsInRole(user, 'admin') authorized = true else patients = Patients.find( - {'studyId': studyId, 'caseManagerId': user._id} + {'studyId': studyId, 'caseManagerIds': user._id} ).map((x) -> x) authorized = patients? and patients.length > 0 if !authorized throw new Meteor.Error(401, 'Unauthorized') @canUpdatePatient = (patientId) -> user = Meteor.user() throw new Meteor.Error(401, "You need to login") unless user authorized = false if Roles.userIsInRole(user, 'admin') authorized = true else patients = Patients.find( - {'_id': patientId, 'caseManagerId': user._id} + {'_id': patientId, 'caseManagerIds': user._id} ).map((x) -> x) authorized = patients? and patients.length > 0 if !authorized throw new Meteor.Error(401, 'Unauthorized') diff --git a/app/lib/collections/answers.coffee b/app/lib/collections/answers.coffee index e653535..173c9ca 100644 --- a/app/lib/collections/answers.coffee +++ b/app/lib/collections/answers.coffee @@ -1,63 +1,63 @@ @Answers = new Meteor.Collection("answers") Answers.before.insert BeforeInsertTimestampHook Answers.before.update BeforeUpdateTimestampHook Meteor.methods "upsertAnswer": (answer) -> check(answer.visitId, String) visit = Visits.findOne answer.visitId throw new Meteor.Error(403, "visit can't be found.") unless visit? patient = Patients.findOne visit.patientId throw new Meteor.Error(403, "patient can't be found.") unless patient? - throw new Meteor.Error(433, "you are not allowed to upsert answers") unless Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) + throw new Meteor.Error(433, "you are not allowed to upsert answers") unless Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and @userId in patient.caseManagerIds) check(answer.questionId, String) question = Questions.findOne answer.questionId throw new Meteor.Error(403, "question can't be found.") unless question? questionnaire = Questionnaires.findOne question.questionnaireId throw new Meteor.Error(403, "questionnaire can't be found.") unless questionnaire? #check if questionnaire is scheduled at visit found = false visit.questionnaireIds.some (questionnaireId) -> if questionnaireId is questionnaire._id found = true found throw new Meteor.Error(403, "questionnaire is not scheduled at this visit.") unless found answerId = null if answer._id? a = Answers.findOne _.pick answer, 'visitId', 'questionId', '_id' throw new Meteor.Error(403, "answer to update can't be found.") unless answer? - + Answers.update answer._id, $set: value: answer.value answerId = answer._id else #check if an answer exists already a = Answers.findOne _.pick(answer, 'visitId', 'questionId') if a? console.log "\n\nError: There already exists an answer for this visitId and questionId." console.log a throw new Meteor.Error(403, "Error: There already exists an answer for this visitId and questionId.") answer = _.pick answer, 'visitId', 'questionId', 'value' answerId = Answers.insert answer if !patient.hasData Patients.update patient._id, $set: hasData: true #update patient.hasDataForDesignIds studyDesign = StudyDesigns.findOne 'visits._id': visit.designVisitId Patients.update patient._id, $addToSet: hasDataForDesignIds: studyDesign._id if !visit.date? Meteor.call "changeVisitDate", visit._id, Date.now() return answerId diff --git a/app/lib/collections/patients.coffee b/app/lib/collections/patients.coffee index 5df3419..01947d1 100644 --- a/app/lib/collections/patients.coffee +++ b/app/lib/collections/patients.coffee @@ -1,268 +1,268 @@ class @Patient constructor: (doc) -> _.extend this, doc - caseManager: -> - return null unless @caseManagerId? - Meteor.users.findOne _id: @caseManagerId - - caseManagerName: -> - getUserDescription @caseManager() + caseManagers: -> + return null unless @caseManagerIds? + Meteor.users.find(_id: $in: @caseManagerIds).map((x) -> x) study: -> return null unless @studyId? Studies.findOne _id: @studyId studyDesigns: -> return [] unless @studyDesignIds? StudyDesigns.find _id: $in: @studyDesignIds , sort: createdAt: 1 languages: -> langs = "" langs += @primaryLanguage if @primaryLanguage? if @secondaryLanguage langs += ", #{@secondaryLanguage}" langs @Patients = new Meteor.Collection("patients", transform: (doc) -> new Patient(doc) ) if Meteor.isServer Patients._ensureIndex( { id: 1 }, { unique: true } ) Patients.before.insert BeforeInsertTimestampHook Patients.before.update BeforeUpdateTimestampHook schema = 'id': type: String 'hrid': type: String optional: true unique: true index: true 'creatorId': type: String 'studyId': type: String 'studyDesignId': type: String optional: true 'studyDesignIds': type: [String] optional: true defaultValue: [] 'caseManagerId': type: String optional: true + 'caseManagerIds': + type: [String] + optional: true 'primaryLanguage': type: String optional: true 'secondaryLanguage': type: String optional: true 'hasData': type: Boolean defaultValue: false 'hasDataForDesignIds': type: [String] optional: true 'isExcluded': type: Boolean defaultValue: false 'excludesIncludes': type: [Object] optional: true 'excludesIncludes.$.reason': type: String 'excludesIncludes.$.createdAt': type: Number 'updatedAt': type: Number optional: true 'createdAt': type: Number optional: true Patients.attachSchema new SimpleSchema(schema) if Meteor.isServer Meteor.methods "createPatient": (studyId) -> canAddPatient(studyId) check studyId, String study = Studies.findOne studyId throw new Meteor.Error(403, "study not found.") unless study? throw new Meteor.Error(400, "Study is locked. Please unlock it first.") if study.isLocked _id = null tries = 0 loop try _id = Patients.insert id: readableRandom(6) creatorId: Meteor.userId() studyId: studyId hasData: false - caseManagerId: Meteor.userId() + caseManagerIds: [Meteor.userId()] catch e console.log "Error: createPatient" console.log e finally tries += 1 break if _id or tries >= 10 throw new Meteor.Error(500, "Can't create patient, id space seems to be full.") unless _id? return _id Meteor.methods 'updatePatients': (ids, update) -> check ids, [String] check update, Object ids.forEach((patientId) -> canUpdatePatient(patientId)) allowedKeys = [ - "$set.caseManagerId", + "$set.caseManagerIds", "$set.studyDesignIds", "$set.primaryLanguage", "$set.secondaryLanguage", "$unset.primaryLanguage", "$unset.secondaryLanguage", ] if Roles.userIsInRole(Meteor.user(), 'admin') allowedKeys = [ allowedKeys..., - "$unset.caseManagerId", + "$unset.caseManagerIds", "$unset.studyDesignIds" ] if ids.length > 1 if update['$set']? #don't overwrite with empty values delete update['$unset'] else if ids.length is 1 allowedKeys = _.union allowedKeys, [ "$set.hrid", "$unset.hrid" ] # If no study designs are selected store an empty array if update['$unset']? and update['$set']? and update['$unset']['studyDesignIds']? delete update['$unset']['studyDesignIds'] update['$set']['studyDesignIds'] = [] #pick whitelisted keys update = _.pickDeep update, allowedKeys Patients.find(_id: $in: ids).forEach (p) -> study = Studies.findOne p.studyId throw new Meteor.Error(403, "study not found.") unless study? throw new Meteor.Error(400, "Study is locked. Please unlock it first.") if study.isLocked #check if changing studyDesign for a patient which has data if update['$set']?['studyDesignIds']? or update['$unset']?['studyDesignIds']? patientIds = [] Patients.find(_id: $in: ids).forEach (p) -> # unset is always a string if update['$unset']?['studyDesignIds']? and p.hasData patientIds.push p.id if p.hasDataForDesignIds? if update['$set']?['studyDesignIds']? allFound = true p.hasDataForDesignIds.forEach (sDId) -> if update['$set']['studyDesignIds'].indexOf(sDId) is -1 allFound = false if !allFound patientIds.push p.id if patientIds.length > 0 throw new Meteor.Error(400, "You have removed one or more study designs from patient(s) (#{patientIds.join(', ')}) which have already entered data for one of these designs. This is not allowed and your changes are therefore discarded.") #remove already created visits with no data Patients.find(_id: $in: ids).forEach (p) -> Visits.find( patientId: p._id ).forEach (v) -> if Answers.find( visitId: v._id).count() is 0 Visits.remove _id: v._id ids.forEach (id) -> try Patients.update _id: id, update catch e if e.code is 11000 #MongoError: E11000 duplicate key error throw new Meteor.Error(400, "The HRID you entered exists already, please choose a unique value.") else throw e return "excludePatient": (patientId, reason) -> checkIfAdmin() check patientId, String check reason, String patient = Patients.findOne patientId throw new Meteor.Error(500, "Patient can't be found.") unless patient? throw new Meteor.Error(500, "Patient is already excluded.") if patient.isExcluded study = Studies.findOne patient.studyId throw new Meteor.Error(403, "study not found.") unless study? throw new Meteor.Error(400, "Study is locked. Please unlock it first.") if study.isLocked Patients.update patientId, $push: excludesIncludes: reason: reason createdAt: Date.now() Patients.update patientId, $set: isExcluded: true return "includePatient": (patientId, reason) -> checkIfAdmin() check patientId, String check reason, String patient = Patients.findOne patientId throw new Meteor.Error(500, "Patient can't be found.") unless patient? throw new Meteor.Error(500, "Patient isn't excluded.") if !patient.isExcluded study = Studies.findOne patient.studyId throw new Meteor.Error(403, "study not found.") unless study? throw new Meteor.Error(400, "Study is locked. Please unlock it first.") if study.isLocked Patients.update patientId, $push: excludesIncludes: reason: reason createdAt: Date.now() Patients.update patientId, $set: isExcluded: false return "removePatient": (patientId, forceReason) -> check patientId, String canUpdatePatient(patientId) patient = Patients.findOne patientId throw new Meteor.Error(500, "Patient can't be found.") unless patient? study = Studies.findOne patient.studyId throw new Meteor.Error(403, "study not found.") unless study? throw new Meteor.Error(400, "Study is locked. Please unlock it first.") if study.isLocked if patient.hasData and !forceReason throw new Meteor.Error(500, "patientHasData") if patient.hasData Meteor.call "logActivity", "remove patient (#{patient.id}) which has data", "critical", forceReason, patient else Meteor.call "logActivity", "remove patient (#{patient.id}) which has no data", "notice", null, patient Visits.find(patientId: patientId).forEach (v) -> Answers.remove visitId: v._id Visits.remove v._id Patients.remove _id: patientId return diff --git a/app/lib/collections/visits.coffee b/app/lib/collections/visits.coffee index b4c60d7..382358c 100644 --- a/app/lib/collections/visits.coffee +++ b/app/lib/collections/visits.coffee @@ -1,275 +1,275 @@ class @Visit constructor: (doc) -> _.extend this, doc study: -> return null unless @studyId? Studies.findOne _id: @studyId studyDesign: -> return null unless @studyDesignId? StudyDesigns.findOne _id: @studyDesignId questionnaires: -> qIds = @questionnaireIds or [] questionnaires = {} sortedQuestionnaires = [] Questionnaires.find( _id: {$in: qIds} ).forEach (q) -> if q? questionnaires[q._id] = q qIds.forEach (qId) -> questionnaire = questionnaires[qId] if questionnaire? sortedQuestionnaires.push questionnaire sortedQuestionnaires validatedDoc: -> valid = true #we can't check, so assume true @physioValid = true validatedQuestionnaires = @getValidatedQuestionnaires() if valid _.some validatedQuestionnaires, (quest) -> if quest.answered is false valid = false !valid @validatedQuestionnaires = validatedQuestionnaires @valid = valid @ getValidatedQuestionnaires: -> visit = @ quests = @questionnaires().map (quest) -> numQuestions = 0 numQuestionsRequired = 0 quest.answered = true quest.numAnswered = 0 quest.numAnsweredRequired = 0 questions = Questions.find questionnaireId: quest._id type: {$ne: "description"} #filter out descriptions .map (question) -> if question.subquestions? numQuestions += question.subquestions.length if !question.optional numQuestionsRequired += question.subquestions.length else numQuestions += 1 if !question.optional numQuestionsRequired++ answer = Answers.findOne visitId: visit._id questionId: question._id answered = answer? if question.type is 'table' or question.type is 'table_polar' answered = answer? and answer.value.length is question.subquestions.length #question.answered = answered if !answered quest.answered = false if question.subquestions? if answer? quest.numAnswered += answer.value.length if !question.optional quest.numAnsweredRequired += answer.value.length else if answered quest.numAnswered += 1 if !question.optional quest.numAnsweredRequired++ question #quest.questions = questions quest.numQuestions = numQuestions quest.numQuestionsRequired = numQuestionsRequired quest.answered = true if quest.numAnsweredRequired >= quest.numQuestionsRequired quest quests @Visits = new Meteor.Collection("visits", transform: (doc) -> new Visit(doc) ) Visits.before.insert BeforeInsertTimestampHook Visits.before.update BeforeUpdateTimestampHook schema = 'patientId': type: String 'designVisitId': type: String 'title': type: String 'questionnaireIds': type: [String] defaultValue: [] 'recordPhysicalData': type: Boolean defaultValue: false 'index': type: Number 'daysOffsetFromPrevious': type: Number optional: true 'daysOffsetFromBaseline': type: Number optional: true 'date': type: Number optional: true 'updatedAt': type: Number optional: true 'createdAt': type: Number optional: true Visits.attachSchema new SimpleSchema(schema) Meteor.methods "initVisit": (designVisitId, patientId) -> check designVisitId, String check patientId, String patient = Patients.findOne patientId throw new Meteor.Error(403, "patient can't be found.") unless patient? - throw new Meteor.Error(433, "you are not allowed to upsert answers") unless Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) + throw new Meteor.Error(433, "you are not allowed to upsert answers") unless Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and @userId in patient.caseManagerIds) # use query from Patient studyDesign = StudyDesigns.findOne _id: $in: patient.studyDesignIds 'visits._id': designVisitId throw new Meteor.Error(403, "studyDesign can't be found.") unless studyDesign? visitTemplate = _.find studyDesign.visits, (visit) -> return visit if visit._id is designVisitId false throw new Meteor.Error(403, "studyDesign visit can't be found.") unless visitTemplate? #check if visit does not exist already c = Visits.find patientId: patient._id designVisitId: visitTemplate._id .count() #happens on reload, shouldn't be a problem to ignore return if c > 0 #throw new Meteor.Error(403, "a visit from this template exists aready for the patient") if c > 0 #we copy the data here from the visit template to #an actuall existing visit here visit = patientId: patient._id designVisitId: visitTemplate._id title: visitTemplate.title questionnaireIds: visitTemplate.questionnaireIds recordPhysicalData: visitTemplate.recordPhysicalData index: visitTemplate.index daysOffsetFromPrevious: visitTemplate.daysOffsetFromPrevious if visitTemplate.daysOffsetFromPrevious? daysOffsetFromBaseline: visitTemplate.daysOffsetFromBaseline if visitTemplate.daysOffsetFromBaseline? _id = Visits.insert visit _id "changeVisitDate": (visitId, date) -> check visitId, String if date? #we allow null values check date, Number check isNaN(date), false visit = Visits.findOne visitId throw new Meteor.Error(403, "visit can't be found.") unless visit? patient = Patients.findOne visit.patientId throw new Meteor.Error(403, "patient can't be found.") unless patient? - throw new Meteor.Error(433, "you are not allowed change this visit") unless Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) + throw new Meteor.Error(433, "you are not allowed change this visit") unless Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and @userId in patient.caseManagerIds) if date? Visits.update visitId, $set: date: date else Visits.update visitId, $unset: date: '' return "removeAnswersOfQuestionnaireFromVisit": (visitId, questionnaireId, reason) -> checkIfAdmin() check(visitId, String) check(questionnaireId, String) check(reason, String) questionnaire = Questionnaires.findOne questionnaireId throw new Meteor.Error(403, "questionnaire not found.") unless questionnaire? visit = Visits.findOne visitId throw new Meteor.Error(403, "visit not found.") unless visit? patient = Patients.findOne visit.patientId throw new Meteor.Error(403, "patient not found.") unless patient? questionIds = Questions.find({questionnaireId: questionnaireId}).map (q) -> q._id answers = Answers.find( questionId: $in: questionIds visitId: visitId ).fetch() Meteor.call "logActivity", "remove all answers of questionnaire (#{questionnaire.id} - #{questionnaire.title}) from visit (#{visit.title} - #{visit._id}) from patient (#{patient.hrid} - #{patient.id})", "critical", reason, answers answerIds = answers.map (a) -> a._id Answers.remove _id: $in: answerIds return @__getScheduledVisitsForPatientId = (patientId, studyDesignId) -> check patientId, String check studyDesignId, String patient = Patients.findOne patientId throw new Meteor.Error(403, "patient can't be found.") unless patient? studyDesign = StudyDesigns.findOne studyDesignId if !studyDesign? return [] visits = studyDesign.visits.map (designVisit) -> visit = Visits.findOne designVisitId: designVisit._id patientId: patient._id #dummy visit for validation to work visit = new Visit(designVisit) if !visit? visit.validatedDoc() visits.sort (a,b) -> a.index - b.index previousDate = null previousVisit = null baselineDate = null visits.forEach (v) -> if previousDate? and v.daysOffsetFromPrevious? v.dateScheduled = moment(previousDate).add(v.daysOffsetFromPrevious, 'days') else if baselineDate? and v.daysOffsetFromBaseline? v.dateScheduled = moment(baselineDate).add(v.daysOffsetFromBaseline, 'days') if !baselineDate? and v.daysOffsetFromBaseline is 0 #this is our baseline if v.date? baselineDate = moment(v.date) #special case for visit before baseline with negative daysOffsetFromBaseline #if there are multiple such vists, the one closest to the baseline wins if previousDate? and previousVisit? and previousVisit.daysOffsetFromBaseline < 0 v.dateScheduled = moment(previousDate).add(-previousVisit.daysOffsetFromBaseline, 'days') baselineDate = moment(v.dateScheduled) if v.date? previousDate = moment(v.date) else if v.dateScheduled? previousDate = moment(v.dateScheduled) previousVisit = v visits.sort (a, b) -> a.index - b.index return visits diff --git a/app/server/mongo_migrations/migrations.coffee b/app/server/mongo_migrations/migrations.coffee index afbe8d4..6e05154 100644 --- a/app/server/mongo_migrations/migrations.coffee +++ b/app/server/mongo_migrations/migrations.coffee @@ -1,448 +1,462 @@ util = Npm.require('util') Migrations.add version: 1 up: -> console.log "delete everything except questionnaires & questions" Answers.remove({}) Patients.remove({}) Studies.remove({}) StudyDesigns.remove({}) Visits.remove({}) Migrations.add version: 2 up: -> console.log "sanitize: choices variables & null values; multiplechoice modes" Questions.find().forEach (question) -> if question.choices? question.choices = question.choices.filter (choice) -> choice? question.choices.forEach (choice) -> choice.variable = choice.value #console.log question.choices Questions.update question._id, $set: choices: question.choices if question.type is 'multipleChoice' if !question.mode? question.mode = 'radio' #console.log question Questions.update question._id, $set: mode: question.mode Migrations.add version: 3 up: -> console.log "sanitize whitespaces in question.code, question.choices.variable and question.subquestions.code" Questionnaires.find().forEach (questionnaire) -> console.log questionnaire.title Questions.find( questionnaireId: questionnaire._id ).forEach (question) -> code = question.code if !code code = questionnaire.title+'_'+question.index+1 code = code.toString() code = code.replace(/\s/g, '_') if code isnt question.code console.log "updating code #{question.code} -> #{code}" Questions.update question._id, $set: code: code if question.choices? updateChoices = false question.choices.forEach (choice) -> variable = choice.variable.toString() variable = variable.replace(/\s/g, '_') if variable.valueOf() isnt choice.variable.valueOf() console.log "updating variable #{choice.variable} -> #{variable}" choice.variable = variable updateChoices = true if updateChoices Questions.update question._id, $set: choices: question.choices if question.subquestions? updateSubquestions = false question.subquestions.forEach (subq) -> code = subq.code code = code.replace(/\s/g, '_') if code.valueOf() isnt subq.code.valueOf() console.log "updating code #{subq.code} -> #{code}" subq.code = code updateSubquestions = true if updateSubquestions Questions.update question._id, $set: subquestions: question.subquestions Migrations.add version: 4 up: -> console.log "fix visit titles" StudyDesigns.find({}).forEach (design) -> design.visits.forEach (v) -> Visits.find( designVisitId: v._id ).forEach (visit) -> if visit.title isnt v.title Visits.update visit._id, $set: title: v.title return Migrations.add version: 5 up: -> console.log "fix empty choices and subquestions" Questions.find({}).forEach (question) -> if question.choices? choices = question.choices.filter (choice) -> choice? if choices.length isnt question.choices.length Questions.update question._id, $set: choices: choices if question.subquestions? subquestions = question.subquestions.filter (subq) -> subq? if subquestions.length isnt question.subquestions.length console.log "fix subquestions" Questions.update question._id, $set: subquestions: subquestions return Migrations.add version: 6 up: -> console.log "fix question indices" Questionnaires.find().forEach (questionnaire) -> i = 1 Questions.find( questionnaireId: questionnaire._id , sort: index: 1 ).forEach (question) -> if i isnt question.index console.log "fix question index" console.log question Questions.update question._id, $set: index: i i += 1 return Migrations.add version: 7 up: -> console.log "migrate question.choices.$.value from Number to String" Questions.find().forEach (question) -> if question.choices? question.choices.forEach (choice) -> choice.variable = choice.variable.toString() choice.value = choice.value.toString() #console.log question.choices Questions.update question._id, $set: choices: question.choices Answers.find().forEach (answer) -> if typeof answer.value is 'object' updated = false answer.value.forEach (v) -> if v.checkedChoices? v.checkedChoices.forEach (cc) -> if typeof(cc.value) isnt 'string' or typeof(cc.variable) isnt 'string' updated = true cc.value = cc.value.toString() cc.variable = cc.variable.toString() if updated console.log(util.inspect(answer, {showHidden: false, depth: null})) Answers.update answer._id, $set: value: answer.value Migrations.add version: 8 up: -> console.log "init hasData for patients" Patients.find().forEach (patient) -> hasData = false Visits.find( patientId: patient._id ).forEach (v) -> c = Answers.find( visitId: v._id ).count() if c > 0 hasData = true if hasData console.log patient._id+" has data!" Patients.update patient._id, $set: hasData: true return Migrations.add version: 9 up: -> console.log "remove all answers" Answers.remove({}) return Migrations.add version: 10 up: -> console.log "remove choices.$.variable from single-selection and choices.$.value multi-selection questions" counter = 0 Questions.find().forEach (q) -> if q.type is "multipleChoice" or q.type is "table" or q.type is "table_polar" #console.log q return if !q.choices? #invalid questions :( if q.mode is 'checkbox' q.choices.forEach (c) -> delete c.value else #if q.mode is 'radio' q.choices.forEach (c) -> delete c.variable #console.log q #console.log "\n\n\n" counter += Questions.update q._id, $set: choices: q.choices console.log counter+" questions updated" return Migrations.add version: 11 up: -> - console.log "rename question.mode to question.selectionMode and it's values from radio to single and checkbox to multi" + console.log "rename question.mode to question.selectionMode and it's values from radio to single and checkbox to multi" c = 0 Questions.find().forEach (q) -> if q.type is "multipleChoice" or q.type is "table" or q.type is "table_polar" #console.log q if q.mode is 'checkbox' q.mode = 'multi' else #if q.mode is 'radio' q.mode = 'single' q.selectionMode = q.mode delete q.mode #console.log q #console.log "\n\n\n" c += Questions.update q._id, $set: selectionMode: q.selectionMode Questions.update q._id, $unset: mode: 1 console.log c+" questions updated" return Migrations.add version: 12 up: -> console.log "unify choices.$.variable into choice.$.value" counter = 0 Questions.find().forEach (q) -> if q.type is "multipleChoice" or q.type is "table" or q.type is "table_polar" if q.selectionMode is 'multi' #console.log q q.choices.forEach (c) -> c.value = c.variable delete c.variable #console.log q #console.log "\n\n\n" counter += Questions.update q._id, $set: choices: q.choices console.log counter+" questions updated" return Migrations.add version: 13 up: -> console.log "fix empty subquestions" Questions.find({}).forEach (question) -> if question.subquestions? subquestions = question.subquestions.filter (subq) -> subq? if subquestions.length isnt question.subquestions.length console.log "fix subquestions" Questions.update question._id, $set: subquestions: subquestions else if question.type is "table" or question.type is "table_polar" console.log "missing subquestions for table question:" console.log question Questions.remove question._id return Migrations.add version: 14 up: -> console.log "uniquify and complement questionnaires ids" ids = {} counter = 0 Questionnaires.find().forEach (q) -> if !q.id q.id = q.title.replace(" ", "-").trim() Questionnaires.update q._id, $set: id: q.id if ids[q.id]? counter += 1 id = q.id+"_1" i = 2 while ids[id]? id = id.slice(0, -(id.length-id.lastIndexOf('_')))+"_"+i i += 1 ids[id] = q Questionnaires.update q._id, $set: id: id else ids[q.id] = q console.log counter+" questionnaires fixed" return Migrations.add version: 15 up: -> console.log "remove question.code for table questions and descriptions" counter = 0 Questions.find({}).forEach (q) -> if (q.type is "table" or q.type is "table_polar" or q.type is "description") and q.code? counter += 1 Questions.update q._id, $unset: code: 1 console.log counter+" questionnaires fixed" return Migrations.add version: 16 up: -> console.log "set new question codes" counter = 0 Questionnaires.find({}).forEach (qn) -> i = 1 Questions.find({questionnaireId: qn._id}, {sort: {index: 1}}).forEach (q) -> if q.type is "table" or q.type is "table_polar" q.subquestions.forEach (sq) -> sq.code = "#{i}" i += 1 counter += 1 Questions.update q._id, $set: subquestions: q.subquestions else if q.type isnt "description" Questions.update q._id, $set: code: "#{i}" i += 1 counter += 1 console.log counter+" question codes set" return Migrations.add version: 17 up: -> console.log "clean existing questions" Questions.find({}).forEach (q) -> #question = JSON.parse(JSON.stringify(q)) schema = new Question(q).getMetaSchemaDict(true) ss = new SimpleSchema(schema) ss.clean(q) check(q, ss) #if Object.keys(question).length isnt Object.keys(q).length #if question.label? and !q.label? # console.log question # console.log q # console.log "\n\n" #replace question entirely #use direct to prevent $set.updatedAt being added Questions.direct.update q._id, q return - + Migrations.add version: 18 up: -> console.log "fix empty choices and subquestions" Questions.find({}).forEach (question) -> if question.choices? choices = question.choices.filter (choice) -> choice? if choices.length isnt question.choices.length Questions.update question._id, $set: choices: choices if question.subquestions? subquestions = question.subquestions.filter (subq) -> subq? if subquestions.length isnt question.subquestions.length console.log "fix subquestions" Questions.update question._id, $set: subquestions: subquestions return Migrations.add version: 19 up: -> console.log "fixing question indices..." counter = 0 Questionnaires.find().forEach (questionnaire) -> i = 1 Questions.find( questionnaireId: questionnaire._id , sort: index: 1 ).forEach (question) -> if i isnt question.index counter += 1 Questions.update question._id, $set: index: i i += 1 console.log "fixed #{counter} question indices" return Migrations.add version: 20 up: -> console.log "fixing question indices..." Questionnaires.find().forEach (questionnaire) -> i = 1 Questions.find( questionnaireId: questionnaire._id , sort: index: 1 ).forEach (question) -> Questions.update question._id, $set: index: i i += 1 console.log "fixed question indices" return Migrations.add version: 21 up: -> console.log "fixing question indices..." Questionnaires.find().forEach (questionnaire) -> i = 1 Questions.find( questionnaireId: questionnaire._id , sort: index: 1 ).forEach (question) -> Questions.update question._id, $set: index: i i += 1 console.log "fixed question indices" return Migrations.add version: 22 up: -> - console.log "migrate from patient.studyDesignId to .studyDesignIds" + console.log "migrate from patient.studyDesignId to .studyDesignIds" Patients.find( studyDesignId: $exists: 1 ).forEach (p) -> console.log p Patients.update p._id, $set: studyDesignIds: [p.studyDesignId] $unset: studyDesignId: 1 console.log Patients.findOne p._id - console.log "fixed migrating studyDesignId(s)" + console.log "fixed migrating studyDesignId(s)" return +Migrations.add + version: 23 + up: -> + console.log "migrate from patient.caseManagerId to .caseManagerIds" + Patients.find( + caseManagerId: $exists: 1 + ).forEach (p) -> + console.log p + Patients.update p._id, + $set: caseManagerIds: [p.caseManagerId] + $unset: caseManagerId: 1 + console.log Patients.findOne p._id + console.log "fixed migrating caseManagerId(s)" + return Meteor.startup -> Migrations.migrateTo('latest') #Migrations.migrateTo('17,rerun') diff --git a/app/server/publications.coffee b/app/server/publications.coffee index 2083b5c..33d2ea9 100644 --- a/app/server/publications.coffee +++ b/app/server/publications.coffee @@ -1,194 +1,194 @@ onlyIfAdmin = -> if Roles.userIsInRole(@userId, ['admin']) return true else @ready() - return + return onlyIfAdminOrCaseManager = -> if Roles.userIsInRole(@userId, ['admin', 'caseManager']) return true else @ready() - return + return onlyIfUser = -> if @userId return true else @ready() return ################################################# Meteor.publish "caseManagers", -> - return unless onlyIfAdmin.call(@) + return unless onlyIfAdmin.call(@) Meteor.users.find( roles: "caseManager" , fields: _id: 1 username: 1 emails: 1 profile: 1 roles: 1 status: 1 createdAt: 1 ) Meteor.publish "users", -> return unless onlyIfAdmin.call(@) Meteor.users.find( {}, fields: _id: 1 username: 1 emails: 1 profile: 1 roles: 1 status: 1 createdAt: 1 ) Meteor.publish "userProfiles", -> return unless onlyIfAdmin.call(@) Meteor.users.find( {}, fields: _id: 1 username: 1 emails: 1 profile: 1 ) Meteor.publish "studies", -> - return unless onlyIfAdminOrCaseManager.call(@) + return unless onlyIfAdminOrCaseManager.call(@) Studies.find() Meteor.publish "study", (_id) -> - return unless onlyIfAdminOrCaseManager.call(@) + return unless onlyIfAdminOrCaseManager.call(@) Studies.find(_id: _id) Meteor.publish "studyDesigns", -> - return unless onlyIfAdminOrCaseManager.call(@) + return unless onlyIfAdminOrCaseManager.call(@) StudyDesigns.find() Meteor.publish "studyDesignsForStudy", (studyIds) -> - return unless onlyIfAdminOrCaseManager.call(@) + return unless onlyIfAdminOrCaseManager.call(@) if typeof studyIds is 'string' studyIds = [studyIds] StudyDesigns.find studyId: {$in: studyIds} Meteor.publish "patients", -> if Roles.userIsInRole(@userId, ['admin']) return Patients.find() else if Roles.userIsInRole(@userId, ['caseManager']) - return Patients.find caseManagerId: @userId + return Patients.find caseManagerIds: @userId else @ready() return Meteor.publish "patientsForStudy", (studyID) -> return unless onlyIfAdmin.call(@) Patients.find studyId: studyID Meteor.publish "studyForPatient", (_id) -> if Roles.userIsInRole(@userId, ['admin']) patient = Patients.findOne _id: _id if patient? return Studies.find _id: patient.studyId else if Roles.userIsInRole(@userId, ['caseManager']) patient = Patients.findOne _id: _id - caseManagerId: @userId + caseManagerIds: @userId if patient? return Studies.find _id: patient.studyId @ready() Meteor.publish "studyDesignForPatient", (_id) -> patient = Patients.findOne _id: _id if patient? studyDesign = StudyDesigns.find studyId: patient.studyId - if Roles.userIsInRole(@userId, ['admin']) or - (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) + if Roles.userIsInRole(@userId, ['admin']) or + (Roles.userIsInRole(@userId, 'caseManager') and @userId in patient.caseManagerIds) return studyDesign @ready() Meteor.publishComposite 'studyCompositesForPatient', (patientId) -> find: -> patient = Patients.findOne _id: patientId if patient? - if Roles.userIsInRole(@userId, ['admin']) or - (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) + if Roles.userIsInRole(@userId, ['admin']) or + (Roles.userIsInRole(@userId, 'caseManager') and @userId in patient.caseManagerIds) return Patients.find _id: patientId return null children: [ find: (patient) -> Studies.find _id: patient.studyId , find: (patient) -> StudyDesigns.find _id: $in: patient.studyDesignIds children: [ find: (studyDesign) -> if studyDesign.questionnaireIds? and studyDesign.questionnaireIds.length > 0 Questionnaires.find _id: {$in: studyDesign.questionnaireIds } else null children: [ find: (questionnaire) -> Questions.find questionnaireId: questionnaire._id ] ] ] - - + + Meteor.publish "visits", -> - return unless onlyIfAdmin.call(@) + return unless onlyIfAdmin.call(@) Visits.find() Meteor.publishComposite 'visitsCompositeForPatient', (patientId) -> find: -> patient = Patients.findOne patientId if patient? - if Roles.userIsInRole(@userId, ['admin']) or - (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) + if Roles.userIsInRole(@userId, ['admin']) or + (Roles.userIsInRole(@userId, 'caseManager') and @userId in patient.caseManagerIds) return Patients.find _id: patientId return null children: [ find: (patient) -> Visits.find patientId: patient._id children: [ find: (visit) -> Answers.find visitId: visit._id ] ] ##################################### Meteor.publish "questionnaires", -> - return unless onlyIfUser.call(@) + return unless onlyIfUser.call(@) Questionnaires.find() Meteor.publish "questions", -> - return unless onlyIfUser.call(@) + return unless onlyIfUser.call(@) Questions.find() Meteor.publish "questionsForQuestionnaire", (questionnaireId)-> - return unless onlyIfUser.call(@) + return unless onlyIfUser.call(@) Questions.find questionnaireId: questionnaireId ##################################### Meteor.publish "activities", -> - return unless onlyIfAdmin.call(@) + return unless onlyIfAdmin.call(@) Activities.find() ##################################### Meteor.publish "exportTables", -> - return unless onlyIfAdmin.call(@) + return unless onlyIfAdmin.call(@) ExportTables.find()