Skip to content
Merged
52 changes: 26 additions & 26 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -107,28 +107,28 @@ GEM
aws-sdk-ses (~> 1, >= 1.50.0)
aws-sdk-sesv2 (~> 1, >= 1.34.0)
aws-eventstream (1.3.2)
aws-partitions (1.1090.0)
aws-sdk-core (3.222.2)
aws-partitions (1.1096.0)
aws-sdk-core (3.223.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.99.0)
aws-sdk-kms (1.100.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-rails (5.1.0)
aws-sdk-core (~> 3)
railties (>= 7.1.0)
aws-sdk-s3 (1.183.0)
aws-sdk-s3 (1.185.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-ses (1.82.0)
aws-sdk-ses (1.83.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-sesv2 (1.74.0)
aws-sdk-sesv2 (1.75.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
Expand Down Expand Up @@ -186,7 +186,7 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.1)
connection_pool (2.5.3)
crass (1.0.6)
csv (3.3.4)
database_cleaner (2.1.0)
Expand Down Expand Up @@ -219,7 +219,7 @@ GEM
railties (>= 5.0.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (2.13.0)
faraday (2.13.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
Expand Down Expand Up @@ -280,7 +280,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.10.2)
json (2.11.3)
json-jwt (1.16.7)
activesupport (>= 4.2)
aes_key_wrap
Expand Down Expand Up @@ -327,7 +327,7 @@ GEM
mime-types (3.6.2)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2025.0415)
mime-types-data (3.2025.0429)
mini_magick (5.2.0)
benchmark
logger
Expand All @@ -336,11 +336,11 @@ GEM
minitest (5.25.5)
msgpack (1.8.0)
multi_json (1.15.0)
multi_xml (0.7.1)
multi_xml (0.7.2)
bigdecimal (~> 3.1)
net-http (0.6.0)
uri
net-imap (0.5.7)
net-imap (0.5.8)
date
net-protocol
net-pop (0.1.2)
Expand All @@ -349,7 +349,7 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
newrelic_rpm (9.18.0)
newrelic_rpm (9.19.0)
nio4r (2.7.4)
nokogiri (1.18.8)
mini_portile2 (~> 2.8.2)
Expand Down Expand Up @@ -407,14 +407,14 @@ GEM
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.2.3)
psych (5.2.4)
date
stringio
public_suffix (6.0.1)
public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.13)
rack (3.1.14)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
Expand All @@ -423,7 +423,7 @@ GEM
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
rack-session (2.1.0)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
Expand Down Expand Up @@ -493,24 +493,24 @@ GEM
rolify (6.0.1)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
rspec-expectations (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
rspec-mocks (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-rails (8.0.0)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
rspec-support (3.13.3)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.75.3)
rubocop (1.75.4)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
Expand Down Expand Up @@ -555,7 +555,7 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sidekiq (8.0.2)
sidekiq (8.0.3)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
Expand Down
1 change: 1 addition & 0 deletions app/controllers/admin/forms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ def permissions
def questions
@form.warn_about_not_too_many_questions
@form.ensure_a11_v2_format if @form.kind == "a11_v2"
@form.ensure_a11_v2_radio_format if @form.kind == "a11_v2_radio"
ensure_form_manager(form: @form) unless @form.template?
@questions = @form.ordered_questions
end
Expand Down
40 changes: 38 additions & 2 deletions app/models/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def self.filtered_forms(user, aasm_state)
def self.kinds
[
"a11",
"a11_v2", # launched fall 2023
"a11_v2", # launched Fall 2023
"a11_v2_radio", # launched May 2025
"a11_yes_no",
"open_ended",
"other", # TODO: deprecate in favor of custom,
Expand Down Expand Up @@ -289,7 +290,7 @@ def self.send_inactive_form_emails_since(days_ago)
def self.find_inactive_forms_since(days_ago)
min_time = Time.now - days_ago.days
max_time = Time.now - (days_ago - 1).days
Form.published.where("last_response_created_at BETWEEN ? AND ?", min_time, max_time)
Form.non_templates.published.where("last_response_created_at BETWEEN ? AND ?", min_time, max_time)
end

def deployable_form?
Expand Down Expand Up @@ -734,6 +735,41 @@ def ensure_a11_v2_format
end
end

def ensure_a11_v2_radio_format
question_1 = self.ordered_questions.find { |q| q.answer_field == "answer_01" }
if question_1.question_type != 'radio_buttons'
errors.add(:base, "The question for `answer_01` must be a Radio Buttons component with 5 options, with values 1-5")
end

# ensure the form has the 4 required questions
required_elements = ["answer_01", "answer_02", "answer_03", "answer_04"]
unless contains_elements?(questions.collect(&:answer_field), required_elements)
errors.add(:base, "The A-11 v2 form must have questions for #{required_elements.to_sentence}")
end

# ensure the positive indicators include ease and effectiveness
question_2 = self.ordered_questions.find { |q| q.answer_field == "answer_02" }
question_options = question_2.question_options

question_option_values = question_options.collect(&:value)
required_options = ["effectiveness", "ease"]
missing_options = required_options - question_option_values
if missing_options.any?
errors.add(:base, "The question options for Question 2 must include: #{missing_options.join(', ')}")
end

# ensure the positive indicators include ease and effectiveness
question_3 = self.ordered_questions.find { |q| q.answer_field == "answer_03" }
question_options = question_3.question_options

question_option_values = question_options.collect(&:value)
required_options = ["effectiveness", "ease"]
missing_options = required_options - question_option_values
if missing_options.any?
errors.add(:base, "The question options for Question 3 must include: #{missing_options.join(', ')}")
end
end

def warn_about_not_too_many_questions
if questions.size > 12
errors.add(:base, "Touchpoints supports a maximum of 20 questions. There are currently #{questions_count} questions. Fewer questions tend to yield higher response rates.")
Expand Down
52 changes: 52 additions & 0 deletions app/views/components/_form_a11_v2_radio_script.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Assumes: 4 questions:
// 1. 5 radio buttons with values 1-5
// 2. positive checkbox indicators
// 3. negative checkbox indicators
// 4. open text
// Hides the 2nd and 3rd questions to start
// reveals 2 when selecting thumbs up
// reveals 3 when selecting thumbs down
<%
question_1 = form.ordered_questions.find { |q| q.answer_field == "answer_01"}
question_2 = form.ordered_questions.find { |q| q.answer_field == "answer_02"}
question_3 = form.ordered_questions.find { |q| q.answer_field == "answer_03"}
%>

document.addEventListener('onTouchpointsFormLoaded', function(e) {
const formElement = e.detail.formComponent.formElement();
const q2_container = formElement.querySelector("#<%= dom_id(question_2) %>");
const q3_container = formElement.querySelector("#<%= dom_id(question_3) %>");

function hideQ2() {
q2_container.style.display = 'none';
}
function showQ2() {
q2_container.style.display = 'block';
}

function hideQ3() {
q3_container.style.display = 'none';
}
function showQ3() {
q3_container.style.display = 'block';
}

function showAndHideQuestions(selectedOption) {
if (selectedOption === "1" || selectedOption === "2" || selectedOption === "3") {
hideQ2()
showQ3()
} else if (selectedOption === "4" || selectedOption === "5") {
showQ2()
hideQ3()
}
}

formElement.querySelectorAll('input[name="<%= question_1.ui_selector %>"]').forEach((radio) => {
radio.addEventListener('change', (event) => {
showAndHideQuestions(event.target.value);
});
});

hideQ2()
hideQ3()
})
14 changes: 10 additions & 4 deletions app/views/components/forms/question_types/_radio_buttons.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
<div class="question-options">
<% question.question_options.each_with_index do |option, index| %>
<% @option_id = dom_id(option) %>
<div class="radio-button usa-radio question-option"
<div
class="radio-button usa-radio question-option"
data-id="<%= option.id %>"
<%- if question.help_text.present? %>
aria-describedby="<%= "question-id-#{question.id}-help-text" %>"
<% end %>
>
<%= radio_button_tag(@option_id, option.value, nil, { id: @option_id, name: question.ui_selector, class: "usa-radio__input usa-radio__input--tile", required: question.is_required }) %>
<%= label_tag(@option_id, nil, class: "usa-radio__label") do %><%= option.text %><% end %>
<%= radio_button_tag(question.ui_selector, option[:value], nil, {
id: @option_id,
class: "usa-radio__input usa-radio__input--tile",
required: question.is_required
}) %>
<%= label_tag(@option_id, option.text, class: "usa-radio__label") %>
<%- if option.other_option %>
<div class="margin-top-1">
<div
class="margin-top-1">
<%= label_tag(nil, for: "#{question.ui_selector}_other", class: "usa-input__label") do %><%= t 'form.enter_other_text' %><% end %>
<input type="text"
name="<%= question.ui_selector %>_other"
Expand Down
2 changes: 1 addition & 1 deletion app/views/components/widget/_fba.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ var touchpointFormOptions<%= form.short_uuid %> = {
'css' : "<%= escape_javascript(render partial: 'components/widget/widget', formats: :css, locals: { form: form }) %>",
'loadCSS' : <%= form.load_css %>,
'formSpecificScript' : function() {
<%- if ["a11_v2", "a11_yes_no"].include?(form.kind) %>
<%- if ["a11_v2", "a11_v2_radio", "a11_yes_no"].include?(form.kind) %>
<%= render "components/form_#{form.kind}_script", form: form %>
<% end %>
},
Expand Down
Loading