diff --git a/app/client/views/questionnaires/questionnaire_wizzard.coffee b/app/client/views/questionnaires/questionnaire_wizzard.coffee index db54718..c95c525 100644 --- a/app/client/views/questionnaires/questionnaire_wizzard.coffee +++ b/app/client/views/questionnaires/questionnaire_wizzard.coffee @@ -1,208 +1,208 @@ _numQuestions = new ReactiveVar(0) _numPages = new ReactiveVar(0) _questionIdsForPage = new ReactiveVar({}) _pageIndex = new ReactiveVar(0) _numFormsToSubmit = 0 nextPage = -> if _pageIndex.get() is _numPages.get()-1 Modal.hide('viewQuestionnaire') else _pageIndex.set _pageIndex.get()+1 previousPage = -> index = _pageIndex.get() index -= 1 if index > 0 _pageIndex.set index _gotoNextPage = null submitAllForms = (gotoNextPage) -> _gotoNextPage = gotoNextPage numFormsToSubmit = 0 $("form").each () -> e = $(@) classes = e.attr('class') if classes? and classes.indexOf('question') > -1 numFormsToSubmit += 1 _numFormsToSubmit = numFormsToSubmit $("form").each () -> e = $(@) classes = e.attr('class') if classes? and classes.indexOf('question') > -1 e.submit() formSubmitted = -> if (_numFormsToSubmit -= 1) <= 0 if _gotoNextPage nextPage() else previousPage() autoformHooks = onSubmit: (insertDoc, updateDoc, currentDoc) -> insertDoc.visitId = currentDoc.visitId insertDoc.questionId = currentDoc.questionId insertDoc._id = currentDoc._id if currentDoc._id? #console.log "submit questionAutoform" #console.log insertDoc if insertDoc.value? and (!currentDoc.value? or (currentDoc.value? and currentDoc.value isnt insertDoc.value)) Meteor.call "upsertAnswer", insertDoc, (error) -> throwError error if error? formSubmitted() @done() false Template.questionnaireWizzard.created = -> @subscribe("questionsForQuestionnaire", @data.questionnaire._id) self = @ @autorun -> count = 0 page = 0 questionIdsForPage = {} didBreakPage = false autoformIds = [] Questions.find questionnaireId: self.data.questionnaire._id , sort: {index: 1} .forEach (q) -> if q.type isnt "description" and q._id isnt "table" and q._id isnt "table_polar" autoformIds.push q._id count += 1 if questionIdsForPage[page]? questionIdsForPage[page].push q._id else questionIdsForPage[page] = [q._id] didBreakPage = false if q.break page += 1 didBreakPage = true page -= 1 if didBreakPage _questionIdsForPage.set questionIdsForPage _numQuestions.set count _numPages.set page+1 _pageIndex.set 0 AutoForm.addHooks(autoformIds, autoformHooks) Template.questionnaireWizzard.helpers templateGestures: 'swipeleft div': (evt, templateInstance) -> nextQuestion() 'swiperight div': (evt, templateInstance) -> previousQuestion() questionsForPage: -> questionIdsForPage = _questionIdsForPage.get()[_pageIndex.get()] Questions.find questionnaireId: @questionnaire._id _id: {$in: questionIdsForPage} , sort: {index: 1} answerForQuestion: (visitId, questionId) -> Answers.findOne visitId: visitId questionId: questionId answerFormSchema: -> schema = _id: type: String optional: true visitId: type: String optional: true questionId: type: String optional: true value: @question.getSchemaDict() new SimpleSchema(schema) doc: -> @answer or visitId: @visit._id questionId: @question._id pages: -> answers = {} questionIds = Questions.find questionnaireId: @questionnaire._id .map (question) -> question._id Answers.find visitId: @visit._id questionId: {$in: questionIds} .forEach (answer) -> answers[answer.questionId] = answer activeIndex = _pageIndex.get() questionIdsForPage = _questionIdsForPage.get() pages = [] for i in [0.._numPages.get()-1] css = "" allQuestionsAnsweredInPage = true Questions.find questionnaireId: @questionnaire._id _id: {$in: questionIdsForPage[i]} .forEach (question) -> return if question.type is "description" if !answers[question._id]? allQuestionsAnsweredInPage = false if allQuestionsAnsweredInPage css = "answered" if i is activeIndex css += " active" pages[i] = index: i+1 css: css pages Template.questionnaireWizzard.events "click #next": (evt, tmpl) -> submitAllForms(true) false "click #back": (evt, tmpl) -> submitAllForms(false) false "click .jumpToPage": (evt) -> _pageIndex.set @index-1 false "submit .questionForm": (evt) -> evt.preventDefault() evt.stopPropagation() if @question.type is "description" formSubmitted() return answer = visitId: @visit._id questionId: @question._id value: [] _id: @answer._id if @answer? for subquestion in @question.subquestions inputs = $(evt.target).find("input[data-subquestion_code=#{subquestion.code}]:checked") checkedChoices=[] inputs.each -> #checked choices input = $(@) checkedChoices.push - value: input.data('choice_value') + value: input.data('choice_value').toString() variable: input.data('choice_variable').toString() if checkedChoices.length > 0 answer.value.push code: subquestion.code checkedChoices: checkedChoices if answer.value.length > 0 Meteor.call "upsertAnswer", answer, (error) -> throwError error if error? formSubmitted() else formSubmitted() false diff --git a/app/lib/collections/questions.coffee b/app/lib/collections/questions.coffee index d9ab6fe..7f3a00e 100644 --- a/app/lib/collections/questions.coffee +++ b/app/lib/collections/questions.coffee @@ -1,260 +1,261 @@ class @Question constructor: (doc) -> _.extend this, doc getSchemaDict: -> s = _.pickDeep @, 'type', 'label', 'optional', 'min', 'max', 'decimal', 'options', 'options.label', 'options.value' switch @type when "text" s.type = String s.autoform = type: "textarea" when "number" s.type = Number when "boolean" s.type = Boolean s.autoform = type: "boolean-radios" when "date" s.type = Date s.autoform = type: "bootstrap-datepicker" when "dateTime" s.type = Date s.autoform = type: "bootstrap-datetimepicker" when "multipleChoice" s.autoform = options: @choices if @mode is "radio" s.type = Number s.autoform.type = "select-radio-inline" else if @mode is "checkbox" s.type = [Number] s.autoform.type = "select-checkbox-inline" when "table" s.type = Number when "description" s.type = String s.label = ' ' s.autoform = type: "description" delete s.options s getMetaSchemaDict: -> schema = {} noWhitespaceRegex = /^\S*$/ #don't match if contains whitespace if @type isnt "description" _.extend schema, code: label: "Code" type: String optional: false regEx: noWhitespaceRegex label: label: if (@type is "table") then "Title" else "Question" type: String optional: (@type is "table") autoform: type: "textarea" optional: label: "Optional" type: Boolean _.extend schema, type: label: "Type" type: String autoform: type: "select" options: -> [ {label: "Text", value: "text"}, {label: "Number", value: "number"}, {label: "Boolean", value: "boolean"}, {label: "Date", value: "date"}, {label: "Date & Time", value: "dateTime"}, {label: "Multiple Choice", value: "multipleChoice"}, {label: "Table Multiple Choice", value: "table"}, {label: "Table Polar", value: "table_polar"}, {label: "Description (no question)", value: "description"}, ] break: label: "insert pagebreak after this item" type: Boolean if @type is "multipleChoice" or @type is "table" or @type is "table_polar" _.extend schema, mode: label: "Mode" type: String autoform: type: "select-radio-inline" options: [ label: "single selection (radios)" value: "radio" , label: "multiple selection (checkboxes)" value: "checkbox" ] if @type is "description" _.extend schema, label: label: "Text (markdown)" type: String autoform: type: "textarea" rows: 10 if @type is "number" _.extend schema, min: type: Number optional: true decimal: true max: type: Number optional: true decimal: true decimal: type: Boolean if @type is "table" _.extend schema, subquestions: type: [Object] label: "Subquestions" minCount: 1 'subquestions.$.label': type: String autoform: type: "textarea" 'subquestions.$.code': type: String regEx: noWhitespaceRegex if @type is "table_polar" _.extend schema, subquestions: type: [Object] label: "Subquestions" minCount: 1 'subquestions.$.minLabel': label: "min label" type: String 'subquestions.$.maxLabel': label: "max label" type: String 'subquestions.$.code': type: String regEx: noWhitespaceRegex if @type is "multipleChoice" or @type is "table" or @type is "table_polar" _.extend schema, choices: type: [Object] label: "Choices" minCount: 1 'choices.$.label': type: String optional: true 'choices.$.value': - type: Number + type: String + regEx: noWhitespaceRegex 'choices.$.variable': type: String regEx: noWhitespaceRegex schema @Questions = new Meteor.Collection("questions", transform: (doc) -> new Question(doc) ) Questions.before.insert BeforeInsertTimestampHook Questions.before.update BeforeUpdateTimestampHook #TODO check if allowed #TODO check consistency Questions.allow insert: (userId, doc) -> true update: (userId, doc, fieldNames, modifier) -> true remove: (userId, doc) -> true Meteor.methods insertQuestion: (question) -> check(question.questionnaireId, String) questionnaire = Questionnaires.findOne _id: question.questionnaireId check(question.label, String) check(question.type, String) numQuestions = Questions.find questionnaireId: questionnaire._id .count() nextIndex = numQuestions+1 if (question.index? and question.index > nextIndex) or !question.index? question.index = nextIndex #TODO filter question atters _id = Questions.insert question _id removeQuestion: (_id) -> check(_id, String) question = Questions.findOne _id questionnaire = Questionnaires.findOne _id: question.questionnaireId Questions.remove _id Questions.update questionnaireId: questionnaire._id index: { $gt: question.index } , $inc: { index: -1 } , multi: true moveQuestion: (questionnaireId, oldIndex, newIndex) -> check(questionnaireId, String) check(oldIndex, Match.Integer) check(newIndex, Match.Integer) questionnaire = Questionnaires.findOne _id: questionnaireId question = Questions.findOne questionnaireId: questionnaireId index: oldIndex throw new Meteor.Error(403, "question with index #{oldIndex} not found.") unless question? Questions.update questionnaireId: questionnaireId index: { $gt: oldIndex } , $inc: { index: -1 } , multi: true Questions.update questionnaireId: questionnaireId index: { $gte: newIndex } , $inc: { index: 1 } , multi: true Questions.update _id: question._id , $set: { index: newIndex} null diff --git a/app/server/mongo_migrations/migrations.coffee b/app/server/mongo_migrations/migrations.coffee index 9fa207f..0d59bfc 100644 --- a/app/server/mongo_migrations/migrations.coffee +++ b/app/server/mongo_migrations/migrations.coffee @@ -1,137 +1,167 @@ +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 + Meteor.startup -> - #Migrations.migrateTo('6,rerun') + #Migrations.migrateTo('7,rerun') Migrations.migrateTo('latest')