diff --git a/app/client/views/studies/edit_study_patients.coffee b/app/client/views/studies/edit_study_patients.coffee index 0dfde70..cbe2767 100644 --- a/app/client/views/studies/edit_study_patients.coffee +++ b/app/client/views/studies/edit_study_patients.coffee @@ -1,255 +1,253 @@ AutoForm.hooks editSessionPatientsForm: onSubmit: (insertDoc, updateDoc, currentDoc) -> self = @ ids = Session.get 'editingPatientIds' - if ids.length > 1 - updateDoc = _.pickDeep updateDoc, "$set.caseManagerId", "$set.studyDesignId" 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: '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: label: "Case Manager" type: String optional: true autoform: type: "select" 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 event.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/collections/patients.coffee b/app/lib/collections/patients.coffee index da916cd..2c690e0 100644 --- a/app/lib/collections/patients.coffee +++ b/app/lib/collections/patients.coffee @@ -1,232 +1,242 @@ class @Patient constructor: (doc) -> _.extend this, doc caseManager: -> return null unless @caseManagerId? Meteor.users.findOne _id: @caseManagerId caseManagerName: -> getUserDescription @caseManager() 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 'caseManagerId': type: String optional: true 'primaryLanguage': type: String optional: true 'secondaryLanguage': type: String optional: true 'hasData': type: Boolean defaultValue: false '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) -> checkIfAdmin() 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 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) -> checkIfAdmin() check ids, [String] check update, Object + allowedKeys = [ + "$set.caseManagerId", + "$set.studyDesignIds", + "$set.primaryLanguage", + "$set.secondaryLanguage", + "$unset.caseManagerId", + "$unset.studyDesignIds", + "$unset.primaryLanguage", + "$unset.secondaryLanguage", + ] + + 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" + ] + #pick whitelisted keys - update = _.pickDeep update, - "$set.caseManagerId", - "$set.studyDesignIds", - "$set.primaryLanguage", - "$set.secondaryLanguage", - "$set.hrid", - "$unset.caseManagerId", - "$unset.studyDesignIds", - "$unset.primaryLanguage", - "$unset.secondaryLanguage", - "$unset.hrid" + 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']?['studyDesignId']? or update['$unset']?['studyDesignId']? patientIds = [] Patients.find(_id: $in: ids).forEach (p) -> if p.hasData and p.studyDesignId isnt update['$set']['studyDesignId'] patientIds.push p.id if patientIds.length > 0 throw new Meteor.Error(400, "The following patients are already mapped to another design and have entered data: #{patientIds.join(', ')}. Please remove these IDs from your selection.") #remove already created visits with no data Patients.find(_id: $in: ids).forEach (p) -> Visits.remove patientId: p._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) -> checkIfAdmin() check patientId, String 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