diff --git a/Gemfile.lock b/Gemfile.lock index ebfeed2b9..be5895b16 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/app/controllers/admin/forms_controller.rb b/app/controllers/admin/forms_controller.rb index 06b6fc151..36c6d4193 100644 --- a/app/controllers/admin/forms_controller.rb +++ b/app/controllers/admin/forms_controller.rb @@ -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 diff --git a/app/models/form.rb b/app/models/form.rb index c624d1d57..c9bbe5d03 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -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, @@ -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? @@ -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.") diff --git a/app/views/components/_form_a11_v2_radio_script.html.erb b/app/views/components/_form_a11_v2_radio_script.html.erb new file mode 100644 index 000000000..70efc876d --- /dev/null +++ b/app/views/components/_form_a11_v2_radio_script.html.erb @@ -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() +}) \ No newline at end of file diff --git a/app/views/components/forms/question_types/_radio_buttons.html.erb b/app/views/components/forms/question_types/_radio_buttons.html.erb index 463a74f1c..bf61d2a12 100644 --- a/app/views/components/forms/question_types/_radio_buttons.html.erb +++ b/app/views/components/forms/question_types/_radio_buttons.html.erb @@ -3,16 +3,22 @@