From df1b101aa2b876018e80e35a62a315effc002b90 Mon Sep 17 00:00:00 2001 From: Mark Moser Date: Wed, 31 Aug 2016 16:59:25 -0500 Subject: [PATCH] introduce check_other and radio_other question types completes issue #48 --- app/controllers/quiz_controller.rb | 8 +++ app/helpers/application_helper.rb | 2 + app/validators/answer_format_validator.rb | 15 +++++ .../admin/question/_checkbox_other.html.erb | 21 +++++++ .../admin/question/_radio_other.html.erb | 21 +++++++ app/views/quiz/_checkbox_other.html.erb | 27 ++++++++ app/views/quiz/_radio_other.html.erb | 27 ++++++++ test/fixtures/answers.yml | 41 ++++++++++--- test/fixtures/questions.yml | 7 +-- .../checkbox_other_test.rb | 61 +++++++++++++++++++ .../answer_format_validator/live_code_test.rb | 6 +- .../radio_other_test.rb | 54 ++++++++++++++++ 12 files changed, 276 insertions(+), 14 deletions(-) create mode 100644 app/views/admin/question/_checkbox_other.html.erb create mode 100644 app/views/admin/question/_radio_other.html.erb create mode 100644 app/views/quiz/_checkbox_other.html.erb create mode 100644 app/views/quiz/_radio_other.html.erb create mode 100644 test/validators/answer_format_validator/checkbox_other_test.rb create mode 100644 test/validators/answer_format_validator/radio_other_test.rb diff --git a/app/controllers/quiz_controller.rb b/app/controllers/quiz_controller.rb index 016bd76..4f57023 100644 --- a/app/controllers/quiz_controller.rb +++ b/app/controllers/quiz_controller.rb @@ -51,6 +51,7 @@ class QuizController < ApplicationController :radio, :text, checkbox: [], + with_other: [:other, options: []], live_code: [:later, :html, :css, :js, :text] ) end @@ -110,4 +111,11 @@ class QuizController < ApplicationController saved: params.key?(:save), submitted: params.key?(:submit)) end + + def process_radio_other + @answer.update(answer: answer_params[:with_other].to_h, + saved: params.key?(:save), + submitted: params.key?(:submit)) + end + alias process_checkbox_other process_radio_other end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 876a05c..a638fed 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,7 +22,9 @@ module ApplicationHelper options_for_select([ %w(Text text), %w(Radio radio), + ['Radio with other', 'radio_other'], %w(Checkbox checkbox), + ['Checkbox with other', 'checkbox_other'], %w(Coder live_code) ], selected: (val.blank? ? '' : val)) end diff --git a/app/validators/answer_format_validator.rb b/app/validators/answer_format_validator.rb index bf76fd2..8214b2b 100644 --- a/app/validators/answer_format_validator.rb +++ b/app/validators/answer_format_validator.rb @@ -38,8 +38,23 @@ class AnswerFormatValidator < ActiveModel::EachValidator record.errors[attribute] << (options[:message] || live_code_error_message(value)) end + def with_other record, attribute, value + return if value.present? && with_other_check(value) + + record.errors[attribute] << (options[:message] || "Please select or provide an answer.") + end + alias radio_other with_other + alias checkbox_other with_other + ################################# + def with_other_check value + return false unless value.respond_to? :keys + return false if Array(value[:options]).join.blank? + return false if value[:options].include?('other') && value[:other].to_s.blank? + true + end + def live_code_error_message value if value.present? && value.keys.count == 1 return "Please check that you will come back to complete the code example." diff --git a/app/views/admin/question/_checkbox_other.html.erb b/app/views/admin/question/_checkbox_other.html.erb new file mode 100644 index 0000000..0a63171 --- /dev/null +++ b/app/views/admin/question/_checkbox_other.html.erb @@ -0,0 +1,21 @@ +Checkbox Options + + + + +<% unless (disable ||= false) %> +
+
Add option
+
  • + <%= text_field_tag 'question[multi_choice][]', nil, { disabled: (disable ||= false), data: { last: nil } } %> +
  • +
    +<% end %> diff --git a/app/views/admin/question/_radio_other.html.erb b/app/views/admin/question/_radio_other.html.erb new file mode 100644 index 0000000..5c2f7e3 --- /dev/null +++ b/app/views/admin/question/_radio_other.html.erb @@ -0,0 +1,21 @@ +Radio Options + + + + +<% unless (disable ||= false) %> +
    +
    Add option
    +
  • + <%= text_field_tag 'question[multi_choice][]', nil, { disabled: (disable ||= false), data: { last: nil } } %> +
  • +
    +<% end %> diff --git a/app/views/quiz/_checkbox_other.html.erb b/app/views/quiz/_checkbox_other.html.erb new file mode 100644 index 0000000..49a04f1 --- /dev/null +++ b/app/views/quiz/_checkbox_other.html.erb @@ -0,0 +1,27 @@ +<% + answers = question.answer.nil? ? [] : Array(question.answer['options']) + other_value = question.answer.nil? ? '' : question.answer['other'] + + question.input_options.each do | option | + option_id = "#{option.parameterize}_#{question.to_i}" + checkbox_html = {class: 'checkbox', id: option_id, data: { last: answers.include?(option) ? 'checked' : '' } } + %> +
    + <%= check_box_tag('answer[with_other][options][]', option, answers.include?(option), checkbox_html) %> + <%= label_tag(option_id, option) %> +
    + <% + end %> + +
    + <% + option_id = "other_#{question.to_i}" + checkbox_html = {class: 'checkbox', id: option_id, data: { last: answers.include?('other') ? 'checked' : '' } } + text_html = {class: 'input-other', id: "text_#{option_id}", data: { last: other_value }} + %> + <%= check_box_tag('answer[with_other][options][]', 'other', answers.include?('other'), checkbox_html) %> + <%= label_tag(option_id, 'Other') %> + <%= text_field_tag 'answer[with_other][other]', other_value, text_html %> +
    + +<%= render partial: "quiz/answer_errors", locals: {question: question, answer: answer} %> diff --git a/app/views/quiz/_radio_other.html.erb b/app/views/quiz/_radio_other.html.erb new file mode 100644 index 0000000..30e9567 --- /dev/null +++ b/app/views/quiz/_radio_other.html.erb @@ -0,0 +1,27 @@ +<% + answer = question.answer.nil? ? '' : Array(question.answer['options']).first + other_value = question.answer.nil? ? '' : question.answer['other'] + + question.input_options.each do | option | + option_id = "#{option.parameterize}_#{question.to_i}" + radio_html = {class: 'radio', id: option_id, data: {last: (answer == option) ? 'checked' : '' }} + %> +
    + <%= radio_button_tag('answer[with_other][options][]', option, (answer == option), radio_html) %> + <%= label_tag(option_id, option) %> +
    + <% + end %> + +
    + <% + option_id = "other_#{question.to_i}" + radio_html = {class: 'radio', id: option_id, data: { last: answer }} + text_html = {class: 'input-other', id: "text_#{option_id}", data: { last: other_value }} + %> + <%= radio_button_tag('answer[with_other][options][]', 'other', (answer == 'other'), radio_html) %> + <%= label_tag option_id, 'Other' %> + <%= text_field_tag 'answer[with_other][other]', other_value, text_html %> +
    + +<%= render partial: "quiz/answer_errors", locals: {question: question, answer: answer} %> diff --git a/test/fixtures/answers.yml b/test/fixtures/answers.yml index c7f03a7..9c3455a 100644 --- a/test/fixtures/answers.yml +++ b/test/fixtures/answers.yml @@ -84,7 +84,9 @@ dawn7: dawn8: candidate: dawn question: fed8 - answer: option2 + answer: + options: + - option2 saved: 0 submitted: true created_at: <%= DateTime.now() - 38.hours - 38.minutes %> @@ -93,7 +95,10 @@ dawn8: dawn9: candidate: dawn question: fed9 - answer: Grunt + answer: + other: + options: + - Grunt saved: 0 submitted: true created_at: <%= DateTime.now() - 38.hours - 38.minutes %> @@ -174,7 +179,10 @@ peggy7: peggy8: candidate: peggy question: fed8 - answer: option2 + answer: + other: Some generic user input + options: + - other saved: 0 submitted: true created_at: <%= DateTime.now() - 38.hours - 38.minutes %> @@ -183,7 +191,11 @@ peggy8: peggy9: candidate: peggy question: fed9 - answer: Grunt + answer: + other: npm + options: + - Grunt + - other saved: 0 submitted: true created_at: <%= DateTime.now() - 38.hours - 38.minutes %> @@ -265,7 +277,10 @@ richard7: richard8: candidate: richard question: fed8 - answer: option-4 + answer: + other: Some generic user input + options: + - other saved: 0 submitted: true created_at: <%= DateTime.now() - 36.hours - 36.minutes %> @@ -274,7 +289,11 @@ richard8: richard9: candidate: richard question: fed9 - answer: Grunt + answer: + other: Brunch + options: + - Neither + - other saved: 0 submitted: true created_at: <%= DateTime.now() - 36.hours - 38.minutes %> @@ -355,7 +374,10 @@ juan7: juan8: candidate: juan question: fed8 - answer: option2 + answer: + other: Some generic user input + options: + - other saved: 0 submitted: true created_at: <%= DateTime.now() - 38.hours - 38.minutes %> @@ -364,7 +386,10 @@ juan8: juan9: candidate: juan question: fed9 - answer: Grunt + answer: + other: Mimosa + options: + - other saved: 0 submitted: true created_at: <%= DateTime.now() - 38.hours - 38.minutes %> diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml index db562de..6460c5f 100644 --- a/test/fixtures/questions.yml +++ b/test/fixtures/questions.yml @@ -68,7 +68,7 @@ fed7: category: Javascript input_type: live_code input_options: - :html: "

    sample seed html

    " + :html: "

    Sample seed HTML

    " :css: "body { color: #644; }" sort: 6 active: true @@ -77,12 +77,11 @@ fed8: quiz: fed question: Select the HTML from below that would create an input field which restricts the number of characters inside it to 10. category: HTML - input_type: radio + input_type: radio_other input_options: - option-1 - option2 - "option 3" - - option-4 sort: 7 active: true @@ -90,7 +89,7 @@ fed9: quiz: fed question: Grunt or Gulp? category: Javascript - input_type: radio + input_type: checkbox_other input_options: - Grunt - Gulp diff --git a/test/validators/answer_format_validator/checkbox_other_test.rb b/test/validators/answer_format_validator/checkbox_other_test.rb new file mode 100644 index 0000000..2d84926 --- /dev/null +++ b/test/validators/answer_format_validator/checkbox_other_test.rb @@ -0,0 +1,61 @@ +require 'test_helper' + +# *_with_other answers expect a hash response: +# with_other: { other: 'TEXT-FIELD-VALUE', options: ['selected', 'answer', 'values'] } +class AnswerFormatValidatorTest < ActiveSupport::TestCase + test "checkbox_other should PASS with populated array" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = { other: nil, options: ['some', 'selections', 'not-other'] } + + assert obj.valid? + assert obj.errors.messages.empty? + end + + test "checkbox_other should PASS with other and value" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = { other: 'some random user input', options: ['other', 'another option'] } + + assert obj.valid? + assert obj.errors.messages.empty? + end + + test "checkbox_other should FAIL with nil" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = nil + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "checkbox_other should FAIL with nil options" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = { other: '', options: nil } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "checkbox_other should FAIL with empty string" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = { other: '', options: [''] } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "checkbox_other should FAIL with other selected and no value" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = { other: '', options: %w(other some more selections) } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "checkbox_other should FAIL with array of empty strings" do + obj = AnswerValidatable.new('checkbox_other') + obj.answer = { other: 'This is an unselected value', options: ["", "", " "] } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end +end diff --git a/test/validators/answer_format_validator/live_code_test.rb b/test/validators/answer_format_validator/live_code_test.rb index 68515d1..78709d3 100644 --- a/test/validators/answer_format_validator/live_code_test.rb +++ b/test/validators/answer_format_validator/live_code_test.rb @@ -50,16 +50,18 @@ class AnswerFormatValidatorTest < ActiveSupport::TestCase end test "live_code should PASS using seed data" do + seeded_answer = questions(:fed7).input_options obj = AnswerValidatable.new('live_code', questions(:fed7).id) - obj.answer = { text: "no thanks", html: "

    sample seed html

    ", css: "body { color: #644; }", js: "" } + obj.answer = seeded_answer.merge(text: "no thanks", js: "") assert obj.valid? assert obj.errors.messages.empty? end test "live_code should FAIL with seed data only" do + seeded_answer = questions(:fed7).input_options obj = AnswerValidatable.new('live_code', questions(:fed7).id) - obj.answer = { text: "", html: "

    sample seed html

    ", css: "body { color: #644; }", js: "" } + obj.answer = seeded_answer.merge(text: "", js: "") refute obj.valid? assert_match(/write.*code/, obj.errors.messages[:answer][0]) diff --git a/test/validators/answer_format_validator/radio_other_test.rb b/test/validators/answer_format_validator/radio_other_test.rb new file mode 100644 index 0000000..8429f2b --- /dev/null +++ b/test/validators/answer_format_validator/radio_other_test.rb @@ -0,0 +1,54 @@ +require 'test_helper' + +# *_with_other answers expect a hash answer: +# with_other: { other: 'TEXT-FIELD-VALUE', options: ['selected', 'answer', 'values'] } + +class AnswerFormatValidatorTest < ActiveSupport::TestCase + test "radio_other should PASS with selection" do + obj = AnswerValidatable.new('radio_other') + obj.answer = { other: nil, options: ['some-selection-not-other'] } + + assert obj.valid? + assert obj.errors.messages.empty? + end + + test "radio_other should PASS with other and value" do + obj = AnswerValidatable.new('radio_other') + obj.answer = { other: 'some random user input', options: ['other'] } + + assert obj.valid? + assert obj.errors.messages.empty? + end + + test "radio_other should FAIL with nil" do + obj = AnswerValidatable.new('radio_other') + obj.answer = nil + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "radio_other should FAIL with nil options" do + obj = AnswerValidatable.new('radio_other') + obj.answer = { other: '', options: nil } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "radio_other should FAIL with empty string" do + obj = AnswerValidatable.new('radio_other') + obj.answer = { other: '', options: [''] } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end + + test "radio_other should FAIL with other selected and no value" do + obj = AnswerValidatable.new('radio_other') + obj.answer = { other: '', options: ['other'] } + + refute obj.valid? + assert_match(/select.*answer/, obj.errors.messages[:answer][0]) + end +end