diff --git a/app/client/views/questionnaires/questionnaire_wizzard.coffee b/app/client/views/questionnaires/questionnaire_wizzard.coffee index adb3fa9..cc86a82 100644 --- a/app/client/views/questionnaires/questionnaire_wizzard.coffee +++ b/app/client/views/questionnaires/questionnaire_wizzard.coffee @@ -1,580 +1,600 @@ @__showQuestionnaireWizzard = (data) -> Session.set 'selectedQuestionnaireWizzard', data if !data.readonly #check if this questionnaire was answered before by this patient #we do this on the client only, because we can't check it properly by the #server anyway questionIds = Questions.find( questionnaireId: data.questionnaire._id ).map (q) -> q._id count = Answers.find( visitId: data.visit._id questionId: $in: questionIds ).count() if count > 0 swal { title: 'Attention!' text: "This questionnaire was alredy filled out. To view data, please use the ‘show’ button. Do you want to proceed? Old data will be overwritten. A log entry will be created. Please state a reason." type: 'input' inputPlaceholder: "Please state a reason." showCancelButton: true confirmButtonText: 'Yes' 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 "logActivity", "reopen questionnaire (#{data.questionnaire.id}) for editing which was already filled out (patient: #{data.patient.id} visit:#{data.visit.title})", "notice", confirmedWithReason, data swal.close() doShowQuestionnaireWizzard(data) return else doShowQuestionnaireWizzard(data) else doShowQuestionnaireWizzard(data) doShowQuestionnaireWizzard = (data) -> Modal.show('questionnaireWizzard', data, keyboard: false) @__closeQuestionnaireWizzard = -> if isAFormDirty() swal { title: 'Unsaved Changes' text: "Do you want to save the changes on this page?" type: 'warning' showCancelButton: true confirmButtonText: 'Save and exit' cancelButtonText: "Exit without saving" closeOnConfirm: false }, (save) -> if save submitAllForms('close') else swal.close() Modal.hide('questionnaireWizzard') else Modal.hide('questionnaireWizzard') _numQuestions = new ReactiveVar(0) _numPages = new ReactiveVar(0) _questionIdsForPage = new ReactiveVar({}) _pageIndex = new ReactiveVar(0) _numFormsToSubmit = 0 _readonly = new ReactiveVar(false) _questionnaire = new ReactiveVar(null) _nextQuestionnaire = null _preview = new ReactiveVar(false) _lang = new ReactiveVar(null) isAFormDirty = -> if _readonly.get() or _preview.get() return false isDirty = false $("form").each () -> return if isDirty e = $(@)[0] dirty = formIsDirty(e) isDirty = dirty if dirty isDirty _goto = null submitAllForms = (goto) -> if _readonly.get() or _preview.get() throw new Error("Can't submitAllForms because _readonly == true") _goto = goto numFormsToSubmit = 0 missingAnswer = false optionalQuestions = [] questions = Questions.find questionnaireId: _questionnaire.get()._id .map (question) -> if question.optional optionalQuestions.push(question._id) #count forms and check for empty inputs $("form").each -> e = $(@) classes = e.attr('class') if classes? and classes.indexOf('question') > -1 numFormsToSubmit += 1 if optionalQuestions.indexOf(e.attr('id')) < 0 if classes? and classes.indexOf('questionForm') > -1 #check multiple subquestions forms lines = e.find('tbody tr') lines.each -> l = $(@) lineAnswered = l.find('input:checked').length > 0 if !lineAnswered missingAnswer = true l.addClass("missing-answer") else l.removeClass("missing-answer") else #check one question forms updateDoc = AutoForm.getFormValues(e.attr('id')).updateDoc if Object.keys(updateDoc).length is 0 or updateDoc?['$unset']?.value is "" missingAnswer = true e.addClass("missing-answer") else e.removeClass("missing-answer") if missingAnswer swal { title: 'missing answers' text: "You have left some questions unanswered, are you sure you want to continue?" type: 'warning' showCancelButton: true confirmButtonText: 'Yes' cancelButtonText: "Cancel" }, -> doSubmitAllForms(numFormsToSubmit) else swal.close() #possibly open swal "unsaved changes" doSubmitAllForms(numFormsToSubmit) _submittingForms = false doSubmitAllForms = (numFormsToSubmit) -> _submittingForms = true _numFormsToSubmit = numFormsToSubmit $("form").each -> e = $(@) classes = e.attr('class') if classes? and classes.indexOf('question') > -1 e.submit() formSubmitted = -> if (_numFormsToSubmit -= 1) <= 0 _submittingForms = false if _goto is 'nextPage' nextPage() else if _goto is 'previousPage' previousPage() else if _goto is 'close' Modal.hide('questionnaireWizzard') else if _goto? and _goto.pageIndex? _pageIndex.set _goto.pageIndex nextPage = -> if _pageIndex.get() is _numPages.get()-1 # deactiavted: go to next questionnaire automatically #if _nextQuestionnaire? # _pageIndex.set 0 # _questionnaire.set _nextQuestionnaire #else Modal.hide('questionnaireWizzard') else $('modal-content').scrollTop(0) _pageIndex.set _pageIndex.get()+1 previousPage = -> index = _pageIndex.get() index -= 1 if index > 0 _pageIndex.set index autoformHooks = onSubmit: (insertDoc, updateDoc, currentDoc) -> if !_submittingForms #ignore enter press @done() return false if _preview.get() or _readonly.get() return 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 = -> _questionnaire.set @data.questionnaire delete @data.questionnaire if @data.readonly _readonly.set true else _readonly.set false if @data.preview _preview.set true else _preview.set false #close on escape key press $(document).on('keyup.wizzard', (e)-> e.stopPropagation() if e.keyCode is 27 #escape __closeQuestionnaireWizzard() return ) #close if user has been logged out @autorun -> if !Meteor.user() __closeQuestionnaireWizzard() self = @ @autorun -> self.subscribe("questionsForQuestionnaire", _questionnaire.get()._id) #manage nextQuestionnaire @autorun -> return if _preview.get() data = Template.currentData() validatedQuestionnaires = data.visit.validatedQuestionnaires i = 0 index = null while i < validatedQuestionnaires.length-1 && index is null q = validatedQuestionnaires[i] if q._id is _questionnaire.get()._id index = i i += 1 if index? and index < validatedQuestionnaires.length-1 _nextQuestionnaire = validatedQuestionnaires[index+1] else _nextQuestionnaire = null #collect autoformIds, count pages @autorun -> count = 0 page = 0 questionIdsForPage = {} didBreakPage = false autoformIds = [] Questions.find questionnaireId: _questionnaire.get()._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, true) #determine language @autorun -> data = Template.currentData() patient = data.patient questionnaire = _questionnaire.get() if !patient #preview _lang.set null return if patient.primaryLanguage? if patient.primaryLanguage is questionnaire.primaryLanguage _lang.set null return if questionnaire.translationLanguages? and questionnaire.translationLanguages.length > 0 lang = questionnaire.translationLanguages.find (t) -> t is patient.primaryLanguage if lang? _lang.set lang return if patient.secondaryLanguage? if patient.secondaryLanguage is questionnaire.primaryLanguage _lang.set null return if questionnaire.translationLanguages? and questionnaire.translationLanguages.length > 0 lang = questionnaire.translationLanguages.find (t) -> t is patient.secondaryLanguage if lang? _lang.set lang return _lang.set null return @autorun -> Template.currentData() #must trigger reselection value = _lang.get() if !value? value = _questionnaire.get().primaryLanguage Meteor.setTimeout -> $("#source-lang option[value=#{value}]").attr('selected', true) , 100 adjustHeaderHeight = -> $('.questionnaireWizzard h2.modal-title').css('font-size', '1pt') width = $('.questionnaireWizzard .modal-header').width() height = $('.questionnaireWizzard .modal-header').height() rWidth = $('.questionnaireWizzard .modal-header .regulations').width() $('.questionnaireWizzard .title-wrapper').width(width-rWidth-5) $('.questionnaireWizzard .title-wrapper').height(height) fs = 16 counter = 0 while(true) if fs > 32 return counter += 1 $('.questionnaireWizzard h2.modal-title').css('font-size', fs+'pt') if $('.questionnaireWizzard h2.modal-title').height() > 60 $('.questionnaireWizzard h2.modal-title').css('font-size', fs-1+'pt') return fs += 1 Template.questionnaireWizzard.rendered = -> $(window).resize(adjustHeaderHeight) Meteor.setTimeout(adjustHeaderHeight, 2000) Template.questionnaireWizzard.destroyed = -> $(document).unbind('keyup.wizzard') Session.set('selectedQuestionnaireWizzard', null) $(window).off("resize", adjustHeaderHeight) Template.questionnaireWizzard.helpers patientDescription: -> d = @patient.id if @patient.hrid d += " (#{@patient.hrid})" d userDescription: -> getUserDescription(Meteor.user()) language: -> lang = _lang.get() questionnaire = _questionnaire.get() if !lang and questionnaire.primaryLanguage? lang = questionnaire.primaryLanguage lang hasLangs: -> _questionnaire.get().primaryLanguage? langs: -> questionnaire = _questionnaire.get() tl = questionnaire.translationLanguages or [] pl = questionnaire.primaryLanguage langs = isoLangs.filter (l) -> l.code is pl or tl.indexOf(l.code) > -1 langs = JSON.parse(JSON.stringify(langs)) _.some langs, (l) -> if l.code is pl l.suffix = "- PRIMARY LANGUAGE -" l.code is pl langs templateGestures: 'swipeleft div': (evt, templateInstance) -> nextQuestion() 'swiperight div': (evt, templateInstance) -> previousQuestion() title: -> _questionnaire.get().title questionsForPage: -> questionIdsForPage = _questionIdsForPage.get()[_pageIndex.get()] cursor = Questions.find( questionnaireId: _questionnaire.get()._id _id: {$in: questionIdsForPage} , sort: {index: 1} ) lang = _lang.get() if lang? #we need to translate all questions questions = cursor.map (q) -> q.translateTo lang return questions else #no need to translate return cursor questionnaire: -> _questionnaire.get() answerForQuestion: (visitId, questionId) -> return if _preview.get() Answers.findOne visitId: visitId questionId: questionId + displayQuestion: (visitId, question) -> + if !question.conditional? + return '' + else + answer_conditional = Answers.findOne + visitId: visitId + questionId: question.conditional + if answer_conditional? + return '' + else + return 'display:none;' + readonly: -> _readonly.get() preview: -> _preview.get() formType: -> if _readonly.get() "disabled" else "normal" answerFormSchema: -> schema = _id: type: String optional: true visitId: type: String optional: true questionId: type: String optional: true value: @question.getSchemaDict() new SimpleSchema(schema) doc: -> return if _preview.get() @answer or visitId: @visit._id questionId: @question._id pages: -> answers = {} questionIds = Questions.find questionnaireId: _questionnaire.get()._id .map (question) -> question._id if !_preview.get() 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 someQuestionsAnsweredInPage = false Questions.find questionnaireId: _questionnaire.get()._id _id: {$in: questionIdsForPage[i]} .forEach (question) -> return if question.type is "description" answer = answers[question._id] if question.type is "table" or question.type is "table_polar" if !answer? or answer.value.length < question.subquestions.length allQuestionsAnsweredInPage = false if answer? and answer.value.length > 0 someQuestionsAnsweredInPage = true else if !answer? allQuestionsAnsweredInPage = false if allQuestionsAnsweredInPage css = "answered" else if someQuestionsAnsweredInPage css = "answeredPartly" if i is activeIndex css += " active" pages[i] = index: i+1 css: css pages isOnFirstPage: -> _pageIndex.get() is 0 isOnLastPageOfLastQuestionnaire: -> # deactiavted: go to next questionnaire automatically _pageIndex.get() is _numPages.get()-1# and (_preview.get() or _nextQuestionnaire is null) Template.questionnaireWizzard.events "click #next": (evt, tmpl) -> if _readonly.get() or _preview.get() nextPage() else submitAllForms('nextPage') false "click #back": (evt, tmpl) -> if _readonly.get() or _preview.get() previousPage() else submitAllForms('previousPage') false "click .jumpToPage": (evt) -> pageIndex = @index-1 if isAFormDirty() swal { title: 'Unsaved Changes' text: "Do you want to save the changes on this page?" type: 'warning' showCancelButton: true confirmButtonText: 'Save' cancelButtonText: "Don't save" closeOnConfirm: false }, (save) -> if save submitAllForms(pageIndex: pageIndex) else swal.close() _pageIndex.set pageIndex else _pageIndex.set pageIndex false "click #close": (evt) -> __closeQuestionnaireWizzard() false "submit .questionForm": (evt) -> #table and table_polar if _readonly.get() or _preview.get() return 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") if @question.selectionMode is "multi" values = [] inputs.each -> input = $(@) values.push input.data('choice_value').toString() if values.length > 0 answer.value.push code: subquestion.code value: values else #if @question.selectionMode is "single" if inputs.length > 1 throw new Meteor.Error('error when processing the values: single selection got multiple values.') else if inputs.length is 1 value = inputs.first().data('choice_value').toString() answer.value.push code: subquestion.code value: value if answer.value.length > 0 Meteor.call "upsertAnswer", answer, (error) -> throwError error if error? formSubmitted() else formSubmitted() false "change #source-lang": (evt) -> _lang.set $(evt.target).find(":selected").attr('value') + + "change form": (evt) -> + value = evt.currentTarget.value + # Check if value is defined and has a true value + if value? and !(value.value in ['false', '0']) + $("form[data-conditional='#{evt.currentTarget.id}']").show() + else + $("form[data-conditional='#{evt.currentTarget.id}']").hide() diff --git a/app/client/views/questionnaires/questionnaire_wizzard.html b/app/client/views/questionnaires/questionnaire_wizzard.html index 6ba29f7..6207dd5 100644 --- a/app/client/views/questionnaires/questionnaire_wizzard.html +++ b/app/client/views/questionnaires/questionnaire_wizzard.html @@ -1,103 +1,111 @@ diff --git a/app/lib/collections/questions.coffee b/app/lib/collections/questions.coffee index b23dfde..89d78f0 100644 --- a/app/lib/collections/questions.coffee +++ b/app/lib/collections/questions.coffee @@ -1,726 +1,736 @@ 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 "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 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 + Questionnaires.update _id: question.questionnaireId , $addToSet: translationLanguages: lang return