Skip to content
Open
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
22 changes: 18 additions & 4 deletions app/controllers/api/v1/live/live_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,33 @@ def podiums
end

def open_round
competition_id = params.require(:competition_id)
competition = Competition.find(params.require(:competition_id))
wcif_id = params.require(:round_id)

round = Round.find_by_wcif_id!(wcif_id, competition_id)
round = Round.find_by_wcif_id!(wcif_id, competition.id)

# TODO: Move these to actual error codes at one point
return render json: { status: "unauthorized" }, status: :unauthorized unless @current_user.can_manage_competition?(competition)
# Also think about if we should auto open all round ones at competition day start and not have this check
return render json: { status: "previous round has empty results" }, status: :bad_request unless round.number == 1 || round.previous_round.score_taking_done?

return render json: { status: "round already open" }, status: :bad_request if round.live_results.any?

result = round.init_round
result = round.open_and_lock_previous(@current_user)

render json: { status: "ok", locked_rows: result }
end

def quit_competitor
competition = Competition.find(params.require(:competition_id))
registration_id = params.require(:registration_id)

return render json: { status: "unauthorized" }, status: :unauthorized unless @current_user.can_manage_competition?(competition)

round = Round.find_by_wcif_id!(wcif_id, competition.id)

quit_count = round.quit_from_round!(registration_id, @current_user)

render json: { status: "ok", added_rows: result.affected_rows }
render json: { status: "ok", quit: quit_count }
end
end
12 changes: 11 additions & 1 deletion app/models/live_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ class LiveResult < ApplicationRecord

belongs_to :round

belongs_to :quit_by, class_name: 'User', optional: true

belongs_to :locked_by, class_name: 'User', optional: true

scope :not_empty, -> { where.not(best: 0) }

scope :without_quitters, -> { where(quit_by_id: nil).or(where.not(best: 0)) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which case do you need the second or(where.not(best: 0)) part?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sidenote: .or(not_empty) should work

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do still want to show quitters for previous rounds.
Currently when someone is quit from round 2 who advanced from round 1 I mark them as quit in round 2 and 1, but I still want to show them in round 1. There is a case to be made to only mark them as quit in round 1 as those results will already be locked so only flipping the advancing will be necessary?


alias_attribute :result_id, :id

has_one :event, through: :round
Expand Down Expand Up @@ -47,6 +53,10 @@ def best_possible_solve_times
end
end

def mark_as_quit(current_user)
update(quit_by_id: current_user.id, advancing: false, advancing_questionable: false)
end

def self.compute_average_and_best(attempts, round)
r = Result.new(
event_id: round.event.id,
Expand Down Expand Up @@ -84,6 +94,6 @@ def notify_users
end

def trigger_recompute_columns
round.recompute_live_columns
round.recompute_live_columns(skip_advancing: locked_by.present?)
end
end
39 changes: 34 additions & 5 deletions app/models/round.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ def format
has_many :wcif_extensions, as: :extendable, dependent: :delete_all

has_many :live_results, -> { order(:global_pos) }
has_many :live_competitors, through: :live_results, source: :registration
has_many :live_results_without_quitters,
-> { without_quitters.order(:global_pos) },
class_name: "LiveResult"
Comment on lines +47 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it feels like this shouldn't be a separate relation. Why does some_round.live_results.without_quitters (ie, applying the without_quitters scope to the live_results relation) not work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because I use it in a scope one line down in the
has_many :live_competitors, through: :live_results_without_quitters, source: :registration

has_many :live_competitors, through: :live_results_without_quitters, source: :registration
has_many :results
has_many :scrambles

Expand Down Expand Up @@ -182,6 +185,13 @@ def advancing_registrations
end
end

def open_and_lock_previous(current_user)
init_round
return 0 if number == 1 || linked_round.present?
Comment on lines +189 to +190
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you ignoring the return value of init_round? And what does the return value 0 represent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value is how many results where locked and it's 0 if there is no previous round to lock

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you returning early in case of a linked round? The linked round's individual rounds would all need to be locked, and returning early feels a bit premature...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is about locking the previous round as the title suggests. Linked Rounds do not have previous rounds


previous_round.lock_results(current_user)
end

def init_round
empty_results = advancing_registrations.map do |r|
{ registration_id: r.id, round_id: id, average: 0, best: 0, last_attempt_entered_at: current_time_from_proper_timezone }
Expand All @@ -193,18 +203,18 @@ def total_competitors
live_competitors.count
end

def recompute_live_columns
def recompute_live_columns(skip_advancing: false)
recompute_local_pos
recompute_global_pos
recompute_advancing
recompute_advancing unless skip_advancing
end

def recompute_advancing
has_linked_round = linked_round.present?
advancement_determining_results = has_linked_round ? linked_round.live_results : live_results

# Only ranked results can be considered for advancing.
round_results = advancement_determining_results.where.not(global_pos: nil)
# Only ranked results that are not locked can be considered for advancing.
round_results = advancement_determining_results.where.not(global_pos: nil).where(locked_by_id: nil)
round_results.update_all(advancing: false, advancing_questionable: false)

missing_attempts = total_competitors - round_results.count
Expand Down Expand Up @@ -353,6 +363,25 @@ def self.wcif_to_round_attributes(event, wcif, round_number, total_rounds)
}
end

def lock_results(locking_user)
results_to_lock = linked_round.present? ? linked_round.live_results : live_results

results_to_lock.update_all(locked_by_id: locking_user.id)
end

def quit_from_round!(registration_id, quitting_user)
result = live_results.find_by!(registration_id: registration_id)

is_quit = result.mark_as_quit(quitting_user)

return is_quit ? 1 : 0 if number == 1 || linked_round.present?

# We need to also quit the result from the previous round so advancement can be correctly shown
previous_round_results = previous_round.linked_round.present? ? previous_round.linked_round.live_results : previous_round.live_results

previous_round_results.where(registration_id: registration_id).map { it.mark_as_quit(quitting_user) }.count { it == true }
end

def wcif_id
"#{self.event_id}-r#{self.number}"
end
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
namespace :live do
get '/rounds/:round_id' => 'live#round_results', as: :live_round_results
put '/rounds/:round_id/open' => "live#open_round", as: :live_round_open
put '/rounds/:round_id/:registration_id' => 'live#quit_competitor', as: :quit_competitor_from_round
get '/podiums' => 'live#podiums', as: :live_podiums
get '/registrations/:registration_id' => 'live#by_person', as: :get_live_by_person
end
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20260128194213_add_locked_by_and_quit_by.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class AddLockedByAndQuitBy < ActiveRecord::Migration[8.1]
def change
change_table :live_results, bulk: true do |t|
t.references :quit_by, type: :integer, foreign_key: { to_table: :users }
t.references :locked_by, type: :integer, foreign_key: { to_table: :users }
end
end
end
8 changes: 7 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_01_26_101113) do
ActiveRecord::Schema[8.1].define(version: 2026_01_28_194213) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "blob_id", null: false
t.datetime "created_at", precision: nil, null: false
Expand Down Expand Up @@ -779,10 +779,14 @@
t.integer "global_pos"
t.datetime "last_attempt_entered_at", null: false
t.integer "local_pos"
t.integer "locked_by_id"
t.integer "quit_by_id"
t.bigint "registration_id", null: false
t.bigint "round_id", null: false
t.string "single_record_tag", limit: 255
t.datetime "updated_at", null: false
t.index ["locked_by_id"], name: "index_live_results_on_locked_by_id"
t.index ["quit_by_id"], name: "index_live_results_on_quit_by_id"
t.index ["registration_id", "round_id"], name: "index_live_results_on_registration_id_and_round_id", unique: true
t.index ["registration_id"], name: "index_live_results_on_registration_id"
t.index ["round_id"], name: "index_live_results_on_round_id"
Expand Down Expand Up @@ -1579,6 +1583,8 @@
add_foreign_key "inbox_scrambles", "inbox_scramble_sets"
add_foreign_key "inbox_scrambles", "inbox_scramble_sets", column: "matched_scramble_set_id"
add_foreign_key "live_attempt_history_entries", "live_attempts"
add_foreign_key "live_results", "users", column: "locked_by_id"
add_foreign_key "live_results", "users", column: "quit_by_id"
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", on_delete: :cascade
add_foreign_key "payment_intents", "users", column: "initiated_by_id"
add_foreign_key "paypal_records", "paypal_records", column: "parent_record_id"
Expand Down
3 changes: 3 additions & 0 deletions spec/factories/live_results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
average { 5000 }
last_attempt_entered_at { Time.now.utc }

