diff --git a/Gemfile b/Gemfile index a4c83f2c8..d4352ce40 100644 --- a/Gemfile +++ b/Gemfile @@ -4,25 +4,25 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.3.4' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 8.0" +gem 'rails', '~> 8.0' # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" +gem 'sprockets-rails' # Use postgresql as the database for Active Record -gem "pg" +gem 'pg' # Use the Puma web server [https://github.com/puma/puma] -gem "puma" +gem 'puma' -# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] -gem "importmap-rails", ">= 2.0.0" + +gem "importmap-rails", ">= 2.2.0" # Hotwire"s SPA-like page accelerator [https://turbo.hotwired.dev] -gem "turbo-rails" +gem "turbo-rails", ">= 2.0.14" # Hotwire"s modest JavaScript framework [https://stimulus.hotwired.dev] -gem "stimulus-rails" +gem 'stimulus-rails' # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" @@ -31,16 +31,16 @@ gem "stimulus-rails" # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] # Reduces boot times through caching; required in config/boot.rb -gem "bootsnap", require: false +gem 'bootsnap', require: false # Use Sass to process CSS -gem "sassc-rails" +gem 'sassc-rails' # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -gem "image_processing", "~> 1.12" +gem 'image_processing', '~> 1.12' gem 'active_model_serializers' gem 'acts-as-list' @@ -51,7 +51,7 @@ gem 'carrierwave', '>= 2.2.1' gem 'csv' gem 'devise', '>= 4.8.1' gem 'fog-aws', '>= 3.15.0' -gem "jbuilder" +gem 'jbuilder' gem 'jquery-rails' gem 'kaminari' gem 'kramdown' @@ -62,35 +62,39 @@ gem 'omniauth-github' gem 'omniauth_login_dot_gov', git: 'https://github.com/18F/omniauth_login_dot_gov.git', branch: 'main' gem 'omniauth-rails_csrf_protection' gem 'rack-attack' -gem 'rack-cors', require: 'rack/cors' -# Use Redis to cache Touchpoints in all envs +gem 'rack-cors', '>= 3.0.0', require: 'rack/cors' +# Use Redis to cache Touchpoints in all envs= gem 'redis-client' gem 'redis-namespace' -gem 'sidekiq', '>= 6.5.0' +gem 'sidekiq', '>= 8.0.4' gem 'json-jwt' gem 'aasm' +gem 'acts-as-taggable-on' +gem 'json-jwt' gem 'logstop' gem 'paper_trail' -gem 'acts-as-taggable-on' -gem "rolify" +gem 'redis-client' +gem 'redis-namespace' +gem 'rolify' +gem 'sidekiq', '>= 6.5.0' group :development, :test do gem 'dotenv' gem 'pry' end -group :development, :staging do +group :development, :staging, :test do gem 'faker' end group :development do gem 'aasm-diagram' - gem "brakeman" + gem 'brakeman' gem 'bullet' - gem "bundler-audit" + gem 'bundler-audit' gem 'listen' gem 'rails-erd' - gem "rubocop-rails" + gem "rubocop-rails", ">= 2.32.0" gem "rubocop-rspec" gem 'web-console' end @@ -99,10 +103,10 @@ group :test do gem 'axe-core-rspec' gem 'capybara' gem 'database_cleaner' - gem 'factory_bot_rails' + gem 'factory_bot_rails', '>= 6.5.0' gem 'rails-controller-testing' gem 'rspec_junit_formatter' - gem 'rspec-rails' + gem 'rspec-rails', '>= 8.0.1' gem 'selenium-webdriver' gem 'simplecov', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index be5895b16..2f9c31343 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,7 +13,7 @@ GIT GEM remote: https://rubygems.org/ specs: - aasm (5.5.0) + aasm (5.5.1) concurrent-ruby (~> 1.0) aasm-diagram (0.1.3) aasm (~> 5.0, >= 4.12) @@ -106,32 +106,33 @@ GEM actionmailer (>= 7.1.0) aws-sdk-ses (~> 1, >= 1.50.0) aws-sdk-sesv2 (~> 1, >= 1.34.0) - aws-eventstream (1.3.2) - aws-partitions (1.1096.0) - aws-sdk-core (3.223.0) + aws-eventstream (1.4.0) + aws-partitions (1.1140.0) + aws-sdk-core (3.228.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 + bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.100.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.109.0) + aws-sdk-core (~> 3, >= 3.228.0) aws-sigv4 (~> 1.5) aws-sdk-rails (5.1.0) aws-sdk-core (~> 3) railties (>= 7.1.0) - aws-sdk-s3 (1.185.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.195.0) + aws-sdk-core (~> 3, >= 3.228.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sdk-ses (1.83.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-ses (1.87.0) + aws-sdk-core (~> 3, >= 3.228.0) aws-sigv4 (~> 1.5) - aws-sdk-sesv2 (1.75.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-sesv2 (1.81.0) + aws-sdk-core (~> 3, >= 3.228.0) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) axe-core-api (4.10.3) dumb_delegator @@ -148,16 +149,16 @@ GEM thread_safe (~> 0.3, >= 0.3.1) base64 (0.2.0) bcrypt (3.1.20) - benchmark (0.4.0) - bigdecimal (3.1.9) + benchmark (0.4.1) + bigdecimal (3.2.2) bindata (2.5.1) bindex (0.8.1) - bootsnap (1.18.4) + bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (7.0.2) + brakeman (7.1.0) racc builder (3.3.0) - bullet (8.0.5) + bullet (8.0.8) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) bundler-audit (0.9.2) @@ -188,12 +189,12 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.3) crass (1.0.6) - csv (3.3.4) + csv (3.3.5) database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.2.0) + database_cleaner-active_record (2.2.2) activerecord (>= 5.a) - database_cleaner-core (~> 2.0.0) + database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) date (3.4.1) descendants_tracker (0.0.4) @@ -204,28 +205,29 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.6.1) + diff-lcs (1.6.2) docile (1.4.1) dotenv (3.1.8) - drb (2.2.1) + drb (2.2.3) dumb_delegator (1.1.0) + erb (5.0.2) erubi (1.13.1) - excon (1.2.5) + excon (1.2.8) logger - factory_bot (6.5.1) + factory_bot (6.5.4) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.0) factory_bot (~> 6.5) - railties (>= 5.0.0) - faker (3.5.1) + railties (>= 6.1.0) + faker (3.5.2) i18n (>= 1.8.11, < 2) - faraday (2.13.1) + faraday (2.13.4) faraday-net_http (>= 2.0, < 3.5) json logger faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) - faraday-net_http (3.4.0) + faraday-net_http (3.4.1) net-http (>= 0.5.0) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) @@ -237,7 +239,7 @@ GEM ffi (1.17.2-x86_64-darwin) ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-musl) - fog-aws (3.31.0) + fog-aws (3.32.0) base64 (~> 0.2.0) fog-core (~> 2.6) fog-json (~> 1.1) @@ -253,7 +255,7 @@ GEM fog-xml (0.1.5) fog-core nokogiri (>= 1.5.11, < 2.0.0) - formatador (1.1.0) + formatador (1.1.1) globalid (1.2.1) activesupport (>= 6.1) hashie (5.0.0) @@ -263,11 +265,11 @@ GEM image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) - importmap-rails (2.1.0) + importmap-rails (2.2.2) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.8.0) + io-console (0.8.1) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -280,7 +282,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.11.3) + json (2.13.2) json-jwt (1.16.7) activesupport (>= 4.2) aes_key_wrap @@ -289,7 +291,7 @@ GEM faraday (~> 2.0) faraday-follow_redirects jsonapi-renderer (0.2.2) - jwt (2.10.1) + jwt (2.10.2) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -305,7 +307,7 @@ GEM kaminari-core (1.2.2) kramdown (2.5.1) rexml (>= 3.3.9) - language_server-protocol (3.17.0.4) + language_server-protocol (3.17.0.5) lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) @@ -313,7 +315,7 @@ GEM logger (1.7.0) logstop (0.4.1) logger - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -322,25 +324,24 @@ GEM net-pop net-smtp marcel (1.0.4) - matrix (0.4.2) + matrix (0.4.3) method_source (1.1.0) - mime-types (3.6.2) + mime-types (3.7.0) logger - mime-types-data (~> 3.2015) - mime-types-data (3.2025.0429) - mini_magick (5.2.0) - benchmark + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2025.0729) + mini_magick (5.3.0) logger mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.0) - multi_json (1.15.0) + multi_json (1.17.0) multi_xml (0.7.2) bigdecimal (~> 3.1) net-http (0.6.0) uri - net-imap (0.5.8) + net-imap (0.5.9) date net-protocol net-pop (0.1.2) @@ -349,34 +350,35 @@ GEM timeout net-smtp (0.5.1) net-protocol - newrelic_rpm (9.19.0) + newrelic_rpm (9.20.0) nio4r (2.7.4) - nokogiri (1.18.8) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-gnu) + nokogiri (1.18.9-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-musl) + nokogiri (1.18.9-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.8-arm-linux-gnu) + nokogiri (1.18.9-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-arm-linux-musl) + nokogiri (1.18.9-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) + nokogiri (1.18.9-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) + nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) + nokogiri (1.18.9-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-musl) + nokogiri (1.18.9-x86_64-linux-musl) racc (~> 1.4) - oauth2 (2.0.9) - faraday (>= 0.17.3, < 3.0) - jwt (>= 1.0, < 3.0) + oauth2 (2.0.12) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) rack (>= 1.2, < 4) - snaky_hash (~> 2.0) - version_gem (~> 1.1) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (>= 1.1.8, < 3) omniauth (2.1.3) hashie (>= 3.4.6) rack (>= 2.2.3) @@ -391,15 +393,19 @@ GEM actionpack (>= 4.2) omniauth (~> 2.0) orm_adapter (0.5.0) - ostruct (0.6.1) + ostruct (0.6.3) paper_trail (16.0.0) activerecord (>= 6.1) request_store (~> 1.4) parallel (1.27.0) - parser (3.3.8.0) + parser (3.3.9.0) ast (~> 2.4.1) racc - pg (1.5.9) + pg (1.6.0) + pg (1.6.0-aarch64-linux) + pg (1.6.0-arm64-darwin) + pg (1.6.0-x86_64-darwin) + pg (1.6.0-x86_64-linux) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -407,18 +413,19 @@ GEM pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - psych (5.2.4) + psych (5.2.6) date stringio public_suffix (6.0.2) - puma (6.6.0) + puma (6.6.1) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.14) + rack (3.2.0) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-cors (2.0.2) - rack (>= 2.0.0) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) rack-protection (4.1.1) base64 (>= 0.1.0) logger (>= 1.6.0) @@ -448,7 +455,7 @@ GEM actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -469,20 +476,21 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.2.1) + rake (13.3.0) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.13.1) + rdoc (6.14.2) + erb psych (>= 4.0.0) - redis (5.4.0) + redis (5.4.1) redis-client (>= 0.22.0) - redis-client (0.24.0) + redis-client (0.25.1) connection_pool redis-namespace (1.11.0) redis (>= 4) regexp_parser (2.10.0) - reline (0.6.1) + reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) @@ -491,15 +499,15 @@ GEM railties (>= 5.2) rexml (3.4.1) rolify (6.0.1) - rspec-core (3.13.3) + rspec-core (3.13.5) rspec-support (~> 3.13.0) - rspec-expectations (3.13.4) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.3) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.0) + rspec-rails (8.0.1) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -507,10 +515,10 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.3) + rspec-support (3.13.4) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.75.4) + rubocop (1.79.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -518,25 +526,25 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.44.0, < 2.0) + rubocop-ast (>= 1.46.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.44.1) + rubocop-ast (1.46.0) parser (>= 3.3.7.2) prism (~> 1.4) - rubocop-rails (2.31.0) + rubocop-rails (2.32.0) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) rubocop-rspec (3.6.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) ruby-graphviz (1.2.5) rexml ruby-progressbar (1.13.0) - ruby-vips (2.2.3) + ruby-vips (2.2.4) ffi (~> 1.12) logger rubyzip (2.4.1) @@ -549,13 +557,13 @@ GEM sprockets-rails tilt securerandom (0.4.1) - selenium-webdriver (4.31.0) + selenium-webdriver (4.34.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sidekiq (8.0.3) + sidekiq (8.0.6) connection_pool (>= 2.5.0) json (>= 2.9.0) logger (>= 1.6.2) @@ -565,11 +573,11 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.1) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - snaky_hash (2.0.1) - hashie - version_gem (~> 1.1, >= 1.1.1) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -578,15 +586,15 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - ssrf_filter (1.2.0) + ssrf_filter (1.3.0) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) - thor (1.3.2) + thor (1.4.0) thread_safe (0.3.6) - tilt (2.6.0) + tilt (2.6.1) timeout (0.4.3) - turbo-rails (2.0.13) + turbo-rails (2.0.16) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -594,10 +602,10 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - uniform_notifier (1.16.0) + uniform_notifier (1.17.0) uri (1.0.3) useragent (0.16.11) - version_gem (1.1.7) + version_gem (1.1.8) virtus (2.0.0) axiom-types (~> 0.1) coercible (~> 1.0) @@ -610,13 +618,13 @@ GEM bindex (>= 0.4.0) railties (>= 6.0.0) websocket (1.2.11) - websocket-driver (0.7.7) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS aarch64-linux @@ -654,11 +662,11 @@ DEPENDENCIES database_cleaner devise (>= 4.8.1) dotenv - factory_bot_rails + factory_bot_rails (>= 6.5.0) faker fog-aws (>= 3.15.0) image_processing (~> 1.12) - importmap-rails (>= 2.0.0) + importmap-rails (>= 2.2.0) jbuilder jquery-rails json-jwt @@ -684,7 +692,7 @@ DEPENDENCIES redis-client redis-namespace rolify - rspec-rails + rspec-rails (>= 8.0.1) rspec_junit_formatter rubocop-rails rubocop-rspec @@ -694,7 +702,7 @@ DEPENDENCIES simplecov sprockets-rails stimulus-rails - turbo-rails + turbo-rails (>= 2.0.14) tzinfo-data web-console diff --git a/app/controllers/admin/cx_collection_details_controller.rb b/app/controllers/admin/cx_collection_details_controller.rb index 7ab54b0fe..54b2b399b 100644 --- a/app/controllers/admin/cx_collection_details_controller.rb +++ b/app/controllers/admin/cx_collection_details_controller.rb @@ -18,6 +18,18 @@ def show def new @cx_collection_detail = CxCollectionDetail.new @cx_collection_detail.cx_collection_id = params[:collection_id] + + if params[:form_id] + @form = Form.find_by_short_uuid(params[:form_id]) + @cx_collection_detail.form = @form + @cx_collection_detail.service_stage_id = @form.service_stage_id + @cx_collection_detail.transaction_point = 'post_interaction' + @cx_collection_detail.survey_type = 'thumbs_up_down' if @form.kind == "a11_v2" + @cx_collection_detail.survey_title = @form.title + @cx_collection_detail.omb_control_number = @form.omb_approval_number + @cx_collection_detail.trust_question_text = @form.questions.first.text + @cx_collection_detail.volume_of_customers_provided_survey_opportunity = @form.survey_form_activations + end end def edit @@ -35,6 +47,11 @@ def create respond_to do |format| if @cx_collection_detail.save Event.log_event(Event.names[:cx_collection_detail_created], @cx_collection_detail.class.to_s, @cx_collection_detail.id, "CX Collection Detail #{@cx_collection_detail.id} created at #{DateTime.now}", current_user.id) + + if @cx_collection_detail.form + CxCollectionDetailUpload.create!(user: current_user, cx_collection_detail: @cx_collection_detail) + end + format.html { redirect_to upload_admin_cx_collection_detail_url(@cx_collection_detail), notice: "CX Collection Detail was successfully created." } format.json { render :upload, status: :created, location: @cx_collection_detail } else @@ -160,6 +177,22 @@ def set_cx_collections end def cx_collection_detail_params - params.require(:cx_collection_detail).permit(:cx_collection_id, :transaction_point, :channel, :service_stage_id, :volume_of_customers, :volume_of_customers_provided_survey_opportunity, :volume_of_respondents, :omb_control_number, :federal_register_url, :reflection_text, :survey_type, :survey_title, :trust_question_text) + params.require(:cx_collection_detail) + .permit( + :cx_collection_id, + :transaction_point, + :channel, + :service_stage_id, + :volume_of_customers, + :volume_of_customers_provided_survey_opportunity, + :volume_of_respondents, + :omb_control_number, + :federal_register_url, + :reflection_text, + :survey_type, + :survey_title, + :trust_question_text, + :form_id, + ) end end diff --git a/app/controllers/admin/cx_collections_controller.rb b/app/controllers/admin/cx_collections_controller.rb index 2c104ad1c..22f41a9c4 100644 --- a/app/controllers/admin/cx_collections_controller.rb +++ b/app/controllers/admin/cx_collections_controller.rb @@ -26,6 +26,8 @@ def show def new @cx_collection = CxCollection.new + @cx_collection.quarter = FiscalYear.fiscal_year_and_quarter(Date.today)[:quarter] + @cx_collection.fiscal_year = FiscalYear.fiscal_year_and_quarter(Date.today)[:year] end def edit diff --git a/app/controllers/admin/questions_controller.rb b/app/controllers/admin/questions_controller.rb index 5ed344ef6..3c938804c 100644 --- a/app/controllers/admin/questions_controller.rb +++ b/app/controllers/admin/questions_controller.rb @@ -101,7 +101,7 @@ def question_params def first_unused_answer_field answer_fields = Question.where(form_id: @form.id).collect(&:answer_field) - (1..20).each do |ind| + (1..30).each do |ind| af = "answer_#{ind.to_s.rjust(2, '0')}" return af unless answer_fields.include?(af) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f7e22540a..be75124e6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -167,7 +167,7 @@ def cx_collection_permissions?(cx_collection:) return true if performance_manager_permissions? cx_collection.organization == current_user.organization || - cx_collection.organization == (current_user.organization.parent ? current_user.organization.parent : nil) + cx_collection.organization == (current_user.organization.parent || nil) end helper_method :website_permissions? diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 3a6cdfd61..c24c9ef80 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -26,7 +26,7 @@ def create # Catch SPAMMERS if @form && submission_params[:fba_directive].present? - ActiveSupport::Notifications.instrument("spam_subverted") do |payload| + ActiveSupport::Notifications.instrument('spam_subverted') do |payload| payload[:request] = request end @@ -82,10 +82,10 @@ def create def create_in_local_database(submission) if submission.form.enable_turnstile? - if verify_turnstile(params["cf-turnstile-response"]) + if verify_turnstile(params['cf-turnstile-response']) submission.spam_prevention_mechanism = :turnstile else - submission.errors.add(:base, "Turnstile verification failed") + submission.errors.add(:base, 'Turnstile verification failed') end end @@ -96,9 +96,11 @@ def create_in_local_database(submission) notice: 'Thank You. Response was submitted successfully.' end format.json do - form_success_text = submission.form.append_id_to_success_text? ? - submission.form.success_text + "

