From 31d4b77fa21199b2d5bb5a6aaf2fc959e6612b7a Mon Sep 17 00:00:00 2001 From: Michael Ball Date: Sun, 21 Jun 2026 10:01:04 +0000 Subject: [PATCH 1/4] chore: begin Rails 8.0 upgrade - Bump rails to ~> 8.0.2 and update dependencies to 8.0.5. - Add `config/initializers/new_framework_defaults_8_0.rb` to facilitate incremental adoption of new defaults. - Migrate strong parameters from `require().permit()` to `expect()` in controllers to align with Rails 8 idioms. - Update RSpec assertions to match Rails 8's updated `ParameterMissing` error message. - Add default `public/400.html` error page. Co-authored-by: Claude Code --- Gemfile | 2 +- Gemfile.lock | 132 +++++++++--------- app/controllers/course_settings_controller.rb | 30 ++-- app/controllers/form_settings_controller.rb | 10 +- app/controllers/requests_controller.rb | 2 +- .../new_framework_defaults_8_0.rb | 30 ++++ public/400.html | 114 +++++++++++++++ .../api/v1/assignments_controller_spec.rb | 2 +- .../api/v1/lmss_controller_spec.rb | 2 +- 9 files changed, 235 insertions(+), 89 deletions(-) create mode 100644 config/initializers/new_framework_defaults_8_0.rb create mode 100644 public/400.html diff --git a/Gemfile b/Gemfile index 9f7eed60..24d39335 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' ruby '~> 3.3' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem 'rails', '~> 7.2.3.1' +gem 'rails', '~> 8.0.2' # Use postgres for all env dbs gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index 7714fd26..6ef508cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,68 +1,65 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.3.1) - actionpack (= 7.2.3.1) - activesupport (= 7.2.3.1) + actioncable (8.0.5) + actionpack (= 8.0.5) + activesupport (= 8.0.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - 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) + actionmailbox (8.0.5) + actionpack (= 8.0.5) + activejob (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) mail (>= 2.8.0) - 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) + actionmailer (8.0.5) + actionpack (= 8.0.5) + actionview (= 8.0.5) + activejob (= 8.0.5) + activesupport (= 8.0.5) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.3.1) - actionview (= 7.2.3.1) - activesupport (= 7.2.3.1) - cgi + actionpack (8.0.5) + actionview (= 8.0.5) + activesupport (= 8.0.5) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.3) + rack (>= 2.2.4) 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.3.1) - actionpack (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + actiontext (8.0.5) + actionpack (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.3.1) - activesupport (= 7.2.3.1) + actionview (8.0.5) + activesupport (= 8.0.5) builder (~> 3.1) - cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.3.1) - activesupport (= 7.2.3.1) + activejob (8.0.5) + activesupport (= 8.0.5) globalid (>= 0.3.6) - 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) + activemodel (8.0.5) + activesupport (= 8.0.5) + activerecord (8.0.5) + activemodel (= 8.0.5) + activesupport (= 8.0.5) timeout (>= 0.4.0) - 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) + activestorage (8.0.5) + actionpack (= 8.0.5) + activejob (= 8.0.5) + activerecord (= 8.0.5) + activesupport (= 8.0.5) marcel (~> 1.0) - activesupport (7.2.3.1) + activesupport (8.0.5) base64 benchmark (>= 0.3) bigdecimal @@ -71,9 +68,10 @@ GEM drb i18n (>= 1.6, < 2) logger (>= 1.4.2) - minitest (>= 5.1, < 6) + minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) annotaterb (4.22.0) @@ -127,7 +125,6 @@ GEM capybara-screenshot (1.0.27) capybara (>= 1.0, < 4) launchy - cgi (0.5.1) chartkick (5.2.1) childprocess (5.1.0) logger (~> 1.5) @@ -247,7 +244,7 @@ GEM multi_xml (>= 0.5.2) hypershield (0.6.1) activerecord (>= 7.2) - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) ice_nine (0.11.2) importmap-rails (2.2.3) @@ -302,13 +299,15 @@ GEM net-imap net-pop net-smtp - marcel (1.1.0) + marcel (1.2.1) matrix (0.4.3) memoist3 (1.0.0) method_source (1.1.0) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.27.0) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) msgpack (1.8.0) multi_test (1.1.0) multi_xml (0.8.1) @@ -380,7 +379,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) reline (>= 0.6.0) - psych (5.3.1) + psych (5.4.0) date stringio public_suffix (7.0.5) @@ -402,20 +401,20 @@ GEM rack (>= 1.0.0) rackup (2.3.1) rack (>= 3) - 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) + rails (8.0.5) + actioncable (= 8.0.5) + actionmailbox (= 8.0.5) + actionmailer (= 8.0.5) + actionpack (= 8.0.5) + actiontext (= 8.0.5) + actionview (= 8.0.5) + activejob (= 8.0.5) + activemodel (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) bundler (>= 1.15.0) - railties (= 7.2.3.1) + railties (= 8.0.5) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -427,10 +426,9 @@ GEM 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.3.1) - actionpack (= 7.2.3.1) - activesupport (= 7.2.3.1) - cgi + railties (8.0.5) + actionpack (= 8.0.5) + activesupport (= 8.0.5) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -596,14 +594,14 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) - websocket-driver (0.8.0) + websocket-driver (0.8.1) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) win32ole (1.9.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.5) + zeitwerk (2.8.2) PLATFORMS aarch64-linux @@ -649,7 +647,7 @@ DEPENDENCIES pg puma (>= 6.0) rack_session_access - rails (~> 7.2.3.1) + rails (~> 8.0.2) rails-controller-testing (~> 1.0) rspec-rails rspec-retry diff --git a/app/controllers/course_settings_controller.rb b/app/controllers/course_settings_controller.rb index 4a2cfe07..b551aa74 100644 --- a/app/controllers/course_settings_controller.rb +++ b/app/controllers/course_settings_controller.rb @@ -59,20 +59,22 @@ def reset_email_templates end def course_settings_params - params.require(:course_settings).permit( - :enable_extensions, - :auto_approve_days, - :auto_approve_extended_request_days, - :max_auto_approve, - :enable_gradescope, - :gradescope_course_url, - :extend_late_due_date, - :enable_emails, - :reply_email, - :email_subject, - :email_template, - :enable_slack_webhook_url, - :slack_webhook_url + params.expect( + course_settings: [ + :enable_extensions, + :auto_approve_days, + :auto_approve_extended_request_days, + :max_auto_approve, + :enable_gradescope, + :gradescope_course_url, + :extend_late_due_date, + :enable_emails, + :reply_email, + :email_subject, + :email_template, + :enable_slack_webhook_url, + :slack_webhook_url + ] ) end diff --git a/app/controllers/form_settings_controller.rb b/app/controllers/form_settings_controller.rb index 8b14e53f..c0c3389c 100644 --- a/app/controllers/form_settings_controller.rb +++ b/app/controllers/form_settings_controller.rb @@ -37,10 +37,12 @@ def update private def form_setting_params - params.require(:form_setting).permit( - :reason_desc, :documentation_desc, :documentation_disp, - :custom_q1, :custom_q1_desc, :custom_q1_disp, - :custom_q2, :custom_q2_desc, :custom_q2_disp + params.expect( + form_setting: [ + :reason_desc, :documentation_desc, :documentation_disp, + :custom_q1, :custom_q1_desc, :custom_q1_disp, + :custom_q2, :custom_q2_desc, :custom_q2_disp + ] ) end diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb index 463f809d..7b6ca17c 100644 --- a/app/controllers/requests_controller.rb +++ b/app/controllers/requests_controller.rb @@ -222,7 +222,7 @@ def set_course_role_from_settings end def request_params - params.require(:request).permit(:assignment_id, :reason, :documentation, :custom_q1, :custom_q2, :requested_due_date, :user_id) + params.expect(request: [ :assignment_id, :reason, :documentation, :custom_q1, :custom_q2, :requested_due_date, :user_id ]) end def authenticate_user diff --git a/config/initializers/new_framework_defaults_8_0.rb b/config/initializers/new_framework_defaults_8_0.rb new file mode 100644 index 00000000..92efa951 --- /dev/null +++ b/config/initializers/new_framework_defaults_8_0.rb @@ -0,0 +1,30 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 8.0 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `8.0`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +### +# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. +# If set to `:zone`, `to_time` methods will use the timezone of their receivers. +# If set to `:offset`, `to_time` methods will use the UTC offset. +# If `false`, `to_time` methods will convert to the local system UTC offset instead. +#++ +# Rails.application.config.active_support.to_time_preserves_timezone = :zone + +### +# When both `If-Modified-Since` and `If-None-Match` are provided by the client +# only consider `If-None-Match` as specified by RFC 7232 Section 6. +# If set to `false` both conditions need to be satisfied. +#++ +# Rails.application.config.action_dispatch.strict_freshness = true + +### +# Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. +#++ +# Regexp.timeout = 1 diff --git a/public/400.html b/public/400.html new file mode 100644 index 00000000..282dbc8c --- /dev/null +++ b/public/400.html @@ -0,0 +1,114 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/spec/controllers/api/v1/assignments_controller_spec.rb b/spec/controllers/api/v1/assignments_controller_spec.rb index 58a0f11e..7fe71b0a 100644 --- a/spec/controllers/api/v1/assignments_controller_spec.rb +++ b/spec/controllers/api/v1/assignments_controller_spec.rb @@ -42,7 +42,7 @@ def json_response it 'returns status :bad_request when name is missing' do post :create, params: valid_params.except(:name) expect(response).to have_http_status(:bad_request) - expect(json_response['error']).to include('param is missing or the value is empty: name') + expect(json_response['error']).to include('param is missing or the value is empty or invalid: name') end end diff --git a/spec/controllers/api/v1/lmss_controller_spec.rb b/spec/controllers/api/v1/lmss_controller_spec.rb index f662384d..7009588a 100644 --- a/spec/controllers/api/v1/lmss_controller_spec.rb +++ b/spec/controllers/api/v1/lmss_controller_spec.rb @@ -29,7 +29,7 @@ def json_response it 'returns status :bad_request' do post :create, params: { course_id: @course.id, external_course_id: @external_course_id } expect(response).to have_http_status(:bad_request) - expect(response.body).to include('param is missing or the value is empty: lms_id') + expect(response.body).to include('param is missing or the value is empty or invalid: lms_id') end end From 805d6f7cc84ebfbaee8bc976a5dc2a59294f450c Mon Sep 17 00:00:00 2001 From: Michael Ball Date: Thu, 2 Jul 2026 04:06:18 +0000 Subject: [PATCH 2/4] refactor: consolidate request params and enable Rails 8 defaults - Refactor `RequestsController#request_params` to use `params.expect` and conditionally permit `assignment_id` based on the action name. - Enable `to_time_preserves_timezone = :zone` in framework defaults to resolve Rails 8.1 deprecation warnings. - Add regression test to ensure `assignment_id` remains immutable during request updates. Co-authored-by: Claude Code --- app/controllers/requests_controller.rb | 14 +++++------ .../new_framework_defaults_8_0.rb | 2 +- spec/controllers/requests_controller_spec.rb | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb index ef20c497..4231a3f9 100644 --- a/app/controllers/requests_controller.rb +++ b/app/controllers/requests_controller.rb @@ -110,7 +110,7 @@ def create_for_student def update Request.merge_date_and_time!(params[:request]) - @request.assign_attributes(update_request_params) + @request.assign_attributes(request_params) return unless ensure_assignment_in_course if @request.save @@ -227,14 +227,12 @@ def set_course_role_from_settings @form_settings = result[:form_settings] end + # The assignment is chosen at creation and is not editable afterwards, so the + # update action is not permitted to write assignment_id; other actions may. def request_params - params.expect(request: [ :assignment_id, :reason, :documentation, :custom_q1, :custom_q2, :requested_due_date ]) - end - - # The assignment is chosen at creation and is not editable afterwards, so it - # is dropped from the params an update is allowed to write. - def update_request_params - request_params.except(:assignment_id) + permitted = [ :reason, :documentation, :custom_q1, :custom_q2, :requested_due_date ] + permitted.unshift(:assignment_id) unless action_name == 'update' + params.expect(request: permitted) end # Every request must reference an assignment in this course. A new request diff --git a/config/initializers/new_framework_defaults_8_0.rb b/config/initializers/new_framework_defaults_8_0.rb index 92efa951..93d81ef9 100644 --- a/config/initializers/new_framework_defaults_8_0.rb +++ b/config/initializers/new_framework_defaults_8_0.rb @@ -15,7 +15,7 @@ # If set to `:offset`, `to_time` methods will use the UTC offset. # If `false`, `to_time` methods will convert to the local system UTC offset instead. #++ -# Rails.application.config.active_support.to_time_preserves_timezone = :zone +Rails.application.config.active_support.to_time_preserves_timezone = :zone ### # When both `If-Modified-Since` and `If-None-Match` are provided by the client diff --git a/spec/controllers/requests_controller_spec.rb b/spec/controllers/requests_controller_spec.rb index fd20f8ea..7a13d977 100644 --- a/spec/controllers/requests_controller_spec.rb +++ b/spec/controllers/requests_controller_spec.rb @@ -292,6 +292,30 @@ expect(response).to redirect_to(course_request_path(course, request)) expect(flash[:notice]).to include('updated') end + + it 'does not reassign the request to a different assignment' do + other_assignment = Assignment.create!( + name: 'A2', + course_to_lms_id: course_to_lms.id, + due_date: 5.days.from_now, + external_assignment_id: 'x2', + enabled: true + ) + + patch :update, params: { + course_id: course.id, + id: request.id, + request: { + assignment_id: other_assignment.id, + reason: 'Updated reason', + requested_due_date: Date.tomorrow.to_s, + due_time: '12:00' + } + } + + expect(response).to redirect_to(course_request_path(course, request)) + expect(request.reload.assignment_id).to eq(assignment.id) + end end describe 'POST #cancel' do From 1a33b553d742960bc511cf45177b3acf4fec5882 Mon Sep 17 00:00:00 2001 From: Michael Ball Date: Thu, 2 Jul 2026 04:50:48 +0000 Subject: [PATCH 3/4] chore: upgrade to Rails 8.1 Bump Rails 8.0.5 -> 8.1.3 and add new_framework_defaults_8_1.rb (staged, all commented). Fix breakages surfaced by 8.1: - ActionMailer::Base.mail (class-level) was removed in 8.1; route templated emails through a real TemplatedMailer / ApplicationMailer instead. - Replace deprecated :unprocessable_entity status symbol with :unprocessable_content (Rack 3.2) in specs. Co-Authored-By: Claude Opus 4.8 --- Gemfile | 2 +- Gemfile.lock | 136 +++++++++--------- app/mailers/application_mailer.rb | 5 + app/mailers/templated_mailer.rb | 17 +++ app/services/email_service.rb | 5 +- .../new_framework_defaults_8_1.rb | 74 ++++++++++ spec/controllers/requests_controller_spec.rb | 8 +- .../user_to_courses_controller_spec.rb | 2 +- 8 files changed, 173 insertions(+), 76 deletions(-) create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/mailers/templated_mailer.rb create mode 100644 config/initializers/new_framework_defaults_8_1.rb diff --git a/Gemfile b/Gemfile index 24d39335..229b4914 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' ruby '~> 3.3' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem 'rails', '~> 8.0.2' +gem 'rails', '~> 8.1.0' # Use postgres for all env dbs gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index 6e81c460..46bc8904 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,31 @@ GEM remote: https://rubygems.org/ specs: - actioncable (8.0.5) - actionpack (= 8.0.5) - activesupport (= 8.0.5) + action_text-trix (2.1.19) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.5) - actionpack (= 8.0.5) - activejob (= 8.0.5) - activerecord (= 8.0.5) - activestorage (= 8.0.5) - activesupport (= 8.0.5) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (8.0.5) - actionpack (= 8.0.5) - actionview (= 8.0.5) - activejob (= 8.0.5) - activesupport (= 8.0.5) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.5) - actionview (= 8.0.5) - activesupport (= 8.0.5) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -31,42 +33,43 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.5) - actionpack (= 8.0.5) - activerecord (= 8.0.5) - activestorage (= 8.0.5) - activesupport (= 8.0.5) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.5) - activesupport (= 8.0.5) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.5) - activesupport (= 8.0.5) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (8.0.5) - activesupport (= 8.0.5) - activerecord (8.0.5) - activemodel (= 8.0.5) - activesupport (= 8.0.5) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) - activestorage (8.0.5) - actionpack (= 8.0.5) - activejob (= 8.0.5) - activerecord (= 8.0.5) - activesupport (= 8.0.5) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (8.0.5) + activesupport (8.1.3) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -85,7 +88,6 @@ GEM axe-core-rspec (4.12.0) axe-core-api (= 4.12.0) base64 (0.3.0) - benchmark (0.5.0) bigdecimal (4.1.2) bindex (0.8.1) blazer (3.4.0) @@ -113,7 +115,6 @@ GEM capybara-screenshot (1.0.27) capybara (>= 1.0, < 4) launchy - cgi (0.5.2) chartkick (5.2.1) childprocess (5.1.0) logger (~> 1.5) @@ -125,7 +126,7 @@ GEM crack (1.0.1) bigdecimal rexml - crass (1.0.6) + crass (1.0.7) csv (3.3.5) cucumber (10.2.0) base64 (~> 0.2) @@ -201,7 +202,7 @@ GEM sassc (~> 2.0) formatador (1.2.3) reline - globalid (1.3.0) + globalid (1.4.0) activesupport (>= 6.1) guard (2.20.1) formatador (>= 0.2.4) @@ -277,7 +278,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.12.0) lumberjack (1.4.2) - mail (2.9.0) + mail (2.9.1) logger mini_mime (>= 0.1.1) net-imap @@ -363,9 +364,6 @@ GEM coderay (~> 1.1) method_source (~> 1.0) reline (>= 0.6.0) - psych (5.4.0) - date - stringio public_suffix (7.0.5) puma (8.0.2) nio4r (~> 2.0) @@ -385,20 +383,20 @@ GEM rack (>= 1.0.0) rackup (2.3.1) rack (>= 3) - rails (8.0.5) - actioncable (= 8.0.5) - actionmailbox (= 8.0.5) - actionmailer (= 8.0.5) - actionpack (= 8.0.5) - actiontext (= 8.0.5) - actionview (= 8.0.5) - activejob (= 8.0.5) - activemodel (= 8.0.5) - activerecord (= 8.0.5) - activestorage (= 8.0.5) - activesupport (= 8.0.5) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 8.0.5) + railties (= 8.1.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -410,9 +408,9 @@ GEM 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 (8.0.5) - actionpack (= 8.0.5) - activesupport (= 8.0.5) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -424,9 +422,14 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (7.2.0) + rbs (4.0.3) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) erb - psych (>= 4.0.0) + prism (>= 1.6.0) + rbs (>= 4.0.0) tsort regexp_parser (2.12.0) reline (0.6.3) @@ -536,7 +539,6 @@ GEM sprockets (>= 3.0.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.2.0) strong_migrations (2.8.0) activerecord (>= 7.2) sys-uname (1.5.1) @@ -574,7 +576,7 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) - websocket-driver (0.8.1) + websocket-driver (0.8.2) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -627,7 +629,7 @@ DEPENDENCIES pg puma (>= 6.0) rack_session_access - rails (~> 8.0.2) + rails (~> 8.1.0) rails-controller-testing (~> 1.0) rspec-rails rspec-retry diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..7c8a5a73 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Base class for all application mailers. +class ApplicationMailer < ActionMailer::Base +end diff --git a/app/mailers/templated_mailer.rb b/app/mailers/templated_mailer.rb new file mode 100644 index 00000000..763ae5f5 --- /dev/null +++ b/app/mailers/templated_mailer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Delivers a pre-rendered HTML email. The subject and body are already +# interpolated by EmailService, so the action just wraps them in a message +# rather than rendering a view template. +class TemplatedMailer < ApplicationMailer + def templated_email(to:, from:, reply_to:, subject:, body:) + mail( + to: to, + from: from, + reply_to: reply_to, + subject: subject, + body: body, + content_type: 'text/html' + ) + end +end diff --git a/app/services/email_service.rb b/app/services/email_service.rb index f6ebcced..96eefd89 100644 --- a/app/services/email_service.rb +++ b/app/services/email_service.rb @@ -27,13 +27,12 @@ def render_templates(subject_template, body_template, mapping) def send_email(to:, from:, reply_to:, subject_template:, body_template:, mapping:, deliver_later: false) rendered = render_templates(subject_template, body_template, mapping) - mail = ActionMailer::Base.mail( + mail = TemplatedMailer.templated_email( to: to, from: from, reply_to: reply_to, subject: rendered[:subject], - body: rendered[:body].gsub("\n", "
\n"), - content_type: 'text/html' + body: rendered[:body].gsub("\n", "
\n") ) deliver_later ? mail.deliver_later : mail.deliver_now diff --git a/config/initializers/new_framework_defaults_8_1.rb b/config/initializers/new_framework_defaults_8_1.rb new file mode 100644 index 00000000..8569b5b1 --- /dev/null +++ b/config/initializers/new_framework_defaults_8_1.rb @@ -0,0 +1,74 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 8.1 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `8.1`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +### +# Skips escaping HTML entities and line separators. When set to `false`, the +# JSON renderer no longer escapes these to improve performance. +# +# Example: +# class PostsController < ApplicationController +# def index +# render json: { key: "\u2028\u2029<>&" } +# end +# end +# +# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"

