introduce check_other and radio_other question types
completes issue #48
This commit is contained in:
parent
229ebf1380
commit
df1b101aa2
@ -51,6 +51,7 @@ class QuizController < ApplicationController
|
|||||||
:radio,
|
:radio,
|
||||||
:text,
|
:text,
|
||||||
checkbox: [],
|
checkbox: [],
|
||||||
|
with_other: [:other, options: []],
|
||||||
live_code: [:later, :html, :css, :js, :text]
|
live_code: [:later, :html, :css, :js, :text]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -110,4 +111,11 @@ class QuizController < ApplicationController
|
|||||||
saved: params.key?(:save),
|
saved: params.key?(:save),
|
||||||
submitted: params.key?(:submit))
|
submitted: params.key?(:submit))
|
||||||
end
|
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
|
end
|
||||||
|
@ -22,7 +22,9 @@ module ApplicationHelper
|
|||||||
options_for_select([
|
options_for_select([
|
||||||
%w(Text text),
|
%w(Text text),
|
||||||
%w(Radio radio),
|
%w(Radio radio),
|
||||||
|
['Radio with other', 'radio_other'],
|
||||||
%w(Checkbox checkbox),
|
%w(Checkbox checkbox),
|
||||||
|
['Checkbox with other', 'checkbox_other'],
|
||||||
%w(Coder live_code)
|
%w(Coder live_code)
|
||||||
], selected: (val.blank? ? '' : val))
|
], selected: (val.blank? ? '' : val))
|
||||||
end
|
end
|
||||||
|
@ -38,8 +38,23 @@ class AnswerFormatValidator < ActiveModel::EachValidator
|
|||||||
record.errors[attribute] << (options[:message] || live_code_error_message(value))
|
record.errors[attribute] << (options[:message] || live_code_error_message(value))
|
||||||
end
|
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
|
def live_code_error_message value
|
||||||
if value.present? && value.keys.count == 1
|
if value.present? && value.keys.count == 1
|
||||||
return "Please check that you will come back to complete the code example."
|
return "Please check that you will come back to complete the code example."
|
||||||
|
21
app/views/admin/question/_checkbox_other.html.erb
Normal file
21
app/views/admin/question/_checkbox_other.html.erb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<strong>Checkbox Options</strong>
|
||||||
|
|
||||||
|
<ul data-id="input_option_list">
|
||||||
|
<% question.input_options.each do | option | %>
|
||||||
|
<li>
|
||||||
|
<%= text_field_tag 'question[multi_choice][]', option, { disabled: (disable ||= false), data: { last: option } } %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li>Other: <input type="text" disabled="disabled" /></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<% unless (disable ||= false) %>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="btn tertiary-btn" data-id="input_option_adder"> Add option </div>
|
||||||
|
<li style="display: none;">
|
||||||
|
<%= text_field_tag 'question[multi_choice][]', nil, { disabled: (disable ||= false), data: { last: nil } } %>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
21
app/views/admin/question/_radio_other.html.erb
Normal file
21
app/views/admin/question/_radio_other.html.erb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<strong>Radio Options</strong>
|
||||||
|
|
||||||
|
<ul data-id="input_option_list">
|
||||||
|
<% question.input_options.each do | option | %>
|
||||||
|
<li>
|
||||||
|
<%= text_field_tag 'question[multi_choice][]', option, { disabled: (disable ||= false), data: { last: option } } %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li>Other: <input type="text" disabled="disabled" /></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<% unless (disable ||= false) %>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="btn tertiary-btn" data-id="input_option_adder"> Add option </div>
|
||||||
|
<li style="display: none;">
|
||||||
|
<%= text_field_tag 'question[multi_choice][]', nil, { disabled: (disable ||= false), data: { last: nil } } %>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
27
app/views/quiz/_checkbox_other.html.erb
Normal file
27
app/views/quiz/_checkbox_other.html.erb
Normal file
@ -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' : '' } }
|
||||||
|
%>
|
||||||
|
<div class="form-group-multiples">
|
||||||
|
<%= check_box_tag('answer[with_other][options][]', option, answers.include?(option), checkbox_html) %>
|
||||||
|
<%= label_tag(option_id, option) %>
|
||||||
|
</div>
|
||||||
|
<%
|
||||||
|
end %>
|
||||||
|
|
||||||
|
<div class="form-group-multiples">
|
||||||
|
<%
|
||||||
|
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 %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= render partial: "quiz/answer_errors", locals: {question: question, answer: answer} %>
|
27
app/views/quiz/_radio_other.html.erb
Normal file
27
app/views/quiz/_radio_other.html.erb
Normal file
@ -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' : '' }}
|
||||||
|
%>
|
||||||
|
<div class="form-group-multiples">
|
||||||
|
<%= radio_button_tag('answer[with_other][options][]', option, (answer == option), radio_html) %>
|
||||||
|
<%= label_tag(option_id, option) %>
|
||||||
|
</div>
|
||||||
|
<%
|
||||||
|
end %>
|
||||||
|
|
||||||
|
<div class="form-group-multiples">
|
||||||
|
<%
|
||||||
|
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 %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= render partial: "quiz/answer_errors", locals: {question: question, answer: answer} %>
|
41
test/fixtures/answers.yml
vendored
41
test/fixtures/answers.yml
vendored
@ -84,7 +84,9 @@ dawn7:
|
|||||||
dawn8:
|
dawn8:
|
||||||
candidate: dawn
|
candidate: dawn
|
||||||
question: fed8
|
question: fed8
|
||||||
answer: option2
|
answer:
|
||||||
|
options:
|
||||||
|
- option2
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
||||||
@ -93,7 +95,10 @@ dawn8:
|
|||||||
dawn9:
|
dawn9:
|
||||||
candidate: dawn
|
candidate: dawn
|
||||||
question: fed9
|
question: fed9
|
||||||
answer: Grunt
|
answer:
|
||||||
|
other:
|
||||||
|
options:
|
||||||
|
- Grunt
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
||||||
@ -174,7 +179,10 @@ peggy7:
|
|||||||
peggy8:
|
peggy8:
|
||||||
candidate: peggy
|
candidate: peggy
|
||||||
question: fed8
|
question: fed8
|
||||||
answer: option2
|
answer:
|
||||||
|
other: Some generic user input
|
||||||
|
options:
|
||||||
|
- other
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
||||||
@ -183,7 +191,11 @@ peggy8:
|
|||||||
peggy9:
|
peggy9:
|
||||||
candidate: peggy
|
candidate: peggy
|
||||||
question: fed9
|
question: fed9
|
||||||
answer: Grunt
|
answer:
|
||||||
|
other: npm
|
||||||
|
options:
|
||||||
|
- Grunt
|
||||||
|
- other
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
||||||
@ -265,7 +277,10 @@ richard7:
|
|||||||
richard8:
|
richard8:
|
||||||
candidate: richard
|
candidate: richard
|
||||||
question: fed8
|
question: fed8
|
||||||
answer: option-4
|
answer:
|
||||||
|
other: Some generic user input
|
||||||
|
options:
|
||||||
|
- other
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 36.hours - 36.minutes %>
|
created_at: <%= DateTime.now() - 36.hours - 36.minutes %>
|
||||||
@ -274,7 +289,11 @@ richard8:
|
|||||||
richard9:
|
richard9:
|
||||||
candidate: richard
|
candidate: richard
|
||||||
question: fed9
|
question: fed9
|
||||||
answer: Grunt
|
answer:
|
||||||
|
other: Brunch
|
||||||
|
options:
|
||||||
|
- Neither
|
||||||
|
- other
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 36.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 36.hours - 38.minutes %>
|
||||||
@ -355,7 +374,10 @@ juan7:
|
|||||||
juan8:
|
juan8:
|
||||||
candidate: juan
|
candidate: juan
|
||||||
question: fed8
|
question: fed8
|
||||||
answer: option2
|
answer:
|
||||||
|
other: Some generic user input
|
||||||
|
options:
|
||||||
|
- other
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
||||||
@ -364,7 +386,10 @@ juan8:
|
|||||||
juan9:
|
juan9:
|
||||||
candidate: juan
|
candidate: juan
|
||||||
question: fed9
|
question: fed9
|
||||||
answer: Grunt
|
answer:
|
||||||
|
other: Mimosa
|
||||||
|
options:
|
||||||
|
- other
|
||||||
saved: 0
|
saved: 0
|
||||||
submitted: true
|
submitted: true
|
||||||
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
created_at: <%= DateTime.now() - 38.hours - 38.minutes %>
|
||||||
|
7
test/fixtures/questions.yml
vendored
7
test/fixtures/questions.yml
vendored
@ -68,7 +68,7 @@ fed7:
|
|||||||
category: Javascript
|
category: Javascript
|
||||||
input_type: live_code
|
input_type: live_code
|
||||||
input_options:
|
input_options:
|
||||||
:html: "<p>sample seed html</p>"
|
:html: "<p>Sample seed HTML</p>"
|
||||||
:css: "body { color: #644; }"
|
:css: "body { color: #644; }"
|
||||||
sort: 6
|
sort: 6
|
||||||
active: true
|
active: true
|
||||||
@ -77,12 +77,11 @@ fed8:
|
|||||||
quiz: fed
|
quiz: fed
|
||||||
question: Select the HTML from below that would create an input field which restricts the number of characters inside it to 10.
|
question: Select the HTML from below that would create an input field which restricts the number of characters inside it to 10.
|
||||||
category: HTML
|
category: HTML
|
||||||
input_type: radio
|
input_type: radio_other
|
||||||
input_options:
|
input_options:
|
||||||
- option-1
|
- option-1
|
||||||
- option2
|
- option2
|
||||||
- "option 3"
|
- "option 3"
|
||||||
- option-4
|
|
||||||
sort: 7
|
sort: 7
|
||||||
active: true
|
active: true
|
||||||
|
|
||||||
@ -90,7 +89,7 @@ fed9:
|
|||||||
quiz: fed
|
quiz: fed
|
||||||
question: Grunt or Gulp?
|
question: Grunt or Gulp?
|
||||||
category: Javascript
|
category: Javascript
|
||||||
input_type: radio
|
input_type: checkbox_other
|
||||||
input_options:
|
input_options:
|
||||||
- Grunt
|
- Grunt
|
||||||
- Gulp
|
- Gulp
|
||||||
|
@ -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
|
@ -50,16 +50,18 @@ class AnswerFormatValidatorTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "live_code should PASS using seed data" do
|
test "live_code should PASS using seed data" do
|
||||||
|
seeded_answer = questions(:fed7).input_options
|
||||||
obj = AnswerValidatable.new('live_code', questions(:fed7).id)
|
obj = AnswerValidatable.new('live_code', questions(:fed7).id)
|
||||||
obj.answer = { text: "no thanks", html: "<p>sample seed html</p>", css: "body { color: #644; }", js: "" }
|
obj.answer = seeded_answer.merge(text: "no thanks", js: "")
|
||||||
|
|
||||||
assert obj.valid?
|
assert obj.valid?
|
||||||
assert obj.errors.messages.empty?
|
assert obj.errors.messages.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "live_code should FAIL with seed data only" do
|
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 = AnswerValidatable.new('live_code', questions(:fed7).id)
|
||||||
obj.answer = { text: "", html: "<p>sample seed html</p>", css: "body { color: #644; }", js: "" }
|
obj.answer = seeded_answer.merge(text: "", js: "")
|
||||||
|
|
||||||
refute obj.valid?
|
refute obj.valid?
|
||||||
assert_match(/write.*code/, obj.errors.messages[:answer][0])
|
assert_match(/write.*code/, obj.errors.messages[:answer][0])
|
||||||
|
54
test/validators/answer_format_validator/radio_other_test.rb
Normal file
54
test/validators/answer_format_validator/radio_other_test.rb
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user