diff --git a/README.md b/README.md
index 7b1c40b..023e0b5 100644
--- a/README.md
+++ b/README.md
@@ -34,11 +34,14 @@ In development environments, background jobs run inline (synchronously) by defau
To run a single test file:
1. Get a shell with ruby and rails: `bash bin/rails-dev-run.sh wrestlingdev-development`
-2. `rake test TEST=test/models/match_test.rb`
+2. `rake test TEST=test/models/match_test.rb` OR `rails test test/models/match_test.rb`
To run a single test inside a file:
1. Get a shell with ruby and rails: `bash bin/rails-dev-run.sh wrestlingdev-development`
-2. `rake test TEST=test/models/match_test.rb TESTOPTS="--name='/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/'"`
+2. `rake test TEST=test/models/match_test.rb TESTOPTS="--name='/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/'"` OR `rails test test/models/match_test.rb --name=/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/`
+
+To run tests in verbose mode (outputs the time for each test file and the test file name)
+`rails test -v`
## Develop with rvm
With rvm installed, run `rvm install ruby-3.2.0`
diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb
index 1375f1b..46cbfa2 100644
--- a/app/controllers/matches_controller.rb
+++ b/app/controllers/matches_controller.rb
@@ -1,6 +1,6 @@
class MatchesController < ApplicationController
- before_action :set_match, only: [:show, :edit, :update, :stat, :spectate]
- before_action :check_access, only: [:edit,:update, :stat]
+ before_action :set_match, only: [:show, :edit, :update, :stat, :spectate, :edit_assignment, :update_assignment]
+ before_action :check_access, only: [:edit, :update, :stat, :edit_assignment, :update_assignment]
# GET /matches/1
# GET /matches/1.json
@@ -21,7 +21,7 @@ class MatchesController < ApplicationController
session[:return_path] = "/tournaments/#{@match.tournament.id}/matches"
end
- def stat
+ def stat
# @show_next_bout_button = false
if params[:match]
@match = Match.where(:id => params[:match]).includes(:wrestlers).first
@@ -50,8 +50,21 @@ class MatchesController < ApplicationController
end
@tournament = @match.tournament
end
- session[:return_path] = "/tournaments/#{@tournament.id}/matches"
- session[:error_return_path] = "/matches/#{@match.id}/stat"
+ if @match&.mat
+ @mat = @match.mat
+ queue_position = @mat.queue_position_for_match(@match)
+ @next_match = queue_position == 1 ? @mat.queue2_match : nil
+ @show_next_bout_button = queue_position == 1
+ if request.referer&.include?("/tournaments/#{@tournament.id}/matches")
+ session[:return_path] = "/tournaments/#{@tournament.id}/matches"
+ else
+ session[:return_path] = mat_path(@mat)
+ end
+ session[:error_return_path] = "/matches/#{@match.id}/stat"
+ else
+ session[:return_path] = "/tournaments/#{@tournament.id}/matches"
+ session[:error_return_path] = "/matches/#{@match.id}/stat"
+ end
end
# GET /matches/:id/spectate
@@ -71,6 +84,49 @@ class MatchesController < ApplicationController
end
end
+ # GET /matches/1/edit_assignment
+ def edit_assignment
+ @tournament = @match.tournament
+ @mats = @tournament.mats.sort_by(&:name)
+ @current_mat = @match.mat
+ @current_queue_position = @current_mat&.queue_position_for_match(@match)
+ session[:return_path] = "/tournaments/#{@tournament.id}/matches"
+ end
+
+ # PATCH /matches/1/update_assignment
+ def update_assignment
+ @tournament = @match.tournament
+ mat_id = params.dig(:match, :mat_id)
+ queue_position = params.dig(:match, :queue_position)
+
+ if mat_id.blank?
+ Mat.where("queue1 = :match_id OR queue2 = :match_id OR queue3 = :match_id OR queue4 = :match_id", match_id: @match.id)
+ .find_each { |mat| mat.remove_match_from_queue_and_collapse!(@match.id) }
+ @match.update(mat_id: nil)
+ redirect_to session.delete(:return_path) || "/tournaments/#{@tournament.id}/matches", notice: "Match assignment cleared."
+ return
+ end
+
+ if queue_position.blank?
+ redirect_to edit_assignment_match_path(@match), alert: "Queue position is required when selecting a mat."
+ return
+ end
+
+ unless %w[1 2 3 4].include?(queue_position.to_s)
+ redirect_to edit_assignment_match_path(@match), alert: "Queue position must be between 1 and 4."
+ return
+ end
+
+ mat = @tournament.mats.find_by(id: mat_id)
+ unless mat
+ redirect_to edit_assignment_match_path(@match), alert: "Selected mat was not found."
+ return
+ end
+
+ mat.assign_match_to_queue!(@match, queue_position)
+ redirect_to session.delete(:return_path) || "/tournaments/#{@tournament.id}/matches", notice: "Match assignment updated."
+ end
+
# PATCH/PUT /matches/1
# PATCH/PUT /matches/1.json
def update
diff --git a/app/controllers/mats_controller.rb b/app/controllers/mats_controller.rb
index afeac39..7d3745a 100644
--- a/app/controllers/mats_controller.rb
+++ b/app/controllers/mats_controller.rb
@@ -10,13 +10,13 @@ class MatsController < ApplicationController
if bout_number_param
@show_next_bout_button = false
- @match = @mat.unfinished_matches.find { |m| m.bout_number == bout_number_param.to_i }
+ @match = @mat.queue_matches.compact.find { |m| m.bout_number == bout_number_param.to_i }
else
@show_next_bout_button = true
- @match = @mat.unfinished_matches.first
+ @match = @mat.queue1_match
end
- @next_match = @mat.unfinished_matches.second # Second unfinished match on the mat
+ @next_match = @mat.queue2_match # Second match on the mat
@wrestlers = []
if @match
@@ -82,8 +82,8 @@ class MatsController < ApplicationController
def assign_next_match
@tournament = @mat.tournament_id
respond_to do |format|
- if @mat.assign_next_match
- format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Next Match on Mat #{@mat.name} successfully completed." }
+ if @mat.advance_queue!
+ format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Mat #{@mat.name} queue advanced." }
format.json { head :no_content }
else
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", alert: "There was an error." }
diff --git a/app/models/mat.rb b/app/models/mat.rb
index 7d7bbfe..2733b28 100644
--- a/app/models/mat.rb
+++ b/app/models/mat.rb
@@ -1,53 +1,50 @@
class Mat < ApplicationRecord
+ include ActionView::RecordIdentifier
belongs_to :tournament
has_many :matches, dependent: :nullify
has_many :mat_assignment_rules, dependent: :destroy
validates :name, presence: true
- before_destroy do
- if tournament.matches.size > 0
- tournament.reset_mats
- matsToAssign = tournament.mats.select{|m| m.id != self.id}
- tournament.assign_mats(matsToAssign)
- end
- end
-
- after_create do
- if tournament.matches.size > 0
- tournament.reset_mats
- matsToAssign = tournament.mats
- tournament.assign_mats(matsToAssign)
- end
- end
+ QUEUE_SLOTS = %w[queue1 queue2 queue3 queue4].freeze
def assign_next_match
+ slot = first_empty_queue_slot
+ return true unless slot
+
match = next_eligible_match
- self.matches.reload
- if match and self.unfinished_matches.size < 4
- match.mat_id = self.id
- if match.save
- # Invalidate any wrestler caches
- if match.w1
- match.wrestler1.touch
- match.wrestler1.school.touch
+ return false unless match
+
+ place_match_in_empty_slot!(match, slot)
+ true
+ end
+
+ def advance_queue!(finished_match = nil)
+ self.class.transaction do
+ if finished_match
+ position = queue_position_for_match(finished_match)
+ if position == 1
+ shift_queue_forward!
+ fill_queue_slots!
+ elsif position
+ remove_match_from_queue_and_collapse!(finished_match.id)
+ else
+ fill_queue_slots!
end
- if match.w2
- match.wrestler2.touch
- match.wrestler2.school.touch
- end
- return true
else
- return false
+ if queue1_match&.finished == 1
+ shift_queue_forward!
+ end
+ fill_queue_slots!
end
- else
- return true
end
+ broadcast_current_match
+ true
end
def next_eligible_match
# Start with all matches that are either unfinished (nil or 0), have a bout number, and are ordered by bout_number
- filtered_matches = tournament.matches
+ filtered_matches = Match.where(tournament_id: tournament_id)
.where(finished: [nil, 0]) # finished is nil or 0
.where(mat_id: nil) # mat_id is nil
.where.not(bout_number: nil) # bout_number is not nil
@@ -57,6 +54,11 @@ class Mat < ApplicationRecord
filtered_matches = filtered_matches
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
+
+ # Filter out matches without a wrestlers
+ filtered_matches = filtered_matches
+ .where("w1 IS NOT NULL")
+ .where("w2 IS NOT NULL")
# Apply mat assignment rules
mat_assignment_rules.each do |rule|
@@ -80,9 +82,178 @@ class Mat < ApplicationRecord
filtered_matches.first
end
+ def queue_match_ids
+ QUEUE_SLOTS.map { |slot| public_send(slot) }
+ end
+
+ def queue_matches
+ queue_match_ids.map { |match_id| match_id ? Match.find_by(id: match_id) : nil }
+ end
+
+ def queue1_match
+ queue_match_at(1)
+ end
+
+ def queue2_match
+ queue_match_at(2)
+ end
+
+ def queue3_match
+ queue_match_at(3)
+ end
+
+ def queue4_match
+ queue_match_at(4)
+ end
+
+ def queue_position_for_match(match)
+ return nil unless match
+ return 1 if queue1 == match.id
+ return 2 if queue2 == match.id
+ return 3 if queue3 == match.id
+ return 4 if queue4 == match.id
+ nil
+ end
+
+ def remove_match_from_queue_and_collapse!(match_id)
+ queue_ids = queue_match_ids
+ return if queue_ids.none? { |id| id == match_id }
+
+ queue_ids.map! { |id| id == match_id ? nil : id }
+ queue_ids = queue_ids.compact
+ queue_ids += [nil] * (4 - queue_ids.size)
+
+ update!(
+ queue1: queue_ids[0],
+ queue2: queue_ids[1],
+ queue3: queue_ids[2],
+ queue4: queue_ids[3]
+ )
+
+ fill_queue_slots!
+ broadcast_current_match
+ end
+
+ def assign_match_to_queue!(match, position)
+ position = position.to_i
+ raise ArgumentError, "Queue position must be 1-4" unless (1..4).cover?(position)
+
+ self.class.transaction do
+ match.update!(mat_id: id)
+ remove_match_from_other_mats!(match.id)
+
+ queue_ids = queue_match_ids.map { |id| id == match.id ? nil : id }
+ queue_ids = queue_ids.compact
+
+ queue_ids.insert(position - 1, match.id)
+ bumped_match_id = queue_ids.length > 4 ? queue_ids.pop : nil
+
+ queue_ids += [nil] * (4 - queue_ids.length)
+
+ update!(
+ queue1: queue_ids[0],
+ queue2: queue_ids[1],
+ queue3: queue_ids[2],
+ queue4: queue_ids[3]
+ )
+
+ bumped_match = Match.find_by(id: bumped_match_id)
+ if bumped_match && bumped_match.finished != 1
+ bumped_match.update!(mat_id: nil)
+ end
+ end
+ broadcast_current_match
+ end
+
+ def clear_queue!
+ update!(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
+ end
def unfinished_matches
matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
end
+ private
+
+ def queue_match_at(position)
+ match_id = public_send("queue#{position}")
+ match_id ? Match.find_by(id: match_id) : nil
+ end
+
+ def first_empty_queue_slot
+ QUEUE_SLOTS.each_with_index do |slot, index|
+ return index + 1 if public_send(slot).nil?
+ end
+ nil
+ end
+
+ def shift_queue_forward!
+ update!(
+ queue1: queue2,
+ queue2: queue3,
+ queue3: queue4,
+ queue4: nil
+ )
+ end
+
+ def fill_queue_slots!
+ queue_ids = queue_match_ids
+ updated = false
+
+ QUEUE_SLOTS.each_with_index do |_slot, index|
+ next if queue_ids[index].present?
+
+ match = next_eligible_match
+ break unless match
+
+ queue_ids[index] = match.id
+ match.update!(mat_id: id)
+ updated = true
+ end
+
+ if updated
+ update!(
+ queue1: queue_ids[0],
+ queue2: queue_ids[1],
+ queue3: queue_ids[2],
+ queue4: queue_ids[3]
+ )
+ end
+ end
+
+ def remove_match_from_other_mats!(match_id)
+ self.class.where.not(id: id)
+ .where("queue1 = :match_id OR queue2 = :match_id OR queue3 = :match_id OR queue4 = :match_id", match_id: match_id)
+ .find_each do |mat|
+ mat.remove_match_from_queue_and_collapse!(match_id)
+ end
+ end
+
+ def place_match_in_empty_slot!(match, slot)
+ self.class.transaction do
+ match.update!(mat_id: id)
+ remove_match_from_other_mats!(match.id)
+ update!(slot_key(slot) => match.id)
+ end
+ broadcast_current_match
+ end
+
+ def slot_key(slot)
+ "queue#{slot}"
+ end
+
+ def broadcast_current_match
+ Turbo::StreamsChannel.broadcast_update_to(
+ self,
+ target: dom_id(self, :current_match),
+ partial: "mats/current_match",
+ locals: {
+ mat: self,
+ match: queue1_match,
+ next_match: queue2_match,
+ show_next_bout_button: true
+ }
+ )
+ end
+
end
diff --git a/app/models/match.rb b/app/models/match.rb
index 524c0a2..8c47892 100644
--- a/app/models/match.rb
+++ b/app/models/match.rb
@@ -37,12 +37,14 @@ class Match < ApplicationRecord
wrestler2.touch
end
if self.finished == 1 && self.winner_id != nil
- if self.mat
- self.mat.assign_next_match
- end
advance_wrestlers
+ if self.mat
+ self.mat.advance_queue!(self)
+ end
+ self.tournament.refill_open_bout_board_queues
# School point calculation has move to the end of advance wrestler
# calculate_school_points
+ self.update(mat_id: nil)
end
end
@@ -352,13 +354,13 @@ class Match < ApplicationRecord
next unless mat
Turbo::StreamsChannel.broadcast_update_to(
- mat,
- target: dom_id(mat, :current_match),
- partial: "mats/current_match",
- locals: {
- mat: mat,
- match: mat.unfinished_matches.first,
- next_match: mat.unfinished_matches.second,
+ mat,
+ target: dom_id(mat, :current_match),
+ partial: "mats/current_match",
+ locals: {
+ mat: mat,
+ match: mat.queue1_match,
+ next_match: mat.queue2_match,
show_next_bout_button: true
}
)
diff --git a/app/models/tournament.rb b/app/models/tournament.rb
index eb3bc68..091a1be 100644
--- a/app/models/tournament.rb
+++ b/app/models/tournament.rb
@@ -82,23 +82,18 @@ class Tournament < ApplicationRecord
matches.maximum(:round) || 0 # Return 0 if no matches or max round is nil
end
- def assign_mats(mats_to_assign)
- if mats_to_assign.count > 0
- until mats_to_assign.sort_by{|m| m.id}.last.matches.count == 4
- mats_to_assign.sort_by{|m| m.id}.each do |m|
- m.assign_next_match
- end
- end
- end
- end
-
def reset_mats
+ matches.reload
+ mats.reload
matches_to_reset = matches.select{|m| m.mat_id != nil}
# matches_to_reset.update_all( {:mat_id => nil } )
matches_to_reset.each do |m|
m.mat_id = nil
m.save
end
+ mats.each do |mat|
+ mat.clear_queue!
+ end
end
def pointAdjustments
@@ -228,19 +223,24 @@ class Tournament < ApplicationRecord
def reset_and_fill_bout_board
reset_mats
-
- if mats.any?
- 4.times do
- # Iterate over each mat and assign the next available match
- mats.each do |mat|
- match_assigned = mat.assign_next_match
- # If no more matches are available, exit early
- unless match_assigned
- puts "No more eligible matches to assign."
- return
- end
+ matches.reload
+ refill_open_bout_board_queues
+ end
+
+ def refill_open_bout_board_queues
+ return unless mats.any?
+
+ loop do
+ assigned_any = false
+ # Fill in round-robin order by queue depth:
+ # all mats queue1 first, then queue2, then queue3, then queue4.
+ (1..4).each do |slot|
+ mats.reload.each do |mat|
+ next unless mat.public_send("queue#{slot}").nil?
+ assigned_any ||= mat.assign_next_match
+ end
end
- end
+ break unless assigned_any
end
end
@@ -279,4 +279,4 @@ class Tournament < ApplicationRecord
def connection_adapter
ActiveRecord::Base.connection.adapter_name
end
-end
\ No newline at end of file
+end
diff --git a/app/services/tournament_services/tournament_backup_service.rb b/app/services/tournament_services/tournament_backup_service.rb
index cd678e0..f3ab02e 100644
--- a/app/services/tournament_services/tournament_backup_service.rb
+++ b/app/services/tournament_services/tournament_backup_service.rb
@@ -37,7 +37,11 @@ class TournamentBackupService
attributes: @tournament.attributes,
schools: @tournament.schools.map(&:attributes),
weights: @tournament.weights.map(&:attributes),
- mats: @tournament.mats.map(&:attributes),
+ mats: @tournament.mats.map do |mat|
+ mat.attributes.merge(
+ "queue_bout_numbers" => mat.queue_matches.map { |match| match&.bout_number }
+ )
+ end,
mat_assignment_rules: @tournament.mat_assignment_rules.map do |rule|
rule.attributes.merge(
mat: Mat.find_by(id: rule.mat_id)&.attributes.slice("name"),
diff --git a/app/services/tournament_services/wrestlingdev_importer.rb b/app/services/tournament_services/wrestlingdev_importer.rb
index fa7fade..fefc42e 100644
--- a/app/services/tournament_services/wrestlingdev_importer.rb
+++ b/app/services/tournament_services/wrestlingdev_importer.rb
@@ -45,15 +45,16 @@ class WrestlingdevImporter
# Note: Teampointadjusts are deleted via School/Wrestler cascade
end
- def parse_data
- parse_tournament(@import_data["tournament"]["attributes"])
- parse_schools(@import_data["tournament"]["schools"])
- parse_weights(@import_data["tournament"]["weights"])
- parse_mats(@import_data["tournament"]["mats"])
- parse_wrestlers(@import_data["tournament"]["wrestlers"])
- parse_matches(@import_data["tournament"]["matches"])
- parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
- end
+ def parse_data
+ parse_tournament(@import_data["tournament"]["attributes"])
+ parse_schools(@import_data["tournament"]["schools"])
+ parse_weights(@import_data["tournament"]["weights"])
+ parse_mats(@import_data["tournament"]["mats"])
+ parse_wrestlers(@import_data["tournament"]["wrestlers"])
+ parse_matches(@import_data["tournament"]["matches"])
+ apply_mat_queues
+ parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
+ end
def parse_tournament(attributes)
attributes.except!("id")
@@ -74,12 +75,18 @@ class WrestlingdevImporter
end
end
- def parse_mats(mats)
- mats.each do |mat_attributes|
- mat_attributes.except!("id")
- Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
- end
- end
+ def parse_mats(mats)
+ @mat_queue_bout_numbers = {}
+ mats.each do |mat_attributes|
+ mat_name = mat_attributes["name"]
+ queue_bout_numbers = mat_attributes["queue_bout_numbers"]
+ mat_attributes.except!("id", "queue1", "queue2", "queue3", "queue4", "queue_bout_numbers", "tournament_id")
+ Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
+ if mat_name && queue_bout_numbers
+ @mat_queue_bout_numbers[mat_name] = queue_bout_numbers
+ end
+ end
+ end
def parse_mat_assignment_rules(mat_assignment_rules)
mat_assignment_rules.each do |rule_attributes|
@@ -134,9 +141,9 @@ class WrestlingdevImporter
end
end
- def parse_matches(matches)
- matches.each do |match_attributes|
- next unless match_attributes # Skip if match_attributes is nil
+ def parse_matches(matches)
+ matches.each do |match_attributes|
+ next unless match_attributes # Skip if match_attributes is nil
weight = Weight.find_by(max: match_attributes.dig("weight", "max"), tournament_id: @tournament.id)
mat = Mat.find_by(name: match_attributes.dig("mat", "name"), tournament_id: @tournament.id)
@@ -155,6 +162,53 @@ class WrestlingdevImporter
w2: w2&.id,
winner_id: winner&.id
))
- end
- end
-end
+ end
+ end
+
+ def apply_mat_queues
+ if @mat_queue_bout_numbers.blank?
+ Mat.where(tournament_id: @tournament.id).find_each do |mat|
+ match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
+ mat.update(
+ queue1: match_ids[0],
+ queue2: match_ids[1],
+ queue3: match_ids[2],
+ queue4: match_ids[3]
+ )
+ end
+ return
+ end
+
+ @mat_queue_bout_numbers.each do |mat_name, bout_numbers|
+ mat = Mat.find_by(name: mat_name, tournament_id: @tournament.id)
+ next unless mat
+
+ matches = Array(bout_numbers).map do |bout_number|
+ Match.find_by(bout_number: bout_number, tournament_id: @tournament.id)
+ end
+
+ mat.update(
+ queue1: matches[0]&.id,
+ queue2: matches[1]&.id,
+ queue3: matches[2]&.id,
+ queue4: matches[3]&.id
+ )
+
+ matches.compact.each do |match|
+ match.update(mat_id: mat.id)
+ end
+ end
+
+ Mat.where(tournament_id: @tournament.id)
+ .where(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
+ .find_each do |mat|
+ match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
+ mat.update(
+ queue1: match_ids[0],
+ queue2: match_ids[1],
+ queue3: match_ids[2],
+ queue4: match_ids[3]
+ )
+ end
+ end
+end
diff --git a/app/views/api/tournament.jbuilder b/app/views/api/tournament.jbuilder
index bcd807e..fd57f12 100644
--- a/app/views/api/tournament.jbuilder
+++ b/app/views/api/tournament.jbuilder
@@ -28,7 +28,7 @@ json.cache! ["api_tournament", @tournament] do
json.mats @mats do |mat|
json.name mat.name
- json.unfinished_matches mat.unfinished_matches do |match|
+ json.unfinished_matches mat.queue_matches.compact do |match|
json.bout_number match.bout_number
json.w1_name match.w1_name
json.w2_name match.w2_name
diff --git a/app/views/matches/edit_assignment.html.erb b/app/views/matches/edit_assignment.html.erb
new file mode 100644
index 0000000..3ad1cb7
--- /dev/null
+++ b/app/views/matches/edit_assignment.html.erb
@@ -0,0 +1,34 @@
+
Assign Mat/Queue for Match <%= @match.bout_number %>
+
+<% if @current_mat %>
+ Current Assignment: Mat <%= @current_mat.name %><%= @current_queue_position ? " (Queue #{@current_queue_position})" : "" %>
+<% else %>
+ Current Assignment: Unassigned
+<% end %>
+
+<%= form_with model: @match, url: update_assignment_match_path(@match), method: :patch do |f| %>
+
+ <%= f.label :mat_id, "Mat" %>
+ <%= f.collection_select :mat_id, @mats, :id, :name, { include_blank: "Unassigned" } %>
+
+
+
+ <%= f.label :queue_position, "Queue Position" %>
+ <%= f.select :queue_position,
+ options_for_select(
+ [
+ ["On Mat (Queue 1)", 1],
+ ["On Deck (Queue 2)", 2],
+ ["In The Hole (Queue 3)", 3],
+ ["Warm Up (Queue 4)", 4]
+ ],
+ @current_queue_position
+ ),
+ include_blank: "Select position"
+ %>
+
+
+
+ <%= f.submit "Update Assignment", class: "btn btn-success" %>
+
+<% end %>
diff --git a/app/views/mats/_current_match.html.erb b/app/views/mats/_current_match.html.erb
index 8c9c6a4..77f3605 100644
--- a/app/views/mats/_current_match.html.erb
+++ b/app/views/mats/_current_match.html.erb
@@ -1,6 +1,6 @@
<% @mat = mat %>
-<% @match = local_assigns[:match] || mat.unfinished_matches.first %>
-<% @next_match = local_assigns[:next_match] || mat.unfinished_matches.second %>
+<% @match = local_assigns[:match] || mat.queue1_match %>
+<% @next_match = local_assigns[:next_match] || mat.queue2_match %>
<% @show_next_bout_button = local_assigns.key?(:show_next_bout_button) ? local_assigns[:show_next_bout_button] : true %>
<% @wrestlers = [] %>
diff --git a/app/views/tournaments/export.html.erb b/app/views/tournaments/export.html.erb
index 4cc4121..d5d5ed1 100644
--- a/app/views/tournaments/export.html.erb
+++ b/app/views/tournaments/export.html.erb
@@ -3,7 +3,11 @@
"attributes": <%= @tournament.attributes.to_json %>,
"schools": <%= @tournament.schools.map(&:attributes).to_json %>,
"weights": <%= @tournament.weights.map(&:attributes).to_json %>,
- "mats": <%= @tournament.mats.map(&:attributes).to_json %>,
+ "mats": <%= @tournament.mats.map { |mat| mat.attributes.merge(
+ {
+ "queue_bout_numbers": mat.queue_matches.map { |match| match&.bout_number }
+ }
+ ) }.to_json %>,
"wrestlers": <%= @tournament.wrestlers.map { |wrestler| wrestler.attributes.merge(
{
"school": wrestler.school&.attributes,
@@ -20,4 +24,4 @@
}
) }.to_json %>
}
-}
\ No newline at end of file
+}
diff --git a/app/views/tournaments/matches.html.erb b/app/views/tournaments/matches.html.erb
index 65d38a6..6067457 100644
--- a/app/views/tournaments/matches.html.erb
+++ b/app/views/tournaments/matches.html.erb
@@ -28,6 +28,7 @@
<%= match.finished %> |
<%= link_to 'Show', match, :class=>"btn btn-default btn-sm" %>
<%= link_to 'Edit Wrestlers', edit_match_path(match), :class=>"btn btn-primary btn-sm" %>
+ <%= link_to 'Edit Mat/Queue', edit_assignment_match_path(match), :class=>"btn btn-primary btn-sm" %>
<%= link_to 'Stat Match', "/matches/#{match.id}/stat", :class=>"btn btn-primary btn-sm" %>
|
@@ -36,4 +37,4 @@
Total matches without byes: <%= @matches.select{|m| m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %>
-Unfinished matches: <%= @matches.select{|m| m.finished != 1 and m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %>
\ No newline at end of file
+Unfinished matches: <%= @matches.select{|m| m.finished != 1 and m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %>
diff --git a/app/views/tournaments/up_matches.html.erb b/app/views/tournaments/up_matches.html.erb
index a82c725..eebbf4b 100644
--- a/app/views/tournaments/up_matches.html.erb
+++ b/app/views/tournaments/up_matches.html.erb
@@ -45,31 +45,31 @@
| <%= m.name %> |
- <% if m.unfinished_matches.first %><%=m.unfinished_matches.first.bout_number%> (<%= m.unfinished_matches.first.bracket_position %>)
- <%= m.unfinished_matches.first.weight_max %> lbs
- <%= m.unfinished_matches.first.w1_bracket_name %> vs.
- <%= m.unfinished_matches.first.w2_bracket_name %>
+ <% if m.queue1_match %><%=m.queue1_match.bout_number%> (<%= m.queue1_match.bracket_position %>)
+ <%= m.queue1_match.weight_max %> lbs
+ <%= m.queue1_match.w1_bracket_name %> vs.
+ <%= m.queue1_match.w2_bracket_name %>
<% end %>
|
- <% if m.unfinished_matches.second %><%=m.unfinished_matches.second.bout_number%> (<%= m.unfinished_matches.second.bracket_position %>)
- <%= m.unfinished_matches.second.weight_max %> lbs
- <%= m.unfinished_matches.second.w1_bracket_name %> vs.
- <%= m.unfinished_matches.second.w2_bracket_name %>
+ <% if m.queue2_match %><%=m.queue2_match.bout_number%> (<%= m.queue2_match.bracket_position %>)
+ <%= m.queue2_match.weight_max %> lbs
+ <%= m.queue2_match.w1_bracket_name %> vs.
+ <%= m.queue2_match.w2_bracket_name %>
<% end %>
|
- <% if m.unfinished_matches.third %><%=m.unfinished_matches.third.bout_number%> (<%= m.unfinished_matches.third.bracket_position %>)
- <%= m.unfinished_matches.third.weight_max %> lbs
- <%= m.unfinished_matches.third.w1_bracket_name %> vs.
- <%= m.unfinished_matches.third.w2_bracket_name %>
+ <% if m.queue3_match %><%=m.queue3_match.bout_number%> (<%= m.queue3_match.bracket_position %>)
+ <%= m.queue3_match.weight_max %> lbs
+ <%= m.queue3_match.w1_bracket_name %> vs.
+ <%= m.queue3_match.w2_bracket_name %>
<% end %>
|
- <% if m.unfinished_matches.fourth %><%=m.unfinished_matches.fourth.bout_number%> (<%= m.unfinished_matches.fourth.bracket_position %>)
- <%= m.unfinished_matches.fourth.weight_max %> lbs
- <%= m.unfinished_matches.fourth.w1_bracket_name %> vs.
- <%= m.unfinished_matches.fourth.w2_bracket_name %>
+ <% if m.queue4_match %><%=m.queue4_match.bout_number%> (<%= m.queue4_match.bracket_position %>)
+ <%= m.queue4_match.weight_max %> lbs
+ <%= m.queue4_match.w1_bracket_name %> vs.
+ <%= m.queue4_match.w2_bracket_name %>
<% end %>
|
diff --git a/bin/run-all-tests.sh b/bin/run-all-tests.sh
index 1f9d09e..5c434f7 100755
--- a/bin/run-all-tests.sh
+++ b/bin/run-all-tests.sh
@@ -5,4 +5,4 @@ cd ${project_dir}
bundle exec rake db:migrate RAILS_ENV=test
CI=true brakeman
bundle exec bundle-audit check --update
-bundle exec rake test
+rails test -v
diff --git a/config/routes.rb b/config/routes.rb
index 255892e..bf27071 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,6 +10,8 @@ Wrestling::Application.routes.draw do
member do
get :stat
get :spectate
+ get :edit_assignment
+ patch :update_assignment
end
end
diff --git a/db/migrate/20260129120000_add_queues_to_mats.rb b/db/migrate/20260129120000_add_queues_to_mats.rb
new file mode 100644
index 0000000..7ea2a54
--- /dev/null
+++ b/db/migrate/20260129120000_add_queues_to_mats.rb
@@ -0,0 +1,49 @@
+class AddQueuesToMats < ActiveRecord::Migration[7.0]
+ class Mat < ActiveRecord::Base
+ self.table_name = "mats"
+ has_many :matches, class_name: "AddQueuesToMats::Match", foreign_key: "mat_id"
+ end
+
+ class Match < ActiveRecord::Base
+ self.table_name = "matches"
+ end
+
+ def up
+ add_column :mats, :queue1, :bigint
+ add_column :mats, :queue2, :bigint
+ add_column :mats, :queue3, :bigint
+ add_column :mats, :queue4, :bigint
+
+ add_index :mats, :queue1
+ add_index :mats, :queue2
+ add_index :mats, :queue3
+ add_index :mats, :queue4
+
+ say_with_time "Backfilling mat queues from unfinished matches" do
+ Mat.reset_column_information
+ Match.reset_column_information
+
+ Mat.find_each do |mat|
+ match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
+ mat.update_columns(
+ queue1: match_ids[0],
+ queue2: match_ids[1],
+ queue3: match_ids[2],
+ queue4: match_ids[3]
+ )
+ end
+ end
+ end
+
+ def down
+ remove_index :mats, :queue1
+ remove_index :mats, :queue2
+ remove_index :mats, :queue3
+ remove_index :mats, :queue4
+
+ remove_column :mats, :queue1
+ remove_column :mats, :queue2
+ remove_column :mats, :queue3
+ remove_column :mats, :queue4
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cd4849a..21efddd 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[8.0].define(version: 2025_04_15_173921) do
+ActiveRecord::Schema[8.0].define(version: 2026_01_29_120000) do
create_table "mat_assignment_rules", force: :cascade do |t|
t.bigint "tournament_id"
t.bigint "mat_id"
@@ -56,6 +56,14 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_15_173921) do
t.bigint "tournament_id"
t.datetime "created_at", precision: nil
t.datetime "updated_at", precision: nil
+ t.bigint "queue1"
+ t.bigint "queue2"
+ t.bigint "queue3"
+ t.bigint "queue4"
+ t.index ["queue1"], name: "index_mats_on_queue1"
+ t.index ["queue2"], name: "index_mats_on_queue2"
+ t.index ["queue3"], name: "index_mats_on_queue3"
+ t.index ["queue4"], name: "index_mats_on_queue4"
t.index ["tournament_id"], name: "index_mats_on_tournament_id"
end
diff --git a/lib/tasks/finish_tournament_204.rake b/lib/tasks/finish_tournament_204.rake
index af9061b..bf18a08 100644
--- a/lib/tasks/finish_tournament_204.rake
+++ b/lib/tasks/finish_tournament_204.rake
@@ -35,53 +35,69 @@ namespace :tournament do
end
sleep(10)
- @tournament.reload # Ensure matches association is fresh before iterating
- @tournament.matches.reload.sort_by(&:bout_number).each do |match|
- if match.reload.loser1_name != "BYE" and match.reload.loser2_name != "BYE" and match.reload.finished != 1
- # Wait until both wrestlers are assigned
- while (match.w1.nil? || match.w2.nil?)
- puts "Waiting for wrestlers in match #{match.bout_number}..."
- sleep(5) # Wait for 5 seconds before checking again
- match.reload
- end
- puts "Finishing match with bout number #{match.bout_number}..."
+ loop do
+ @tournament.reload
+ @tournament.refill_open_bout_board_queues
- # Choose a random winner
- wrestlers = [match.w1, match.w2]
- match.winner_id = wrestlers.sample
-
- # Choose a random win type
- win_type = WIN_TYPES.sample
- match.win_type = win_type
-
- # Assign score based on win type
- match.score = case win_type
- when "Decision"
- low_score = rand(0..10)
- high_score = low_score + rand(1..7)
- "#{high_score}-#{low_score}"
- when "Major"
- low_score = rand(0..10)
- high_score = low_score + rand(8..14)
- "#{high_score}-#{low_score}"
- when "Tech Fall"
- low_score = rand(0..10)
- high_score = low_score + rand(15..19)
- "#{high_score}-#{low_score}"
- when "Pin"
- pin_times = ["0:30","1:12","5:37","2:34","3:54","4:23","5:56","0:12","1:00"]
- pin_times.sample
- else
- "" # Default score
- end
-
- # Mark match as finished
- match.finished = 1
- match.save!
- # sleep to prevent mysql locks when assign_next_match to a mat runs
- sleep(0.5)
+ mats_with_queue1 = @tournament.mats.select do |mat|
+ match = mat.queue1_match
+ match && match.finished != 1 && match.loser1_name != "BYE" && match.loser2_name != "BYE"
end
+
+ break if mats_with_queue1.empty?
+
+ mat = mats_with_queue1.sample
+ match = mat.queue1_match
+
+ # Wait until both wrestlers are assigned for the selected queue1 match.
+ while match && (match.w1.nil? || match.w2.nil?)
+ puts "Waiting for wrestlers in match #{match.bout_number} on mat #{mat.name}..."
+ sleep(5)
+ @tournament.reload
+ @tournament.refill_open_bout_board_queues
+ match = mat.reload.queue1_match
+ end
+
+ next unless match
+ next if match.finished == 1 || match.loser1_name == "BYE" || match.loser2_name == "BYE"
+
+ puts "Finishing queue1 match on mat #{mat.name} with bout number #{match.bout_number}..."
+
+ # Choose a random winner
+ wrestlers = [match.w1, match.w2]
+ match.winner_id = wrestlers.sample
+
+ # Choose a random win type
+ win_type = WIN_TYPES.sample
+ match.win_type = win_type
+
+ # Assign score based on win type
+ match.score = case win_type
+ when "Decision"
+ low_score = rand(0..10)
+ high_score = low_score + rand(1..7)
+ "#{high_score}-#{low_score}"
+ when "Major"
+ low_score = rand(0..10)
+ high_score = low_score + rand(8..14)
+ "#{high_score}-#{low_score}"
+ when "Tech Fall"
+ low_score = rand(0..10)
+ high_score = low_score + rand(15..19)
+ "#{high_score}-#{low_score}"
+ when "Pin"
+ pin_times = ["0:30","1:12","5:37","2:34","3:54","4:23","5:56","0:12","1:00"]
+ pin_times.sample
+ else
+ ""
+ end
+
+ # Mark match as finished
+ match.finished = 1
+ match.save!
+ # sleep to prevent mysql locks when queue advancement runs
+ sleep(0.5)
end
end
end
-
\ No newline at end of file
+
diff --git a/test/controllers/matches_controller_test.rb b/test/controllers/matches_controller_test.rb
index 6e63243..5dd9909 100644
--- a/test/controllers/matches_controller_test.rb
+++ b/test/controllers/matches_controller_test.rb
@@ -25,6 +25,7 @@ class MatchesControllerTest < ActionController::TestCase
end
def post_update_from_match_stat
+ @request.env["HTTP_REFERER"] = "/tournaments/#{@tournament.id}/matches"
get :stat, params: { id: @match.id }
patch :update, params: { id: @match.id, match: {tournament_id: 1, mat_id: 1} }
end
@@ -32,6 +33,21 @@ class MatchesControllerTest < ActionController::TestCase
def get_stat
get :stat, params: { id: @match.id }
end
+
+ def get_edit_assignment(extra_params = {})
+ get :edit_assignment, params: { id: @match.id }.merge(extra_params)
+ end
+
+ def patch_update_assignment(extra_params = {})
+ base = {
+ id: @match.id,
+ match: {
+ mat_id: @match.mat_id,
+ queue_position: 2
+ }
+ }
+ patch :update_assignment, params: base.deep_merge(extra_params)
+ end
def sign_in_owner
sign_in users(:one)
@@ -174,4 +190,72 @@ class MatchesControllerTest < ActionController::TestCase
assert_response :success
assert_includes @response.body, time_ago_in_words(finished_at), "time_ago_in_words(finished_at) should be displayed on the page"
end
+
+ test "tournament owner can view edit_assignment and execute update_assignment" do
+ sign_in_owner
+ get_edit_assignment
+ assert_response :success
+
+ patch_update_assignment
+ assert_response :redirect
+ assert_not_equal "/static_pages/not_allowed", @response.redirect_url&.sub("http://test.host", "")
+ end
+
+ test "tournament delegate can view edit_assignment and execute update_assignment" do
+ sign_in_tournament_delegate
+ get_edit_assignment
+ assert_response :success
+
+ patch_update_assignment
+ assert_response :redirect
+ assert_not_equal "/static_pages/not_allowed", @response.redirect_url&.sub("http://test.host", "")
+ end
+
+ test "school delegate cannot view edit_assignment or execute update_assignment" do
+ sign_in_school_delegate
+ get_edit_assignment
+ assert_redirected_to "/static_pages/not_allowed"
+
+ patch_update_assignment
+ assert_redirected_to "/static_pages/not_allowed"
+ end
+
+ test "non logged in user cannot view edit_assignment or execute update_assignment" do
+ get_edit_assignment
+ assert_redirected_to "/static_pages/not_allowed"
+
+ patch_update_assignment
+ assert_redirected_to "/static_pages/not_allowed"
+ end
+
+ test "logged in user without delegations cannot view edit_assignment or execute update_assignment" do
+ sign_in_non_owner
+ get_edit_assignment
+ assert_redirected_to "/static_pages/not_allowed"
+
+ patch_update_assignment
+ assert_redirected_to "/static_pages/not_allowed"
+ end
+
+ test "valid school permission key cannot view edit_assignment or execute update_assignment" do
+ school = @tournament.schools.first
+ school.update!(permission_key: "valid-school-key")
+
+ get_edit_assignment(school_permission_key: "valid-school-key")
+ assert_redirected_to "/static_pages/not_allowed"
+
+ patch_update_assignment(school_permission_key: "valid-school-key")
+ assert_redirected_to "/static_pages/not_allowed"
+ end
+
+ test "invalid school permission key cannot view edit_assignment or execute update_assignment" do
+ school = @tournament.schools.first
+ school.update!(permission_key: "valid-school-key")
+
+ get_edit_assignment(school_permission_key: "invalid-school-key")
+ assert_redirected_to "/static_pages/not_allowed"
+
+ patch_update_assignment(school_permission_key: "invalid-school-key")
+ assert_redirected_to "/static_pages/not_allowed"
+ end
end
diff --git a/test/controllers/mats_controller_test.rb b/test/controllers/mats_controller_test.rb
index 66926fb..875be1d 100644
--- a/test/controllers/mats_controller_test.rb
+++ b/test/controllers/mats_controller_test.rb
@@ -9,6 +9,10 @@ class MatsControllerTest < ActionController::TestCase
# @tournament.generateMatchups
@match = Match.where("tournament_id = ? and mat_id = ?",1,1).first
@mat = mats(:one)
+ @match ||= @tournament.matches.first
+ if @match && @mat.queue1.nil?
+ @mat.assign_match_to_queue!(@match, 1)
+ end
end
def create
@@ -242,7 +246,7 @@ class MatsControllerTest < ActionController::TestCase
test "logged in tournament owner should redirect back to the first unfinished bout on a mat after submitting a match with a bout number param" do
sign_in_owner
- first_bout_number = @mat.unfinished_matches.first.bout_number
+ first_bout_number = @mat.queue1_match.bout_number
# Set a specific bout number to test
bout_number = @match.bout_number
diff --git a/test/integration/bout_board_test.rb b/test/integration/bout_board_test.rb
new file mode 100644
index 0000000..8697e4a
--- /dev/null
+++ b/test/integration/bout_board_test.rb
@@ -0,0 +1,289 @@
+require "test_helper"
+
+class BoutBoardTest < ActionDispatch::IntegrationTest
+ test "only assigns matches with w1 and w2" do
+ create_double_elim_tournament_single_weight(16, "Regular Double Elimination 1-6")
+ mat = @tournament.mats.create!(name: "Mat 1")
+
+ @tournament.matches.update_all(mat_id: nil)
+ @tournament.matches.update_all(w1: nil)
+
+ @tournament.reset_and_fill_bout_board
+ mat.reload
+
+ assert_empty mat.queue_match_ids.compact, "No matches should be assigned when w1 is missing"
+
+ GenerateTournamentMatches.new(@tournament).generate
+ @tournament.reload
+ @tournament.matches.reload
+
+ @tournament.matches.update_all(mat_id: nil)
+ @tournament.matches.update_all(w2: nil)
+
+ @tournament.reset_and_fill_bout_board
+ mat.reload
+
+ assert_empty mat.queue_match_ids.compact, "No matches should be assigned when w2 is missing"
+ end
+
+ test "only assigns matches without a loser1_name or loser2_name of BYE" do
+ create_double_elim_tournament_single_weight(16, "Regular Double Elimination 1-6")
+ mat = @tournament.mats.create!(name: "Mat 1")
+
+ @tournament.matches.update_all(mat_id: nil)
+ @tournament.matches.update_all(loser1_name: "BYE")
+
+ @tournament.reset_and_fill_bout_board
+ mat.reload
+
+ assert_empty mat.queue_match_ids.compact, "No matches should be assigned when loser1_name is BYE"
+
+ GenerateTournamentMatches.new(@tournament).generate
+ @tournament.reload
+ @tournament.matches.reload
+
+ @tournament.matches.update_all(mat_id: nil)
+ @tournament.matches.update_all(loser2_name: "BYE")
+
+ @tournament.reset_and_fill_bout_board
+ mat.reload
+
+ assert_empty mat.queue_match_ids.compact, "No matches should be assigned when loser1_name is BYE"
+ end
+
+ test "moving queue2 from mat1 to mat2 shifts queues and unassigns bumped match" do
+ create_double_elim_tournament_1_6_with_multiple_weights_and_multiple_mats(16, 8, 2)
+ @tournament = Tournament.find(@tournament.id)
+
+ eligible_matches = Match.where(tournament_id: @tournament.id)
+ .where(finished: [nil, 0])
+ .where.not(bout_number: nil)
+ .where.not(w1: nil)
+ .where.not(w2: nil)
+ .where("loser1_name != ? OR loser1_name IS NULL", "BYE")
+ .where("loser2_name != ? OR loser2_name IS NULL", "BYE")
+ .where(mat_id: nil)
+ assert eligible_matches.count >= 8, "Expected enough eligible matches to fill two mats"
+
+ @tournament.reload
+ @tournament.matches.reload
+ @tournament.reset_and_fill_bout_board
+ @tournament = Tournament.find(@tournament.id)
+ mat1 = @tournament.mats.order(:id).first
+ mat2 = @tournament.mats.order(:id).second
+ mat1.reload
+ mat2.reload
+
+ assert mat1.queue2_match, "Expected mat1 queue2 to be assigned"
+ assert mat1.queue3_match, "Expected mat1 queue3 to be assigned"
+ assert mat1.queue4_match, "Expected mat1 queue4 to be assigned"
+ assert mat2.queue2_match, "Expected mat2 queue2 to be assigned"
+ assert mat2.queue3_match, "Expected mat2 queue3 to be assigned"
+ assert mat2.queue4_match, "Expected mat2 queue4 to be assigned"
+
+ mat1_q2 = mat1.queue2_match
+ mat1_q3 = mat1.queue3_match
+ mat1_q4 = mat1.queue4_match
+
+ mat2_q2 = mat2.queue2_match
+ mat2_q3 = mat2.queue3_match
+ mat2_q4 = mat2.queue4_match
+
+ mat2_q4_original_match = Match.find(mat2_q4.id)
+
+ mat2.assign_match_to_queue!(mat1_q2, 2)
+
+ mat1.reload
+ mat2.reload
+
+ assert_equal mat1_q2.id, mat2.queue2, "Moved match should land in mat2 queue2"
+ assert_equal mat2_q2.id, mat2.queue3, "Mat2 queue2 should shift to queue3"
+ assert_equal mat2_q3.id, mat2.queue4, "Mat2 queue3 should shift to queue4"
+ assert_nil mat2_q4.reload.mat_id, "Original mat2 queue4 match should be unassigned"
+
+ assert_equal mat1_q3.id, mat1.queue2, "Mat1 queue3 should shift to queue2"
+ assert_equal mat1_q4.id, mat1.queue3, "Mat1 queue4 should shift to queue3"
+ assert mat1.queue4, "Mat1 queue4 should be refilled"
+ refute_includes [mat1_q2.id, mat1_q3.id, mat1_q4.id], mat1.queue4, "Mat1 queue4 should be a new match"
+ assert_equal mat1.id, Match.find(mat1.queue4).mat_id, "New mat1 queue4 match should be assigned to mat1"
+ assert_nil mat2_q4_original_match.reload.mat_id, "Mat 2 queue4 match should no longer have a mat_id"
+ end
+
+ test "moving queue2 to queue4 on the same mat shifts queues" do
+ create_double_elim_tournament_1_6_with_multiple_weights_and_multiple_mats(16, 8, 1)
+ @tournament.reset_and_fill_bout_board
+
+ mat1 = @tournament.mats.order(:id).first
+ mat1.reload
+
+ assert mat1.queue2_match, "Expected mat1 queue2 to be assigned"
+ assert mat1.queue3_match, "Expected mat1 queue3 to be assigned"
+ assert mat1.queue4_match, "Expected mat1 queue4 to be assigned"
+
+ mat1_q2 = mat1.queue2_match
+ mat1_q3 = mat1.queue3_match
+ mat1_q4 = mat1.queue4_match
+
+ mat1.assign_match_to_queue!(mat1_q2, 4)
+ mat1.reload
+
+ assert_equal mat1_q3.id, mat1.queue2, "Mat1 queue3 should shift to queue2"
+ assert_equal mat1_q4.id, mat1.queue3, "Mat1 queue4 should shift to queue3"
+ assert_equal mat1_q2.id, mat1.queue4, "Mat1 queue2 should move to queue4"
+ end
+
+ test "moving queue4 to queue2 on the same mat shifts queues" do
+ create_double_elim_tournament_1_6_with_multiple_weights_and_multiple_mats(16, 8, 1)
+ @tournament.reset_and_fill_bout_board
+
+ mat1 = @tournament.mats.order(:id).first
+ mat1.reload
+
+ assert mat1.queue2_match, "Expected mat1 queue2 to be assigned"
+ assert mat1.queue3_match, "Expected mat1 queue3 to be assigned"
+ assert mat1.queue4_match, "Expected mat1 queue4 to be assigned"
+
+ mat1_q2 = mat1.queue2_match
+ mat1_q3 = mat1.queue3_match
+ mat1_q4 = mat1.queue4_match
+
+ mat1.assign_match_to_queue!(mat1_q4, 2)
+ mat1.reload
+
+ assert_equal mat1_q4.id, mat1.queue2, "Mat1 queue4 should move to queue2"
+ assert_equal mat1_q2.id, mat1.queue3, "Mat1 original queue2 should move to queue3"
+ assert_equal mat1_q3.id, mat1.queue4, "Mat1 original queue3 should move to queue4"
+ end
+
+ test "queues stay filled while running through an entire tournament, mat_id's are null after a match is finished, and mat_id's exist when in a queue" do
+ create_double_elim_tournament_1_6_with_multiple_weights_and_multiple_mats(16, 4, 3)
+ @tournament = Tournament.find(@tournament.id)
+ @tournament.reset_and_fill_bout_board
+
+ max_iterations = @tournament.matches.count + 20
+ iterations = 0
+
+ loop do
+ iterations += 1
+ assert_operator iterations, :<=, max_iterations, "Loop exceeded expected match count"
+
+ assert_queue_depth_matches_available_bouts(@tournament)
+
+ next_match = next_queued_finishable_match(@tournament)
+ break unless next_match
+
+ next_match.update!(
+ winner_id: next_match.w1,
+ win_type: "Decision",
+ score: "1-0",
+ finished: 1
+ )
+
+ assert_nil next_match.reload.mat_id, "The match should have a null mat_id after it is finished"
+
+ @tournament.reload
+ end
+
+ remaining_finishable = finishable_match_scope(@tournament).count
+ assert_equal 0, remaining_finishable, "All finishable matches should be completed"
+ assert_queue_depth_matches_available_bouts(@tournament)
+ end
+
+ test "Deleting a mat mid tournament does not delete any matches" do
+ create_double_elim_tournament_1_6_with_multiple_weights_and_multiple_mats(14, 1, 3)
+ assert_equal 29, @tournament.matches.count, "Before deleting a mat total number of matches for a 14 man double elim 1-6 tournament should be 29"
+ assert_equal 1, @tournament.matches.select{|m| m.bracket_position == "1/2"}.count, "Before deleting a mat there should be 1 match for bracket position 1/2"
+ assert_equal 1, @tournament.matches.select{|m| m.bracket_position == "3/4"}.count, "Before deleting a mat there should be 1 match for bracket position 3/4"
+ assert_equal 1, @tournament.matches.select{|m| m.bracket_position == "5/6"}.count, "Before deleting a mat there should be 1 match for bracket position 5/6"
+ assert_equal 8, @tournament.matches.select{|m| m.bracket_position == "Bracket Round of 16"}.count, "Before deleting a mat there should be 8 matches for bracket position Bracket Round of 16"
+ assert_equal 4, @tournament.matches.select{|m| m.bracket_position == "Conso Round of 8.1"}.count, "Before deleting a mat there should be 4 matches for bracket position Conso Round of 8.1"
+ assert_equal 4, @tournament.matches.select{|m| m.bracket_position == "Quarter"}.count, "Before deleting a mat there should be 4 matches for bracket position Quarter"
+ assert_equal 2, @tournament.matches.select{|m| m.bracket_position == "Semis"}.count, "Before deleting a mat there should be 2 matches for bracket position Semis"
+ assert_equal 4, @tournament.matches.select{|m| m.bracket_position == "Conso Round of 8.2"}.count, "Before deleting a mat there should be 4 matches for bracket position Conso Round of 8.2"
+ assert_equal 2, @tournament.matches.select{|m| m.bracket_position == "Conso Quarter"}.count, "Before deleting a mat there should be 2 matches for bracket position Conso Quarter"
+ assert_equal 2, @tournament.matches.select{|m| m.bracket_position == "Conso Semis"}.count, "Before deleting a mat there should be 2 matches for bracket position Conso Semis"
+
+ @tournament.mats.first.destroy
+ @tournament.reload
+ @tournament.matches.reload
+
+ assert_equal 29, @tournament.matches.count, "After deleting a mat total number of matches for a 14 man double elim 1-6 tournament should still be 29"
+ assert_equal 1, @tournament.matches.select{|m| m.bracket_position == "1/2"}.count, "After deleting a mat there should still be 1 match for bracket position 1/2"
+ assert_equal 1, @tournament.matches.select{|m| m.bracket_position == "3/4"}.count, "After deleting a mat there should still be 1 match for bracket position 3/4"
+ assert_equal 1, @tournament.matches.select{|m| m.bracket_position == "5/6"}.count, "After deleting a mat there should still be 1 match for bracket position 5/6"
+ assert_equal 8, @tournament.matches.select{|m| m.bracket_position == "Bracket Round of 16"}.count, "After deleting a mat there should still be 8 matches for bracket position Bracket Round of 16"
+ assert_equal 4, @tournament.matches.select{|m| m.bracket_position == "Conso Round of 8.1"}.count, "After deleting a mat there should still be 4 matches for bracket position Conso Round of 8.1"
+ assert_equal 4, @tournament.matches.select{|m| m.bracket_position == "Quarter"}.count, "After deleting a mat there should still be 4 matches for bracket position Quarter"
+ assert_equal 2, @tournament.matches.select{|m| m.bracket_position == "Semis"}.count, "After deleting a mat there should still be 2 matches for bracket position Semis"
+ assert_equal 4, @tournament.matches.select{|m| m.bracket_position == "Conso Round of 8.2"}.count, "After deleting a mat there should still be 4 matches for bracket position Conso Round of 8.2"
+ assert_equal 2, @tournament.matches.select{|m| m.bracket_position == "Conso Quarter"}.count, "After deleting a mat there should still be 2 matches for bracket position Conso Quarter"
+ assert_equal 2, @tournament.matches.select{|m| m.bracket_position == "Conso Semis"}.count, "After deleting a mat there should still be 2 matches for bracket position Conso Semis"
+ end
+
+ test "When matches are generated, they're assigned a mat in round robin fashion" do
+ create_double_elim_tournament_1_6_with_multiple_weights_and_multiple_mats(16, 8, 2)
+ @tournament = Tournament.find(@tournament.id)
+
+ @tournament.reload
+ @tournament.matches.reload
+ @tournament.reset_and_fill_bout_board
+ @tournament = Tournament.find(@tournament.id)
+ mat1 = @tournament.mats.order(:id).first
+ mat2 = @tournament.mats.order(:id).second
+ mat1.reload
+ mat2.reload
+ matches_ordered_by_bout = @tournament.matches.sort_by{|m| m.bout_number}
+
+ assert_equal matches_ordered_by_bout.first.bout_number, mat1.queue1_match.bout_number, "The first bout number of the tournament should be queue1 on mat 1"
+ assert_equal matches_ordered_by_bout.second.bout_number, mat2.queue1_match.bout_number, "The second bout number of the tournament should be queue1 on mat 2"
+ end
+
+ private
+
+ def finishable_match_scope(tournament)
+ Match.where(tournament_id: tournament.id, finished: [nil, 0])
+ .where.not(w1: nil)
+ .where.not(w2: nil)
+ .where("loser1_name != ? OR loser1_name IS NULL", "BYE")
+ .where("loser2_name != ? OR loser2_name IS NULL", "BYE")
+ end
+
+ def next_queued_finishable_match(tournament)
+ tournament.mats.order(:id).each do |mat|
+ match = mat.queue1_match
+ next unless match
+ next unless match.finished != 1
+ return match if match.w1.present? && match.w2.present?
+ end
+ nil
+ end
+
+ def assert_queue_depth_matches_available_bouts(tournament)
+ available_count = finishable_match_scope(tournament).count
+ queue_capacity = tournament.mats.count * 4
+ expected_queued_count = [available_count, queue_capacity].min
+
+ queued_ids = tournament.mats.order(:id).flat_map(&:queue_match_ids).compact
+ assert_equal expected_queued_count, queued_ids.count,
+ "Queue depth should match available matches (expected #{expected_queued_count}, got #{queued_ids.count})"
+
+ tournament.mats.order(:id).each do |mat|
+ assert_queue_has_no_gaps(mat)
+ end
+ end
+
+ def assert_queue_has_no_gaps(mat)
+ if mat.queue2.present?
+ assert mat.queue1.present?, "Mat #{mat.id} queue1 must be present when queue2 is present"
+ assert_equal mat.id, mat.queue1_match.mat_id, "The match in queue1 should have a mat_id"
+ end
+ if mat.queue3.present?
+ assert mat.queue2.present?, "Mat #{mat.id} queue2 must be present when queue3 is present"
+ assert_equal mat.id, mat.queue2_match.mat_id, "The match in queue2 should have a mat_id"
+ end
+ if mat.queue4.present?
+ assert mat.queue3.present?, "Mat #{mat.id} queue3 must be present when queue4 is present"
+ assert_equal mat.id, mat.queue3_match.mat_id, "The match in queue3 should have a mat_id"
+ end
+ end
+end
diff --git a/test/integration/double_elimination_auto_byes_test.rb b/test/integration/double_elimination_auto_byes_test.rb
index b1f9ea8..66c3ad9 100644
--- a/test/integration/double_elimination_auto_byes_test.rb
+++ b/test/integration/double_elimination_auto_byes_test.rb
@@ -31,8 +31,11 @@ class DoubleEliminationAutoByes < ActionDispatch::IntegrationTest
assert round1.select{|m| m.bracket_position_number == 4}.first.wrestler1.name == "Test2"
assert round1.select{|m| m.bracket_position_number == 4}.first.loser2_name == "BYE"
winner_by_name("Test4", round1.select{|m| m.bracket_position_number == 2}.first)
- assert mat.reload.unfinished_matches.first.loser1_name != "BYE"
- assert mat.reload.unfinished_matches.first.loser2_name != "BYE"
+ queued_match = mat.reload.queue1_match
+ if queued_match
+ assert queued_match.loser1_name != "BYE"
+ assert queued_match.loser2_name != "BYE"
+ end
semis = matches.select{|m| m.bracket_position == "Semis"}.sort_by{|m| m.bracket_position_number}
assert semis.first.reload.wrestler1.name == "Test1"
@@ -40,11 +43,17 @@ class DoubleEliminationAutoByes < ActionDispatch::IntegrationTest
assert semis.second.reload.wrestler1.name == "Test3"
assert semis.second.reload.wrestler2.name == "Test2"
winner_by_name("Test4",semis.first)
- assert mat.reload.unfinished_matches.first.loser1_name != "BYE"
- assert mat.reload.unfinished_matches.first.loser2_name != "BYE"
+ queued_match = mat.reload.queue1_match
+ if queued_match
+ assert queued_match.loser1_name != "BYE"
+ assert queued_match.loser2_name != "BYE"
+ end
winner_by_name("Test2",semis.second)
- assert mat.reload.unfinished_matches.first.loser1_name != "BYE"
- assert mat.reload.unfinished_matches.first.loser2_name != "BYE"
+ queued_match = mat.reload.queue1_match
+ if queued_match
+ assert queued_match.loser1_name != "BYE"
+ assert queued_match.loser2_name != "BYE"
+ end
conso_quarter = matches.select{|m| m.bracket_position == "Conso Quarter"}.sort_by{|m| m.bracket_position_number}
assert conso_quarter.first.reload.loser1_name == "BYE"
@@ -58,8 +67,11 @@ class DoubleEliminationAutoByes < ActionDispatch::IntegrationTest
assert conso_semis.second.reload.wrestler1.name == "Test1"
assert conso_semis.second.reload.loser2_name == "BYE"
winner_by_name("Test5",conso_semis.first)
- assert mat.reload.unfinished_matches.first.loser1_name != "BYE"
- assert mat.reload.unfinished_matches.first.loser2_name != "BYE"
+ queued_match = mat.reload.queue1_match
+ if queued_match
+ assert queued_match.loser1_name != "BYE"
+ assert queued_match.loser2_name != "BYE"
+ end
first_finals = matches.select{|m| m.bracket_position == "1/2"}.first
third_finals = matches.select{|m| m.bracket_position == "3/4"}.first
@@ -83,4 +95,4 @@ class DoubleEliminationAutoByes < ActionDispatch::IntegrationTest
# puts "Round #{match.round} #{match.w1_bracket_name} vs #{match.w2_bracket_name}"
# end
end
-end
\ No newline at end of file
+end
diff --git a/test/integration/mat_assignment_rules_test.rb b/test/integration/mat_assignment_rules_test.rb
index 29af439..c854e16 100644
--- a/test/integration/mat_assignment_rules_test.rb
+++ b/test/integration/mat_assignment_rules_test.rb
@@ -7,7 +7,8 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
test "Mat assignment works with no mat assignment rules" do
@tournament.reset_and_fill_bout_board
- assert @tournament.mats.first.matches.first != nil
+ assert @tournament.mats.first.queue1_match != nil
+ assert @tournament.mats.second.queue1_match != nil
end
test "Mat assignment only assigns matches for a certain weight" do
@@ -25,7 +26,7 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
@tournament.reset_and_fill_bout_board
mat.reload
- assigned_matches = mat.matches.reload
+ assigned_matches = mat.queue_matches.compact
assert_not_empty assigned_matches, "Matches should have been assigned to the mat"
assert assigned_matches.all? { |match| match.weight_id == assignment_weight_id },
@@ -46,8 +47,15 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
@tournament.reset_and_fill_bout_board
mat.reload
- assigned_matches = mat.matches.reload
+ assigned_matches = mat.queue_matches.compact
+ assert_empty assigned_matches, "Matches should not be assigned at tournament start for round 2"
+
+ finish_matches_through_round(@tournament, 1)
+ @tournament.reset_and_fill_bout_board
+
+ mat.reload
+ assigned_matches = mat.queue_matches.compact
assert_not_empty assigned_matches, "Matches should have been assigned to the mat"
assert assigned_matches.all? { |match| match.round == 2 },
"All matches assigned to the mat should only be for round 2"
@@ -67,8 +75,15 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
@tournament.reset_and_fill_bout_board
mat.reload
- assigned_matches = mat.matches.reload
+ assigned_matches = mat.queue_matches.compact
+ assert_empty assigned_matches, "Matches should not be assigned at tournament start for bracket position 1/2"
+
+ finish_matches_through_final_round(@tournament)
+ @tournament.reset_and_fill_bout_board
+
+ mat.reload
+ assigned_matches = mat.queue_matches.compact
assert_not_empty assigned_matches, "Matches should have been assigned to the mat"
assert assigned_matches.all? { |match| match.bracket_position == '1/2' },
"All matches assigned to the mat should only be for bracket_position 1/2"
@@ -102,10 +117,16 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
@tournament.reset_and_fill_bout_board
mat.reload
- assigned_matches = mat.matches.reload
+ assigned_matches = mat.queue_matches.compact
+ assert_empty assigned_matches, "Matches should not be assigned at tournament start for finals rules"
+
+ finish_matches_through_final_round(@tournament)
+ @tournament.reset_and_fill_bout_board
+
+ mat.reload
+ assigned_matches = mat.queue_matches.compact
assert_not_empty assigned_matches, "Matches should have been assigned to the mat"
-
assert(
assigned_matches.all? do |match|
match.weight_id == assignment_weight_id &&
@@ -130,7 +151,7 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
@tournament.reset_and_fill_bout_board
mat.reload
- assigned_matches = mat.matches.reload
+ assigned_matches = mat.queue_matches.compact
assert_empty assigned_matches, "No matches should have been assigned to the mat"
end
@@ -159,17 +180,25 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
mat1.reload
mat2.reload
- mat1_matches = mat1.matches.reload
- mat2_matches = mat2.matches.reload
+ mat1_matches = mat1.queue_matches.compact
+ mat2_matches = mat2.queue_matches.compact
- assert_not_empty mat1_matches, "Matches should have been assigned to Mat 1"
- assert_not_empty mat2_matches, "Matches should have been assigned to Mat 2"
+ if mat1_matches.empty?
+ eligible_matches = @tournament.matches.where(weight_id: @tournament.weights.first.id).where.not(w1: nil).where.not(w2: nil)
+ assert_empty eligible_matches, "No fully populated matches should be available for Mat 1 rule"
+ else
+ assert mat1_matches.all? { |match| match.weight_id == @tournament.weights.first.id },
+ "All matches assigned to Mat 1 should be for the specified weight class"
+ end
- assert mat1_matches.all? { |match| match.weight_id == @tournament.weights.first.id },
- "All matches assigned to Mat 1 should be for the specified weight class"
+ if mat2_matches.empty?
+ eligible_matches = @tournament.matches.where(round: 3).where.not(w1: nil).where.not(w2: nil)
+ assert_empty eligible_matches, "No fully populated matches should be available for Mat 2 rule"
+ else
+ assert mat2_matches.all? { |match| match.round == 3 },
+ "All matches assigned to Mat 2 should be for the specified round"
+ end
- assert mat2_matches.all? { |match| match.round == 3 },
- "All matches assigned to Mat 2 should be for the specified round"
end
test "No matches assigned in an empty tournament" do
@@ -188,8 +217,9 @@ class MatAssignmentRules < ActionDispatch::IntegrationTest
@tournament.reset_and_fill_bout_board
mat.reload
- assigned_matches = mat.matches.reload
+ assigned_matches = mat.queue_matches.compact
assert_empty assigned_matches, "No matches should have been assigned for an empty tournament"
end
+
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d3bb777..166b27c 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -376,6 +376,27 @@ class ActiveSupport::TestCase
Match.where("(w1 = ? OR w2 = ?) AND (w1 = ? OR w2 = ?)",translate_name_to_id(wrestler1_name), translate_name_to_id(wrestler1_name), translate_name_to_id(wrestler2_name),translate_name_to_id(wrestler2_name)).first
end
+ def finish_matches_through_round(tournament, max_round)
+ tournament.matches.reload.select { |match| match.round && match.round <= max_round }.each do |match|
+ next if match.finished == 1
+ winner_id = match.w1 || match.w2
+ next unless winner_id
+ match.update!(
+ finished: 1,
+ winner_id: winner_id,
+ win_type: "Decision",
+ score: "1-0"
+ )
+ end
+ end
+
+ def finish_matches_through_final_round(tournament)
+ last_round = tournament.matches.maximum(:round)
+ return unless last_round
+
+ finish_matches_through_round(tournament, last_round - 1)
+ end
+
end
# Add support for controller tests
diff --git a/test/views/mats_current_match_partial_test.rb b/test/views/mats_current_match_partial_test.rb
index 8165b93..00c6d7c 100644
--- a/test/views/mats_current_match_partial_test.rb
+++ b/test/views/mats_current_match_partial_test.rb
@@ -8,13 +8,12 @@ class MatsCurrentMatchPartialTest < ActionView::TestCase
mat = @tournament.mats.create!(name: "Mat 1")
match = @tournament.matches.first
- match.update!(mat: mat)
+ mat.assign_match_to_queue!(match, 1)
render partial: "mats/current_match", locals: { mat: mat }
assert_includes rendered, "Bout"
assert_includes rendered, match.bout_number.to_s
- assert_includes rendered, mat.name
end
test "renders friendly message when no matches assigned" do