Your Response ID is: #{submission.uuid[-12..-1]}" : - submission.form.success_text + form_success_text = if submission.form.append_id_to_success_text? + submission.form.success_text + "

Your Response ID is: #{submission.uuid[-12..-1]}" + else + submission.form.success_text + end render json: { submission: { @@ -123,6 +125,16 @@ def create_in_local_database(submission) answer_18: submission.answer_18, answer_19: submission.answer_19, answer_20: submission.answer_20, + answer_21: submission.answer_21, + answer_22: submission.answer_22, + answer_23: submission.answer_23, + answer_24: submission.answer_24, + answer_25: submission.answer_25, + answer_26: submission.answer_26, + answer_27: submission.answer_27, + answer_28: submission.answer_28, + answer_29: submission.answer_29, + answer_30: submission.answer_30, form: { id: submission.form.uuid, name: submission.form.name, @@ -173,20 +185,19 @@ def form_requires_verification @form.verify_csrf? end - private def verify_turnstile(response_token) - secret_key = ENV.fetch("TURNSTILE_SECRET_KEY", nil) - uri = URI("https://challenges.cloudflare.com/turnstile/v0/siteverify") + secret_key = ENV.fetch('TURNSTILE_SECRET_KEY', nil) + uri = URI('https://challenges.cloudflare.com/turnstile/v0/siteverify') response = Net::HTTP.post_form(uri, { - "secret" => secret_key, - "response" => response_token, - "remoteip" => request.remote_ip - }) + 'secret' => secret_key, + 'response' => response_token, + 'remoteip' => request.remote_ip, + }) json = JSON.parse(response.body) - json["success"] == true + json['success'] == true end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 368027753..b8e11ece5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,7 +4,7 @@ module ApplicationHelper def suppress_main_layout_flash? - return true if flash && ['User successfully added', 'User successfully removed'].include?(flash.notice) + true if flash && ['User successfully added', 'User successfully removed'].include?(flash.notice) end def to_markdown(text) @@ -23,11 +23,11 @@ def organization_abbreviation_dropdown_options def us_timezones [ - ActiveSupport::TimeZone["Eastern Time (US & Canada)"], - ActiveSupport::TimeZone["Central Time (US & Canada)"], - ActiveSupport::TimeZone["Mountain Time (US & Canada)"], - ActiveSupport::TimeZone["Pacific Time (US & Canada)"], - ActiveSupport::TimeZone["Hawaii"] + ActiveSupport::TimeZone['Eastern Time (US & Canada)'], + ActiveSupport::TimeZone['Central Time (US & Canada)'], + ActiveSupport::TimeZone['Mountain Time (US & Canada)'], + ActiveSupport::TimeZone['Pacific Time (US & Canada)'], + ActiveSupport::TimeZone['Hawaii'], ] end @@ -101,37 +101,31 @@ def website_status_label_tags(status) end def cx_collections_filters_applied?(quarter, year, status) - [quarter, year, status].any? { |param| param.present? && param.downcase != "all" } + [quarter, year, status].any? { |param| param.present? && param.downcase != 'all' } end def cx_collections_filter_message(quarter, year, status) parts = [] - if quarter.present? && quarter.downcase != "all" - parts << "Q#{quarter}" - end + parts << "Q#{quarter}" if quarter.present? && quarter.downcase != 'all' - if year.present? && year.downcase != "all" - parts << "FY#{year}" - end + parts << "FY#{year}" if year.present? && year.downcase != 'all' - if status.present? && status.downcase != "all" - parts << status - end + parts << status if status.present? && status.downcase != 'all' - return "" if parts.empty? + return '' if parts.empty? - "for " + parts.join(" ") + 'for ' + parts.join(' ') end def form_edit_component_path(question_type) - case question_type - when "radio_buttons", "combobox" - "components/forms/edit/question_types/radio_button_option" - when "dropdown" - "components/forms/edit/question_types/dropdown_option" - when "checkbox" - "components/forms/edit/question_types/checkbox_option" + case question_type + when 'radio_buttons', 'combobox' + 'components/forms/edit/question_types/radio_button_option' + when 'dropdown' + 'components/forms/edit/question_types/dropdown_option' + when 'checkbox' + 'components/forms/edit/question_types/checkbox_option' end end @@ -174,7 +168,7 @@ def question_type_javascript_params(question) def is_at_least_form_manager?(user:, form:) user.admin? || - user.organizational_form_approver && user.organization_id == form.organization_id || + (user.organizational_form_approver && user.organization_id == form.organization_id) || form.user_role?(user:) == UserRole::Role::FormManager end @@ -204,6 +198,16 @@ def answer_fields answer_18 answer_19 answer_20 + answer_21 + answer_22 + answer_23 + answer_24 + answer_25 + answer_26 + answer_27 + answer_28 + answer_29 + answer_30 ] end @@ -222,11 +226,11 @@ def format_submission_time(datetime, time_zone) start_of_year = today.beginning_of_year if created_at >= today - created_at.strftime("%-I:%M %p") # Today: 1:23 PM + created_at.strftime('%-I:%M %p') # Today: 1:23 PM elsif created_at >= start_of_year - created_at.strftime("%b %e") # Current year: Jan 5 + created_at.strftime('%b %e') # Current year: Jan 5 else - created_at.strftime("%m/%d/%Y") # Last year: 01/05/2024 + created_at.strftime('%m/%d/%Y') # Last year: 01/05/2024 end end @@ -240,7 +244,5 @@ def form_integrity_checksum(form:) Digest::SHA256.base64digest(data_to_encode) end - def fiscal_year_and_quarter(date) - FiscalYear.fiscal_year_and_quarter(date) - end + delegate :fiscal_year_and_quarter, to: :FiscalYear end diff --git a/app/models/cx_collection_detail.rb b/app/models/cx_collection_detail.rb index 06470351f..9e5541203 100644 --- a/app/models/cx_collection_detail.rb +++ b/app/models/cx_collection_detail.rb @@ -3,6 +3,7 @@ class CxCollectionDetail < ApplicationRecord belongs_to :cx_collection belongs_to :service_stage, optional: true + belongs_to :form, optional: true has_many :cx_responses, dependent: :delete_all has_many :cx_collection_detail_uploads has_one :service_provider, through: :cx_collection diff --git a/app/models/cx_collection_detail_upload.rb b/app/models/cx_collection_detail_upload.rb index db825fb80..e972a089f 100644 --- a/app/models/cx_collection_detail_upload.rb +++ b/app/models/cx_collection_detail_upload.rb @@ -6,7 +6,7 @@ class CxCollectionDetailUpload < ApplicationRecord belongs_to :cx_collection_detail has_many :cx_responses, dependent: :delete_all - after_create :process_csv_in_a_worker + after_create :process_records_in_a_worker aasm do state :created, initial: true @@ -24,8 +24,15 @@ class CxCollectionDetailUpload < ApplicationRecord end end - def process_csv_in_a_worker - process_csv + def process_records_in_a_worker + if self.key? + process_csv + elsif self.cx_collection_detail.form + fiscal_quarter_dates = FiscalYear.fiscal_quarter_dates(self.cx_collection_detail.cx_collection.fiscal_year, self.cx_collection_detail.cx_collection.quarter) + start_date = fiscal_quarter_dates[:start_date] + end_date = fiscal_quarter_dates[:end_date] + upload_form_results(form_id: self.cx_collection_detail.form_id, start_date:, end_date:) + end end def process_csv @@ -73,4 +80,39 @@ def process_csv end end + def upload_form_results(form_id:, start_date:, end_date:) + @form = Form.find(form_id) + + job_id = SecureRandom.hex[0..9] + update_attribute(:job_id, job_id) + + responses = @form.to_a11_v2_array(start_date:, end_date:) + responses.each do |response| + # Create the CxResponse record + CxResponse.create!({ + cx_collection_detail_id: cx_collection_detail.id, + cx_collection_detail_upload_id: self.id, + job_id: job_id, + external_id: response[:id], + question_1: response[:answer_01], + positive_effectiveness: response[:answer_02_effectiveness], + positive_ease: response[:answer_02_ease], + positive_efficiency: response[:answer_02_efficiency], + positive_transparency: response[:answer_02_transparency], + positive_humanity: response[:answer_02_humanity], + positive_employee: response[:answer_02_employee], + positive_other: response[:answer_02_other], + negative_effectiveness: response[:answer_03_effectiveness], + negative_ease: response[:answer_03_ease], + negative_efficiency: response[:answer_03_efficiency], + negative_transparency: response[:answer_03_transparency], + negative_humanity: response[:answer_03_humanity], + negative_employee: response[:answer_03_employee], + negative_other: response[:answer_03_other], + question_4: response[:answer_04], + }) + end + + end + end diff --git a/app/models/form.rb b/app/models/form.rb index c9bbe5d03..dc1af4ae7 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -50,36 +50,34 @@ def self.filtered_forms(user, aasm_state) end items = items.non_templates - items = items.where(aasm_state: aasm_state) if aasm_state.present? && aasm_state != "all" + items = items.where(aasm_state: aasm_state) if aasm_state.present? && aasm_state != 'all' items end def self.kinds [ - "a11", - "a11_v2", # launched Fall 2023 - "a11_v2_radio", # launched May 2025 - "a11_yes_no", - "open_ended", - "other", # TODO: deprecate in favor of custom, - "recruiter", - "yes_no", - "custom" + 'a11', + 'a11_v2', # launched Fall 2023 + 'a11_v2_radio', # launched May 2025 + 'a11_yes_no', + 'open_ended', + 'other', # TODO: deprecate in favor of custom, + 'recruiter', + 'yes_no', + 'custom', ] end def valid_form_kinds - if !Form.kinds.include?(kind) - errors.add(:kind, "kind must be one of the following: #{Form.kinds.sort.join(', ')}") - end + errors.add(:kind, "kind must be one of the following: #{Form.kinds.sort.join(', ')}") unless Form.kinds.include?(kind) end def target_for_delivery_method - errors.add(:element_selector, "can't be blank for an inline form") if (delivery_method == 'custom-button-modal' || delivery_method == 'inline') && (element_selector == '') + errors.add(:element_selector, "can't be blank for an inline form") if %w[custom-button-modal inline].include?(delivery_method) && (element_selector == '') end def roles - user_roles.map { |role| { role: role.role, user: role.user }} + user_roles.map { |role| { role: role.role, user: role.user } } end def form_managers @@ -87,7 +85,7 @@ def form_managers end def response_viewers - roles.select { |role| role[:role] == 'response_viewer' }.map { |r| r[:user]} + roles.select { |role| role[:role] == 'response_viewer' }.map { |r| r[:user] } end def ensure_modal_text @@ -154,18 +152,18 @@ def create_first_form_section # used to initially set tags (or reset them, if necessary) def set_submission_tags! submission_tags = submissions.collect(&:tags).uniq.sort_by { |i| i.name } - self.update!(submission_tags: submission_tags) + update!(submission_tags: submission_tags) end # called when a tag is added to a submission def update_submission_tags!(tag_list) submission_tags = (self.submission_tags + tag_list).uniq.compact.sort - self.update!(submission_tags: submission_tags) + update!(submission_tags: submission_tags) end # lazily called from a view when a tag is used to search, but returns 0 results def remove_submission_tag!(tag) - self.update!(submission_tags: submission_tags - [tag]) + update!(submission_tags: submission_tags - [tag]) end aasm do @@ -177,15 +175,15 @@ def remove_submission_tag!(tag) event :submit do transitions from: %i[created], - to: :submitted, - guard: :organization_has_form_approval_enabled?, - after: :set_submitted_at + to: :submitted, + guard: :organization_has_form_approval_enabled?, + after: :set_submitted_at end event :approve do transitions from: %i[submitted], - to: :approved, - guard: :organization_has_form_approval_enabled?, - after: :set_approved_at + to: :approved, + guard: :organization_has_form_approval_enabled?, + after: :set_approved_at end event :publish do transitions from: %i[created approved], to: :published @@ -208,7 +206,7 @@ def all_states end def events - Event.where(object_type: 'Form', object_uuid: self.uuid).order(:created_at) + Event.where(object_type: 'Form', object_uuid: uuid).order(:created_at) end def duplicate!(new_user:) @@ -266,7 +264,7 @@ def check_expired end def has_rich_text_questions? - questions.where(question_type: "rich_textarea").exists? + questions.where(question_type: 'rich_textarea').exists? end def self.archive_expired! @@ -290,7 +288,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.non_templates.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? @@ -336,56 +334,112 @@ def to_combined_a11_v2_csv(start_date: nil, end_date: nil) attributes = fields_for_export header_attributes = hashed_fields_for_export.values - a11_v2_header_attributes = [ - :external_id, - :question_1, - :positive_effectiveness, - :positive_ease, - :positive_efficiency, - :positive_transparency, - :positive_humanity, - :positive_employee, - :positive_other, - :negative_effectiveness, - :negative_ease, - :negative_efficiency, - :negative_transparency, - :negative_humanity, - :negative_employee, - :negative_other, - :question_4 + a11_v2_header_attributes = %i[ + external_id + question_1 + positive_effectiveness + positive_ease + positive_efficiency + positive_transparency + positive_humanity + positive_employee + positive_other + negative_effectiveness + negative_ease + negative_efficiency + negative_transparency + negative_humanity + negative_employee + negative_other + question_4 ] attributes = fields_for_export - answer_02_options = self.questions.where(answer_field: "answer_02").first.question_options.collect(&:value) - answer_03_options = self.questions.where(answer_field: "answer_03").first.question_options.collect(&:value) + answer_02_options = questions.where(answer_field: 'answer_02').first.question_options.collect(&:value) + answer_03_options = questions.where(answer_field: 'answer_03').first.question_options.collect(&:value) CSV.generate(headers: true) do |csv| - csv << header_attributes + a11_v2_header_attributes + csv << (header_attributes + a11_v2_header_attributes) reportable_submissions.each do |submission| - csv << attributes.map { |attr| submission.send(attr) } + [ + csv << (attributes.map { |attr| submission.send(attr) } + [ submission.id, submission.answer_01, - submission.answer_02 && submission.answer_02.split(",").include?("effectiveness") ? 1 :(answer_02_options.include?("effectiveness") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("ease") ? 1 : (answer_02_options.include?("ease") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("efficiency") ? 1 : (answer_02_options.include?("efficiency") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("transparency") ? 1 : (answer_02_options.include?("transparency") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("humanity") ? 1 : (answer_02_options.include?("humanity") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("employee") ? 1 : (answer_02_options.include?("employee") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("other") ? 1 : (answer_02_options.include?("other") ? 0 : 'null'), - - submission.answer_03 && submission.answer_03.split(",").include?("effectiveness") ? 1 : (answer_03_options.include?("effectiveness") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("ease") ? 1 : (answer_03_options.include?("ease") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("efficiency") ? 1 : (answer_03_options.include?("efficiency") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("transparency") ? 1 : (answer_03_options.include?("transparency") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("humanity") ? 1 : (answer_03_options.include?("humanity") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("employee") ? 1 : (answer_03_options.include?("employee") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("other") ? 1 : (answer_03_options.include?("other") ? 0 : 'null'), - - submission.answer_04 - ] + if submission.answer_02 && submission.answer_02.split(',').include?('effectiveness') + 1 + else + (answer_02_options.include?('effectiveness') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('ease') + 1 + else + (answer_02_options.include?('ease') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('efficiency') + 1 + else + (answer_02_options.include?('efficiency') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('transparency') + 1 + else + (answer_02_options.include?('transparency') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('humanity') + 1 + else + (answer_02_options.include?('humanity') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('employee') + 1 + else + (answer_02_options.include?('employee') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('other') + 1 + else + (answer_02_options.include?('other') ? 0 : 'null') + end, + + if submission.answer_03 && submission.answer_03.split(',').include?('effectiveness') + 1 + else + (answer_03_options.include?('effectiveness') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('ease') + 1 + else + (answer_03_options.include?('ease') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('efficiency') + 1 + else + (answer_03_options.include?('efficiency') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('transparency') + 1 + else + (answer_03_options.include?('transparency') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('humanity') + 1 + else + (answer_03_options.include?('humanity') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('employee') + 1 + else + (answer_03_options.include?('employee') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('other') + 1 + else + (answer_03_options.include?('other') ? 0 : 'null') + end, + + submission.answer_04, + ]) end end end @@ -399,30 +453,30 @@ def to_a11_v2_csv(start_date: nil, end_date: nil) return nil if reportable_submissions.blank? header_attributes = hashed_fields_for_export.values - header_attributes = [ - :external_id, - :question_1, - :positive_effectiveness, - :positive_ease, - :positive_efficiency, - :positive_transparency, - :positive_humanity, - :positive_employee, - :positive_other, - :negative_effectiveness, - :negative_ease, - :negative_efficiency, - :negative_transparency, - :negative_humanity, - :negative_employee, - :negative_other, - :question_4 + header_attributes = %i[ + external_id + question_1 + positive_effectiveness + positive_ease + positive_efficiency + positive_transparency + positive_humanity + positive_employee + positive_other + negative_effectiveness + negative_ease + negative_efficiency + negative_transparency + negative_humanity + negative_employee + negative_other + question_4 ] attributes = fields_for_export - answer_02_options = self.questions.where(answer_field: "answer_02").first.question_options.collect(&:value) - answer_03_options = self.questions.where(answer_field: "answer_03").first.question_options.collect(&:value) + answer_02_options = questions.where(answer_field: 'answer_02').first.question_options.collect(&:value) + answer_03_options = questions.where(answer_field: 'answer_03').first.question_options.collect(&:value) CSV.generate(headers: true) do |csv| csv << header_attributes @@ -431,28 +485,117 @@ def to_a11_v2_csv(start_date: nil, end_date: nil) csv << [ submission.id, submission.answer_01, - submission.answer_02 && submission.answer_02.split(",").include?("effectiveness") ? 1 :(answer_02_options.include?("effectiveness") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("ease") ? 1 : (answer_02_options.include?("ease") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("efficiency") ? 1 : (answer_02_options.include?("efficiency") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("transparency") ? 1 : (answer_02_options.include?("transparency") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("humanity") ? 1 : (answer_02_options.include?("humanity") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("employee") ? 1 : (answer_02_options.include?("employee") ? 0 : 'null'), - submission.answer_02 && submission.answer_02.split(",").include?("other") ? 1 : (answer_02_options.include?("other") ? 0 : 'null'), - - submission.answer_03 && submission.answer_03.split(",").include?("effectiveness") ? 1 : (answer_03_options.include?("effectiveness") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("ease") ? 1 : (answer_03_options.include?("ease") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("efficiency") ? 1 : (answer_03_options.include?("efficiency") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("transparency") ? 1 : (answer_03_options.include?("transparency") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("humanity") ? 1 : (answer_03_options.include?("humanity") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("employee") ? 1 : (answer_03_options.include?("employee") ? 0 : 'null'), - submission.answer_03 && submission.answer_03.split(",").include?("other") ? 1 : (answer_03_options.include?("other") ? 0 : 'null'), - - submission.answer_04 + if submission.answer_02 && submission.answer_02.split(',').include?('effectiveness') + 1 + else + (answer_02_options.include?('effectiveness') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('ease') + 1 + else + (answer_02_options.include?('ease') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('efficiency') + 1 + else + (answer_02_options.include?('efficiency') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('transparency') + 1 + else + (answer_02_options.include?('transparency') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('humanity') + 1 + else + (answer_02_options.include?('humanity') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('employee') + 1 + else + (answer_02_options.include?('employee') ? 0 : 'null') + end, + if submission.answer_02 && submission.answer_02.split(',').include?('other') + 1 + else + (answer_02_options.include?('other') ? 0 : 'null') + end, + + if submission.answer_03 && submission.answer_03.split(',').include?('effectiveness') + 1 + else + (answer_03_options.include?('effectiveness') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('ease') + 1 + else + (answer_03_options.include?('ease') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('efficiency') + 1 + else + (answer_03_options.include?('efficiency') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('transparency') + 1 + else + (answer_03_options.include?('transparency') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('humanity') + 1 + else + (answer_03_options.include?('humanity') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('employee') + 1 + else + (answer_03_options.include?('employee') ? 0 : 'null') + end, + if submission.answer_03 && submission.answer_03.split(',').include?('other') + 1 + else + (answer_03_options.include?('other') ? 0 : 'null') + end, + + submission.answer_04, ] end end end + def to_a11_v2_array(start_date: nil, end_date: nil) + non_flagged_submissions = submissions + .non_flagged + .where(created_at: start_date..end_date) + .order('created_at') + return nil if non_flagged_submissions.blank? + + answer_02_options = self.questions.where(answer_field: "answer_02").first.question_options.collect(&:value) + answer_03_options = self.questions.where(answer_field: "answer_03").first.question_options.collect(&:value) + + non_flagged_submissions.map do |submission| + { + id: submission.id, + answer_01: submission.answer_01, + answer_02_effectiveness: submission.answer_02 && submission.answer_02.split(",").include?("effectiveness") ? 1 :(answer_02_options.include?("effectiveness") ? 0 : 'null'), + answer_02_ease: submission.answer_02 && submission.answer_02.split(",").include?("ease") ? 1 : (answer_02_options.include?("ease") ? 0 : 'null'), + answer_02_efficiency: submission.answer_02 && submission.answer_02.split(",").include?("efficiency") ? 1 : (answer_02_options.include?("efficiency") ? 0 : 'null'), + answer_02_transparency: submission.answer_02 && submission.answer_02.split(",").include?("transparency") ? 1 : (answer_02_options.include?("transparency") ? 0 : 'null'), + answer_02_humanity: submission.answer_02 && submission.answer_02.split(",").include?("humanity") ? 1 : (answer_02_options.include?("humanity") ? 0 : 'null'), + answer_02_employee: submission.answer_02 && submission.answer_02.split(",").include?("employee") ? 1 : (answer_02_options.include?("employee") ? 0 : 'null'), + answer_02_other: submission.answer_02 && submission.answer_02.split(",").include?("other") ? 1 : (answer_02_options.include?("other") ? 0 : 'null'), + answer_03_effectiveness: submission.answer_03 && submission.answer_03.split(",").include?("effectiveness") ? 1 : (answer_03_options.include?("effectiveness") ? 0 : 'null'), + answer_03_ease: submission.answer_03 && submission.answer_03.split(",").include?("ease") ? 1 : (answer_03_options.include?("ease") ? 0 : 'null'), + answer_03_efficiency: submission.answer_03 && submission.answer_03.split(",").include?("efficiency") ? 1 : (answer_03_options.include?("efficiency") ? 0 : 'null'), + answer_03_transparency: submission.answer_03 && submission.answer_03.split(",").include?("transparency") ? 1 : (answer_03_options.include?("transparency") ? 0 : 'null'), + answer_03_humanity: submission.answer_03 && submission.answer_03.split(",").include?("humanity") ? 1 : (answer_03_options.include?("humanity") ? 0 : 'null'), + answer_03_employee: submission.answer_03 && submission.answer_03.split(",").include?("employee") ? 1 : (answer_03_options.include?("employee") ? 0 : 'null'), + answer_03_other: submission.answer_03 && submission.answer_03.split(",").include?("other") ? 1 : (answer_03_options.include?("other") ? 0 : 'null'), + answer_04: submission.answer_04, + } + end + end + def user_role?(user:) role = user_roles.find_by_user_id(user.id) role.present? ? role.role : nil @@ -582,12 +725,14 @@ def to_a11_submissions_csv(start_date:, end_date:) when :answer_06 question = questions.where(answer_field: key).first next unless question.present? + response_volume = values.values.collect(&:to_i).sum @question_text = question.text standardized_question_number = 6 when :answer_07 question = questions.where(answer_field: key).first next unless question.present? + response_volume = values.values.collect(&:to_i).sum @question_text = question.text standardized_question_number = 7 @@ -621,36 +766,36 @@ def fields_for_export def hashed_fields_for_export ordered_hash = ActiveSupport::OrderedHash.new ordered_hash.merge!({ - id: 'ID', - uuid: 'UUID', - }) + id: 'ID', + uuid: 'UUID', + }) ordered_questions.map { |q| ordered_hash[q.answer_field] = q.text } ordered_hash.merge!({ - location_code: 'Location Code', - user_agent: 'User Agent', - aasm_state: 'Status', - archived: 'Archived', - flagged: 'Flagged', - deleted: 'Deleted', - deleted_at: 'Deleted at', - page: 'Page', - query_string: 'Query string', - hostname: 'Hostname', - referer: 'Referrer', - created_at: 'Created at', - }) + location_code: 'Location Code', + user_agent: 'User Agent', + aasm_state: 'Status', + archived: 'Archived', + flagged: 'Flagged', + deleted: 'Deleted', + deleted_at: 'Deleted at', + page: 'Page', + query_string: 'Query string', + hostname: 'Hostname', + referer: 'Referrer', + created_at: 'Created at', + }) if organization.enable_ip_address? ordered_hash.merge!({ - ip_address: 'IP Address', - }) + ip_address: 'IP Address', + }) end ordered_hash.merge!({ - tags: 'Tags', - }) + tags: 'Tags', + }) ordered_hash end @@ -664,7 +809,7 @@ def ordered_questions end def rendered_questions - ordered_questions.select { |q| q.text.include?("email") || q.text.include?("name") } + ordered_questions.select { |q| q.text.include?('email') || q.text.include?('name') } end def omb_number_with_expiration_date @@ -701,78 +846,62 @@ def organization_has_form_approval_enabled? # use this validator to provide soft UI guidance, rather than strong model validation def ensure_a11_v2_format # ensure `answer_01` is a big thumbs question - question_1 = self.ordered_questions.find { |q| q.answer_field == "answer_01" } - if question_1.question_type != 'big_thumbs_up_down_buttons' - errors.add(:base, "The question for `answer_01` must be a \"Big Thumbs Up/Down\" component") - end + question_1 = ordered_questions.find { |q| q.answer_field == 'answer_01' } + errors.add(:base, 'The question for `answer_01` must be a "Big Thumbs Up/Down" component') if question_1.question_type != 'big_thumbs_up_down_buttons' # 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 + required_elements = %w[answer_01 answer_02 answer_03 answer_04] + errors.add(:base, "The A-11 v2 form must have questions for #{required_elements.to_sentence}") unless contains_elements?(questions.collect(&:answer_field), required_elements) # ensure the positive indicators include ease and effectiveness - question_2 = self.ordered_questions.find { |q| q.answer_field == "answer_02" } + question_2 = 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"] + required_options = %w[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 + errors.add(:base, "The question options for Question 2 must include: #{missing_options.join(', ')}") if missing_options.any? # ensure the positive indicators include ease and effectiveness - question_3 = self.ordered_questions.find { |q| q.answer_field == "answer_03" } + question_3 = 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"] + required_options = %w[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 + errors.add(:base, "The question options for Question 3 must include: #{missing_options.join(', ')}") if missing_options.any? 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 + question_1 = ordered_questions.find { |q| q.answer_field == 'answer_01' } + errors.add(:base, 'The question for `answer_01` must be a Radio Buttons component with 5 options, with values 1-5') if question_1.question_type != 'radio_buttons' # 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 + required_elements = %w[answer_01 answer_02 answer_03 answer_04] + errors.add(:base, "The A-11 v2 form must have questions for #{required_elements.to_sentence}") unless contains_elements?(questions.collect(&:answer_field), required_elements) # ensure the positive indicators include ease and effectiveness - question_2 = self.ordered_questions.find { |q| q.answer_field == "answer_02" } + question_2 = 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"] + required_options = %w[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 + errors.add(:base, "The question options for Question 2 must include: #{missing_options.join(', ')}") if missing_options.any? # ensure the positive indicators include ease and effectiveness - question_3 = self.ordered_questions.find { |q| q.answer_field == "answer_03" } + question_3 = 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"] + required_options = %w[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 + errors.add(:base, "The question options for Question 3 must include: #{missing_options.join(', ')}") if missing_options.any? 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.") + if questions.size >= 30 + errors.add(:base, "Touchpoints supports a maximum of 30 questions. There are currently #{questions_count} questions. Fewer questions tend to yield higher response rates.") end end @@ -784,7 +913,7 @@ def self.forms_whose_retention_period_has_passed cutoff_year = FiscalYear.last_fiscal_year - 3 cutoff_date = FiscalYear.last_day_of_fiscal_quarter(cutoff_year, 4) Form.where("aasm_state = 'archived'") - .where("archived_at < ?", cutoff_date) + .where('archived_at < ?', cutoff_date) end private @@ -795,14 +924,14 @@ def set_uuid end def set_submitted_at - self.update(submitted_at: Time.current) + update(submitted_at: Time.current) end def set_approved_at - self.update(approved_at: Time.current) + update(approved_at: Time.current) end def set_archived_at - self.update(archived_at: Time.current) + update(archived_at: Time.current) end end diff --git a/app/serializers/submission_serializer.rb b/app/serializers/submission_serializer.rb index d90ef81e5..3d89cb07e 100644 --- a/app/serializers/submission_serializer.rb +++ b/app/serializers/submission_serializer.rb @@ -30,6 +30,16 @@ class SubmissionSerializer < ActiveModel::Serializer :answer_18, :answer_19, :answer_20, + :answer_21, + :answer_22, + :answer_23, + :answer_24, + :answer_25, + :answer_26, + :answer_27, + :answer_28, + :answer_29, + :answer_30, :ip_address, :location_code, :flagged, diff --git a/app/views/admin/cx_collection_details/_form.html.erb b/app/views/admin/cx_collection_details/_form.html.erb index 7cd41dff1..792742210 100644 --- a/app/views/admin/cx_collection_details/_form.html.erb +++ b/app/views/admin/cx_collection_details/_form.html.erb @@ -132,6 +132,17 @@
+ <% if @cx_collection_detail.form %> + <%= form.hidden_field :form_id, value: @cx_collection_detail.form_id %> +
+
+

