diff --git a/app/client/views/studies/edit_study_designs.coffee b/app/client/views/studies/edit_study_designs.coffee index 449659b..d056811 100644 --- a/app/client/views/studies/edit_study_designs.coffee +++ b/app/client/views/studies/edit_study_designs.coffee @@ -1,288 +1,304 @@ listQuestionnaireIds = new ReactiveVar([]) listRecordPhysicalData = new ReactiveVar(false) remainingQuestionnaires = (design) -> qIds = design.questionnaireIds or [] qIds = _.union(qIds, (listQuestionnaireIds.get() or []) ) Questionnaires.find _id: {$nin: qIds} _bloodhound = null Template.editStudyDesignsTags.created = -> questionnaires = Questionnaires.find().fetch() questionnaires.push _id: "recordPhysicalData" title: "record physical data" id: "record physical data" _bloodhound = new Bloodhound( datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title', 'id') queryTokenizer: Bloodhound.tokenizers.whitespace local: questionnaires ) _bloodhound.initialize() Template.editStudyDesignsTags.rendered = -> elt = @$('.tags') elt.tagsinput itemValue: '_id' itemText: 'title' typeaheadjs: name: 'tags' displayKey: 'title' source: _bloodhound.ttAdapter() allowDuplicates: true visit = @data.visit elt.tagsinput('removeAll') if visit.questionnaireIds? and visit.questionnaireIds.length > 0 + #make sure questionnaires are in order + questionnaires = {} Questionnaires.find _id: {$in: visit.questionnaireIds} .forEach (q) -> - elt.tagsinput 'add', q + questionnaires[q._id] = q + visit.questionnaireIds.forEach (qId) -> + questionnaire = questionnaires[qId] + elt.tagsinput 'add', questionnaire if visit.recordPhysicalData? and visit.recordPhysicalData elt.tagsinput 'add', _id: "recordPhysicalData" title: "record physical data" + return _ignoreAddEvents = true Template.editStudyDesignsTags.events = "itemAdded input, itemRemoved input": (evt) -> return if _ignoreAddEvents questionnaireIds = _.pluck $(evt.target).tagsinput('items'), '_id' + #print questionnaires by name + #questionnaires = {} + #sortedQuestionnaires = [] + #Questionnaires.find( + # _id: {$in: questionnaireIds} + #).forEach (q) -> + # questionnaires[q._id] = q + #questionnaireIds.forEach (qId) -> + # sortedQuestionnaires.push questionnaires[qId].title + #console.log sortedQuestionnaires recordPhysicalData = false questionnaireIds = _.filter questionnaireIds, (id) -> recordPhysicalData = true if id.valueOf() is "recordPhysicalData" id.valueOf() isnt "recordPhysicalData" Meteor.call "scheduleQuestionnairesAtVisit", @design._id, @visit._id, questionnaireIds, (error) -> throwError error if error? if @visit.recordPhysicalData isnt recordPhysicalData Meteor.call "scheduleRecordPhysicalDataAtVisit", @design._id, @visit._id, recordPhysicalData, (error) -> throwError error if error? return Template.editStudyDesigns.destroyed = -> _ignoreAddEvents = true Template.editStudyDesigns.rendered = -> _ignoreAddEvents = false Meteor.setTimeout -> $("button.accordion-toggle").first().click() , 400 Meteor.setTimeout -> _ignoreAddEvents = false , 1000 Template.editStudyDesigns.helpers allQuestionnaires: -> Questionnaires.find({}, sort: title: 1) designs: -> StudyDesigns.find studyId: @_id, sort: {createdAt: 1} #this design=design titleEO: -> design = @design value: design.title emptytext: "no title" success: (response, newVal) -> Meteor.call "updateStudyDesignTitle", design._id, newVal, (error) -> throwError error if error? return #this design=design hasRemainingQuestionnaires: -> remainingQuestionnaires(@design).count() #this design=design remainingQuestionnaires: -> remainingQuestionnaires(@design) #this design=design questionnaires: -> qIds = @design.questionnaireIds or [] qIds = _.union(qIds, (listQuestionnaireIds.get() or []) ) Questionnaires.find _id: {$in: qIds} listRecordPhysicalData: -> @design.recordPhysicalData || listRecordPhysicalData.get() #this design=design visits: -> @design.visits.sort (a, b)-> a.index - b.index prevDay = 0 #augment visits #http://stackoverflow.com/questions/13789622/accessing-parent-context-in-meteor-templates-and-template-helpers @design.visits.map (v)-> if v.day? daysBetween = v.day-prevDay _.extend v, daysBetween: daysBetween prevDay = v.day if daysBetween is 0 delete v.daysBetween v #this visit design visitQuestionnaires: -> #Questionnaires.find # _id: {$in: @visit.questionnaireIds} @visit.questionnaireIds #this visit design visitTitleEO: -> visit = @visit design = @design value: visit.title emptytext: "no title" success: (response, newVal) -> dv = design.visits.find (v) -> v.title is newVal if dv? return "a visit with this title already exists." Meteor.call "changeStudyDesignVisitTitle", design._id, visit._id, newVal, (error) -> throwError error if error? return #this visit design visitDayEO: -> visit = @visit design = @design value: visit.day emptytext: "no day set" success: (response, newVal) -> Meteor.call "changeStudyDesignVisitDay", design._id, visit._id, newVal, (error) -> throwError error if error? return #this design:StudyDesign visit:StudyDesign.visit questionnaire:Questionnaire questionnaireIconClass: -> questionnaire = @questionnaire found = false if @visit.questionnaireIds _.some @visit.questionnaireIds, (qId)-> found = qId is questionnaire._id found if found return "fa-check-square-o brand-primary" else return "fa-square-o hoverOpaqueExtreme" #this design:StudyDesign visit:StudyDesign.visit physicalIconClass: -> if @visit.recordPhysicalData? and @visit.recordPhysicalData return "fa-check-square-o brand-primary" else return "fa-square-o hoverOpaqueExtreme" Template.editStudyDesigns.events "click .editable-click": (evt) -> evt.preventDefault() evt.stopPropagation() "click #createStudyDesign": (evt) -> Meteor.call "createStudyDesign", @_id, (error, studyDesignId) -> throwError error if error? $("#collapse_#{studyDesignId}").collapse('show') __scrollToBottom() "click .copyDesign": (evt) -> evt.preventDefault() _ignoreAddEvents = true Meteor.call "copyStudyDesign", @design._id, (error, studyDesignId) -> if error? _ignoreAddEvents = false throwError error $("#collapse_#{studyDesignId}").collapse('show') __scrollToBottom() Meteor.setTimeout -> _ignoreAddEvents = false , 500 return false "click .removeDesign": (evt) -> evt.preventDefault() designId = @design._id swal { title: 'Are you sure?' text: 'Do you really want to delete this design?' type: 'warning' showCancelButton: true confirmButtonText: 'Yes' closeOnConfirm: false }, -> Meteor.call "removeStudyDesign", designId, (error) -> if error? throwError error else swal.close() return return false "click #addVisit": (evt) -> evt.preventDefault() Meteor.call "addStudyDesignVisit", @design._id, (error) -> throwError error if error? "click .listQuestionnaire": (evt) -> evt.preventDefault() questionnaireId = $(evt.target).data("id") qIds = listQuestionnaireIds.get() or [] qIds.push questionnaireId listQuestionnaireIds.set qIds "click .listRecordPhysicalData": (evt) -> evt.preventDefault() listRecordPhysicalData.set !listRecordPhysicalData.get() #"click .toggleQuestionnaireAtVisit": (evt) -> # evt.preventDefault() # doSchedule = not $(evt.target).hasClass('fa-check-square-o') #isChecked # questionnaireIds = @visit.questionnaireIds || [] # questionnaire = @questionnaire # if doSchedule # questionnaireIds.push questionnaire._id # $("input.tags[data-visit-id=#{@visit._id}]").tagsinput('add', questionnaire) # else # questionnaireIds = _.filter questionnaireIds, (qId)-> # qId isnt questionnaire._id # $("input.tags[data-visit-id=#{@visit._id}]").tagsinput('remove', questionnaire) # Meteor.call "scheduleQuestionnairesAtVisit", @design._id, @visit._id, questionnaireIds, (error) -> # throwError error if error? #"click .toggleRecordPhysicalDataAtVisit": (evt) -> # evt.preventDefault() # Meteor.call "scheduleRecordPhysicalDataAtVisit", @design._id, @visit._id, !@visit.recordPhysicalData, (error) -> # throwError error if error? "click .moveUp": (evt) -> Meteor.call "moveStudyDesignVisit", @design._id, @visit._id, true, (error) -> throwError error if error? "click .moveDown": (evt) -> Meteor.call "moveStudyDesignVisit", @design._id, @visit._id, false, (error) -> throwError error if error? "click .removeVisit": (evt) -> evt.preventDefault() designId = @design._id visitId = @visit._id swal { title: 'Are you sure?' text: 'Do you really want to delete this visit?' type: 'warning' showCancelButton: true confirmButtonText: 'Yes' closeOnConfirm: false }, -> Meteor.call "removeStudyDesignVisit", designId, visitId, (error) -> if error? throwError error else swal.close() return return false diff --git a/app/lib/collections/studie_designs.coffee b/app/lib/collections/studie_designs.coffee index 5a9c829..45fbb85 100644 --- a/app/lib/collections/studie_designs.coffee +++ b/app/lib/collections/studie_designs.coffee @@ -1,388 +1,381 @@ @StudyDesigns = new Meteor.Collection("study_designs") StudyDesigns.before.insert BeforeInsertTimestampHook StudyDesigns.before.update BeforeUpdateTimestampHook schema = 'title': type: String 'studyId': type: String 'creatorId': type: String 'visits': type: [Object] optional: true 'visits.$._id': type: String 'visits.$.title': type: String 'visits.$.index': type: Number 'visits.$.day': type: Number optional: true 'visits.$.questionnaireIds': type: [String] optional: true 'visits.$.recordPhysicalData': type: Boolean optional: true 'questionnaireIds': type: [String] optional: true 'recordPhysicalData': type: Boolean optional: true 'updatedAt': type: Number optional: true 'createdAt': type: Number optional: true StudyDesigns.attachSchema new SimpleSchema(schema) Meteor.methods "createStudyDesign": (studyId) -> checkIfAdmin() count = StudyDesigns.find( studyId: studyId ).count() _id = StudyDesigns.insert title: "design #{count+1}" studyId: studyId creatorId: Meteor.userId() visits: [ _id: new Meteor.Collection.ObjectID()._str day: 0 index: 0 title: "visit 1" ] _id "updateStudyDesignTitle": (studyDesignId, title) -> checkIfAdmin() check title, String studyDesign = StudyDesigns.findOne studyDesignId throw new Meteor.Error(403, "studyDesign not found.") unless studyDesign? StudyDesigns.update studyDesignId, $set: title: title return "copyStudyDesign": (studyDesignId) -> checkIfAdmin() check studyDesignId, String design = StudyDesigns.findOne studyDesignId throw new Meteor.Error(400, "studyDesign (#{studyDesignId}) not found") unless design? study = Studies.findOne design.studyId throw new Meteor.Error(400, "study (#{design.studyDesignId}) not found") unless study? delete design._id design.title += " copy" design.creatorId = Meteor.userId() design.visits.forEach (v) -> v._id = new Mongo.ObjectID()._str StudyDesigns.insert design "removeStudyDesign": (studyDesignId) -> checkIfAdmin() check studyDesignId, String design = StudyDesigns.findOne studyDesignId throw new Meteor.Error(400, "removeStudyDesign: studyDesign (#{studyDesignId}) not found") unless design? #check patients patientIds = Patients.find studyDesignId: design._id .map (patient) -> patient.id if patientIds.length > 0 throw new Meteor.Error(500, "Can't remove study design because these patients are mapped to it: #{patientIds.join(", ")}") error = null _.some design.visits, (visit) -> next = true try Meteor.call "removeStudyDesignVisit", design._id, visit._id catch e error = e next = false next if error? throw error StudyDesigns.remove design._id return "addStudyDesignVisit": (studyDesignId) -> checkIfAdmin() check studyDesignId, String design = StudyDesigns.findOne studyDesignId throw new Meteor.Error(500, "StudyDesign #{studyDesignId} not found!") unless design? index = design.visits.length title = "visit #{index+1}" visit = _id: new Meteor.Collection.ObjectID()._str title: title index: index StudyDesigns.update studyDesignId, $push: visits: visit "changeStudyDesignVisitTitle": (studyDesignId, visitId, title) -> checkIfAdmin() check studyDesignId, String check visitId, String check title, String n = StudyDesigns.update _id: studyDesignId 'visits._id': visitId , $set: 'visits.$.title': title throw new Meteor.Error(500, "changeStudyVisitTitle: no StudyDesign.visit to update found") unless n > 0 #update existing visits Visits.update designVisitId: visitId , $set: title: title , multi: true return "changeStudyDesignVisitDay": (studyDesignId, visitId, day) -> checkIfAdmin() check studyDesignId, String check visitId, String day = parseInt(day) check day, Number day = null if isNaN(day) n = StudyDesigns.update _id: studyDesignId 'visits._id': visitId , $set: 'visits.$.day': day throw new Meteor.Error(500, "changeStudyVisitTitle: no StudyDesign.visit to update found") unless n > 0 #update existing visits Visits.update designVisitId: visitId , $set: day: day , multi: true return "scheduleQuestionnairesAtVisit": (studyDesignId, visitId, questionnaireIds) -> checkIfAdmin() check studyDesignId, String check visitId, String check questionnaireIds, [String] n = StudyDesigns.update _id: studyDesignId 'visits._id': visitId , $set: 'visits.$.questionnaireIds': questionnaireIds throw new Meteor.Error(500, "scheduleQuestionnaireAtVisit: no StudyDesign with that visit found") unless n > 0 #update existing visits Visits.find designVisitId: visitId .forEach (visit) -> - addedQuestionnaireIds = _.difference questionnaireIds, visit.questionnaireIds - if addedQuestionnaireIds.length > 0 - #console.log "pushing:" - #console.log addedQuestionnaireIds - Visits.update visit._id, - $pushAll: - questionnaireIds: addedQuestionnaireIds - removedQuestionnaireIds = _.difference visit.questionnaireIds, questionnaireIds - removeQuestionnaireIds = removedQuestionnaireIds.filter (rQuestId) -> + usedQuestionnaireIds = removedQuestionnaireIds.filter (rQuestId) -> rQuestionIds = Questions.find( questionnaireId: rQuestId ).map( (q) -> q._id ) c = Answers.find( visitId: visit._id questionId: {$in: rQuestionIds} ).count() if c > 0 - return false - return true - if removeQuestionnaireIds.length > 0 - #console.log "pulling:" - #console.log removeQuestionnaireIds - Visits.update visit._id, - $pullAll: - questionnaireIds: removeQuestionnaireIds + return true + return false + + questionnaireIdsForVisit = _.clone questionnaireIds + usedQuestionnaireIds.forEach (qId) -> + questionnaireIdsForVisit.push qId + + Visits.update visit._id, + $set: questionnaireIds: questionnaireIdsForVisit updateQuestionnaireIdsOfStudyDesign(studyDesignId) return "scheduleRecordPhysicalDataAtVisit": (studyDesignId, visitId, doSchedule) -> checkIfAdmin() check visitId, String check studyDesignId, String n = StudyDesigns.update _id: studyDesignId 'visits._id': visitId , $set: 'visits.$.recordPhysicalData': doSchedule throw new Meteor.Error(500, "scheduleRecordPhysicalDataAtVisit: no StudyDesign with that visit found") unless n > 0 updateRecordPhysicalDataOfStudyDesign(studyDesignId) #update existing visits Visits.find designVisitId: visitId .forEach (visit) -> if doSchedule Visits.update visit._id, $set: recordPhysicalData: true else Visits.update visit._id, $set: recordPhysicalData: false return "moveStudyDesignVisit": (studyDesignId, visitId, up) -> checkIfAdmin() check visitId, String check studyDesignId, String design = StudyDesigns.findOne studyDesignId throw new Meteor.Error(500, "removeStudyDesignVisit: studyDesign not found") unless design? visit = _.find design.visits, (v) -> v._id is visitId throw new Meteor.Error(500, "removeStudyDesignVisit: visit not found") unless visit? move = -1 move = 1 if !up return if visit.index is 0 and move is -1 return if visit.index+1 >= design.visits.length and move is 1 StudyDesigns.update _id: studyDesignId 'visits.index': visit.index+move , $inc: 'visits.$.index': -move StudyDesigns.update _id: studyDesignId 'visits._id': visitId , $inc: 'visits.$.index': move #update existing visits designVisitIds = design.visits.map (designVisit) -> designVisit._id Visits.update designVisitId: { $in: designVisitIds } index: visit.index+move , $inc: index: -move , multi: true Visits.update designVisitId: visitId , $inc: index: move , multi: true return "removeStudyDesignVisit": (studyDesignId, visitId) -> checkIfAdmin() check visitId, String check studyDesignId, String design = StudyDesigns.findOne _id: studyDesignId throw new Meteor.Error(500, "removeStudyDesignVisit: studyDesign not found") unless design? visit = _.find design.visits, (v) -> v._id is visitId throw new Meteor.Error(500, "removeStudyDesignVisit: visit not found") unless visit? #check existing visits visits = Visits.find designVisitId: visitId .fetch() foundData = _.some visits, (visit) -> questionIds = Questions.find questionnaireId: $in: visit.questionnaireIds .map( (q) -> q._id ) c1 = Answers.find visitId: visit._id questionId: {$in: questionIds} .count() if c1 > 0 console.log "the following visit of the template has data attached:" console.log visit return true return false throw new Meteor.Error(500, "The visit is used by at least one patient, has data attached to it and can therefore not be deleted. Please consider using a copy of this design and assign it to new patients.") if foundData #remove existing visits Visits.remove designVisitId: visitId StudyDesigns.update studyDesignId, $pull: {visits: {_id: visitId}} #TODO normalize visits into it's own collection #to avoid stuff like this index = visit.index+1 loop n = StudyDesigns.update _id: studyDesignId 'visits.index': index , $inc: {'visits.$.index': -1} index += 1 break if n is 0 updateQuestionnaireIdsOfStudyDesign(studyDesignId) updateRecordPhysicalDataOfStudyDesign(studyDesignId) return updateQuestionnaireIdsOfStudyDesign = (studyDesignId) -> design = StudyDesigns.findOne studyDesignId throw new Meteor.Error(500, "updateQuestionnaireIdsOfStudyDesign: studyDesign not found") unless design? questionnaireIds = [] design.visits.forEach (visit) -> if visit.questionnaireIds? and visit.questionnaireIds.length > 0 questionnaireIds = _.union questionnaireIds, visit.questionnaireIds #questionnaireIds from design.visits and real visits may differt in case a questionnaire, for which #data was collected already, got removed from the design.visit #here we search for other questionnaireIds Visits.find( designVisitId: visit._id ).forEach (v) -> questionnaireIds = _.union questionnaireIds, v.questionnaireIds StudyDesigns.update studyDesignId, $set: questionnaireIds: questionnaireIds return updateRecordPhysicalDataOfStudyDesign = (studyDesignId) -> design = StudyDesigns.findOne studyDesignId throw new Meteor.Error(500, "updateRecordPhysicalDataOfStudyDesign: studyDesign not found") unless design? recordPhysicalData = false _.some design.visits, (visit) -> if visit.recordPhysicalData? recordPhysicalData = visit.recordPhysicalData recordPhysicalData StudyDesigns.update studyDesignId, $set: recordPhysicalData: recordPhysicalData return diff --git a/app/lib/collections/visits.coffee b/app/lib/collections/visits.coffee index 317e3da..374c391 100644 --- a/app/lib/collections/visits.coffee +++ b/app/lib/collections/visits.coffee @@ -1,173 +1,178 @@ 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 [] - qs = Questionnaires.find + questionnaires = {} + sortedQuestionnaires = [] + Questionnaires.find( _id: {$in: qIds} - qs - + ).forEach (q) -> + questionnaires[q._id] = q + qIds.forEach (qId) -> + sortedQuestionnaires.push questionnaires[qId] + 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 quest.answered = true quest.numAnswered = 0 questions = Questions.find questionnaireId: quest._id type: {$ne: "description"} #filter out descriptions .map (question) -> if question.subquestions? numQuestions += question.subquestions.length else numQuestions += 1 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 else if answered quest.numAnswered += 1 question #quest.questions = questions quest.numQuestions = numQuestions quest.answered = false if questions.length is 0 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] 'recordPhysicalData': type: Boolean 'index': type: Number 'day': 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, 'therapist') and patient.therapistId is @userId) # use query from Patient studyDesign = patient.studyDesign() 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 day: visitTemplate.day if visitTemplate.day? _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, 'therapist') and patient.therapistId is @userId) if date? Visits.update visitId, $set: date: date else Visits.update visitId, $unset: date: '' return