From a777b3a5c9d42672b5fd2e042a074cde9ef03b65 Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Tue, 4 Feb 2025 16:37:33 -0800 Subject: [PATCH 01/25] pre-fill Cx Collection Detail if using an A11 form --- .../admin/cx_collection_details_controller.rb | 11 +++++++++++ .../admin/cx_collections_controller.rb | 2 ++ .../admin/cx_collection_details/upload.html.erb | 8 ++++---- app/views/admin/cx_collections/_form.html.erb | 2 +- app/views/admin/cx_collections/show.html.erb | 15 +++++++++++++-- .../admin/forms/_form_manager_options.html.erb | 2 +- app/views/admin/services/show.html.erb | 2 +- 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/controllers/admin/cx_collection_details_controller.rb b/app/controllers/admin/cx_collection_details_controller.rb index 4a907e2bc..eac771941 100644 --- a/app/controllers/admin/cx_collection_details_controller.rb +++ b/app/controllers/admin/cx_collection_details_controller.rb @@ -18,6 +18,17 @@ 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.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.trust_question_text = @form.activations + end end def edit 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/views/admin/cx_collection_details/upload.html.erb b/app/views/admin/cx_collection_details/upload.html.erb index 30eb620ae..d3f477906 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 %> 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 @@
UserTimestamp Uploaded record countJob IDProcess file?Delete?
<%- 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 db6b91ac0..f09baf2e7 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? %> - +
From 6929f2a8c0552003807a56f09a900e35a9f56673 Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Wed, 5 Feb 2025 16:53:25 -0800 Subject: [PATCH 02/25] return the start and end dates for a fiscal year and quarter --- lib/fiscal_year.rb | 16 ++++++++++++++++ spec/lib/fiscal_year_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/fiscal_year.rb b/lib/fiscal_year.rb index 25bb8f6d5..150633c83 100644 --- a/lib/fiscal_year.rb +++ b/lib/fiscal_year.rb @@ -1,4 +1,20 @@ module FiscalYear + def self.fiscal_quarter_dates(fiscal_year, fiscal_quarter) + start_date, end_date = case fiscal_quarter + when 1 then [Date.new(fiscal_year - 1, 10, 1), Date.new(fiscal_year - 1, 12, 31)] # Q1: Oct - Dec of the prior calendar year relativ to the fiscal year + when 2 then [Date.new(fiscal_year, 1, 1), Date.new(fiscal_year, 3, 31)] # Q2: Jan - Mar + when 3 then [Date.new(fiscal_year, 4, 1), Date.new(fiscal_year, 6, 30)] # Q3: Apr - Jun + when 4 then [Date.new(fiscal_year, 7, 1), Date.new(fiscal_year, 9, 30)] # Q4: Jul - Sep + else + raise ArgumentError, "Invalid quarter: #{fiscal_quarter}. Must be 1, 2, 3, or 4." + end + + { + start_date: start_date.beginning_of_day, + end_date: end_date.end_of_day + } + end + def self.first_day_of_fiscal_quarter(date) # Adjust the month to align with the fiscal year starting in October fiscal_month = (date.month - 10) % 12 + 1 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 From bd77877f45f4f1bfd2f783ba7fd1273b6cdc7d97 Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Wed, 5 Feb 2025 18:23:56 -0800 Subject: [PATCH 03/25] add CxCollectionDetail.form_id * to trigger an automated import of CxResponses --- .../admin/cx_collection_details_controller.rb | 28 +++++++++++++++++-- ...46_add_form_id_to_cx_collection_details.rb | 5 ++++ db/schema.rb | 3 +- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20250206004746_add_form_id_to_cx_collection_details.rb diff --git a/app/controllers/admin/cx_collection_details_controller.rb b/app/controllers/admin/cx_collection_details_controller.rb index eac771941..8a4045c25 100644 --- a/app/controllers/admin/cx_collection_details_controller.rb +++ b/app/controllers/admin/cx_collection_details_controller.rb @@ -27,7 +27,7 @@ def new @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.trust_question_text = @form.activations + @cx_collection_detail.trust_question_text = @form.survey_form_activations end end @@ -46,6 +46,14 @@ 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_id + fiscal_quarter_dates = FiscalYear.fiscal_quarter_dates(@cx_collection_detail.cx_collection.fiscal_year, @cx_collection_detail.cx_collection.quarter) + start_date = fiscal_quarter_dates[:start_date] + end_date = fiscal_quarter_dates[:end_date] + CxCollectionDetailUpload.upload_form_results(form_id: @cx_collection_detail.form_id, start_date:, end_date:) + 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 @@ -167,6 +175,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/db/migrate/20250206004746_add_form_id_to_cx_collection_details.rb b/db/migrate/20250206004746_add_form_id_to_cx_collection_details.rb new file mode 100644 index 000000000..371146ed1 --- /dev/null +++ b/db/migrate/20250206004746_add_form_id_to_cx_collection_details.rb @@ -0,0 +1,5 @@ +class AddFormIdToCxCollectionDetails < ActiveRecord::Migration[7.2] + def change + add_column :cx_collection_details, :form_id, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 3ff9899e3..91df72ab1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_01_30_195535) do +ActiveRecord::Schema[7.2].define(version: 2025_02_06_004746) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -106,6 +106,7 @@ t.text "trust_question_text" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "form_id" end create_table "cx_collections", force: :cascade do |t| From c0bb844bc8080706cb6a843e2b5887b08e89870d Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Wed, 5 Feb 2025 18:49:41 -0800 Subject: [PATCH 04/25] wip - create CxResponses from Form responses --- .../admin/cx_collection_details_controller.rb | 10 ++-- app/models/cx_collection_detail.rb | 1 + app/models/cx_collection_detail_upload.rb | 48 +++++++++++++++++-- app/models/form.rb | 33 +++++++++++++ .../cx_collection_details/_form.html.erb | 12 +++++ .../cx_collection_details/upload.html.erb | 2 +- 6 files changed, 96 insertions(+), 10 deletions(-) diff --git a/app/controllers/admin/cx_collection_details_controller.rb b/app/controllers/admin/cx_collection_details_controller.rb index 8a4045c25..d4f559de6 100644 --- a/app/controllers/admin/cx_collection_details_controller.rb +++ b/app/controllers/admin/cx_collection_details_controller.rb @@ -21,13 +21,14 @@ def new 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.trust_question_text = @form.survey_form_activations + @cx_collection_detail.volume_of_customers_provided_survey_opportunity = @form.survey_form_activations end end @@ -47,11 +48,8 @@ def create 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_id - fiscal_quarter_dates = FiscalYear.fiscal_quarter_dates(@cx_collection_detail.cx_collection.fiscal_year, @cx_collection_detail.cx_collection.quarter) - start_date = fiscal_quarter_dates[:start_date] - end_date = fiscal_quarter_dates[:end_date] - CxCollectionDetailUpload.upload_form_results(form_id: @cx_collection_detail.form_id, start_date:, end_date:) + 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." } 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..5436f2a7a 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[0], + 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 fac749083..798b4c197 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -449,6 +449,39 @@ def to_a11_v2_csv(start_date: nil, end_date: nil) 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 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 d3f477906..f3c0defba 100644 --- a/app/views/admin/cx_collection_details/upload.html.erb +++ b/app/views/admin/cx_collection_details/upload.html.erb @@ -87,7 +87,7 @@ <%= upload.user.email %>