<>&"}` with the config +# set to `false`. +# +# Applications that want to keep the escaping behavior can set the config to `true`. +#++ +# Rails.configuration.action_controller.escape_json_responses = false + +### +# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON. +# +# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019. +# As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset. +#++ +# Rails.configuration.active_support.escape_js_separators_in_json = false + +### +# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values +# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or +# `primary_key`) to fall back on. +# +# The current behavior of not raising an error has been deprecated, and this configuration option will be removed in +# Rails 8.2. +#++ +# Rails.configuration.active_record.raise_on_missing_required_finder_order_columns = true + +### +# Controls how Rails handles path relative URL redirects. +# When set to `:raise`, Rails will raise an `ActionController::Redirecting::UnsafeRedirectError` +# for relative URLs without a leading slash, which can help prevent open redirect vulnerabilities. +# +# Example: +# redirect_to "example.com" # Raises UnsafeRedirectError +# redirect_to "@attacker.com" # Raises UnsafeRedirectError +# redirect_to "/safe/path" # Works correctly +# +# Applications that want to allow these redirects can set the config to `:log` (previous default) +# to only log warnings, or `:notify` to send ActiveSupport notifications. +#++ +# Rails.configuration.action_controller.action_on_path_relative_redirect = :raise + +### +# Use a Ruby parser to track dependencies between Action View templates +#++ +# Rails.configuration.action_view.render_tracker = :ruby + +### +# When enabled, hidden inputs generated by `form_tag`, `token_tag`, `method_tag`, and the hidden parameter fields +# included in `button_to` forms will omit the `autocomplete="off"` attribute. +# +# Applications that want to keep generating the `autocomplete` attribute for those tags can set it to `false`. +#++ +# Rails.configuration.action_view.remove_hidden_field_autocomplete = true diff --git a/spec/controllers/requests_controller_spec.rb b/spec/controllers/requests_controller_spec.rb index 7a13d977..4664a1b6 100644 --- a/spec/controllers/requests_controller_spec.rb +++ b/spec/controllers/requests_controller_spec.rb @@ -416,7 +416,7 @@ post :approve, params: { course_id: course.id, id: request.id }, format: :json payload = response.parsed_body - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(payload['success']).to be(false) expect(payload['message']).to match(/failed/i) end @@ -459,7 +459,7 @@ post :reject, params: { course_id: course.id, id: request.id }, format: :json payload = response.parsed_body - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(payload['success']).to be(false) expect(payload['message']).to match(/failed/i) end @@ -525,7 +525,7 @@ post :mass_approve, params: { course_id: course.id, request_ids: [] }, format: :json payload = response.parsed_body - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(payload['success']).to be(false) expect(payload['message']).to match(/select at least one/i) end @@ -583,7 +583,7 @@ }, format: :json payload = response.parsed_body - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(payload['success']).to be(false) expect(payload['message']).to match(/no pending requests/i) end diff --git a/spec/controllers/user_to_courses_controller_spec.rb b/spec/controllers/user_to_courses_controller_spec.rb index eaaa61e7..745cc916 100644 --- a/spec/controllers/user_to_courses_controller_spec.rb +++ b/spec/controllers/user_to_courses_controller_spec.rb @@ -58,7 +58,7 @@ allow_extended_requests: true } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body['redirect_to']).to be_present end end From 48239447ddb66c48236a981eb5ac05daba570b48 Mon Sep 17 00:00:00 2001 From: Michael Ball Date: Thu, 2 Jul 2026 04:55:34 +0000 Subject: [PATCH 4/4] chore: adopt Rails 8.1 framework defaults Set config.load_defaults 8.1 and delete the now-obsolete new_framework_defaults_8_0.rb and new_framework_defaults_8_1.rb staging initializers. Full rspec + cucumber suites pass with all 8.0/8.1 defaults active. Co-Authored-By: Claude Opus 4.8 --- config/application.rb | 2 +- .../new_framework_defaults_8_0.rb | 30 -------- .../new_framework_defaults_8_1.rb | 74 ------------------- 3 files changed, 1 insertion(+), 105 deletions(-) delete mode 100644 config/initializers/new_framework_defaults_8_0.rb delete mode 100644 config/initializers/new_framework_defaults_8_1.rb diff --git a/config/application.rb b/config/application.rb index e298613f..09dc8b9d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,7 +21,7 @@ module Flextensions class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 + config.load_defaults 8.1 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. diff --git a/config/initializers/new_framework_defaults_8_0.rb b/config/initializers/new_framework_defaults_8_0.rb deleted file mode 100644 index 93d81ef9..00000000 --- a/config/initializers/new_framework_defaults_8_0.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 8.0 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `8.0`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -### -# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. -# If set to `:zone`, `to_time` methods will use the timezone of their receivers. -# If set to `:offset`, `to_time` methods will use the UTC offset. -# If `false`, `to_time` methods will convert to the local system UTC offset instead. -#++ -Rails.application.config.active_support.to_time_preserves_timezone = :zone - -### -# When both `If-Modified-Since` and `If-None-Match` are provided by the client -# only consider `If-None-Match` as specified by RFC 7232 Section 6. -# If set to `false` both conditions need to be satisfied. -#++ -# Rails.application.config.action_dispatch.strict_freshness = true - -### -# Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. -#++ -# Regexp.timeout = 1 diff --git a/config/initializers/new_framework_defaults_8_1.rb b/config/initializers/new_framework_defaults_8_1.rb deleted file mode 100644 index 8569b5b1..00000000 --- a/config/initializers/new_framework_defaults_8_1.rb +++ /dev/null @@ -1,74 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 8.1 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `8.1`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -### -# Skips escaping HTML entities and line separators. When set to `false`, the -# JSON renderer no longer escapes these to improve performance. -# -# Example: -# class PostsController < ApplicationController -# def index -# render json: { key: "\u2028\u2029<>&" } -# end -# end -# -# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"

