diff --git a/app/lib/collections/questions.coffee b/app/lib/collections/questions.coffee index 5ecd0db..fceeedb 100644 --- a/app/lib/collections/questions.coffee +++ b/app/lib/collections/questions.coffee @@ -1,758 +1,758 @@ class @Question constructor: (doc) -> _.extend this, doc getModifiedPlainObject: (modifier) -> TempCollection = new Mongo.Collection("temp", connection: null) TempCollection.insert(@) selector = { _id: @_id } TempCollection.update(selector, modifier) simulation = TempCollection.findOne(selector) TempCollection.remove(selector) simulation.updatedAt = Date.now() JSON.parse(JSON.stringify(simulation)) copy: -> copy = _.clone @ #http://stackoverflow.com/questions/597588/how-do-you-clone-an-array-of-objects-in-javascript if @choices? copy.choices = JSON.parse(JSON.stringify(@choices)) if @subquestions? copy.subquestions = JSON.parse(JSON.stringify(@subquestions)) copy translateTo: (lang) -> return if !lang try translation = @translations[lang] try @label = translation.label if translation.label? try @choices.forEach (c) -> translatedChoice = translation.choices.find (tc) -> tc.value is c.value if translatedChoice? c.label = translatedChoice.label try @subquestions.forEach (s) -> tSubquestion = translation.subquestions.find (ts) -> ts.code is s.code if tSubquestion? s.label = tSubquestion.label if tSubquestion.label? s.minLabel = tSubquestion.minLabel if tSubquestion.minLabel? s.maxLabel = tSubquestion.maxLabel if tSubquestion.maxLabel? return @ getSchemaDict: (lang) -> 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 @selectionMode is "multi" s.type = [String] if @orientation is 'inline' s.autoform.type = "select-checkbox-inline" else #if @orientation is 'vertical' s.autoform.type = "select-checkbox" else #if @selectionMode is "single" s.type = String if @orientation is 'inline' s.autoform.type = "select-radio-inline" else #if @orientation is 'vertical' s.autoform.type = "select-radio" when "description" s.type = String s.label = ' ' s.autoform = type: "description" delete s.options return s getMetaSchemaDict: (finalValidation)-> schema = {} noWhitespaceRegex = /^\S*$/ #don't match if contains whitespace if @type is "table" or @type is "table_polar" _.extend schema, label: label: "Title" type: String optional: true autoform: type: "textarea" else if @type isnt "description" _.extend schema, code: label: "Code" type: String regEx: noWhitespaceRegex defaultValue: new Mongo.ObjectID()._str label: label: "Question" type: String defaultValue: "Insert label here" autoform: type: "textarea" _.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"}, ] conditional: label: "Conditional" type: String optional: true autoform: type: "select" options: -> Questions.find({'questionnaireId': Session.get('editingQuestionnaireId'), '_id': {'$ne': Session.get('selectedQuestionId')}}).map (x) -> {label: "#{x.index}: #{x.label[0..40]}", value: "#{x._id}"} break: label: "insert pagebreak after this item" type: Boolean defaultValue: false if @type isnt "description" _.extend schema, optional: label: "Optional" type: Boolean defaultValue: true if @type is "multipleChoice" or @type is "table" or @type is "table_polar" _.extend schema, selectionMode: label: "Mode" type: String defaultValue: "single" autoform: type: "select-radio-inline" options: [ label: "single selection (radios)" value: "single" , label: "multiple selections (checkboxes)" value: "multi" ] if @type is "multipleChoice" _.extend schema, orientation: label: "Orientation" type: String defaultValue: "vertical" autoform: type: "select-radio-inline" options: [ label: "inline" value: "inline" , label: "vertical" value: "vertical" ] if @type is "description" _.extend schema, label: label: "Text (markdown)" type: String defaultValue: "Insert text here" 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 defaultValue: false if @type is "boolean" _.extend schema, showCondtionalQuestionsOn: label: "Show conditional questions on:" type: [String] optional: true - defaultValue: false + defaultValue: ["true"] autoform: type: "select-checkbox-inline" options: [ label: "true" value: "true" , label: "false" value: "false" ] if @type is "multipleChoice" or @type is "table" or @type is "table_polar" _.extend schema, choices: type: [Object] label: "Choices" minCount: 1 defaultValue: [{label: "Insert choice here", value: "1"}] 'choices.$.label': type: String optional: true 'choices.$.value': type: String regEx: noWhitespaceRegex 'choices.$.conditional': label: '?' type: Boolean optional: true defaultValue: false if @selectionMode is "multi" schema['choices.$.value'].custom = -> #console.log "> #{@value} #{@key} <" #console.log "----" digitRegex = /(\d+)/g matches = digitRegex.exec(@key) if matches.length > 0 index = parseInt(matches[0])-1 while index >= 0 v = @field("choices.#{index}.value").value #console.log v if v? and v.valueOf() is @value.valueOf() return "notUnique" index -= 1 return if @type is "table" _.extend schema, subquestions: type: [Object] label: "Subquestions" minCount: 1 defaultValue: [{label: "Insert question here", code: new Mongo.ObjectID()._str}] 'subquestions.$.label': type: String autoform: type: "textarea" 'subquestions.$.code': type: String regEx: noWhitespaceRegex custom: -> digitRegex = /(\d+)/g matches = digitRegex.exec(@key) if matches.length > 0 index = parseInt(matches[0])-1 while index >= 0 v = @field("subquestions.#{index}.code").value if v? and v.valueOf() is @value.valueOf() return "notUnique" index -= 1 return if @type is "table_polar" _.extend schema, subquestions: type: [Object] label: "Subquestions" minCount: 1 defaultValue: [{code: new Mongo.ObjectID()._str}] 'subquestions.$.minLabel': label: "min label" type: String optional: true 'subquestions.$.maxLabel': label: "max label" type: String optional: true 'subquestions.$.code': type: String regEx: noWhitespaceRegex custom: -> digitRegex = /(\d+)/g matches = digitRegex.exec(@key) if matches.length > 0 index = parseInt(matches[0])-1 while index >= 0 v = @field("subquestions.#{index}.code").value if v? and v.valueOf() is @value.valueOf() return "notUnique" index -= 1 return _.extend schema, translations: type: Object blackbox: true optional: true if finalValidation _.extend schema, _id: type: String optional: true questionnaireId: type: String index: type: Number createdAt: type: Number optional: true updatedAt: type: Number optional: true return schema getTranslationSchemaDict: -> schema = {} noWhitespaceRegex = /^\S*$/ #don't match if contains whitespace if @type is "table" or @type is "table_polar" _.extend schema, label: label: "Title" type: String optional: true autoform: type: "textarea" else if @type isnt "description" _.extend schema, label: label: "Question" type: String autoform: type: "textarea" if @type is "description" _.extend schema, label: label: "Text (markdown)" type: String autoform: type: "textarea" rows: 10 if @type is "multipleChoice" or @type is "table" or @type is "table_polar" _.extend schema, choices: type: [Object] label: "Choices" minCount: @choices.length or 1 maxCount: @choices.length or 1 'choices.$.label': type: String optional: true 'choices.$.value': type: String autoform: readonly: true if @type is "table" _.extend schema, subquestions: type: [Object] label: "Subquestions" minCount: @subquestions.length or 1 maxCount: @subquestions.length or 1 'subquestions.$.label': type: String autoform: type: "textarea" 'subquestions.$.code': type: String autoform: readonly: true if @type is "table_polar" _.extend schema, subquestions: type: [Object] label: "Subquestions" minCount: 0 maxCount: @subquestions.length 'subquestions.$.minLabel': label: "min label" type: String optional: true 'subquestions.$.maxLabel': label: "max label" type: String optional: true 'subquestions.$.code': type: String autoform: readonly: true return schema @Questions = new Meteor.Collection("questions", transform: (doc) -> new Question(doc) ) Questions.before.insert BeforeInsertTimestampHook Questions.before.update BeforeUpdateTimestampHook Meteor.methods insertQuestion: (question) -> checkIfAdmin() check(question.questionnaireId, String) questionnaire = Questionnaires.findOne question.questionnaireId throw new Meteor.Error(400, "questionnaire #{question.questionnaireId}) not found.") unless questionnaire? check(question.type, String) if question.type isnt "text" and question.type isnt "description" throw new Meteor.Error(400, "only text and description questions can be inserted") delete question._id delete question.code numQuestions = Questions.find(questionnaireId: questionnaire._id).count() nextIndex = numQuestions+1 if (question.index? and question.index > nextIndex) or !question.index? question.index = nextIndex q = new Question(question) ss = new SimpleSchema(q.getMetaSchemaDict(true)) ss.clean(question) check(question, ss) Questions.insert question copyQuestion: (questionId) -> checkIfAdmin() check questionId, String question = Questions.findOne questionId throw new Meteor.Error(403, "question (#{questionId}) not found.") unless question? questionnaire = Questionnaires.findOne question.questionnaireId throw new Meteor.Error(400, "questionnaire #{question.questionnaireId}) not found.") unless questionnaire? delete question._id if question.code? question.code += ":#{new Mongo.ObjectID()._str}" if question.subquestions? question.subquestions.forEach (q) -> q.code += ":#{new Mongo.ObjectID()._str}" question.index = Questions.find(questionnaireId: questionnaire._id).count()+1 q = new Question(question) ss = new SimpleSchema(q.getMetaSchemaDict(true)) ss.clean(question) check(question, ss) Questions.insert question updateQuestion: (modifier, docId) -> checkIfAdmin() check(modifier, Object) check(docId, String) question = Questions.findOne docId throw new Meteor.Error(403, "question (#{docId}) not found.") unless question? typeChange = false #check if question.code is unique if (code = modifier['$set'].code) and code isnt question.code count = Questions.find( _id: $ne: question._id questionnaireId: question.questionnaireId $or: [ {code: code}, {'subquestions.code': code} ] ).count() if count > 0 details = EJSON.stringify [ {name: "code", type: "notUnique", value: code} ] throw new Meteor.Error(400, "validationError", details) #check for dangerous changes not allowed for already answered questions #don't check if subquestions.$.code and choices.$.value are unique, we do that in the schema dangerousChange = false if (type=modifier['$set'].type)? and Object.keys(modifier['$set']).length is 1 typeChange = true dangerousChange = true if (choices = modifier['$set'].choices)? if choices.length < question.choices.length dangerousChange = true else i = 0 values = {} while i 0 details = EJSON.stringify [ {name: "subquestions.#{i}.code", type: "notUnique", value: s.code} ] throw new Meteor.Error(400, "validationError", details) i += 1 if (selectionMode=modifier['$set'].selectionMode)? and selectionMode isnt question.selectionMode dangerousChange = true if dangerousChange count = Answers.find( questionId: docId ).count() if count > 0 throw new Meteor.Error(400, "validationErrorQuestionInUse") q = new Question(question).getModifiedPlainObject(modifier) schema = new Question(q).getMetaSchemaDict(true) ss = new SimpleSchema(schema) if typeChange #only the type changed: we need to clean the new object #to ornament it with default values #apply modifier ss.clean(q) check(q, ss) # delete translations entirely on typeChange if question.translations? delete q.translations #replace question entirely #use direct to prevent $set.updatedAt being added Questions.direct.update docId, q else check(q, ss) Questions.update docId, modifier # after save tasks # update translated choices and subquestions question = Questions.findOne question._id checkTranslations = false if dangerousChange and question.translations? Object.keys(question.translations).forEach (lang) -> translation = question.translations[lang] if question.choices? checkTranslations = true translatedChoices = [] question.choices.forEach (c) -> translatedChoice = translation.choices.find (tc) -> tc.value is c.value if !translatedChoice? translatedChoice = c translatedChoices.push translatedChoice Questions.update question._id, $set: "translations.#{lang}.choices": translatedChoices if question.subquestions? checkTranslations = true translatedSubquestions = [] question.subquestions.forEach (s) -> tSubquestion = translation.subquestions.find (ts) -> ts.code is s.code if !tSubquestion? tSubquestion = s translatedSubquestions.push tSubquestion Questions.update question._id, $set: "translations.#{lang}.subquestions": translatedSubquestions if checkTranslations throw new Meteor.Error(400, "validationWarningCheckTranslations") return # changed at: 2016/08/03: didn't work in production. not able to reproduce, reason unknown. # moveQuestion: (questionnaireId, oldIndex, newIndex) -> # checkIfAdmin() # 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} # return moveQuestion: (questionId, up) -> checkIfAdmin() check(questionId, String) check(up, Boolean) question = Questions.findOne questionId throw new Meteor.Error(403, "question not found.") unless question? questionnaire = Questionnaires.findOne _id: question.questionnaireId throw new Meteor.Error(403, "questionnaire not found.") unless questionnaire? numQuestions = Questions.find( questionnaireId: questionnaire._id ).count() if question.index is 1 and up return if question.index >= numQuestions and !up return addend = -1 addend = 1 if !up Questions.update questionnaireId: question.questionnaireId index: question.index + addend , $inc: { index: -addend } Questions.update _id: questionId , $inc: { index: addend } return removeQuestion: (id) -> checkIfAdmin() check(id, String) question = Questions.findOne id throw new Meteor.Error(403, "question (#{id}) not found.") unless question? questionnaire = Questionnaires.findOne _id: question.questionnaireId throw new Meteor.Error(403, "questionnaire (#{question.questionnaireId}) not found.") unless questionnaire? #check if question is used count = Answers.find( questionId: id ).count() if count > 0 throw new Meteor.Error(400, "validationErrorQuestionInUse") Questions.remove id #update index of remaining questions Questions.update questionnaireId: questionnaire._id index: { $gt: question.index } , $inc: { index: -1 } , multi: true return translateQuestion: (questionId, translation, lang) -> checkIfAdmin() check(questionId, String) check(translation, Object) check(lang, String) question = Questions.findOne questionId throw new Meteor.Error(403, "question (#{questionId}) not found.") unless question? ss = new SimpleSchema(question.getTranslationSchemaDict()) ss.clean(translation) check(translation, ss) Questions.update _id: question._id, $set: "translations.#{lang}": translation Questionnaires.update _id: question.questionnaireId , $addToSet: translationLanguages: lang return