locked_by_id { nil }
quit_by_id { nil }

transient do
attempts_count { 5 }
end
Expand Down
63 changes: 63 additions & 0 deletions spec/requests/live_recompute_advancing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,68 @@ def attempt_result_condition
expect(round.live_results.pluck(:advancing)).to eq([true, true, false, false, false])
end
end

context "with locked results" do
it "doesn't change advancing of locked results" do
round = create(:round, number: 1, total_number_of_rounds: 2, event_id: "333", competition: competition, advancement_condition: attempt_result_condition)

5.times do |i|
create(:live_result, registration: registrations[i], round: round, average: (i + 1) * 100)
end

expect(round.total_competitors).to eq 5
expect(round.competitors_live_results_entered).to eq 5

round.lock_results(User.first)
# Update best/average after locking
round.live_results.last.update(average: 50)

# Advancing is not updated, but ranking is
expect(round.live_results.pluck(:global_pos, :advancing)).to eq([[1, false], [2, true], [3, true], [4, false], [5, false]])
end
end

context "with quit results" do
it "quit from first round excludes from competitors" do
round = create(:round, number: 1, total_number_of_rounds: 2, event_id: "333", competition: competition, advancement_condition: attempt_result_condition)
registration_1 = registrations.first

# Open Round
round.open_and_lock_previous(User.first)

# Quit Competitor
round.quit_from_round!(registration_1.id, User.first)

# Quit users is not part of the rounds competitors
expect(round.live_competitors.count).to eq 4
expect(round.live_competitors.pluck(:registration_id)).not_to include registrations.first.id
end

it "quit from next round marks as no advancing in previous round" do
round = create(:round, number: 1, total_number_of_rounds: 2, event_id: "333", competition: competition, advancement_condition: attempt_result_condition)
final = create(:round, number: 2, total_number_of_rounds: 2, event_id: "333", competition: competition)

5.times do |i|
create(:live_result, registration: registrations[i], round: round, average: (i + 1) * 100)
end

expect(round.total_competitors).to eq 5
expect(round.competitors_live_results_entered).to eq 5

# Open next round and quit first result from it
final.open_and_lock_previous(User.first)
final.quit_from_round!(registrations.first.id, User.first)

# Quit user is marked as not advancing
expect(round.live_results.reload.pluck(:global_pos, :advancing)).to eq([[1, false], [2, true], [3, false], [4, false], [5, false]])

# Quit users is not part of the final round competitors
expect(final.live_competitors.count).to eq 1
expect(final.live_competitors.first.id).to eq registrations.second.id

# But still part of the first round competitors
expect(round.live_competitors.count).to eq 5
end
end
end
end