diff --git a/app/client/views/studies/edit_study_designs.coffee b/app/client/views/studies/edit_study_designs.coffee index 011149a..95ebd7d 100644 --- a/app/client/views/studies/edit_study_designs.coffee +++ b/app/client/views/studies/edit_study_designs.coffee @@ -1,262 +1,276 @@ 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 Questionnaires.find _id: {$in: visit.questionnaireIds} .forEach (q) -> elt.tagsinput 'add', q if visit.recordPhysicalData? and visit.recordPhysicalData elt.tagsinput 'add', _id: "recordPhysicalData" title: "record physical data" _ignoreAddEvents = true Template.editStudyDesignsTags.events = "itemAdded input, itemRemoved input": (evt) -> return if _ignoreAddEvents questionnaireIds = _.pluck $(evt.target).tagsinput('items'), '_id' 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) -> - StudyDesigns.update design._id, - $set: {title: 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) -> dv = design.visits.find (v) -> parseInt(v.day) is parseInt(newVal) if dv? return "a visit already exists on this day!" 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? "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) -> - throwError error if 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) -> - throwError error if 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 dedbced..1c05bc8 100644 --- a/app/lib/collections/studie_designs.coffee +++ b/app/lib/collections/studie_designs.coffee @@ -1,369 +1,370 @@ -class @StudyDesign - constructor: (doc) -> - _.extend this, doc - -@StudyDesigns = new Meteor.Collection("study_designs", - transform: (doc) -> - new StudyDesign(doc) -) +@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': + 'visits.$._id': type: String - 'visits.title': + 'visits.$.title': type: String - 'visits.index': + 'visits.$.index': type: Number -#TODO: attach schema -#StudyDesigns.attachSchema new SimpleSchema(schema) - -StudyDesigns.allow - update: (userId, doc, fieldNames, modifier) -> - #TODO check if allowed - notAllowedFields = _.without fieldNames, 'title', 'updatedAt' - return false if notAllowedFields.length > 0 - true + '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) -#TODO secure methods Meteor.methods - "createStudyDesign": (studyId, title) -> + "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 + "removeStudyDesign": (studyDesignId) -> + checkIfAdmin() check studyDesignId, String - design = StudyDesigns.findOne - _id: studyDesignId - throw new Meteor.Error(500, "removeStudyDesign: studyDesign (#{studyDesignId}) not found") unless design? + 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 e + throw error - StudyDesigns.remove - _id: design._id + StudyDesigns.remove design._id return "addStudyDesignVisit": (studyDesignId) -> + checkIfAdmin() check studyDesignId, String - design = StudyDesigns.findOne - _id: studyDesignId + 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 - _id: studyDesignId - , - $push: - visits: visit + 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 + $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 + $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 + $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 + $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 + $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) -> 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 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 + $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 + $set: recordPhysicalData: true else Visits.update visit._id, - $set: - recordPhysicalData: false + $set: recordPhysicalData: false return "moveStudyDesignVisit": (studyDesignId, visitId, up) -> + checkIfAdmin() check visitId, String check studyDesignId, String - design = StudyDesigns.findOne - _id: studyDesignId + 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 + $inc: 'visits.$.index': -move StudyDesigns.update _id: studyDesignId 'visits._id': visitId , - $inc: - 'visits.$.index': move + $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 + $inc: index: -move , multi: true Visits.update designVisitId: visitId , - $inc: - index: move + $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 and has data attached to it. Please consult your system operator for further information.") if foundData - StudyDesigns.update - _id: studyDesignId - , + #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 - _id: studyDesignId - , - $set: - questionnaireIds: 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 - _id: studyDesignId - , - $set: - recordPhysicalData: recordPhysicalData + StudyDesigns.update studyDesignId, + $set: recordPhysicalData: recordPhysicalData + return