+ CxResponses will be created for the form titled "<%= @cx_collection_detail.form.title %>" + for Q<%= @cx_collection_detail.cx_collection.quarter %>FY<%= @cx_collection_detail.cx_collection.fiscal_year %>. +

+
+
+ <% else %>

@@ -140,6 +151,7 @@

+ <% end %>

<%= form.submit class: "usa-button" %> diff --git a/app/views/admin/cx_collection_details/upload.html.erb b/app/views/admin/cx_collection_details/upload.html.erb index 30eb620ae..12694fed6 100644 --- a/app/views/admin/cx_collection_details/upload.html.erb +++ b/app/views/admin/cx_collection_details/upload.html.erb @@ -61,7 +61,7 @@ <% end %> - +
@@ -70,9 +70,9 @@ <%- if service_manager_permissions? %> - - - + + + <% end %> @@ -87,7 +87,7 @@ <%= upload.user.email %>
UserTimestamp Uploaded record countJob IDProcess file?Delete?
- <%= link_to "Uploaded file", s3_presigned_url(upload.key) %> + <%= link_to "Uploaded file", s3_presigned_url(upload.key) if upload.key.present? %> <%= upload.size %> diff --git a/app/views/admin/cx_collections/_form.html.erb b/app/views/admin/cx_collections/_form.html.erb index 7491e10a9..ab045f5b8 100644 --- a/app/views/admin/cx_collections/_form.html.erb +++ b/app/views/admin/cx_collections/_form.html.erb @@ -74,7 +74,7 @@

