From 6ff20f2c990a553ab25eaf668882e5e1d1374312 Mon Sep 17 00:00:00 2001 From: Ryan Wold <64987852+ryanwoldatwork@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:36:59 -0700 Subject: [PATCH] admin can enable turnstile option * add location code help * show IP address for a submission * persist text responses prior to submission * asset that text entered on a form persists, and is cleared on successful submission * note spam_prevention_mechanism * attach touchpoints form to window --- app/controllers/submissions_controller.rb | 8 ++- app/models/submission.rb | 1 + app/views/admin/forms/_admin_options.html.erb | 23 ++++++++- .../forms/_form_manager_options.html.erb | 12 ----- app/views/admin/submissions/show.html.erb | 17 ++++++- app/views/components/widget/_fba.js.erb | 49 ++++++++++++++----- ...17_submission_spam_prevention_mechanism.rb | 5 ++ db/schema.rb | 3 +- spec/features/touchpoints_spec.rb | 21 ++++++++ 9 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 db/migrate/20250402195517_submission_spam_prevention_mechanism.rb diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 6c645a431..3a6cdfd61 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -81,8 +81,12 @@ def create private def create_in_local_database(submission) - if submission.form.enable_turnstile? && !verify_turnstile(params["cf-turnstile-response"]) - submission.errors.add(:base, "Turnstile verification failed") + if submission.form.enable_turnstile? + if verify_turnstile(params["cf-turnstile-response"]) + submission.spam_prevention_mechanism = :turnstile + else + submission.errors.add(:base, "Turnstile verification failed") + end end respond_to do |format| diff --git a/app/models/submission.rb b/app/models/submission.rb index 9efb6fffd..22b441cb1 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -65,6 +65,7 @@ def validate_custom_form answered_questions.delete('referer') answered_questions.delete('aasm_state') answered_questions.delete('tags') + answered_questions.delete('spam_prevention_mechanism') answered_questions.delete('spam_score') answered_questions.delete('flagged') answered_questions.delete('spam') diff --git a/app/views/admin/forms/_admin_options.html.erb b/app/views/admin/forms/_admin_options.html.erb index 0aaddfa11..04230a7aa 100644 --- a/app/views/admin/forms/_admin_options.html.erb +++ b/app/views/admin/forms/_admin_options.html.erb @@ -58,11 +58,30 @@
- enforce_new_submission_validations + Enable Cloudfront Turnstile +
+ <%= f.check_box :enable_turnstile, class: "usa-checkbox__input" %> + <%= f.label :enable_turnstile, class: "usa-checkbox__label" do %> + Enable Cloudfront Turnstile +   + + A spam prevention mechanism + + <% end %> +
+
+
+
+
+ Enforce new submission validations
<%= f.check_box :enforce_new_submission_validations, class: "usa-checkbox__input" %> <%= f.label :enforce_new_submission_validations, class: "usa-checkbox__label" do %> - enforce_new_submission_validations + Enforce submission validations +   + + Validate question responses match question types (helps some types of spam) + <% end %>
diff --git a/app/views/admin/forms/_form_manager_options.html.erb b/app/views/admin/forms/_form_manager_options.html.erb index 5815d269b..fc04762ec 100644 --- a/app/views/admin/forms/_form_manager_options.html.erb +++ b/app/views/admin/forms/_form_manager_options.html.erb @@ -95,18 +95,6 @@ <%= f.text_field :expiration_date, class: "usa-input" %>
-
- Enable Cloudfront Turnstile -
- <%= f.check_box :enable_turnstile, class: "usa-checkbox__input" %> - <%= f.label :enable_turnstile, class: "usa-checkbox__label" do %> - Enable Cloudfront Turnstile - - As a spam prevention mechanism - - <% end %> -
-
Append ID to the form's Success Text
diff --git a/app/views/admin/submissions/show.html.erb b/app/views/admin/submissions/show.html.erb index 95f37dff4..a0130dd7e 100644 --- a/app/views/admin/submissions/show.html.erb +++ b/app/views/admin/submissions/show.html.erb @@ -126,6 +126,11 @@ Location code +   + + <%= h(@submission.location_code) %> @@ -139,6 +144,14 @@ <%= h(@submission.user_agent) %> + + + IP Address + + + <%= h(@submission.ip_address) %> + + Submitted from hostname @@ -168,7 +181,7 @@ Referer - <%= sanitize(@submission.referer) %> + <%= h(@submission.referer) %> @@ -176,7 +189,7 @@ Language - <%= @submission.language %> + <%= h(@submission.language) %> diff --git a/app/views/components/widget/_fba.js.erb b/app/views/components/widget/_fba.js.erb index 35e3ee987..8c9f2dde5 100644 --- a/app/views/components/widget/_fba.js.erb +++ b/app/views/components/widget/_fba.js.erb @@ -10,6 +10,9 @@ function FBAform(d, N) { formElement: function() { return this.formComponent().querySelector("form"); }, + formLocalStorageKey: function() { + return `touchpoints:${this.options.formId}` + }, isFormSubmitted: false, // defaults to false // enable Javascript experience javascriptIsEnabled: function() { @@ -38,9 +41,10 @@ function FBAform(d, N) { if (this.options.formSpecificScript) { this.options.formSpecificScript(); } - <% if form.enable_turnstile? %> - this.loadTurnstile() + <%- if form.enable_turnstile? %> + this.loadTurnstile(); <% end %> + this.enableLocalStorage(); d.dispatchEvent(new CustomEvent('onTouchpointsFormLoaded', { detail: { formComponent: this @@ -433,6 +437,7 @@ function FBAform(d, N) { if (formElement) { // And clear the Form's Fields formElement.reset(); + localStorage.removeItem(this.formLocalStorageKey()); if (formElement.querySelector('.touchpoints-form-body')) { var formBody = formElement.querySelector('.touchpoints-form-body'); if(formBody) { @@ -555,7 +560,7 @@ function FBAform(d, N) { xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8;"); xhr.onload = callback.bind(this); xhr.send(JSON.stringify({ - <% if form.enable_turnstile? %> + <%- if form.enable_turnstile? %> "cf-turnstile-response" : form.querySelector("input[name='cf-turnstile-response']") ? form.querySelector("input[name='cf-turnstile-response']").value : null, <% end %> "submission": params, @@ -645,23 +650,46 @@ function FBAform(d, N) { modalId: function() { return `fba-modal-${this.options.formId}`; }, - <% if form.enable_turnstile? %> + <%- if form.enable_turnstile? %> loadTurnstile: function() { let script = document.createElement("script"); script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js"; script.async = true; script.defer = true; - script.onload = function() { - document.querySelector("input[name='cf-turnstile-response']").value = token; - }; document.head.appendChild(script); }, <% end %> + enableLocalStorage: function() { + const form = this.formElement(); + const savedData = localStorage.getItem(this.formLocalStorageKey()); + + <%# Restore form data from localStorage %> + if (savedData) { + const formData = JSON.parse(savedData); + for (const key in formData) { + const input = form.querySelector(`[name="${key}"]`); + if (input) { + input.value = formData[key]; + } + } + } + + <%# Save data to localStorage as the user types %> + form.addEventListener('input', (event) => { + const inputData = {}; + const formData = new FormData(form); + formData.forEach((value, key) => { + inputData[key] = value; + }); + + localStorage.setItem(this.formLocalStorageKey(), JSON.stringify(inputData)); + }); + }, }; }; // Specify the options for your form -const touchpointFormOptions<%= form.short_uuid %> = { +var touchpointFormOptions<%= form.short_uuid %> = { 'formId': "<%= form.short_uuid %>", 'modalButtonText': "<%= form.modal_button_text %>", 'elementSelector': "<%= form.element_selector %>", @@ -703,10 +731,10 @@ const touchpointFormOptions<%= form.short_uuid %> = { } // Create an instance of a Touchpoints form object -const touchpointForm<%= form.short_uuid %> = new FBAform(document, window).init(touchpointFormOptions<%= form.short_uuid %>); +window.touchpointForm<%= form.short_uuid %> = new FBAform(document, window); +window.touchpointForm<%= form.short_uuid %>.init(touchpointFormOptions<%= form.short_uuid %>); <%- if form.load_css && form.delivery_method != "touchpoints-hosted-only" %> - // Load the USWDS JS, loads as module 'fbaUswds' in global scope <%= render partial: 'components/widget/widget-uswds', formats: :js %> @@ -724,5 +752,4 @@ const touchpointForm<%= form.short_uuid %> = new FBAform(document, window).init( fbaUswds.Modal.on(fbaModalElement); } })(); - <% end %> \ No newline at end of file diff --git a/db/migrate/20250402195517_submission_spam_prevention_mechanism.rb b/db/migrate/20250402195517_submission_spam_prevention_mechanism.rb new file mode 100644 index 000000000..23986155e --- /dev/null +++ b/db/migrate/20250402195517_submission_spam_prevention_mechanism.rb @@ -0,0 +1,5 @@ +class SubmissionSpamPreventionMechanism < ActiveRecord::Migration[8.0] + def change + add_column :submissions, :spam_prevention_mechanism, :string, default: "", comment: "Specify which spam prevention mechanism was used, if any." + end +end diff --git a/db/schema.rb b/db/schema.rb index 35f2e180f..1ca396e5a 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[8.0].define(version: 2025_04_01_223209) do +ActiveRecord::Schema[8.0].define(version: 2025_04_02_195517) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -613,6 +613,7 @@ 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" diff --git a/spec/features/touchpoints_spec.rb b/spec/features/touchpoints_spec.rb index dd4938f5c..4ee85b3a4 100644 --- a/spec/features/touchpoints_spec.rb +++ b/spec/features/touchpoints_spec.rb @@ -20,6 +20,27 @@ expect(page).to be_axe_clean end + describe 'persist text responses in localStorage' do + let(:two_question_form) { FactoryBot.create(:form, :two_question_open_ended_form, organization:) } + + before do + visit touchpoint_path(two_question_form) + fill_in(two_question_form.ordered_questions.first.ui_selector, with: 'Question one') + fill_in(two_question_form.ordered_questions.last.ui_selector, with: 'Question two') + visit touchpoint_path(two_question_form) + end + + it "enters text, refreshes to ensure it still there, submits, and ensures it has been cleared" do + expect(find("#" + two_question_form.ordered_questions.first.ui_selector).value).to eq('Question one') + expect(find("#" + two_question_form.ordered_questions.last.ui_selector).value).to eq('Question two') + click_button 'Submit' + expect(page).to have_content('Thank you. Your feedback has been received.') + visit touchpoint_path(two_question_form) + expect(find("#" + two_question_form.ordered_questions.first.ui_selector).value).to be_blank + expect(find("#" + two_question_form.ordered_questions.last.ui_selector).value).to be_blank + end + end + context 'default success text' do before do visit touchpoint_path(form)