Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,12 @@ jobs:
- build_and_run_auth_server

# Replace both archive and security repositories
# https://support.circleci.com/hc/en-us/articles/37474192881179-Resolving-Unable-to-connect-to-archive-ubuntu-com-Error-in-CircleCI
# https://discuss.circleci.com/t/connection-issues-with-apt-get-from-archive-ubuntu-com/48094/16
- run:
name: Change Ubuntu archive mirrors
command: |
sudo sed -i 's|http://archive.ubuntu.com|http://mirrors.rit.edu|g' /etc/apt/sources.list
sudo sed -i 's|http://security.ubuntu.com|http://mirrors.rit.edu|g' /etc/apt/sources.list
sudo sed -i 's|http://archive.ubuntu.com|https://mirror.math.princeton.edu/pub|g' /etc/apt/sources.list
sudo sed -i 's|http://security.ubuntu.com|https://mirror.math.princeton.edu/pub|g' /etc/apt/sources.list

- run:
name: Install Playwright dependencies
Expand Down
9 changes: 5 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ gem 'csv'
# For Windows devs
gem 'tzinfo-data', platforms: [:mswin, :mswin64]

gem 'rails', '~> 8.0.0'
gem 'rails', '~> 8.1.0'

# Use PostgreSQL for the backend
gem 'pg'
Expand All @@ -32,9 +32,10 @@ gem 'workflow-activerecord', '>= 4.1', '< 7.0'
# Add creator_id and updater_id attributes to models
gem 'activerecord-userstamp', git: 'https://github.com/Coursemology/activerecord-userstamp.git'
# Allow declaring the calculated attributes of a record
# Upstream v1.1.1 (supports Rails 7.0/7.1/7.2/8.0); retires the Coursemology fork.
# TODO: bump to upstream master (v1.2.0) for Rails 8.1 during that upgrade — v1.1.1 ships no 8.1 patch.
gem 'calculated_attributes', git: 'https://github.com/aha-app/calculated_attributes.git', ref: 'ecaf6c9'
# Upstream v1.2.0 — first ref shipping rails_8_1_patches.rb
# NOTE: v1.2.0 also carries a broken Relation#calculate override — neutralised
# in config/initializers/calculated_attributes_patch.rb; read that before bumping this ref again.
gem 'calculated_attributes', git: 'https://github.com/aha-app/calculated_attributes.git', ref: '992fdd9'
# For multiple table inheritance
# TODO: Figure out breaking changes in v2 as polymorphism is not working correctly.
gem 'active_record-acts_as', git: 'https://github.com/Coursemology/active_record-acts_as.git'
Expand Down
134 changes: 68 additions & 66 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ GIT

GIT
remote: https://github.com/aha-app/calculated_attributes.git
revision: ecaf6c95c8d097e58e2660e84e131df4aed16600
ref: ecaf6c9
revision: 992fdd9785d201e8e9ad7d6c2f22659ba1cc1c81
ref: 992fdd9
specs:
calculated_attributes (1.1.1)
activerecord (>= 7.0.0, < 9)
calculated_attributes (1.2.0)
activerecord (>= 8.0.0, < 9)