- After creating this collection, you can add survey results in the following screen. + After creating this collection, you can add survey results on the following screen.

diff --git a/app/views/admin/cx_collections/show.html.erb b/app/views/admin/cx_collections/show.html.erb index 8ae93a66e..953a8deb8 100644 --- a/app/views/admin/cx_collections/show.html.erb +++ b/app/views/admin/cx_collections/show.html.erb @@ -144,12 +144,23 @@
<%- if @cx_collection.draft? %> -

+

<%= link_to new_admin_cx_collection_detail_path(collection_id: @cx_collection.id), class: "usa-button usa-button--outline" do %> Add detail record <% end %> -

+ +
+ <% Form.where(kind: "a11_v2").where(service_id: @cx_collection.service_id).each do |form| %> + <%= link_to new_admin_cx_collection_detail_path(collection_id: @cx_collection.id, form_id: form.to_param), class: "usa-button usa-button--outline" do %> + + Add detail record from A-11 v2 form + <% end %> + <%= form.name %> - + <%= form.title %> + <% end %> +
+
<% end %> diff --git a/app/views/admin/forms/_form_manager_options.html.erb b/app/views/admin/forms/_form_manager_options.html.erb index fc04762ec..f7c49ee9d 100644 --- a/app/views/admin/forms/_form_manager_options.html.erb +++ b/app/views/admin/forms/_form_manager_options.html.erb @@ -92,7 +92,7 @@
YYYY-MM-DD
- <%= f.text_field :expiration_date, class: "usa-input" %> + <%= f.date_field :expiration_date, class: "usa-input" %>
diff --git a/app/views/admin/services/show.html.erb b/app/views/admin/services/show.html.erb index 2a2756f51..e0a0267bb 100644 --- a/app/views/admin/services/show.html.erb +++ b/app/views/admin/services/show.html.erb @@ -284,7 +284,7 @@ CX Data Collections (V2) <%- if @cx_collections.present? %> - +
diff --git a/app/views/components/forms/question_types/_big_thumbs_up_down_buttons.html.erb b/app/views/components/forms/question_types/_big_thumbs_up_down_buttons.html.erb index 9fd61901b..94c8f09fa 100644 --- a/app/views/components/forms/question_types/_big_thumbs_up_down_buttons.html.erb +++ b/app/views/components/forms/question_types/_big_thumbs_up_down_buttons.html.erb @@ -33,13 +33,9 @@ viewBox="0 0 24 24" class="usa-banner__lock-image" role="img" - aria-labelledby="thumbs-up-icon" focusable="false" > Thumbs-up - - Thumbs-up icon - Thumbs-down - - Thumbs-down icon - = 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3405,6 +3435,21 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -3517,14 +3562,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -3539,6 +3581,19 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3808,13 +3863,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/for-in": { @@ -3927,17 +3988,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3946,6 +4012,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4099,13 +4179,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4319,23 +4399,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -4745,13 +4812,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -5058,6 +5125,16 @@ "dev": true, "license": "MIT" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -5537,22 +5614,57 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", "dev": true, "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" }, "engines": { "node": ">=0.12" } }, + "node_modules/pbkdf2/node_modules/create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/pbkdf2/node_modules/hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7023,6 +7135,28 @@ "node": ">=0.6.0" } }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-through": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", @@ -7050,6 +7184,21 @@ "dev": true, "license": "MIT" }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -7506,16 +7655,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { diff --git a/public/api/v0/openapi.yml b/public/api/v0/openapi.yml index 384254345..e50e8c3fa 100644 --- a/public/api/v0/openapi.yml +++ b/public/api/v0/openapi.yml @@ -232,6 +232,26 @@ components: type: string answer_20: type: string + answer_21: + type: string + answer_22: + type: string + answer_23: + type: string + answer_24: + type: string + answer_25: + type: string + answer_26: + type: string + answer_27: + type: string + answer_28: + type: string + answer_29: + type: string + answer_30: + type: string ip_address: type: string location_code: diff --git a/public/api/v1/openapi.yml b/public/api/v1/openapi.yml index 7d92905c8..a77344e64 100644 --- a/public/api/v1/openapi.yml +++ b/public/api/v1/openapi.yml @@ -500,6 +500,26 @@ type: string answer_20: type: string + answer_21: + type: string + answer_22: + type: string + answer_23: + type: string + answer_24: + type: string + answer_25: + type: string + answer_26: + type: string + answer_27: + type: string + answer_28: + type: string + answer_29: + type: string + answer_30: + type: string ip_address: type: string location_code: diff --git a/spec/features/admin/cx_collections_spec.rb b/spec/features/admin/cx_collections_spec.rb index 816ee583b..e59e0f119 100644 --- a/spec/features/admin/cx_collections_spec.rb +++ b/spec/features/admin/cx_collections_spec.rb @@ -98,7 +98,7 @@ let(:current_year) { Time.zone.now.strftime('%Y') } before do - expect(page).to have_content('After creating this collection, you can add survey results in the following screen.') + expect(page).to have_content('After creating this collection, you can add survey results on the following screen.') fill_in("cx_collection_service_provider_id", with: service_provider.name) find("#cx_collection_service_provider_id--list").click fill_in("cx_collection_service_id", with: service.name) diff --git a/spec/features/admin/forms_spec.rb b/spec/features/admin/forms_spec.rb index 592debe8a..68e89c75e 100644 --- a/spec/features/admin/forms_spec.rb +++ b/spec/features/admin/forms_spec.rb @@ -740,7 +740,7 @@ describe 'editing form PRA info' do before do fill_in 'form_omb_approval_number', with: 'OAN-1234' - fill_in 'form_expiration_date', with: '2022-01-30' + fill_in 'form_expiration_date', with: '01/30/2022' click_on 'Update Form Options' expect(page).to have_content('Form Manager forms options updated successfully') end diff --git a/spec/fixtures/form.json b/spec/fixtures/form.json index 94ce1aeba..c71f57452 100644 --- a/spec/fixtures/form.json +++ b/spec/fixtures/form.json @@ -134,6 +134,16 @@ "answer_18": null, "answer_19": null, "answer_20": null, + "answer_21": null, + "answer_22": null, + "answer_23": null, + "answer_24": null, + "answer_25": null, + "answer_26": null, + "answer_27": null, + "answer_28": null, + "answer_29": null, + "answer_30": null, "ip_address": null, "location_code": null, "flagged": false, @@ -170,6 +180,16 @@ "answer_18": null, "answer_19": null, "answer_20": null, + "answer_21": null, + "answer_22": null, + "answer_23": null, + "answer_24": null, + "answer_25": null, + "answer_26": null, + "answer_27": null, + "answer_28": null, + "answer_29": null, + "answer_30": null, "ip_address": null, "location_code": null, "flagged": false, @@ -242,6 +262,16 @@ "answer_18": null, "answer_19": null, "answer_20": null, + "answer_21": null, + "answer_22": null, + "answer_23": null, + "answer_24": null, + "answer_25": null, + "answer_26": null, + "answer_27": null, + "answer_28": null, + "answer_29": null, + "answer_30": null, "ip_address": null, "location_code": null, "flagged": false, @@ -314,6 +344,16 @@ "answer_18": null, "answer_19": null, "answer_20": null, + "answer_21": null, + "answer_22": null, + "answer_23": null, + "answer_24": null, + "answer_25": null, + "answer_26": null, + "answer_27": null, + "answer_28": null, + "answer_29": null, + "answer_30": null, "ip_address": null, "location_code": null, "flagged": false, @@ -386,6 +426,16 @@ "answer_18": null, "answer_19": null, "answer_20": null, + "answer_21": null, + "answer_22": null, + "answer_23": null, + "answer_24": null, + "answer_25": null, + "answer_26": null, + "answer_27": null, + "answer_28": null, + "answer_29": null, + "answer_30": null, "ip_address": null, "location_code": null, "flagged": false, @@ -422,6 +472,16 @@ "answer_18": null, "answer_19": null, "answer_20": null, + "answer_21": null, + "answer_22": null, + "answer_23": null, + "answer_24": null, + "answer_25": null, + "answer_26": null, + "answer_27": null, + "answer_28": null, + "answer_29": null, + "answer_30": null, "ip_address": null, "location_code": null, "flagged": false, diff --git a/spec/lib/fiscal_year_spec.rb b/spec/lib/fiscal_year_spec.rb index d4a189bf1..737cd623c 100644 --- a/spec/lib/fiscal_year_spec.rb +++ b/spec/lib/fiscal_year_spec.rb @@ -3,6 +3,34 @@ describe FiscalYear do include ActiveSupport::Testing::TimeHelpers + describe '#fiscal_quarter_dates' do + context 'when the current date is before October 1st' do + it 'returns the start and end dates for Q2 2025' do + start_and_end_dates = FiscalYear.fiscal_quarter_dates(2025, 2) + expect(start_and_end_dates[:start_date]).to eq(Date.parse("2025-01-01").beginning_of_day) + expect(start_and_end_dates[:end_date]).to eq(Date.parse("2025-03-31").end_of_day) + end + + it 'returns the start and end dates for Q3 2026' do + start_and_end_dates = FiscalYear.fiscal_quarter_dates(2026, 3) + expect(start_and_end_dates[:start_date]).to eq(Date.parse("2026-04-01").beginning_of_day) + expect(start_and_end_dates[:end_date]).to eq(Date.parse("2026-06-30").end_of_day) + end + + it 'returns the start and end dates for Q4 2027' do + start_and_end_dates = FiscalYear.fiscal_quarter_dates(2027, 4) + expect(start_and_end_dates[:start_date]).to eq(Date.parse("2027-07-01").beginning_of_day) + expect(start_and_end_dates[:end_date]).to eq(Date.parse("2027-09-30").end_of_day) + end + + it 'returns the start and end dates for Q1 2028' do + start_and_end_dates = FiscalYear.fiscal_quarter_dates(2028, 1) + expect(start_and_end_dates[:start_date]).to eq(Date.parse("2027-10-01").beginning_of_day) + expect(start_and_end_dates[:end_date]).to eq(Date.parse("2027-12-31").end_of_day) + end + end + end + describe '#fiscal_year_and_quarter' do context 'when the current date is before October 1st' do it 'returns the correct fiscal year and quarter for First Day of the Fiscal Year' do diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index 9a0c42d5c..eae1e7291 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -48,7 +48,7 @@ it "returns a hash of questions, location_code, and 'standard' attributes" do expect(form.hashed_fields_for_export.class).to eq(ActiveSupport::OrderedHash) expect(form.hashed_fields_for_export.keys).to eq([ - :id, + :id, :uuid, # question fields 'answer_01', @@ -72,15 +72,15 @@ :referer, :created_at, :ip_address, - :tags - ]) + :tags, + ]) end end end describe '#kinds' do context 'invalid form kind' do - let!(:form_with_invalid_kind) { FactoryBot.build(:form, organization:, kind: "some_non_valid_kind") } + let!(:form_with_invalid_kind) { FactoryBot.build(:form, organization:, kind: 'some_non_valid_kind') } before do form_with_invalid_kind.save @@ -92,7 +92,7 @@ end context 'valid form kind' do - let!(:form_with_valid_kind) { FactoryBot.build(:form, organization:, kind: "open_ended") } + let!(:form_with_valid_kind) { FactoryBot.build(:form, organization:, kind: 'open_ended') } before do form_with_valid_kind.save @@ -331,4 +331,35 @@ end end end + + describe '#warn_about_not_too_many_questions' do + context 'when form has 20 or fewer questions' do + it 'does not add an error' do + form.warn_about_not_too_many_questions + expect(form.errors[:base]).to be_empty + end + end + + context 'when form has more than 20 questions' do + before do + form_section = form.form_sections.first + + 19.times do |i| + form_section.questions.create!( + form: form, + answer_field: "answer_#{i + 10}", + text: "Question #{i + 10}", + question_type: 'text_field', + position: i + 10, + ) + end + form.reload + end + + it 'adds a warning error to base' do + form.warn_about_not_too_many_questions + expect(form.errors[:base]).to include("Touchpoints supports a maximum of 30 questions. There are currently #{form.questions_count} questions. Fewer questions tend to yield higher response rates.") + end + end + end end