diff --git a/.tool-versions b/.tool-versions index 88e34f9..cfaa906 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -ruby 3.3.4 +ruby 3.4.9 nodejs 18.18.2 bundler 2.5.6 yarn 1.22.19 diff --git a/Gemfile b/Gemfile index 08d44aa..f19be3a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,16 @@ source 'https://rubygems.org' gem 'rails', '~> 7.2.2' -ruby '3.3.4' +ruby '3.4.9' -gem 'activeadmin' +gem 'activeadmin', '~> 3.5' gem 'bootsnap', require: false gem 'country_select', '~> 8.0' gem 'cssbundling-rails' -gem 'devise' +gem 'devise', '~> 5.0', '>= 5.0.3' gem 'jbuilder' gem 'jsbundling-rails' +gem 'ostruct', '~> 0.6.3' gem 'pg', '~> 1.1' gem 'puma', '>= 5.0' gem 'redis', '>= 4.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index a6007c4..bfe47d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,51 +1,53 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + actioncable (7.2.3.1) + actionpack (= 7.2.3.1) + activesupport (= 7.2.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailbox (7.2.3.1) + actionpack (= 7.2.3.1) + activejob (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) mail (>= 2.8.0) - actionmailer (7.2.2.2) - actionpack (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailer (7.2.3.1) + actionpack (= 7.2.3.1) + actionview (= 7.2.3.1) + activejob (= 7.2.3.1) + activesupport (= 7.2.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.2) - actionview (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionpack (7.2.3.1) + actionview (= 7.2.3.1) + activesupport (= 7.2.3.1) + cgi nokogiri (>= 1.8.5) racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.2.2) - actionpack (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actiontext (7.2.3.1) + actionpack (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.2) - activesupport (= 7.2.2.2) + actionview (7.2.3.1) + activesupport (= 7.2.3.1) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activeadmin (3.2.5) + activeadmin (3.5.0) arbre (~> 1.2, >= 1.2.1) csv formtastic (>= 3.1) @@ -55,22 +57,22 @@ GEM kaminari (>= 1.2.1) railties (>= 6.1) ransack (>= 4.0) - activejob (7.2.2.2) - activesupport (= 7.2.2.2) + activejob (7.2.3.1) + activesupport (= 7.2.3.1) globalid (>= 0.3.6) - activemodel (7.2.2.2) - activesupport (= 7.2.2.2) - activerecord (7.2.2.2) - activemodel (= 7.2.2.2) - activesupport (= 7.2.2.2) + activemodel (7.2.3.1) + activesupport (= 7.2.3.1) + activerecord (7.2.3.1) + activemodel (= 7.2.3.1) + activesupport (= 7.2.3.1) timeout (>= 0.4.0) - activestorage (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activesupport (= 7.2.2.2) + activestorage (7.2.3.1) + actionpack (= 7.2.3.1) + activejob (= 7.2.3.1) + activerecord (= 7.2.3.1) + activesupport (= 7.2.3.1) marcel (~> 1.0) - activesupport (7.2.2.2) + activesupport (7.2.3.1) base64 benchmark (>= 0.3) bigdecimal @@ -79,7 +81,7 @@ GEM drb i18n (>= 1.6, < 2) logger (>= 1.4.2) - minitest (>= 5.1) + minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) @@ -91,13 +93,13 @@ GEM activesupport (>= 3.0.0) ruby2_keywords (>= 0.0.2) base64 (0.3.0) - bcrypt (3.1.20) - benchmark (0.4.1) + bcrypt (3.1.22) + benchmark (0.5.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.2.2) + bigdecimal (4.0.1) bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) @@ -114,11 +116,12 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cgi (0.5.1) childprocess (5.1.0) logger (~> 1.5) coderay (1.1.3) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) countries (5.7.2) unaccent (~> 0.3) country_select (8.0.3) @@ -126,16 +129,16 @@ GEM crass (1.0.6) cssbundling-rails (1.4.2) railties (>= 6.0.0) - csv (3.3.2) + csv (3.3.5) date (3.4.1) debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) - devise (4.9.4) + devise (5.0.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) diff-lcs (1.6.0) @@ -155,15 +158,15 @@ GEM ffi (1.17.1-x86-linux-gnu) ffi (1.17.1-x86_64-darwin) ffi (1.17.1-x86_64-linux-gnu) - formtastic (5.0.0) - actionpack (>= 6.0.0) + formtastic (6.0.0) + actionpack (>= 7.2.0) formtastic_i18n (0.7.0) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) - has_scope (0.8.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - i18n (1.14.7) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + i18n (1.14.8) concurrent-ruby (~> 1.0) inherited_resources (1.14.0) actionpack (>= 6.0) @@ -178,7 +181,7 @@ GEM jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) - jquery-rails (4.6.0) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -208,7 +211,7 @@ GEM railties (>= 6.1) rexml logger (1.7.0) - loofah (2.24.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -216,12 +219,12 @@ GEM net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.1.0) matrix (0.4.2) method_source (1.1.0) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.25.5) + minitest (5.27.0) msgpack (1.8.0) net-imap (0.5.7) date @@ -233,20 +236,21 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.19.1) + nokogiri (1.19.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.19.1-aarch64-linux-gnu) + nokogiri (1.19.2-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-arm-linux-gnu) + nokogiri (1.19.2-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-arm64-darwin) + nokogiri (1.19.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-darwin) + nokogiri (1.19.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) orm_adapter (0.5.0) + ostruct (0.6.3) pg (1.5.9) pp (0.6.2) prettyprint @@ -266,7 +270,7 @@ GEM puma (6.6.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.20) + rack (3.2.5) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -274,43 +278,45 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (7.2.2.2) - actioncable (= 7.2.2.2) - actionmailbox (= 7.2.2.2) - actionmailer (= 7.2.2.2) - actionpack (= 7.2.2.2) - actiontext (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activemodel (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + rails (7.2.3.1) + actioncable (= 7.2.3.1) + actionmailbox (= 7.2.3.1) + actionmailer (= 7.2.3.1) + actionpack (= 7.2.3.1) + actiontext (= 7.2.3.1) + actionview (= 7.2.3.1) + activejob (= 7.2.3.1) + activemodel (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) bundler (>= 1.15.0) - railties (= 7.2.2.2) + railties (= 7.2.3.1) rails-controller-testing (1.0.5) 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) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + railties (7.2.3.1) + actionpack (= 7.2.3.1) + activesupport (= 7.2.3.1) + cgi irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rake (13.2.1) - ransack (4.3.0) - activerecord (>= 6.1.5) - activesupport (>= 6.1.5) + ransack (4.4.1) + activerecord (>= 7.2) + activesupport (>= 7.2) i18n rdoc (6.12.0) psych (>= 4.0.0) @@ -384,7 +390,8 @@ GEM stringio (3.1.5) thor (1.4.0) tilt (2.6.0) - timeout (0.4.3) + timeout (0.6.1) + tsort (0.2.0) turbo-rails (2.0.11) actionpack (>= 6.0.0) railties (>= 6.0.0) @@ -421,7 +428,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - activeadmin + activeadmin (~> 3.5) annotate better_errors binding_of_caller @@ -430,12 +437,13 @@ DEPENDENCIES country_select (~> 8.0) cssbundling-rails debug - devise + devise (~> 5.0, >= 5.0.3) factory_bot_rails faker jbuilder jsbundling-rails letter_opener_web + ostruct (~> 0.6.3) pg (~> 1.1) pry-byebug pry-rails @@ -456,7 +464,7 @@ DEPENDENCIES webdrivers RUBY VERSION - ruby 3.3.4p94 + ruby 3.4.9 BUNDLED WITH - 2.5.6 + 4.0.8 diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index 5294d4d..824c167 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -14,13 +14,55 @@ end end + special_invitees = Payment.current_conference_payments + .where(account_type: %w[special scholarship]) + .includes(:user) + .order(created_at: :desc, id: :desc) + .group_by(&:user_id) + .map { |_user_id, payments| payments.first } + .sort_by { |payment| payment.user.email.to_s.downcase } + invitee_user_ids = special_invitees.map(&:user_id).uniq + invitee_conf_years = special_invitees.map(&:conf_year).uniq + latest_applications_by_user_and_year = + Application.where(user_id: invitee_user_ids, conf_year: invitee_conf_years) + .order(created_at: :desc) + .each_with_object({}) do |application, apps_by_user_and_year| + key = [application.user_id, application.conf_year] + apps_by_user_and_year[key] ||= application + end + columns do column do - panel "Recent #{ApplicationSetting.get_current_app_year} Applications" do - applications = Array(Application.active_conference_applications).select { |app| app.respond_to?(:display_name) } - table_for applications do + current_year_applications_scope = Application.active_conference_applications + current_year_application_count = current_year_applications_scope.count + recent_applications = current_year_applications_scope.order(created_at: :desc).limit(25) + + panel "Latest 25 of #{current_year_application_count} Applications for the #{ApplicationSetting.get_current_app_year} conference" do + table_for recent_applications do column(:id) { |app| link_to(app.display_name, admin_application_path(app)) } end + div do + link_to 'See all current applications', admin_applications_path(q: { applications_conf_year_eq: ApplicationSetting.get_current_app_year }) + end + end + end + + column do + panel "Special invitees (#{special_invitees.size})" do + table_for special_invitees do + column('Email Address') { |payment| payment.user.email } + column('Application Status') do |payment| + application = latest_applications_by_user_and_year[[payment.user_id, payment.conf_year]] + + if application.present? + full_name = "#{application.first_name} #{application.last_name}".squish + link_to(full_name, admin_application_path(application)) + else + "Needs to submit an application to #{ApplicationSetting.get_current_app_year}" + end + end + column('Account Type') { |payment| payment.account_type } + end end end @@ -39,7 +81,6 @@ end end end - end begin diff --git a/app/controllers/application_settings_controller.rb b/app/controllers/application_settings_controller.rb index 70f6dd5..b7be596 100644 --- a/app/controllers/application_settings_controller.rb +++ b/app/controllers/application_settings_controller.rb @@ -36,7 +36,7 @@ def create render turbo_stream: turbo_stream.replace(@application_setting, partial: "application_settings/form", locals: { application_setting: @application_setting }) end format.html { render :new } - format.json { render json: @application_setting.errors, status: :unprocessable_entity } + format.json { render json: @application_setting.errors, status: :unprocessable_content } end end end @@ -53,7 +53,7 @@ def update render turbo_stream: turbo_stream.replace(@application_setting, partial: "application_settings/form", locals: { application_setting: @application_setting }) end format.html { render :edit } - format.json { render json: @application_setting.errors, status: :unprocessable_entity } + format.json { render json: @application_setting.errors, status: :unprocessable_content } end end end diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb index f49c43f..0546316 100644 --- a/app/controllers/applications_controller.rb +++ b/app/controllers/applications_controller.rb @@ -49,7 +49,7 @@ def create render turbo_stream: turbo_stream.replace(@application, partial: "applications/form", locals: { application: @application }) end format.html { render :new } - format.json { render json: @application.errors, status: :unprocessable_entity } + format.json { render json: @application.errors, status: :unprocessable_content } end end end @@ -71,7 +71,7 @@ def update render turbo_stream: turbo_stream.replace(@application, partial: "applications/form", locals: { application: @application }) end format.html { render :edit } - format.json { render json: @application.errors, status: :unprocessable_entity } + format.json { render json: @application.errors, status: :unprocessable_content } end end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b747f27..9f7b4bd 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -302,7 +302,7 @@ # apps is `200 OK` and `302 Found` respectively, but new apps are generated with # these new defaults that match Hotwire/Turbo behavior. # Note: These might become the new default in future versions of Devise. - config.responder.error_status = :unprocessable_entity + config.responder.error_status = :unprocessable_content config.responder.redirect_status = :see_other # ==> Configuration for :registerable diff --git a/spec/controllers/payments_controller_spec.rb b/spec/controllers/payments_controller_spec.rb index fea37df..1ff25a4 100644 --- a/spec/controllers/payments_controller_spec.rb +++ b/spec/controllers/payments_controller_spec.rb @@ -476,7 +476,7 @@ timestamp: Time.current.to_i.to_s, hash: 'valid_hash_here' } - expect(response).not_to have_http_status(:unprocessable_entity) + expect(response).not_to have_http_status(:unprocessable_content) end it 'requires user authentication for most actions' do diff --git a/spec/requests/admin_dashboard_spec.rb b/spec/requests/admin_dashboard_spec.rb index bd15e5d..7258ef9 100644 --- a/spec/requests/admin_dashboard_spec.rb +++ b/spec/requests/admin_dashboard_spec.rb @@ -7,14 +7,9 @@ before { sign_in admin_user } it 'displays user email when payment has no current application' do - application_setting = ApplicationSetting.create!( + application_setting = create( + :application_setting, contest_year: Time.current.year, - opendate: Time.current, - subscription_cost: 0, - application_buffer: 1, - registration_fee: 50, - lottery_buffer: 50, - application_open_period: 48, active_application: true ) @@ -31,5 +26,43 @@ expect(response).to be_successful expect(response.body).to include('noapp@example.com ( - waiting for application to be submitted)') end + + it 'shows special invitees with application status and account type' do + application_setting = create( + :application_setting, + contest_year: Time.current.year, + active_application: true + ) + + special_user = create(:user, email: 'special@example.com') + scholarship_user = create(:user, email: 'scholarship@example.com') + other_user = create(:user, email: 'other@example.com') + + create(:payment, :special, user: special_user, conf_year: application_setting.contest_year) + create(:payment, :scholarship, user: scholarship_user, conf_year: application_setting.contest_year) + create(:payment, user: other_user, conf_year: application_setting.contest_year) + + application = create( + :application, + user: special_user, + first_name: 'Ada', + last_name: 'Lovelace', + conf_year: application_setting.contest_year, + partner_registration: create(:partner_registration) + ) + + get admin_root_path + + expect(response).to be_successful + expect(response.body).to include('Special invitees (2)') + expect(response.body).to include('special@example.com') + expect(response.body).to include('scholarship@example.com') + expect(response.body).to include('Needs to submit an application to') + expect(response.body).to include('special') + expect(response.body).to include('scholarship') + expect(response.body).to include("href=\"#{admin_application_path(application)}\"") + expect(response.body).to include('Ada Lovelace') + expect(response.body.index('scholarship@example.com')).to be < response.body.index('special@example.com') + end end end diff --git a/spec/requests/application_settings_spec.rb b/spec/requests/application_settings_spec.rb index d18ef1c..5d50de8 100644 --- a/spec/requests/application_settings_spec.rb +++ b/spec/requests/application_settings_spec.rb @@ -115,7 +115,7 @@ post application_settings_path, params: { application_setting: { contest_year: nil } # Invalid params }, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end @@ -167,7 +167,7 @@ patch application_setting_path(application_setting), params: { application_setting: { contest_year: nil } # Invalid params }, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/spec/requests/applications_spec.rb b/spec/requests/applications_spec.rb index acace78..f834d3d 100644 --- a/spec/requests/applications_spec.rb +++ b/spec/requests/applications_spec.rb @@ -78,18 +78,10 @@ # Removing "does not create a new Application" test since it was skipped it "renders a response with 422 status (i.e. to display the 'new' template)" do - # Keep this test as it was not skipped - mock_application = instance_double(Application, save: false) - allow(Application).to receive(:new).and_return(mock_application) - allow(mock_application).to receive(:email=) - allow(mock_application).to receive(:user=) - allow(mock_application).to receive(:errors).and_return(double(empty?: false)) - - allow_any_instance_of(ApplicationsController).to receive(:render).and_return(nil) - allow_any_instance_of(ActionDispatch::Response).to receive(:status).and_return(422) - - post applications_path, params: { application: { first_name: nil } } - expect(response).to have_http_status(:unprocessable_entity) + post applications_path, + params: { application: valid_attributes.merge(first_name: nil) }, + as: :json + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/spec/support/application_mock.rb b/spec/support/application_mock.rb index fcd2fec..ea15f4c 100644 --- a/spec/support/application_mock.rb +++ b/spec/support/application_mock.rb @@ -13,6 +13,9 @@ allow(mock_active_applications).to receive(:last).and_return(nil) allow(mock_active_applications).to receive(:pluck).and_return([]) allow(mock_active_applications).to receive(:each).and_return([]) + allow(mock_active_applications).to receive(:count).and_return(0) + allow(mock_active_applications).to receive(:order).and_return(mock_active_applications) + allow(mock_active_applications).to receive(:limit).and_return([]) # Mock the Application.active_conference_applications class method allow(Application).to receive(:active_conference_applications).and_return(mock_active_applications)