GIT
remote: https://github.com/jesjos/active_record_upsert
Expand All @@ -62,74 +62,77 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (2.0.1)
actioncable (8.0.5.1)
actionpack (= 8.0.5.1)
activesupport (= 8.0.5.1)
action_text-trix (2.1.19)
railties
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.5.1)
actionpack (= 8.0.5.1)
activejob (= 8.0.5.1)
activerecord (= 8.0.5.1)
activestorage (= 8.0.5.1)
activesupport (= 8.0.5.1)
actionmailbox (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
actionmailer (8.0.5.1)
actionpack (= 8.0.5.1)
actionview (= 8.0.5.1)
activejob (= 8.0.5.1)
activesupport (= 8.0.5.1)
actionmailer (8.1.3.1)
actionpack (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.5.1)
actionview (= 8.0.5.1)
activesupport (= 8.0.5.1)
actionpack (8.1.3.1)
actionview (= 8.1.3.1)
activesupport (= 8.1.3.1)
nokogiri (>= 1.8.5)
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 (8.0.5.1)
actionpack (= 8.0.5.1)
activerecord (= 8.0.5.1)
activestorage (= 8.0.5.1)
activesupport (= 8.0.5.1)
actiontext (8.1.3.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.5.1)
activesupport (= 8.0.5.1)
actionview (8.1.3.1)
activesupport (= 8.1.3.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.5.1)
activesupport (= 8.0.5.1)
activejob (8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.3.6)
activemodel (8.0.5.1)
activesupport (= 8.0.5.1)
activerecord (8.0.5.1)
activemodel (= 8.0.5.1)
activesupport (= 8.0.5.1)
activemodel (8.1.3.1)
activesupport (= 8.1.3.1)
activerecord (8.1.3.1)
activemodel (= 8.1.3.1)
activesupport (= 8.1.3.1)
timeout (>= 0.4.0)
activerecord-import (2.2.0)
activerecord (>= 4.2)
activestorage (8.0.5.1)
actionpack (= 8.0.5.1)
activejob (= 8.0.5.1)
activerecord (= 8.0.5.1)
activesupport (= 8.0.5.1)
activestorage (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activesupport (= 8.1.3.1)
marcel (~> 1.0)
activesupport (8.0.5.1)
activesupport (8.1.3.1)
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)
Expand Down Expand Up @@ -166,7 +169,6 @@ GEM
baran (0.1.12)
base64 (0.3.0)
bcrypt (3.1.22)
benchmark (0.5.0)
bigdecimal (3.3.1)
builder (3.3.0)
bullet (8.1.3)
Expand Down Expand Up @@ -235,7 +237,7 @@ GEM
htmlentities (~> 4.3)
launchy (>= 2.1, < 4.0)
mail (~> 2.7)
erb (6.0.6)
erb (6.0.7)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
Expand Down Expand Up @@ -311,7 +313,7 @@ GEM
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2)
io-console (0.9.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
Expand Down Expand Up @@ -428,7 +430,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.6)
rack (3.2.7)
rack-cors (3.0.0)
logger
rack (>= 3.0.14)
Expand All @@ -441,20 +443,20 @@ GEM
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (8.0.5.1)
actioncable (= 8.0.5.1)
actionmailbox (= 8.0.5.1)
actionmailer (= 8.0.5.1)
actionpack (= 8.0.5.1)
actiontext (= 8.0.5.1)
actionview (= 8.0.5.1)
activejob (= 8.0.5.1)
activemodel (= 8.0.5.1)
activerecord (= 8.0.5.1)
activestorage (= 8.0.5.1)
activesupport (= 8.0.5.1)
rails (8.1.3.1)
actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3.1)
actionpack (= 8.1.3.1)
actiontext (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activemodel (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
bundler (>= 1.15.0)
railties (= 8.0.5.1)
railties (= 8.1.3.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
Expand All @@ -469,9 +471,9 @@ GEM
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.5.1)
actionpack (= 8.0.5.1)
activesupport (= 8.0.5.1)
railties (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
Expand All @@ -483,7 +485,7 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rbs (4.1.1)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
Expand All @@ -500,7 +502,7 @@ GEM
redis-client (0.28.0)
connection_pool
regexp_parser (2.12.0)
reline (0.6.3)
reline (0.7.0)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
Expand Down Expand Up @@ -642,7 +644,7 @@ GEM
nokogiri (~> 1.8)
yajl-ruby (1.4.3)
yard (0.9.45)
zeitwerk (2.8.2)
zeitwerk (2.8.3)

PLATFORMS
ruby
Expand Down Expand Up @@ -706,7 +708,7 @@ DEPENDENCIES
puma (~> 7.2)
rack-cors
rack-mini-profiler
rails (~> 8.0.0)
rails (~> 8.1.0)
rails-controller-testing
rails-html-sanitizer (>= 1.7.1)
recaptcha
Expand Down
25 changes: 18 additions & 7 deletions app/controllers/user/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,27 @@ def create
return
end

# Validate the enrolment target *before* +super+. Devise's +create+ renders via +respond_with+,
# and since Rails 8.1 +head+ raises +AbstractController::DoubleRenderError+ when a response body
# has already been set, so these guards can no longer run after it.
#
# Checking first is also correct on its own terms: a non-local +return+ out of a transaction
# block commits the transaction (Rails 7.1+), so the previous ordering created the user account
# and *then* returned 404/403, leaving an orphaned registration behind.
enrol_course = nil
enrol_course_id = invitation_params[:enrol_course_id].presence
if enrol_course_id
enrol_course = Course.find_by(id: enrol_course_id)
return head :not_found unless enrol_course

# this endpoint is accessible to unauthenticated users, so authorize! isn't used
return head :forbidden unless enrol_course.published && enrol_course.enrollable
end

User.transaction do
super

if resource.persisted? && invitation_params[:enrol_course_id]
enrol_course = Course.find_by(id: invitation_params[:enrol_course_id])
head :not_found and return unless enrol_course

# this endpoint is accessible to unauthenticated users, so authorize! isn't used
head :forbidden and return unless enrol_course.published && enrol_course.enrollable

if resource.persisted? && enrol_course
@enrol_request = Course::EnrolRequest.create!(
user: @user,
course_id: invitation_params[:enrol_course_id],
Expand Down
54 changes: 54 additions & 0 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,58 @@ class ApplicationRecord < ActiveRecord::Base

include ApplicationUserstampConcern
include ApplicationActsAsConcern

# Builds the raw SQL expression behind a `calculated_attributes` attribute, for use in `order`.
#
# Ordering by the *alias* (`order('average_achievement_count DESC')`) only works while the
# aliased projection survives in the relation's select list. That is not guaranteed:
# `calculated_attributes` v1.2.0 wipes `select_values` in place inside `Relation#calculate`, so
# any `count` on the relation (`each_cons` and Bullet both trigger one) leaves an ORDER BY
# referencing a column that is no longer selected — `PG::UndefinedColumn`.
#
# Ordering by the expression instead is self-contained, and costs nothing: PostgreSQL emits an
# identical plan either way, reusing the same `SubPlan` for the select-list and sort-key copies
# (verified with EXPLAIN — byte-identical plans).
#
# Derives the expression from the same lambda that defines the projection, so the two cannot
# drift apart.
#
# Mirrors the resolution and normalisation that `calculated_attributes` itself performs in
# `ActiveRecord::Relation#calculated` (`model_methods.rb`), so the ORDER BY expression and the
# SELECT projection are always built the same way:
# - falls back to `base_class` for MTI/STI models, as the gem does;
# - forwards `*args`, since attributes may be parameterised
# (e.g. `calculated :topic_unread_count, ->(user) { ... }` in `Course::Forum`);
# - accepts every shape a lambda may return — a relation, a raw SQL string, a
# `sanitize_sql` array, or a bare Arel node.
#
# @param [Symbol] attribute The calculated attribute name.
# @param [Array] args Arguments for a parameterised attribute, forwarded to its lambda.
# @return [Arel::Nodes::Node] An expression suitable for `order`.
# @raise [ArgumentError] If the attribute is not defined on this model or its base class.
def self.calculated_expression(attribute, *)
calculated_expression_to_arel(calculated_attribute_lambda(attribute).call(*))
end

# Resolves a calculated attribute's lambda, falling back to +base_class+ for MTI/STI models the
# way the gem does. Fails loudly rather than letting a typo surface as +NoMethodError+ on +nil+.
def self.calculated_attribute_lambda(attribute)
calculated.calculated[attribute] || base_class.calculated.calculated[attribute] ||
raise(ArgumentError, "#{name} has no calculated attribute #{attribute.inspect}")
end
private_class_method :calculated_attribute_lambda

# Normalises whatever a calculated lambda returns into an Arel expression, matching the gem's own
# handling: a +sanitize_sql+ array, a raw SQL string, a relation, or an Arel node.
def self.calculated_expression_to_arel(sql)
# NB: the array is passed whole, not splatted. The gem's own copy of this does
# `send(:sanitize_sql, *sql)`, which hands `['... ?', value]` to a one-argument method and
# raises. `sanitize_sql(condition)` expects the array itself.
sql = send(:sanitize_sql, sql) if sql.is_a?(Array)
return Arel.sql("(#{sql})") if sql.is_a?(String)
return Arel.sql("(#{sql.to_sql})") if sql.respond_to?(:to_sql)

sql
end
private_class_method :calculated_expression_to_arel
end
Loading