diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 781c2de88..aae1e7072 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -27,6 +27,7 @@ //= require dietary-restrictions //= require cocoon //= require font_awesome5 +//= require how-you-found-us $(function() { $("body").removeClass("no-js"); diff --git a/app/assets/javascripts/how-you-found-us.js b/app/assets/javascripts/how-you-found-us.js new file mode 100644 index 000000000..20105c3fe --- /dev/null +++ b/app/assets/javascripts/how-you-found-us.js @@ -0,0 +1,20 @@ +$(document).ready(function() { + const $otherRadioButton = $('#member_how_you_found_us_other'); + const $otherReason = $('#member_how_you_found_us_other_reason'); + const $elementToToggle = $otherReason.parent(); + + function toggleOtherReason() { + if ($otherRadioButton.is(':checked')) { + $elementToToggle.removeClass('d-none').hide().slideDown(50); + $otherReason.prop('disabled', false).focus(); // Optional — disabling is not needed + } else { + $elementToToggle.slideUp(50, function() { + $elementToToggle.addClass('d-none'); + $otherReason.val(''); + }); + } + } + + toggleOtherReason(); + $('input[name="member[how_you_found_us]"]').on('change', toggleOtherReason); +}); \ No newline at end of file diff --git a/app/controllers/concerns/member_concerns.rb b/app/controllers/concerns/member_concerns.rb index fa8f7eed4..1f4abffb6 100644 --- a/app/controllers/concerns/member_concerns.rb +++ b/app/controllers/concerns/member_concerns.rb @@ -10,8 +10,8 @@ module InstanceMethods def member_params params.require(:member).permit( - :pronouns, :name, :surname, :email, :mobile, :about_you, :skill_list, :newsletter, :other_dietary_restrictions, - dietary_restrictions: [], + :pronouns, :name, :surname, :email, :mobile, :about_you, :skill_list, :newsletter, :other_dietary_restrictions, :how_you_found_us, + :how_you_found_us_other_reason, dietary_restrictions: [] ).tap do |params| # We want to keep Rails' hidden blank field in the form so that all dietary restrictions for a member can be # removed by submitting the form with all check boxes unticked. However, we want to remove the blank value @@ -29,5 +29,14 @@ def suppress_notices def set_member @member = current_user end + + def how_you_found_us_selections_valid? + how_found_present = member_params[:how_you_found_us].present? + other_reason_present = member_params[:how_you_found_us_other_reason].present? + return false if member_params[:how_you_found_us] == 'other' && !other_reason_present + return true if member_params[:how_you_found_us] == 'other' && other_reason_present + + how_found_present != other_reason_present + end end end diff --git a/app/controllers/member/details_controller.rb b/app/controllers/member/details_controller.rb index 403e460d2..848a01e10 100644 --- a/app/controllers/member/details_controller.rb +++ b/app/controllers/member/details_controller.rb @@ -8,13 +8,26 @@ class Member::DetailsController < ApplicationController def edit accept_terms - flash[notice] = I18n.t('notifications.signing_up') @member.newsletter ||= true end def update - return render :edit unless @member.update(member_params) + attrs = member_params + attrs[:how_you_found_us_other_reason] = nil if attrs[:how_you_found_us] != 'other' + + unless how_you_found_us_selections_valid? + @member.errors.add(:how_you_found_us, 'You must select one option') + return render :edit + end + attrs[:how_you_found_us] = params[:member][:how_you_found_us] if params[:member][:how_you_found_us].present? + + if params[:member][:how_you_found_us_other_reason].present? && attrs[:how_you_found_us] == 'other' + attrs[:how_you_found_us_other_reason] = + params[:member][:how_you_found_us_other_reason] + end + + return render :edit unless @member.update(attrs) member_params[:newsletter] ? subscribe_to_newsletter(@member) : unsubscribe_from_newsletter(@member) redirect_to step2_member_path diff --git a/app/models/member.rb b/app/models/member.rb index 59ef29027..81b7d8905 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,6 +1,14 @@ class Member < ApplicationRecord include Permissions + enum how_you_found_us: { + from_a_friend: 0, + search_engine: 1, + social_media: 2, + codebar_host_or_partner: 3, + other: 4 + } + has_many :attendance_warnings has_many :bans has_many :eligibility_inquiries diff --git a/app/views/member/details/edit.html.haml b/app/views/member/details/edit.html.haml index 1fc548ffe..37eefda82 100644 --- a/app/views/member/details/edit.html.haml +++ b/app/views/member/details/edit.html.haml @@ -19,6 +19,30 @@ label_method: ->(r) { r.humanize.upcase_first } = f.input :other_dietary_restrictions, placeholder: 'Other dietary restrictions', wrapper_html: { class: class_names('mt-n3', 'd-none': !@member.other_dietary_restrictions?) }, label_html: { class: 'sr-only' } + - if @member.errors.any? + #error_explanation + %h2= "#{pluralize(@member.errors.count, 'error')} prohibited this member from being saved:" + %ul + - @member.errors.full_messages.each do |msg| + %li= msg + - if @member.errors[:how_you_found_us]&.any? + %span.text-danger= @member.errors[:how_you_found_us].first + .col-12.mb-3 + %fieldset + = f.input :how_you_found_us, + as: :radio_buttons, + collection: Member.how_you_found_us.keys, + label_method: ->(option) { t("member.details.edit.how_you_found_us_options.#{option}") }, + value_method: :to_s, + label: t('member.details.edit.how_you_found_us_label'), + item_wrapper_class: 'form-check d-flex align-items-center mb-2', + label_item: true, + input_html: { class: 'form-check-input me-2', style: 'margin-top: 0;' }, + label_html: { class: 'form-check-label', style: 'margin-left: 0;' } + + = f.input :how_you_found_us_other_reason, + label: t('member.details.edit.how_you_found_us_other_reason'), + input_html: { class: 'form-control w-100' } = f.input :newsletter, as: :boolean, checked_value: true, unchecked_value: false .text-right.mb-4 = hidden_field_tag :next_page, step2_member_path(member_type: @type) diff --git a/config/locales/en.yml b/config/locales/en.yml index a9ccfb5ac..a3eaf9372 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -442,6 +442,14 @@ en: edit: title: Almost there... summary: We need some more details from you to finish creating your account. We use these to help run our events. + how_you_found_us_label: "How did you find out about us?*" + how_you_found_us_other_reason: "Please specify how you found us" + how_you_found_us_options: + from_a_friend: "From a friend" + search_engine: "Search engine (Google etc.)" + social_media: "Social media" + codebar_host_or_partner: "One of Codebar's hosts or partners" + other: "Other" coach: about_you: What experience do you have? What languages do you like to use? Tell us a little bit about yourself! student: diff --git a/db/migrate/20250823151717_add_how_you_found_us_options.rb b/db/migrate/20250823151717_add_how_you_found_us_options.rb new file mode 100644 index 000000000..b247c3a77 --- /dev/null +++ b/db/migrate/20250823151717_add_how_you_found_us_options.rb @@ -0,0 +1,6 @@ +class AddHowYouFoundUsOptions < ActiveRecord::Migration[7.0] + def change + add_column :members, :how_you_found_us, :integer + add_column :members, :how_you_found_us_other_reason, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 5c5dbc99c..6c2636bc6 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.1].define(version: 2025_08_20_145012) do +ActiveRecord::Schema[7.1].define(version: 2025_08_23_151717) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -397,6 +397,8 @@ t.datetime "opt_in_newsletter_at", precision: nil t.enum "dietary_restrictions", default: [], array: true, enum_type: "dietary_restriction_enum" t.string "other_dietary_restrictions" + t.integer "how_you_found_us" + t.string "how_you_found_us_other_reason" t.index ["email"], name: "index_members_on_email", unique: true end diff --git a/spec/controllers/member/details_controller_spec.rb b/spec/controllers/member/details_controller_spec.rb new file mode 100644 index 000000000..9c98c487b --- /dev/null +++ b/spec/controllers/member/details_controller_spec.rb @@ -0,0 +1,102 @@ +RSpec.describe Member::DetailsController do + render_views + let(:member) { Fabricate(:member) } + + before do + allow(controller).to receive(:current_user).and_return(member) + allow_any_instance_of(Services::MailingList).to receive(:subscribe).and_return(true) + end + + describe 'PATCH #update' do + context 'with valid params' do + it 'updates how_you_found_us with radio option' do + patch :update, params: { + id: member.id, + member: { + how_you_found_us: 'social_media', + newsletter: 'true' + } + } + + member.reload + expect(I18n.t("member.details.edit.how_you_found_us_options.#{member.how_you_found_us}")).to eq('Social media') + expect(member.how_you_found_us_other_reason).to eq(nil) + expect(response).to redirect_to(step2_member_path) + end + + it 'adds other_reason to how_you_found_us when provided' do + patch :update, params: { + id: member.id, + member: { + how_you_found_us: 'other', + how_you_found_us_other_reason: 'Saw a pamphlet', + newsletter: 'false' + }, + } + + member.reload + expect(member.how_you_found_us).to eq('other') + expect(member.how_you_found_us_other_reason).to eq('Saw a pamphlet') + expect(response).to redirect_to(step2_member_path) + end + + it 'updates how_you_found_us with only other_reason' do + patch :update, params: { + id: member.id, + member: { + how_you_found_us: 'other', + how_you_found_us_other_reason: 'At a meetup', + newsletter: 'true' + }, + } + + member.reload + expect(member.how_you_found_us).to eq('other') + expect(member.how_you_found_us_other_reason).to eq('At a meetup') + expect(response).to redirect_to(step2_member_path) + end + + it 'removes duplicates and blank entries' do + patch :update, params: { + id: member.id, + member: { + how_you_found_us: 'other', + how_you_found_us_other_reason: 'From a colleague', + newsletter: 'true' + }, + } + + member.reload + expect(member.how_you_found_us).to eq('other') + expect(member.how_you_found_us_other_reason).to eq('From a colleague') + expect(response).to redirect_to(step2_member_path) + end + end + + context 'when update fails (invalid data)' do + it 'error raised when no how you found us selection given' do + patch :update, params: { + id: member.id, + member: { + how_you_found_us: 'other', + how_you_found_us_other_reason: nil, + } + } + + expect(response.body).to include('You must select one option') + end + + it 'error raised when both how you found us fields popoulated' do + patch :update, params: { + id: member.id, + member: { + how_you_found_us: 'from_a_friend', + how_you_found_us_other_reason: 'something else', + } + } + + expect(response.body).to include('You must select one option') + end + end + end +end diff --git a/spec/features/member_joining_spec.rb b/spec/features/member_joining_spec.rb index e8050b037..bf844bae7 100644 --- a/spec/features/member_joining_spec.rb +++ b/spec/features/member_joining_spec.rb @@ -15,7 +15,7 @@ end scenario 'A visitor must fill in all mandatory fields in order to sign up' do - member = Fabricate(:member, name: nil, surname: nil, email: nil, about_you: nil) + member = Fabricate(:member, name: nil, surname: nil, email: nil, about_you: nil, how_you_found_us: nil, how_you_found_us_other_reason: nil) member.update(can_log_in: true) login member @@ -27,6 +27,7 @@ expect(page).to have_content "Surname can't be blank" expect(page).to have_content "Email address can't be blank" expect(page).to have_content "About you can't be blank" + expect(page).to have_content "You must select one option" end scenario 'A new member details are successfully captured' do @@ -43,6 +44,11 @@ check 'Vegan' check 'Other' fill_in 'Other dietary restrictions', with: 'peanut allergy' + find('#member_how_you_found_us_from_a_friend').click + + find('#member_how_you_found_us_other').click + expect(page).to have_content('Please specify how you found us') + fill_in 'member_how_you_found_us_other_reason', with: 'found on a poster', id: true click_on 'Next' expect(page).to have_current_path(step2_member_path) diff --git a/spec/features/subscribing_to_newsletter_spec.rb b/spec/features/subscribing_to_newsletter_spec.rb index 38c24b940..7a166d009 100644 --- a/spec/features/subscribing_to_newsletter_spec.rb +++ b/spec/features/subscribing_to_newsletter_spec.rb @@ -27,6 +27,7 @@ fill_in 'member_surname', with: 'Doe' fill_in 'member_email', with: 'jane@codebar.io' fill_in 'member_about_you', with: Faker::Lorem.paragraph + find('#member_how_you_found_us_from_a_friend').click click_on 'Next' end @@ -46,6 +47,8 @@ fill_in 'member_surname', with: 'Doe' fill_in 'member_email', with: 'jane@codebar.io' fill_in 'member_about_you', with: Faker::Lorem.paragraph + find('#member_how_you_found_us_other').click + fill_in 'member_how_you_found_us_other_reason', with: Faker::Lorem.paragraph, id: true uncheck 'newsletter'