<>&"}` with the config -# set to `false`. -# -# Applications that want to keep the escaping behavior can set the config to `true`. -#++ -# Rails.configuration.action_controller.escape_json_responses = false - -### -# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON. -# -# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019. -# As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset. -#++ -# Rails.configuration.active_support.escape_js_separators_in_json = false - -### -# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values -# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or -# `primary_key`) to fall back on. -# -# The current behavior of not raising an error has been deprecated, and this configuration option will be removed in -# Rails 8.2. -#++ -# Rails.configuration.active_record.raise_on_missing_required_finder_order_columns = true - -### -# Controls how Rails handles path relative URL redirects. -# When set to `:raise`, Rails will raise an `ActionController::Redirecting::UnsafeRedirectError` -# for relative URLs without a leading slash, which can help prevent open redirect vulnerabilities. -# -# Example: -# redirect_to "example.com" # Raises UnsafeRedirectError -# redirect_to "@attacker.com" # Raises UnsafeRedirectError -# redirect_to "/safe/path" # Works correctly -# -# Applications that want to allow these redirects can set the config to `:log` (previous default) -# to only log warnings, or `:notify` to send ActiveSupport notifications. -#++ -# Rails.configuration.action_controller.action_on_path_relative_redirect = :raise - -### -# Use a Ruby parser to track dependencies between Action View templates -#++ -# Rails.configuration.action_view.render_tracker = :ruby - -### -# When enabled, hidden inputs generated by `form_tag`, `token_tag`, `method_tag`, and the hidden parameter fields -# included in `button_to` forms will omit the `autocomplete="off"` attribute. -# -# Applications that want to keep generating the `autocomplete` attribute for those tags can set it to `false`. -#++ -# Rails.configuration.action_view.remove_hidden_field_autocomplete = true