- <%= link_to "Uploaded file", s3_presigned_url(upload.key) %> + <%= link_to "Uploaded file", s3_presigned_url(upload.key) if upload.key %> <%= upload.size %> From 8eed195217963ec06ab49fe481c78c4a06f43893 Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Wed, 5 Feb 2025 18:50:19 -0800 Subject: [PATCH 05/25] integers for fy and quarter --- lib/fiscal_year.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/fiscal_year.rb b/lib/fiscal_year.rb index 150633c83..732f7e762 100644 --- a/lib/fiscal_year.rb +++ b/lib/fiscal_year.rb @@ -1,6 +1,7 @@ module FiscalYear def self.fiscal_quarter_dates(fiscal_year, fiscal_quarter) - start_date, end_date = case fiscal_quarter + fiscal_year = fiscal_year.to_i + start_date, end_date = case fiscal_quarter.to_i when 1 then [Date.new(fiscal_year - 1, 10, 1), Date.new(fiscal_year - 1, 12, 31)] # Q1: Oct - Dec of the prior calendar year relativ to the fiscal year when 2 then [Date.new(fiscal_year, 1, 1), Date.new(fiscal_year, 3, 31)] # Q2: Jan - Mar when 3 then [Date.new(fiscal_year, 4, 1), Date.new(fiscal_year, 6, 30)] # Q3: Apr - Jun From 58e68f8f6eb183466905b254c7308ada3335c17d Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Thu, 6 Feb 2025 09:55:23 -0800 Subject: [PATCH 06/25] fill in date field with DD/MM/YYYY * because expiration is now a date_field --- spec/features/admin/forms_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/forms_spec.rb b/spec/features/admin/forms_spec.rb index 49e1ec453..dbe5804b9 100644 --- a/spec/features/admin/forms_spec.rb +++ b/spec/features/admin/forms_spec.rb @@ -741,7 +741,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 From 651ffce5cc3ba41bb48bf764f3d049e853bcc9d0 Mon Sep 17 00:00:00 2001 From: Ryan Wold Date: Thu, 6 Feb 2025 12:43:27 -0800 Subject: [PATCH 07/25] on --- spec/features/admin/cx_collections_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 54fa60bcec24c30854a3b6f055b2181f1b24391f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 04:50:08 +0000 Subject: [PATCH 08/25] Bump pbkdf2 from 3.1.2 to 3.1.3 Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.1.2 to 3.1.3. - [Changelog](https://github.com/browserify/pbkdf2/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/pbkdf2/compare/v3.1.2...v3.1.3) --- updated-dependencies: - dependency-name: pbkdf2 dependency-version: 3.1.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 273 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 212 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61e6e4fce..59846a6a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2627,17 +2627,47 @@ "license": "MIT" }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 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": { From a65f40da4235c935c117907389169c4c18eee841 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 17 Jul 2025 23:55:30 +0000 Subject: [PATCH 09/25] fix: Gemfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-RACK-10303186 --- Gemfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index a4c83f2c8..437e194a8 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,7 @@ gem "puma" gem "importmap-rails", ">= 2.0.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" @@ -62,11 +62,11 @@ 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' +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 'logstop' @@ -90,7 +90,7 @@ group :development do 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 +99,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 From 54ccfe4f41b2e35ab0542875a6ef9575c3cb053b Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Fri, 18 Jul 2025 08:46:29 -0400 Subject: [PATCH 10/25] initial updates for 30 questions --- Gemfile | 45 +- app/controllers/admin/questions_controller.rb | 2 +- app/controllers/application_controller.rb | 2 +- app/controllers/submissions_controller.rb | 39 +- app/helpers/application_helper.rb | 66 +- app/models/form.rb | 432 +++-- app/serializers/submission_serializer.rb | 10 + config/database.yml | 5 +- config/initializers/touchpoints.rb | 2 +- ...717034402_increase_submission_questions.rb | 14 + db/schema.rb | 1510 +++++++++-------- public/api/v0/openapi.yml | 20 + public/api/v1/openapi.yml | 20 + spec/fixtures/form.json | 60 + 14 files changed, 1235 insertions(+), 992 deletions(-) create mode 100644 db/migrate/20250717034402_increase_submission_questions.rb diff --git a/Gemfile b/Gemfile index a4c83f2c8..13f649d11 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.0.0' # Hotwire"s SPA-like page accelerator [https://turbo.hotwired.dev] -gem "turbo-rails" +gem 'turbo-rails' # 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' @@ -64,15 +64,15 @@ gem 'omniauth-rails_csrf_protection' gem 'rack-attack' gem 'rack-cors', require: 'rack/cors' # Use Redis to cache Touchpoints in all envs -gem 'redis-client' -gem 'redis-namespace' -gem 'sidekiq', '>= 6.5.0' -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' @@ -85,13 +85,13 @@ 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-rspec" + gem 'rubocop-rails' + gem 'rubocop-rspec' gem 'web-console' end @@ -100,6 +100,7 @@ group :test do gem 'capybara' gem 'database_cleaner' gem 'factory_bot_rails' + gem 'faker' gem 'rails-controller-testing' gem 'rspec_junit_formatter' gem 'rspec-rails' 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/form.rb b/app/models/form.rb index c9bbe5d03..a96279d60 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,23 +485,79 @@ 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 @@ -582,12 +692,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 +733,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 +776,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,79 +813,61 @@ 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.") - end + errors.add(:base, "Touchpoints supports a maximum of 30 questions. There are currently #{questions_count} questions. Fewer questions tend to yield higher response rates.") if questions.size > 12 end def contains_elements?(array, required_elements) @@ -784,7 +878,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 +889,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/config/database.yml b/config/database.yml index 9fa30a00a..05325a748 100644 --- a/config/database.yml +++ b/config/database.yml @@ -58,8 +58,9 @@ development: test: <<: *default # Uncomment host and username if running docker containers ( docker-compose ) - #host: postgres - #username: postgres + host: localhost + username: postgres + password: changeme database: touchpoints_test # As with config/secrets.yml, you never want to store sensitive information, diff --git a/config/initializers/touchpoints.rb b/config/initializers/touchpoints.rb index d78ba6def..abc36c515 100644 --- a/config/initializers/touchpoints.rb +++ b/config/initializers/touchpoints.rb @@ -1,2 +1,2 @@ APPROVED_DOMAINS = ['.gov', '.mil', '.edu'].freeze -APPROVED_WEBSITE_DOMAINS = (APPROVED_DOMAINS + ['.org']).freeze \ No newline at end of file +APPROVED_WEBSITE_DOMAINS = (APPROVED_DOMAINS + ['.org']).freeze diff --git a/db/migrate/20250717034402_increase_submission_questions.rb b/db/migrate/20250717034402_increase_submission_questions.rb new file mode 100644 index 000000000..846891688 --- /dev/null +++ b/db/migrate/20250717034402_increase_submission_questions.rb @@ -0,0 +1,14 @@ +class IncreaseSubmissionQuestions < ActiveRecord::Migration[8.0] + def change + add_column :submissions, :answer_21, :text + add_column :submissions, :answer_22, :text + add_column :submissions, :answer_23, :text + add_column :submissions, :answer_24, :text + add_column :submissions, :answer_25, :text + add_column :submissions, :answer_26, :text + add_column :submissions, :answer_27, :text + add_column :submissions, :answer_28, :text + add_column :submissions, :answer_29, :text + add_column :submissions, :answer_30, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index ede6519c7..3fc6acc82 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,755 +10,765 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_04_23_163606) do +ActiveRecord::Schema[8.0].define(version: 20_250_717_034_402) do # These are extensions that must be enabled in order to support this database - enable_extension "pg_catalog.plpgsql" - - create_table "active_storage_attachments", force: :cascade do |t| - t.string "name", null: false - t.string "record_type", null: false - t.bigint "record_id", null: false - t.bigint "blob_id", null: false - t.datetime "created_at", null: false - t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" - t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true - end - - create_table "active_storage_blobs", force: :cascade do |t| - t.string "key", null: false - t.string "filename", null: false - t.string "content_type" - t.text "metadata" - t.string "service_name", null: false - t.bigint "byte_size", null: false - t.string "checksum" - t.datetime "created_at", null: false - t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true - end - - create_table "active_storage_variant_records", force: :cascade do |t| - t.bigint "blob_id", null: false - t.string "variation_digest", null: false - t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true - end - - create_table "collections", comment: "Quarterly CX Data Collection", force: :cascade do |t| - t.string "name" - t.date "start_date" - t.date "end_date" - t.integer "organization_id" - t.string "year" - t.string "quarter" - t.integer "user_id" - t.string "integrity_hash" - t.string "aasm_state" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "reflection" - t.string "rating" - t.integer "service_provider_id" - t.index ["organization_id"], name: "index_collections_on_organization_id" - t.index ["service_provider_id"], name: "index_collections_on_service_provider_id" - t.index ["user_id"], name: "index_collections_on_user_id" - end - - create_table "cx_collection_detail_uploads", force: :cascade do |t| - t.integer "user_id" - t.integer "cx_collection_detail_id" - t.integer "size", comment: "file size of the s3 object" - t.string "key", comment: "s3 path to the asset" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "aasm_state" - t.string "record_count" - t.string "job_id" - end - - create_table "cx_collection_details", force: :cascade do |t| - t.integer "cx_collection_id" - t.string "transaction_point" - t.string "channel" - t.integer "service_stage_id" - t.integer "volume_of_customers" - t.integer "volume_of_customers_provided_survey_opportunity" - t.integer "volume_of_respondents" - t.string "omb_control_number" - t.string "federal_register_url" - t.text "reflection_text" - t.text "survey_type" - t.text "survey_title" - t.text "trust_question_text" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "cx_collections", force: :cascade do |t| - t.integer "user_id" - t.string "name" - t.integer "organization_id" - t.integer "service_provider_id" - t.integer "service_id" - t.string "service_type" - t.string "digital_service_or_contact_center" - t.string "url" - t.string "fiscal_year" - t.string "quarter" - t.string "transaction_point" - t.integer "service_stage_id" - t.string "channel" - t.string "survey_title" - t.string "trust_question_text" - t.string "likert_or_thumb_question" - t.integer "number_of_interactions" - t.string "number_of_people_offered_the_survey" - t.string "aasm_state" - t.string "rating" - t.string "integrity_hash" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.datetime "submitted_at" - end - - create_table "cx_responses", force: :cascade do |t| - t.integer "cx_collection_detail_id" - t.integer "cx_collection_detail_upload_id" - t.string "question_1", comment: "thumbs up/down" - t.string "positive_effectiveness" - t.string "positive_ease" - t.string "positive_efficiency" - t.string "positive_transparency" - t.string "positive_humanity" - t.string "positive_employee" - t.string "positive_other" - t.string "negative_effectiveness" - t.string "negative_ease" - t.string "negative_efficiency" - t.string "negative_transparency" - t.string "negative_humanity" - t.string "negative_employee" - t.string "negative_other" - t.string "question_4", comment: "open text" - t.string "job_id", comment: "a unique ID assigned when a batch of responses is imported" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "external_id" - end - - create_table "digital_product_versions", force: :cascade do |t| - t.bigint "digital_product_id" - t.string "store_url" - t.string "platform" - t.string "version_number" - t.date "publish_date" - t.string "description" - t.string "whats_new" - t.string "screenshot_url" - t.string "device" - t.string "language" - t.string "average_rating" - t.integer "number_of_ratings" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "legacy_id" - t.text "legacy_notes" - t.index ["digital_product_id"], name: "index_digital_product_versions_on_digital_product_id" - end - - create_table "digital_products", force: :cascade do |t| - t.integer "user_id" - t.string "service" - t.string "url" - t.string "code_repository_url" - t.string "language" - t.string "aasm_state" - t.string "short_description" - t.text "long_description" - t.text "notes" - t.string "tags" - t.datetime "certified_at", precision: nil - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "name" - t.integer "legacy_id" - t.text "legacy_notes" - t.index ["aasm_state"], name: "index_digital_products_on_aasm_state" - end - - create_table "digital_service_accounts", force: :cascade do |t| - t.integer "user_id" - t.string "service" - t.string "service_url" - t.string "language" - t.string "short_description" - t.text "long_description" - t.text "notes" - t.string "tags" - t.datetime "certified_at", precision: nil - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "name" - t.string "aasm_state" - t.integer "legacy_id" - t.text "legacy_notes" - t.index ["aasm_state"], name: "index_digital_service_accounts_on_aasm_state" - end - - create_table "events", force: :cascade do |t| - t.string "name", null: false - t.string "object_type" - t.string "object_uuid", null: false - t.string "description", null: false - t.integer "user_id" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - end - - create_table "form_sections", force: :cascade do |t| - t.integer "form_id" - t.string "title" - t.integer "position" - t.integer "next_section_id" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.index ["form_id"], name: "index_form_sections_on_form_id" - end - - create_table "forms", force: :cascade do |t| - t.string "name" - t.string "title" - t.string "instructions" - t.string "disclaimer_text" - t.string "kind" - t.text "notes" - t.string "status" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.string "whitelist_url", default: "" - t.string "whitelist_test_url", default: "" - t.boolean "display_header_logo", default: false - t.text "success_text" - t.string "modal_button_text" - t.boolean "display_header_square_logo" - t.boolean "early_submission", default: false - t.integer "user_id" - t.boolean "template", default: false - t.string "uuid" - t.integer "organization_id" - t.string "omb_approval_number" - t.date "expiration_date" - t.string "medium" - t.string "federal_register_url" - t.integer "anticipated_delivery_count" - t.string "service_name" - t.text "data_submission_comment" - t.string "survey_instrument_reference" - t.string "agency_poc_email" - t.string "agency_poc_name" - t.string "department" - t.string "bureau" - t.string "notification_emails" - t.datetime "start_date", precision: nil - t.datetime "end_date", precision: nil - t.string "aasm_state" - t.string "delivery_method" - t.string "element_selector" - t.integer "survey_form_activations", default: 0 - t.integer "legacy_touchpoint_id" - t.string "legacy_touchpoint_uuid" - t.boolean "load_css", default: true - t.string "logo" - t.string "occasion" - t.string "time_zone", default: "Eastern Time (US & Canada)" - t.integer "response_count", default: 0 - t.datetime "last_response_created_at", precision: nil - t.boolean "ui_truncate_text_responses", default: true - t.string "success_text_heading" - t.string "notification_frequency", default: "instant" - t.integer "service_id" - t.integer "questions_count", default: 0 - t.boolean "verify_csrf", default: false - t.string "submissions_tags", array: true - t.string "whitelist_url_1" - t.string "whitelist_url_2" - t.string "whitelist_url_3" - t.string "whitelist_url_4" - t.string "whitelist_url_5" - t.string "whitelist_url_6" - t.string "whitelist_url_7" - t.string "whitelist_url_8" - t.string "whitelist_url_9" - t.string "submission_tags", default: [], comment: "cache the form's submissions tags for reporting", array: true - t.datetime "submitted_at" - t.datetime "approved_at" - t.datetime "archived_at" - t.string "audience", default: "public", comment: "indicates whether a form is intended for a public or internal audience" - t.string "short_uuid", limit: 8 - t.boolean "enforce_new_submission_validations", default: true - t.integer "service_stage_id" - t.boolean "append_id_to_success_text", default: false, comment: "Set to true to append a response ID to the form's success_text" - t.boolean "enable_turnstile", default: false, comment: "Set to true to enable Cloudfront Turnstile" - t.index ["legacy_touchpoint_id"], name: "index_forms_on_legacy_touchpoint_id" - t.index ["legacy_touchpoint_uuid"], name: "index_forms_on_legacy_touchpoint_uuid" - t.index ["organization_id"], name: "index_forms_on_organization_id" - t.index ["service_id"], name: "index_forms_on_service_id" - t.index ["short_uuid"], name: "index_forms_on_short_uuid", unique: true - t.index ["user_id"], name: "index_forms_on_user_id" - t.index ["uuid"], name: "index_forms_on_uuid", unique: true - end - - create_table "omb_cx_reporting_collections", comment: "A detailed record belonging to a Collection; a quarterly CX Data Collection", force: :cascade do |t| - t.integer "collection_id" - t.string "service_provided" - t.text "transaction_point" - t.string "channel" - t.integer "volume_of_customers", default: 0 - t.integer "volume_of_customers_provided_survey_opportunity", default: 0 - t.integer "volume_of_respondents", default: 0 - t.string "omb_control_number" - t.string "federal_register_url" - t.string "q1_text" - t.integer "q1_1", default: 0 - t.integer "q1_2", default: 0 - t.integer "q1_3", default: 0 - t.integer "q1_4", default: 0 - t.integer "q1_5", default: 0 - t.string "q2_text" - t.integer "q2_1", default: 0 - t.integer "q2_2", default: 0 - t.integer "q2_3", default: 0 - t.integer "q2_4", default: 0 - t.integer "q2_5", default: 0 - t.string "q3_text" - t.integer "q3_1", default: 0 - t.integer "q3_2", default: 0 - t.integer "q3_3", default: 0 - t.integer "q3_4", default: 0 - t.integer "q3_5", default: 0 - t.string "q4_text" - t.integer "q4_1", default: 0 - t.integer "q4_2", default: 0 - t.integer "q4_3", default: 0 - t.integer "q4_4", default: 0 - t.integer "q4_5", default: 0 - t.string "q5_text" - t.integer "q5_1", default: 0 - t.integer "q5_2", default: 0 - t.integer "q5_3", default: 0 - t.integer "q5_4", default: 0 - t.integer "q5_5", default: 0 - t.string "q6_text" - t.integer "q6_1", default: 0 - t.integer "q6_2", default: 0 - t.integer "q6_3", default: 0 - t.integer "q6_4", default: 0 - t.integer "q6_5", default: 0 - t.string "q7_text" - t.integer "q7_1", default: 0 - t.integer "q7_2", default: 0 - t.integer "q7_3", default: 0 - t.integer "q7_4", default: 0 - t.integer "q7_5", default: 0 - t.string "q8_text" - t.integer "q8_1", default: 0 - t.integer "q8_2", default: 0 - t.integer "q8_3", default: 0 - t.integer "q8_4", default: 0 - t.integer "q8_5", default: 0 - t.string "q9_text" - t.integer "q9_1", default: 0 - t.integer "q9_2", default: 0 - t.integer "q9_3", default: 0 - t.integer "q9_4", default: 0 - t.integer "q9_5", default: 0 - t.string "q10_text" - t.integer "q10_1", default: 0 - t.integer "q10_2", default: 0 - t.integer "q10_3", default: 0 - t.integer "q10_4", default: 0 - t.integer "q10_5", default: 0 - t.string "q11_text" - t.integer "q11_1", default: 0 - t.integer "q11_2", default: 0 - t.integer "q11_3", default: 0 - t.integer "q11_4", default: 0 - t.integer "q11_5", default: 0 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "operational_metrics" - t.integer "service_id" - t.index ["collection_id"], name: "index_omb_cx_reporting_collections_on_collection_id" - t.index ["service_id"], name: "index_omb_cx_reporting_collections_on_service_id" - end - - create_table "organizations", force: :cascade do |t| - t.string "name", null: false - t.string "url" - t.string "abbreviation" - t.text "notes" - t.integer "external_id" - t.string "domain" - t.string "logo" - t.boolean "enable_ip_address", default: true - t.string "digital_analytics_path" - t.text "mission_statement" - t.string "mission_statement_url" - t.string "performance_url" - t.string "strategic_plan_url" - t.string "learning_agenda_url" - t.boolean "cfo_act_agency", default: false - t.integer "parent_id" - t.boolean "form_approval_enabled", default: false, comment: "Indicate whether this organization requires a Submission and Approval process for forms" - end - - create_table "organizations_roles", id: false, force: :cascade do |t| - t.bigint "organization_id" - t.bigint "role_id" - t.index ["organization_id", "role_id"], name: "index_organizations_roles_on_organization_id_and_role_id" - t.index ["organization_id"], name: "index_organizations_roles_on_organization_id" - t.index ["role_id"], name: "index_organizations_roles_on_role_id" - end - - create_table "personas", force: :cascade do |t| - t.string "name" - t.text "description" - t.string "tags", array: true - t.integer "user_id" - t.text "notes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["tags"], name: "index_personas_on_tags", using: :gin - end - - create_table "question_options", force: :cascade do |t| - t.integer "question_id" - t.string "text" - t.integer "position" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.string "value" - t.boolean "other_option", default: false - t.index ["question_id"], name: "index_question_options_on_question_id" - end - - create_table "questions", force: :cascade do |t| - t.integer "form_id" - t.string "text" - t.string "question_type" - t.string "answer_field" - t.integer "position" - t.boolean "is_required" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.integer "form_section_id" - t.integer "character_limit" - t.string "placeholder_text" - t.string "help_text" - t.index ["form_id"], name: "index_questions_on_form_id" - t.index ["form_section_id"], name: "index_questions_on_form_section_id" - end - - create_table "registry_searches", force: :cascade do |t| - t.string "agency" - t.string "keywords" - t.string "platform" - t.string "status" - t.string "session_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "roles", force: :cascade do |t| - t.string "name" - t.string "resource_type" - t.bigint "resource_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" - t.index ["resource_type", "resource_id"], name: "index_roles_on_resource" - end - - create_table "service_providers", comment: "A Service Provider, or HISP, as defined in OMB Circular A-11 Section 280", force: :cascade do |t| - t.integer "organization_id" - t.string "name" - t.text "description" - t.text "notes" - t.string "slug" - t.string "department" - t.string "department_abbreviation" - t.string "bureau" - t.string "bureau_abbreviation" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "new" - t.boolean "inactive", default: true, null: false - t.string "url" - t.integer "cx_maturity_mapping_value", default: 0 - t.integer "services_count", default: 0 - t.integer "impact_mapping_value", default: 0 - t.string "portfolio_manager_email" - t.integer "year_designated" - t.index ["organization_id"], name: "index_service_providers_on_organization_id" - end - - create_table "service_stages", comment: "A step or stage within a Service, as used in a Business Process Model. eg: start, middle, end", force: :cascade do |t| - t.string "name" - t.text "description" - t.integer "service_id" - t.text "notes" - t.integer "time" - t.integer "position" - t.integer "total_eligible_population" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.integer "persona_id" - t.index ["service_id"], name: "index_service_stages_on_service_id" - end - - create_table "services", id: { comment: "Unique identifier for a Service" }, comment: "Services provided by an Agency, often by a Service Provider within an Agency", force: :cascade do |t| - t.string "name", comment: "Name of the service" - t.text "description", comment: "Description of the designated service" - t.integer "organization_id", comment: "Unique number for each department. A department may contain several HISPs" - t.text "notes", comment: "Field for HISP to provide additional notes" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.boolean "hisp", default: false, comment: "True or False - Is this Service considered a HISP service?" - t.string "department", default: "", comment: "Abbreviation of department name" - t.string "bureau", default: "", comment: "Name of the Bureau to which a service belongs" - t.string "service_slug", default: "", comment: "a unique text string to identify the service" - t.string "url", default: "", comment: "A website link to their service" - t.integer "service_provider_id", comment: "Unique number for each Service Provider" - t.integer "service_owner_id", comment: "ID of the User record for which a Service is owned or managed by" - t.string "kind", comment: "Identifies the category of service: compliance, administrative, benefits, recreation, informational, data and research, and regulatory", array: true - t.string "aasm_state", default: "created", comment: "State/status that a Service is in. eg: created, submitted, approved, verified, archived" - t.text "non_digital_explanation", comment: "If applicable, explain why a service is not available via a digital channel" - t.integer "service_stages_count", default: 0, comment: "Helper field that counts how many Service Stages this Service has" - t.string "homepage_url", comment: "A primary website link to the service" - t.string "budget_code", comment: "The budget code for this service" - t.string "uii_code", comment: "The UII code for this service" - t.boolean "transactional", default: false, comment: "True or False for whether the service is transactional" - t.boolean "digital_service", default: false, comment: "Is this a digital service or not?" - t.string "estimated_annual_volume_of_customers", default: "", comment: "Estimated volume of customers on an annual basis" - t.string "channels", comment: "One or more channels where the service is delivered", array: true - t.boolean "fully_digital_service", default: false, comment: "Is this a fully digital service or not?" - t.text "barriers_to_fully_digital_service", comment: "If applicable, describe the barriers preventing this service from being a fully digital service" - t.boolean "multi_agency_service", default: false, comment: "Do multiple agencies collaborate to provide this service?" - t.text "multi_agency_explanation", comment: "If applicable, describe how multiple agencies collaborate to provide this service" - t.string "other_service_type" - t.string "customer_volume_explanation" - t.text "resources_needed_to_provide_digital_service", comment: "If applicable, what resources are needed to provide this service digitally?" - t.string "office", comment: "Text description for the office (below a Bureau)" - t.boolean "designated_for_improvement_a11_280", default: false, comment: "Is this Service designated, per the OMB Circular A-11 Section 280" - t.boolean "contact_center", default: false, comment: "True or False for whether the service involves a contact center and/or an interaction with a contact center" - t.integer "year_designated" - t.text "short_description" - t.boolean "previously_reported", default: false - t.integer "cx_collections_count", default: 0 - t.index ["organization_id"], name: "index_services_on_organization_id" - end - - create_table "submissions", force: :cascade do |t| - t.integer "user_id" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.string "referer" - t.string "page" - t.string "user_agent" - t.text "answer_01" - t.text "answer_02" - t.text "answer_03" - t.text "answer_04" - t.text "answer_05" - t.text "answer_06" - t.text "answer_07" - t.text "answer_08" - t.text "answer_09" - t.text "answer_10" - t.text "answer_11" - t.text "answer_12" - t.text "answer_13" - t.text "answer_14" - t.text "answer_15" - t.text "answer_16" - t.text "answer_17" - t.text "answer_18" - t.text "answer_19" - t.text "answer_20" - t.string "ip_address" - t.string "location_code" - t.boolean "flagged", default: false - t.string "language" - t.integer "form_id" - t.string "uuid" - t.string "aasm_state", default: "received" - t.string "hostname" - t.string "tags", default: [], array: true - t.integer "spam_score", default: 0 - t.text "query_string" - t.boolean "spam", default: false - t.boolean "archived", default: false - t.boolean "deleted", default: false - t.datetime "deleted_at" - t.string "preview", default: "" - t.string "spam_prevention_mechanism", default: "", comment: "Specify which spam prevention mechanism was used, if any." - t.index ["archived"], name: "index_submissions_on_archived" - t.index ["created_at"], name: "index_submissions_on_created_at" - t.index ["flagged"], name: "index_submissions_on_flagged" - t.index ["form_id"], name: "index_submissions_on_form_id" - t.index ["spam"], name: "index_submissions_on_spam" - t.index ["uuid"], name: "index_submissions_on_uuid", unique: true - end - - create_table "taggings", id: :serial, force: :cascade do |t| - t.integer "tag_id" - t.string "taggable_type" - t.integer "taggable_id" - t.string "tagger_type" - t.integer "tagger_id" - t.string "context", limit: 128 - t.datetime "created_at", precision: nil - t.string "tenant", limit: 128 - t.index ["context"], name: "index_taggings_on_context" - t.index ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true - t.index ["tag_id"], name: "index_taggings_on_tag_id" - t.index ["taggable_id", "taggable_type", "context"], name: "taggings_taggable_context_idx" - t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy" - t.index ["taggable_id"], name: "index_taggings_on_taggable_id" - t.index ["taggable_type"], name: "index_taggings_on_taggable_type" - t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type" - t.index ["tagger_id"], name: "index_taggings_on_tagger_id" - t.index ["tenant"], name: "index_taggings_on_tenant" - end - - create_table "tags", id: :serial, force: :cascade do |t| - t.string "name" - t.datetime "created_at", precision: nil - t.datetime "updated_at", precision: nil - t.integer "taggings_count", default: 0 - t.index ["name"], name: "index_tags_on_name", unique: true - end - - create_table "user_roles", force: :cascade do |t| - t.integer "user_id", null: false - t.integer "form_id" - t.string "role" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.index ["user_id", "form_id"], name: "index_user_roles_on_user_id_and_form_id", unique: true - end - - create_table "users", force: :cascade do |t| - t.integer "organization_id" - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at", precision: nil - t.datetime "remember_created_at", precision: nil - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at", precision: nil - t.datetime "last_sign_in_at", precision: nil - t.inet "current_sign_in_ip" - t.inet "last_sign_in_ip" - t.string "confirmation_token" - t.datetime "confirmed_at", precision: nil - t.datetime "confirmation_sent_at", precision: nil - t.string "unconfirmed_email" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.boolean "admin", default: false - t.string "provider" - t.string "uid" - t.boolean "inactive", default: false, null: false - t.string "time_zone", default: "Eastern Time (US & Canada)" - t.string "api_key" - t.datetime "api_key_updated_at", precision: nil - t.boolean "organizational_website_manager", default: false - t.boolean "performance_manager", default: false - t.boolean "registry_manager", default: false - t.boolean "service_manager", default: false - t.string "first_name" - t.string "last_name" - t.string "position_title" - t.string "profile_photo" - t.boolean "organizational_admin", default: false - t.boolean "organizational_form_approver", default: false - t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true - t.index ["email"], name: "index_users_on_email", unique: true - t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true - end - - create_table "users_roles", id: false, force: :cascade do |t| - t.bigint "user_id" - t.bigint "role_id" - t.index ["role_id"], name: "index_users_roles_on_role_id" - t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" - t.index ["user_id"], name: "index_users_roles_on_user_id" - end - - create_table "versions", force: :cascade do |t| - t.string "item_type", null: false - t.bigint "item_id", null: false - t.string "event", null: false - t.string "whodunnit" - t.text "old_object" - t.datetime "created_at", precision: nil - t.text "old_object_changes" - t.jsonb "object" - t.jsonb "object_changes" - t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" - end - - create_table "websites", force: :cascade do |t| - t.string "domain" - t.string "office" - t.integer "office_id" - t.string "sub_office" - t.integer "suboffice_id" - t.string "contact_email" - t.string "site_owner_email" - t.string "production_status" - t.string "type_of_site" - t.string "digital_brand_category" - t.string "redirects_to" - t.string "status_code" - t.string "cms_platform" - t.string "required_by_law_or_policy" - t.boolean "has_dap" - t.string "dap_gtm_code" - t.string "cost_estimator_url" - t.string "modernization_plan_url" - t.float "annual_baseline_cost" - t.float "modernization_cost" - t.string "analytics_url" - t.boolean "uses_feedback" - t.string "feedback_tool" - t.string "sitemap_url" - t.boolean "mobile_friendly" - t.boolean "has_search" - t.boolean "uses_tracking_cookies" - t.boolean "has_authenticated_experience" - t.string "authentication_tool" - t.text "notes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "repository_url" - t.string "hosting_platform" - t.float "modernization_cost_2021" - t.float "modernization_cost_2022" - t.float "modernization_cost_2023" - t.string "uswds_version" - t.boolean "https" - t.integer "service_id" - t.integer "organization_id" - t.string "backlog_tool", default: "" - t.string "backlog_url", default: "" - t.string "aasm_state" - t.date "target_decommission_date" - t.index ["aasm_state"], name: "index_websites_on_aasm_state" - t.index ["organization_id"], name: "index_websites_on_organization_id" - t.index ["service_id"], name: "index_websites_on_service_id" - end - - add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" - add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" - add_foreign_key "taggings", "tags" + enable_extension 'pg_catalog.plpgsql' + + create_table 'active_storage_attachments', force: :cascade do |t| + t.string 'name', null: false + t.string 'record_type', null: false + t.bigint 'record_id', null: false + t.bigint 'blob_id', null: false + t.datetime 'created_at', null: false + t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id' + t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true + end + + create_table 'active_storage_blobs', force: :cascade do |t| + t.string 'key', null: false + t.string 'filename', null: false + t.string 'content_type' + t.text 'metadata' + t.string 'service_name', null: false + t.bigint 'byte_size', null: false + t.string 'checksum' + t.datetime 'created_at', null: false + t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true + end + + create_table 'active_storage_variant_records', force: :cascade do |t| + t.bigint 'blob_id', null: false + t.string 'variation_digest', null: false + t.index %w[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true + end + + create_table 'collections', comment: 'Quarterly CX Data Collection', force: :cascade do |t| + t.string 'name' + t.date 'start_date' + t.date 'end_date' + t.integer 'organization_id' + t.string 'year' + t.string 'quarter' + t.integer 'user_id' + t.string 'integrity_hash' + t.string 'aasm_state' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'reflection' + t.string 'rating' + t.integer 'service_provider_id' + t.index ['organization_id'], name: 'index_collections_on_organization_id' + t.index ['service_provider_id'], name: 'index_collections_on_service_provider_id' + t.index ['user_id'], name: 'index_collections_on_user_id' + end + + create_table 'cx_collection_detail_uploads', force: :cascade do |t| + t.integer 'user_id' + t.integer 'cx_collection_detail_id' + t.integer 'size', comment: 'file size of the s3 object' + t.string 'key', comment: 's3 path to the asset' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'aasm_state' + t.string 'record_count' + t.string 'job_id' + end + + create_table 'cx_collection_details', force: :cascade do |t| + t.integer 'cx_collection_id' + t.string 'transaction_point' + t.string 'channel' + t.integer 'service_stage_id' + t.integer 'volume_of_customers' + t.integer 'volume_of_customers_provided_survey_opportunity' + t.integer 'volume_of_respondents' + t.string 'omb_control_number' + t.string 'federal_register_url' + t.text 'reflection_text' + t.text 'survey_type' + t.text 'survey_title' + t.text 'trust_question_text' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + end + + create_table 'cx_collections', force: :cascade do |t| + t.integer 'user_id' + t.string 'name' + t.integer 'organization_id' + t.integer 'service_provider_id' + t.integer 'service_id' + t.string 'service_type' + t.string 'digital_service_or_contact_center' + t.string 'url' + t.string 'fiscal_year' + t.string 'quarter' + t.string 'transaction_point' + t.integer 'service_stage_id' + t.string 'channel' + t.string 'survey_title' + t.string 'trust_question_text' + t.string 'likert_or_thumb_question' + t.integer 'number_of_interactions' + t.string 'number_of_people_offered_the_survey' + t.string 'aasm_state' + t.string 'rating' + t.string 'integrity_hash' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.datetime 'submitted_at' + end + + create_table 'cx_responses', force: :cascade do |t| + t.integer 'cx_collection_detail_id' + t.integer 'cx_collection_detail_upload_id' + t.string 'question_1', comment: 'thumbs up/down' + t.string 'positive_effectiveness' + t.string 'positive_ease' + t.string 'positive_efficiency' + t.string 'positive_transparency' + t.string 'positive_humanity' + t.string 'positive_employee' + t.string 'positive_other' + t.string 'negative_effectiveness' + t.string 'negative_ease' + t.string 'negative_efficiency' + t.string 'negative_transparency' + t.string 'negative_humanity' + t.string 'negative_employee' + t.string 'negative_other' + t.string 'question_4', comment: 'open text' + t.string 'job_id', comment: 'a unique ID assigned when a batch of responses is imported' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'external_id' + end + + create_table 'digital_product_versions', force: :cascade do |t| + t.bigint 'digital_product_id' + t.string 'store_url' + t.string 'platform' + t.string 'version_number' + t.date 'publish_date' + t.string 'description' + t.string 'whats_new' + t.string 'screenshot_url' + t.string 'device' + t.string 'language' + t.string 'average_rating' + t.integer 'number_of_ratings' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'legacy_id' + t.text 'legacy_notes' + t.index ['digital_product_id'], name: 'index_digital_product_versions_on_digital_product_id' + end + + create_table 'digital_products', force: :cascade do |t| + t.integer 'user_id' + t.string 'service' + t.string 'url' + t.string 'code_repository_url' + t.string 'language' + t.string 'aasm_state' + t.string 'short_description' + t.text 'long_description' + t.text 'notes' + t.string 'tags' + t.datetime 'certified_at', precision: nil + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'name' + t.integer 'legacy_id' + t.text 'legacy_notes' + t.index ['aasm_state'], name: 'index_digital_products_on_aasm_state' + end + + create_table 'digital_service_accounts', force: :cascade do |t| + t.integer 'user_id' + t.string 'service' + t.string 'service_url' + t.string 'language' + t.string 'short_description' + t.text 'long_description' + t.text 'notes' + t.string 'tags' + t.datetime 'certified_at', precision: nil + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'name' + t.string 'aasm_state' + t.integer 'legacy_id' + t.text 'legacy_notes' + t.index ['aasm_state'], name: 'index_digital_service_accounts_on_aasm_state' + end + + create_table 'events', force: :cascade do |t| + t.string 'name', null: false + t.string 'object_type' + t.string 'object_uuid', null: false + t.string 'description', null: false + t.integer 'user_id' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + end + + create_table 'form_sections', force: :cascade do |t| + t.integer 'form_id' + t.string 'title' + t.integer 'position' + t.integer 'next_section_id' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.index ['form_id'], name: 'index_form_sections_on_form_id' + end + + create_table 'forms', force: :cascade do |t| + t.string 'name' + t.string 'title' + t.string 'instructions' + t.string 'disclaimer_text' + t.string 'kind' + t.text 'notes' + t.string 'status' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.string 'whitelist_url', default: '' + t.string 'whitelist_test_url', default: '' + t.boolean 'display_header_logo', default: false + t.text 'success_text' + t.string 'modal_button_text' + t.boolean 'display_header_square_logo' + t.boolean 'early_submission', default: false + t.integer 'user_id' + t.boolean 'template', default: false + t.string 'uuid' + t.integer 'organization_id' + t.string 'omb_approval_number' + t.date 'expiration_date' + t.string 'medium' + t.string 'federal_register_url' + t.integer 'anticipated_delivery_count' + t.string 'service_name' + t.text 'data_submission_comment' + t.string 'survey_instrument_reference' + t.string 'agency_poc_email' + t.string 'agency_poc_name' + t.string 'department' + t.string 'bureau' + t.string 'notification_emails' + t.datetime 'start_date', precision: nil + t.datetime 'end_date', precision: nil + t.string 'aasm_state' + t.string 'delivery_method' + t.string 'element_selector' + t.integer 'survey_form_activations', default: 0 + t.integer 'legacy_touchpoint_id' + t.string 'legacy_touchpoint_uuid' + t.boolean 'load_css', default: true + t.string 'logo' + t.string 'occasion' + t.string 'time_zone', default: 'Eastern Time (US & Canada)' + t.integer 'response_count', default: 0 + t.datetime 'last_response_created_at', precision: nil + t.boolean 'ui_truncate_text_responses', default: true + t.string 'success_text_heading' + t.string 'notification_frequency', default: 'instant' + t.integer 'service_id' + t.integer 'questions_count', default: 0 + t.boolean 'verify_csrf', default: false + t.string 'submissions_tags', array: true + t.string 'whitelist_url_1' + t.string 'whitelist_url_2' + t.string 'whitelist_url_3' + t.string 'whitelist_url_4' + t.string 'whitelist_url_5' + t.string 'whitelist_url_6' + t.string 'whitelist_url_7' + t.string 'whitelist_url_8' + t.string 'whitelist_url_9' + t.string 'submission_tags', default: [], comment: "cache the form's submissions tags for reporting", array: true + t.datetime 'submitted_at' + t.datetime 'approved_at' + t.datetime 'archived_at' + t.string 'audience', default: 'public', comment: 'indicates whether a form is intended for a public or internal audience' + t.string 'short_uuid', limit: 8 + t.boolean 'enforce_new_submission_validations', default: true + t.integer 'service_stage_id' + t.boolean 'append_id_to_success_text', default: false, comment: "Set to true to append a response ID to the form's success_text" + t.boolean 'enable_turnstile', default: false, comment: 'Set to true to enable Cloudfront Turnstile' + t.index ['legacy_touchpoint_id'], name: 'index_forms_on_legacy_touchpoint_id' + t.index ['legacy_touchpoint_uuid'], name: 'index_forms_on_legacy_touchpoint_uuid' + t.index ['organization_id'], name: 'index_forms_on_organization_id' + t.index ['service_id'], name: 'index_forms_on_service_id' + t.index ['short_uuid'], name: 'index_forms_on_short_uuid', unique: true + t.index ['user_id'], name: 'index_forms_on_user_id' + t.index ['uuid'], name: 'index_forms_on_uuid', unique: true + end + + create_table 'omb_cx_reporting_collections', comment: 'A detailed record belonging to a Collection; a quarterly CX Data Collection', force: :cascade do |t| + t.integer 'collection_id' + t.string 'service_provided' + t.text 'transaction_point' + t.string 'channel' + t.integer 'volume_of_customers', default: 0 + t.integer 'volume_of_customers_provided_survey_opportunity', default: 0 + t.integer 'volume_of_respondents', default: 0 + t.string 'omb_control_number' + t.string 'federal_register_url' + t.string 'q1_text' + t.integer 'q1_1', default: 0 + t.integer 'q1_2', default: 0 + t.integer 'q1_3', default: 0 + t.integer 'q1_4', default: 0 + t.integer 'q1_5', default: 0 + t.string 'q2_text' + t.integer 'q2_1', default: 0 + t.integer 'q2_2', default: 0 + t.integer 'q2_3', default: 0 + t.integer 'q2_4', default: 0 + t.integer 'q2_5', default: 0 + t.string 'q3_text' + t.integer 'q3_1', default: 0 + t.integer 'q3_2', default: 0 + t.integer 'q3_3', default: 0 + t.integer 'q3_4', default: 0 + t.integer 'q3_5', default: 0 + t.string 'q4_text' + t.integer 'q4_1', default: 0 + t.integer 'q4_2', default: 0 + t.integer 'q4_3', default: 0 + t.integer 'q4_4', default: 0 + t.integer 'q4_5', default: 0 + t.string 'q5_text' + t.integer 'q5_1', default: 0 + t.integer 'q5_2', default: 0 + t.integer 'q5_3', default: 0 + t.integer 'q5_4', default: 0 + t.integer 'q5_5', default: 0 + t.string 'q6_text' + t.integer 'q6_1', default: 0 + t.integer 'q6_2', default: 0 + t.integer 'q6_3', default: 0 + t.integer 'q6_4', default: 0 + t.integer 'q6_5', default: 0 + t.string 'q7_text' + t.integer 'q7_1', default: 0 + t.integer 'q7_2', default: 0 + t.integer 'q7_3', default: 0 + t.integer 'q7_4', default: 0 + t.integer 'q7_5', default: 0 + t.string 'q8_text' + t.integer 'q8_1', default: 0 + t.integer 'q8_2', default: 0 + t.integer 'q8_3', default: 0 + t.integer 'q8_4', default: 0 + t.integer 'q8_5', default: 0 + t.string 'q9_text' + t.integer 'q9_1', default: 0 + t.integer 'q9_2', default: 0 + t.integer 'q9_3', default: 0 + t.integer 'q9_4', default: 0 + t.integer 'q9_5', default: 0 + t.string 'q10_text' + t.integer 'q10_1', default: 0 + t.integer 'q10_2', default: 0 + t.integer 'q10_3', default: 0 + t.integer 'q10_4', default: 0 + t.integer 'q10_5', default: 0 + t.string 'q11_text' + t.integer 'q11_1', default: 0 + t.integer 'q11_2', default: 0 + t.integer 'q11_3', default: 0 + t.integer 'q11_4', default: 0 + t.integer 'q11_5', default: 0 + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'operational_metrics' + t.integer 'service_id' + t.index ['collection_id'], name: 'index_omb_cx_reporting_collections_on_collection_id' + t.index ['service_id'], name: 'index_omb_cx_reporting_collections_on_service_id' + end + + create_table 'organizations', force: :cascade do |t| + t.string 'name', null: false + t.string 'url' + t.string 'abbreviation' + t.text 'notes' + t.integer 'external_id' + t.string 'domain' + t.string 'logo' + t.boolean 'enable_ip_address', default: true + t.string 'digital_analytics_path' + t.text 'mission_statement' + t.string 'mission_statement_url' + t.string 'performance_url' + t.string 'strategic_plan_url' + t.string 'learning_agenda_url' + t.boolean 'cfo_act_agency', default: false + t.integer 'parent_id' + t.boolean 'form_approval_enabled', default: false, comment: 'Indicate whether this organization requires a Submission and Approval process for forms' + end + + create_table 'organizations_roles', id: false, force: :cascade do |t| + t.bigint 'organization_id' + t.bigint 'role_id' + t.index %w[organization_id role_id], name: 'index_organizations_roles_on_organization_id_and_role_id' + t.index ['organization_id'], name: 'index_organizations_roles_on_organization_id' + t.index ['role_id'], name: 'index_organizations_roles_on_role_id' + end + + create_table 'personas', force: :cascade do |t| + t.string 'name' + t.text 'description' + t.string 'tags', array: true + t.integer 'user_id' + t.text 'notes' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['tags'], name: 'index_personas_on_tags', using: :gin + end + + create_table 'question_options', force: :cascade do |t| + t.integer 'question_id' + t.string 'text' + t.integer 'position' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.string 'value' + t.boolean 'other_option', default: false + t.index ['question_id'], name: 'index_question_options_on_question_id' + end + + create_table 'questions', force: :cascade do |t| + t.integer 'form_id' + t.string 'text' + t.string 'question_type' + t.string 'answer_field' + t.integer 'position' + t.boolean 'is_required' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.integer 'form_section_id' + t.integer 'character_limit' + t.string 'placeholder_text' + t.string 'help_text' + t.index ['form_id'], name: 'index_questions_on_form_id' + t.index ['form_section_id'], name: 'index_questions_on_form_section_id' + end + + create_table 'registry_searches', force: :cascade do |t| + t.string 'agency' + t.string 'keywords' + t.string 'platform' + t.string 'status' + t.string 'session_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + end + + create_table 'roles', force: :cascade do |t| + t.string 'name' + t.string 'resource_type' + t.bigint 'resource_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index %w[name resource_type resource_id], name: 'index_roles_on_name_and_resource_type_and_resource_id' + t.index %w[resource_type resource_id], name: 'index_roles_on_resource' + end + + create_table 'service_providers', comment: 'A Service Provider, or HISP, as defined in OMB Circular A-11 Section 280', force: :cascade do |t| + t.integer 'organization_id' + t.string 'name' + t.text 'description' + t.text 'notes' + t.string 'slug' + t.string 'department' + t.string 'department_abbreviation' + t.string 'bureau' + t.string 'bureau_abbreviation' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.boolean 'new' + t.boolean 'inactive', default: true, null: false + t.string 'url' + t.integer 'cx_maturity_mapping_value', default: 0 + t.integer 'services_count', default: 0 + t.integer 'impact_mapping_value', default: 0 + t.string 'portfolio_manager_email' + t.integer 'year_designated' + t.index ['organization_id'], name: 'index_service_providers_on_organization_id' + end + + create_table 'service_stages', comment: 'A step or stage within a Service, as used in a Business Process Model. eg: start, middle, end', force: :cascade do |t| + t.string 'name' + t.text 'description' + t.integer 'service_id' + t.text 'notes' + t.integer 'time' + t.integer 'position' + t.integer 'total_eligible_population' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.integer 'persona_id' + t.index ['service_id'], name: 'index_service_stages_on_service_id' + end + + create_table 'services', id: { comment: 'Unique identifier for a Service' }, comment: 'Services provided by an Agency, often by a Service Provider within an Agency', force: :cascade do |t| + t.string 'name', comment: 'Name of the service' + t.text 'description', comment: 'Description of the designated service' + t.integer 'organization_id', comment: 'Unique number for each department. A department may contain several HISPs' + t.text 'notes', comment: 'Field for HISP to provide additional notes' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.boolean 'hisp', default: false, comment: 'True or False - Is this Service considered a HISP service?' + t.string 'department', default: '', comment: 'Abbreviation of department name' + t.string 'bureau', default: '', comment: 'Name of the Bureau to which a service belongs' + t.string 'service_slug', default: '', comment: 'a unique text string to identify the service' + t.string 'url', default: '', comment: 'A website link to their service' + t.integer 'service_provider_id', comment: 'Unique number for each Service Provider' + t.integer 'service_owner_id', comment: 'ID of the User record for which a Service is owned or managed by' + t.string 'kind', comment: 'Identifies the category of service: compliance, administrative, benefits, recreation, informational, data and research, and regulatory', array: true + t.string 'aasm_state', default: 'created', comment: 'State/status that a Service is in. eg: created, submitted, approved, verified, archived' + t.text 'non_digital_explanation', comment: 'If applicable, explain why a service is not available via a digital channel' + t.integer 'service_stages_count', default: 0, comment: 'Helper field that counts how many Service Stages this Service has' + t.string 'homepage_url', comment: 'A primary website link to the service' + t.string 'budget_code', comment: 'The budget code for this service' + t.string 'uii_code', comment: 'The UII code for this service' + t.boolean 'transactional', default: false, comment: 'True or False for whether the service is transactional' + t.boolean 'digital_service', default: false, comment: 'Is this a digital service or not?' + t.string 'estimated_annual_volume_of_customers', default: '', comment: 'Estimated volume of customers on an annual basis' + t.string 'channels', comment: 'One or more channels where the service is delivered', array: true + t.boolean 'fully_digital_service', default: false, comment: 'Is this a fully digital service or not?' + t.text 'barriers_to_fully_digital_service', comment: 'If applicable, describe the barriers preventing this service from being a fully digital service' + t.boolean 'multi_agency_service', default: false, comment: 'Do multiple agencies collaborate to provide this service?' + t.text 'multi_agency_explanation', comment: 'If applicable, describe how multiple agencies collaborate to provide this service' + t.string 'other_service_type' + t.string 'customer_volume_explanation' + t.text 'resources_needed_to_provide_digital_service', comment: 'If applicable, what resources are needed to provide this service digitally?' + t.string 'office', comment: 'Text description for the office (below a Bureau)' + t.boolean 'designated_for_improvement_a11_280', default: false, comment: 'Is this Service designated, per the OMB Circular A-11 Section 280' + t.boolean 'contact_center', default: false, comment: 'True or False for whether the service involves a contact center and/or an interaction with a contact center' + t.integer 'year_designated' + t.text 'short_description' + t.boolean 'previously_reported', default: false + t.integer 'cx_collections_count', default: 0 + t.index ['organization_id'], name: 'index_services_on_organization_id' + end + + create_table 'submissions', force: :cascade do |t| + t.integer 'user_id' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.string 'referer' + t.string 'page' + t.string 'user_agent' + t.text 'answer_01' + t.text 'answer_02' + t.text 'answer_03' + t.text 'answer_04' + t.text 'answer_05' + t.text 'answer_06' + t.text 'answer_07' + t.text 'answer_08' + t.text 'answer_09' + t.text 'answer_10' + t.text 'answer_11' + t.text 'answer_12' + t.text 'answer_13' + t.text 'answer_14' + t.text 'answer_15' + t.text 'answer_16' + t.text 'answer_17' + t.text 'answer_18' + t.text 'answer_19' + t.text 'answer_20' + t.string 'ip_address' + t.string 'location_code' + t.boolean 'flagged', default: false + t.string 'language' + t.integer 'form_id' + t.string 'uuid' + t.string 'aasm_state', default: 'received' + t.string 'hostname' + t.string 'tags', default: [], array: true + t.integer 'spam_score', default: 0 + t.text 'query_string' + t.boolean 'spam', default: false + t.boolean 'archived', default: false + t.boolean 'deleted', default: false + t.datetime 'deleted_at' + t.string 'preview', default: '' + t.string 'spam_prevention_mechanism', default: '', comment: 'Specify which spam prevention mechanism was used, if any.' + t.text 'answer_21' + t.text 'answer_22' + t.text 'answer_23' + t.text 'answer_24' + t.text 'answer_25' + t.text 'answer_26' + t.text 'answer_27' + t.text 'answer_28' + t.text 'answer_29' + t.text 'answer_30' + t.index ['archived'], name: 'index_submissions_on_archived' + t.index ['created_at'], name: 'index_submissions_on_created_at' + t.index ['flagged'], name: 'index_submissions_on_flagged' + t.index ['form_id'], name: 'index_submissions_on_form_id' + t.index ['spam'], name: 'index_submissions_on_spam' + t.index ['uuid'], name: 'index_submissions_on_uuid', unique: true + end + + create_table 'taggings', id: :serial, force: :cascade do |t| + t.integer 'tag_id' + t.string 'taggable_type' + t.integer 'taggable_id' + t.string 'tagger_type' + t.integer 'tagger_id' + t.string 'context', limit: 128 + t.datetime 'created_at', precision: nil + t.string 'tenant', limit: 128 + t.index ['context'], name: 'index_taggings_on_context' + t.index %w[tag_id taggable_id taggable_type context tagger_id tagger_type], name: 'taggings_idx', unique: true + t.index ['tag_id'], name: 'index_taggings_on_tag_id' + t.index %w[taggable_id taggable_type context], name: 'taggings_taggable_context_idx' + t.index %w[taggable_id taggable_type tagger_id context], name: 'taggings_idy' + t.index ['taggable_id'], name: 'index_taggings_on_taggable_id' + t.index ['taggable_type'], name: 'index_taggings_on_taggable_type' + t.index %w[tagger_id tagger_type], name: 'index_taggings_on_tagger_id_and_tagger_type' + t.index ['tagger_id'], name: 'index_taggings_on_tagger_id' + t.index ['tenant'], name: 'index_taggings_on_tenant' + end + + create_table 'tags', id: :serial, force: :cascade do |t| + t.string 'name' + t.datetime 'created_at', precision: nil + t.datetime 'updated_at', precision: nil + t.integer 'taggings_count', default: 0 + t.index ['name'], name: 'index_tags_on_name', unique: true + end + + create_table 'user_roles', force: :cascade do |t| + t.integer 'user_id', null: false + t.integer 'form_id' + t.string 'role' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.index %w[user_id form_id], name: 'index_user_roles_on_user_id_and_form_id', unique: true + end + + create_table 'users', force: :cascade do |t| + t.integer 'organization_id' + t.string 'email', default: '', null: false + t.string 'encrypted_password', default: '', null: false + t.string 'reset_password_token' + t.datetime 'reset_password_sent_at', precision: nil + t.datetime 'remember_created_at', precision: nil + t.integer 'sign_in_count', default: 0, null: false + t.datetime 'current_sign_in_at', precision: nil + t.datetime 'last_sign_in_at', precision: nil + t.inet 'current_sign_in_ip' + t.inet 'last_sign_in_ip' + t.string 'confirmation_token' + t.datetime 'confirmed_at', precision: nil + t.datetime 'confirmation_sent_at', precision: nil + t.string 'unconfirmed_email' + t.datetime 'created_at', precision: nil, null: false + t.datetime 'updated_at', precision: nil, null: false + t.boolean 'admin', default: false + t.string 'provider' + t.string 'uid' + t.boolean 'inactive', default: false, null: false + t.string 'time_zone', default: 'Eastern Time (US & Canada)' + t.string 'api_key' + t.datetime 'api_key_updated_at', precision: nil + t.boolean 'organizational_website_manager', default: false + t.boolean 'performance_manager', default: false + t.boolean 'registry_manager', default: false + t.boolean 'service_manager', default: false + t.string 'first_name' + t.string 'last_name' + t.string 'position_title' + t.string 'profile_photo' + t.boolean 'organizational_admin', default: false + t.boolean 'organizational_form_approver', default: false + t.index ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true + t.index ['email'], name: 'index_users_on_email', unique: true + t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true + end + + create_table 'users_roles', id: false, force: :cascade do |t| + t.bigint 'user_id' + t.bigint 'role_id' + t.index ['role_id'], name: 'index_users_roles_on_role_id' + t.index %w[user_id role_id], name: 'index_users_roles_on_user_id_and_role_id' + t.index ['user_id'], name: 'index_users_roles_on_user_id' + end + + create_table 'versions', force: :cascade do |t| + t.string 'item_type', null: false + t.bigint 'item_id', null: false + t.string 'event', null: false + t.string 'whodunnit' + t.text 'old_object' + t.datetime 'created_at', precision: nil + t.text 'old_object_changes' + t.jsonb 'object' + t.jsonb 'object_changes' + t.index %w[item_type item_id], name: 'index_versions_on_item_type_and_item_id' + end + + create_table 'websites', force: :cascade do |t| + t.string 'domain' + t.string 'office' + t.integer 'office_id' + t.string 'sub_office' + t.integer 'suboffice_id' + t.string 'contact_email' + t.string 'site_owner_email' + t.string 'production_status' + t.string 'type_of_site' + t.string 'digital_brand_category' + t.string 'redirects_to' + t.string 'status_code' + t.string 'cms_platform' + t.string 'required_by_law_or_policy' + t.boolean 'has_dap' + t.string 'dap_gtm_code' + t.string 'cost_estimator_url' + t.string 'modernization_plan_url' + t.float 'annual_baseline_cost' + t.float 'modernization_cost' + t.string 'analytics_url' + t.boolean 'uses_feedback' + t.string 'feedback_tool' + t.string 'sitemap_url' + t.boolean 'mobile_friendly' + t.boolean 'has_search' + t.boolean 'uses_tracking_cookies' + t.boolean 'has_authenticated_experience' + t.string 'authentication_tool' + t.text 'notes' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'repository_url' + t.string 'hosting_platform' + t.float 'modernization_cost_2021' + t.float 'modernization_cost_2022' + t.float 'modernization_cost_2023' + t.string 'uswds_version' + t.boolean 'https' + t.integer 'service_id' + t.integer 'organization_id' + t.string 'backlog_tool', default: '' + t.string 'backlog_url', default: '' + t.string 'aasm_state' + t.date 'target_decommission_date' + t.index ['aasm_state'], name: 'index_websites_on_aasm_state' + t.index ['organization_id'], name: 'index_websites_on_organization_id' + t.index ['service_id'], name: 'index_websites_on_service_id' + end + + add_foreign_key 'active_storage_attachments', 'active_storage_blobs', column: 'blob_id' + add_foreign_key 'active_storage_variant_records', 'active_storage_blobs', column: 'blob_id' + add_foreign_key 'taggings', 'tags' end 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/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, From ec1ecabc4cf1776ac4179321bb0fb39c2ed6e9fa Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Mon, 21 Jul 2025 07:29:55 -0500 Subject: [PATCH 11/25] update test config --- config/database.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/database.yml b/config/database.yml index 05325a748..10bdcf7c5 100644 --- a/config/database.yml +++ b/config/database.yml @@ -58,9 +58,9 @@ development: test: <<: *default # Uncomment host and username if running docker containers ( docker-compose ) - host: localhost - username: postgres - password: changeme + # host: localhost + # username: postgres + # password: changeme database: touchpoints_test # As with config/secrets.yml, you never want to store sensitive information, From e01183b18911341856bfeaccec7f8ec4be67e659 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Mon, 21 Jul 2025 16:16:51 -0500 Subject: [PATCH 12/25] update max question logic and test --- app/models/form.rb | 2 +- config/database.yml | 6 +++--- spec/models/form_spec.rb | 41 +++++++++++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/models/form.rb b/app/models/form.rb index a96279d60..059857ca9 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -867,7 +867,7 @@ def ensure_a11_v2_radio_format end def warn_about_not_too_many_questions - errors.add(:base, "Touchpoints supports a maximum of 30 questions. There are currently #{questions_count} questions. Fewer questions tend to yield higher response rates.") if questions.size > 12 + errors.add(:base, "Touchpoints supports a maximum of 30 questions. There are currently #{questions_count} questions. Fewer questions tend to yield higher response rates.") if questions.size > 20 end def contains_elements?(array, required_elements) diff --git a/config/database.yml b/config/database.yml index 10bdcf7c5..05325a748 100644 --- a/config/database.yml +++ b/config/database.yml @@ -58,9 +58,9 @@ development: test: <<: *default # Uncomment host and username if running docker containers ( docker-compose ) - # host: localhost - # username: postgres - # password: changeme + host: localhost + username: postgres + password: changeme database: touchpoints_test # As with config/secrets.yml, you never want to store sensitive information, 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 From 1b852bd82af88bee3f6197429a367c2204b35835 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Mon, 21 Jul 2025 16:21:42 -0500 Subject: [PATCH 13/25] test config --- config/database.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/database.yml b/config/database.yml index 05325a748..10bdcf7c5 100644 --- a/config/database.yml +++ b/config/database.yml @@ -58,9 +58,9 @@ development: test: <<: *default # Uncomment host and username if running docker containers ( docker-compose ) - host: localhost - username: postgres - password: changeme + # host: localhost + # username: postgres + # password: changeme database: touchpoints_test # As with config/secrets.yml, you never want to store sensitive information, From 8893c5e17c176fd2210d48875df66ad49d31d68d Mon Sep 17 00:00:00 2001 From: Jonathan Hutchison Date: Tue, 22 Jul 2025 12:50:40 -0400 Subject: [PATCH 14/25] Remove redundant id's and aria-label attributes Signed-off-by: Jonathan Hutchison --- .../question_types/_big_thumbs_up_down_buttons.html.erb | 8 -------- 1 file changed, 8 deletions(-) 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 - Date: Thu, 24 Jul 2025 20:22:34 +0000 Subject: [PATCH 15/25] fix: Gemfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-RACK-10303186 - https://snyk.io/vuln/SNYK-RUBY-THOR-10843853 --- Gemfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index a4c83f2c8..73f11a8bc 100644 --- a/Gemfile +++ b/Gemfile @@ -16,10 +16,10 @@ gem "pg" 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" @@ -99,10 +99,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 From 51c13d890224ad757e09c795f7f9ea2367708100 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:40:31 +0000 Subject: [PATCH 16/25] Bump nokogiri from 1.18.8 to 1.18.9 Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.18.8 to 1.18.9. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.8...v1.18.9) --- updated-dependencies: - dependency-name: nokogiri dependency-version: 1.18.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index be5895b16..1c68240be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,9 +214,9 @@ GEM logger factory_bot (6.5.1) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.0) factory_bot (~> 6.5) - railties (>= 5.0.0) + railties (>= 6.1.0) faker (3.5.1) i18n (>= 1.8.11, < 2) faraday (2.13.1) @@ -263,7 +263,7 @@ 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.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -332,7 +332,7 @@ GEM benchmark 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) @@ -351,24 +351,24 @@ GEM net-protocol newrelic_rpm (9.19.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) @@ -499,7 +499,7 @@ GEM rspec-mocks (3.13.3) 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) @@ -586,7 +586,7 @@ GEM thread_safe (0.3.6) tilt (2.6.0) 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) @@ -654,11 +654,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 +684,7 @@ DEPENDENCIES redis-client redis-namespace rolify - rspec-rails + rspec-rails (>= 8.0.1) rspec_junit_formatter rubocop-rails rubocop-rspec @@ -694,7 +694,7 @@ DEPENDENCIES simplecov sprockets-rails stimulus-rails - turbo-rails + turbo-rails (>= 2.0.14) tzinfo-data web-console From e9abf64b225078670f4e2d61923050ee9e89057a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:42:37 +0000 Subject: [PATCH 17/25] Bump thor from 1.3.2 to 1.4.0 Bumps [thor](https://github.com/rails/thor) from 1.3.2 to 1.4.0. - [Release notes](https://github.com/rails/thor/releases) - [Commits](https://github.com/rails/thor/compare/v1.3.2...v1.4.0) --- updated-dependencies: - dependency-name: thor dependency-version: 1.4.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index be5895b16..29dd84c32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,9 +214,9 @@ GEM logger factory_bot (6.5.1) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.0) factory_bot (~> 6.5) - railties (>= 5.0.0) + railties (>= 6.1.0) faker (3.5.1) i18n (>= 1.8.11, < 2) faraday (2.13.1) @@ -263,7 +263,7 @@ 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.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -499,7 +499,7 @@ GEM rspec-mocks (3.13.3) 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) @@ -582,11 +582,11 @@ GEM 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) 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) @@ -654,11 +654,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 +684,7 @@ DEPENDENCIES redis-client redis-namespace rolify - rspec-rails + rspec-rails (>= 8.0.1) rspec_junit_formatter rubocop-rails rubocop-rspec @@ -694,7 +694,7 @@ DEPENDENCIES simplecov sprockets-rails stimulus-rails - turbo-rails + turbo-rails (>= 2.0.14) tzinfo-data web-console From 8cc6815f31ffdf9f0adab49d987f0467766586d5 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Fri, 1 Aug 2025 17:59:57 -0500 Subject: [PATCH 18/25] Upgrade dependencies --- Gemfile | 3 +- Gemfile.lock | 180 +++++++++++++++++++++++++++------------------------ 2 files changed, 95 insertions(+), 88 deletions(-) diff --git a/Gemfile b/Gemfile index 7cb9d0d96..00686ad0a 100644 --- a/Gemfile +++ b/Gemfile @@ -80,7 +80,7 @@ group :development, :test do gem 'pry' end -group :development, :staging do +group :development, :staging, :test do gem 'faker' end @@ -100,7 +100,6 @@ group :test do gem 'axe-core-rspec' gem 'capybara' gem 'database_cleaner' - gem 'faker' gem 'factory_bot_rails', '>= 6.5.0' gem 'rails-controller-testing' gem 'rspec_junit_formatter' diff --git a/Gemfile.lock b/Gemfile.lock index c52fcc969..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.5.0) factory_bot (~> 6.5) railties (>= 6.1.0) - faker (3.5.1) + 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.2.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.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,7 +350,7 @@ 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.9) mini_portile2 (~> 2.8.2) @@ -370,13 +371,14 @@ GEM racc (~> 1.4) 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,12 +499,12 @@ 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.1) @@ -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,13 +586,13 @@ 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.4.0) thread_safe (0.3.6) - tilt (2.6.0) + tilt (2.6.1) timeout (0.4.3) turbo-rails (2.0.16) actionpack (>= 7.1.0) @@ -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 From 5b7affeb1f324071ccab48f1672c15f181ea473a Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 11:40:44 -0500 Subject: [PATCH 19/25] update form test --- app/models/form.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/form.rb b/app/models/form.rb index 059857ca9..3e451c33f 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -867,7 +867,9 @@ def ensure_a11_v2_radio_format end def warn_about_not_too_many_questions - errors.add(:base, "Touchpoints supports a maximum of 30 questions. There are currently #{questions_count} questions. Fewer questions tend to yield higher response rates.") if questions.size > 20 + 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 def contains_elements?(array, required_elements) From 9d62a81e649b244915a35e06557bf050a0400cea Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 11:42:35 -0500 Subject: [PATCH 20/25] update question limit validation to allow exactly 30 questions --- app/models/form.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/form.rb b/app/models/form.rb index 3e451c33f..96124a574 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -867,7 +867,7 @@ def ensure_a11_v2_radio_format end def warn_about_not_too_many_questions - if questions.size > 30 + 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 From 04c6ae587694ff2485992d4349b8b41cab53c4f2 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 12:02:54 -0500 Subject: [PATCH 21/25] Update lib/fiscal_year.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Riley Seaburg --- lib/fiscal_year.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fiscal_year.rb b/lib/fiscal_year.rb index 732f7e762..f30687ed6 100644 --- a/lib/fiscal_year.rb +++ b/lib/fiscal_year.rb @@ -2,7 +2,7 @@ module FiscalYear def self.fiscal_quarter_dates(fiscal_year, fiscal_quarter) fiscal_year = fiscal_year.to_i start_date, end_date = case fiscal_quarter.to_i - when 1 then [Date.new(fiscal_year - 1, 10, 1), Date.new(fiscal_year - 1, 12, 31)] # Q1: Oct - Dec of the prior calendar year relativ to the fiscal year + when 1 then [Date.new(fiscal_year - 1, 10, 1), Date.new(fiscal_year - 1, 12, 31)] # Q1: Oct - Dec of the prior calendar year relative to the fiscal year when 2 then [Date.new(fiscal_year, 1, 1), Date.new(fiscal_year, 3, 31)] # Q2: Jan - Mar when 3 then [Date.new(fiscal_year, 4, 1), Date.new(fiscal_year, 6, 30)] # Q3: Apr - Jun when 4 then [Date.new(fiscal_year, 7, 1), Date.new(fiscal_year, 9, 30)] # Q4: Jul - Sep From b8e298caae8c6eacaad88879f0f6665bc8ec4d0b Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 12:03:07 -0500 Subject: [PATCH 22/25] Update app/models/cx_collection_detail_upload.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Riley Seaburg --- app/models/cx_collection_detail_upload.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/cx_collection_detail_upload.rb b/app/models/cx_collection_detail_upload.rb index 5436f2a7a..e972a089f 100644 --- a/app/models/cx_collection_detail_upload.rb +++ b/app/models/cx_collection_detail_upload.rb @@ -93,7 +93,7 @@ def upload_form_results(form_id:, start_date:, end_date:) cx_collection_detail_id: cx_collection_detail.id, cx_collection_detail_upload_id: self.id, job_id: job_id, - external_id: response[0], + external_id: response[:id], question_1: response[:answer_01], positive_effectiveness: response[:answer_02_effectiveness], positive_ease: response[:answer_02_ease], From fe8d4b1a25d8edf4a9e05e30439112c7019b63b9 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 12:03:35 -0500 Subject: [PATCH 23/25] Update app/controllers/admin/cx_collection_details_controller.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Riley Seaburg --- app/controllers/admin/cx_collection_details_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/cx_collection_details_controller.rb b/app/controllers/admin/cx_collection_details_controller.rb index f2d29e550..a2d5f652c 100644 --- a/app/controllers/admin/cx_collection_details_controller.rb +++ b/app/controllers/admin/cx_collection_details_controller.rb @@ -23,7 +23,7 @@ def new @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.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 From 6cd518692638515b2fbe0e9bf531604cb3465ece Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 12:03:53 -0500 Subject: [PATCH 24/25] Update app/controllers/admin/cx_collection_details_controller.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Riley Seaburg --- app/controllers/admin/cx_collection_details_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/cx_collection_details_controller.rb b/app/controllers/admin/cx_collection_details_controller.rb index a2d5f652c..54b2b399b 100644 --- a/app/controllers/admin/cx_collection_details_controller.rb +++ b/app/controllers/admin/cx_collection_details_controller.rb @@ -24,7 +24,7 @@ def new @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_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 From b4886e452433437b2362e455a2b77f155c966c8c Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 11 Aug 2025 12:04:03 -0500 Subject: [PATCH 25/25] Update app/views/admin/cx_collection_details/upload.html.erb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Riley Seaburg --- app/views/admin/cx_collection_details/upload.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/cx_collection_details/upload.html.erb b/app/views/admin/cx_collection_details/upload.html.erb index f3c0defba..12694fed6 100644 --- a/app/views/admin/cx_collection_details/upload.html.erb +++ b/app/views/admin/cx_collection_details/upload.html.erb @@ -87,7 +87,7 @@ <%= upload.user.email %>
- <%= link_to "Uploaded file", s3_presigned_url(upload.key) if upload.key %> + <%= link_to "Uploaded file", s3_presigned_url(upload.key) if upload.key.present? %> <%= upload.size %>