diff --git a/app/models/match.rb b/app/models/match.rb index 4dd23fc..04f5a73 100644 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -1,4 +1,6 @@ class Match < ApplicationRecord + include ActionView::RecordIdentifier + belongs_to :tournament, touch: true belongs_to :weight, touch: true belongs_to :mat, touch: true, optional: true @@ -10,6 +12,10 @@ class Match < ApplicationRecord # Callback to update finished_at when a match is finished before_save :update_finished_at + # update mat show with correct match if bout board is reset + # this is done with a turbo stream + after_commit :broadcast_mat_assignment_change, if: :saved_change_to_mat_id?, on: [:create, :update] + # Enqueue advancement and related actions after the DB transaction has committed. # Using after_commit ensures any background jobs enqueued inside these callbacks # will see the committed state of the match (e.g. finished == 1). Enqueuing @@ -50,7 +56,7 @@ class Match < ApplicationRecord errors.add(:winner_id, "cannot be blank") end if win_type == "Pin" and ! score.match(/^[0-5]?[0-9]:[0-5][0-9]/) - errors.add(:score, "needs to be in time format MM:SS when win type is Pin example: 1:23 or 10:03") + errors.add(:score, "needs to be in time format MM:SS when win type is Pin example: 2:23, 0:25, 10:03") end if win_type == "Decision" or win_type == "Tech Fall" or win_type == "Major" and ! score.match(/^[0-9]?[0-9]-[0-9]?[0-9]/) errors.add(:score, "needs to be in Number-Number format when win type is Decision, Tech Fall, and Major example: 10-2") @@ -334,4 +340,26 @@ class Match < ApplicationRecord self.finished_at = Time.current.utc end end + + def broadcast_mat_assignment_change + old_mat_id, new_mat_id = saved_change_to_mat_id || previous_changes["mat_id"] + return unless old_mat_id || new_mat_id + + [old_mat_id, new_mat_id].compact.uniq.each do |mat_id| + mat = Mat.find_by(id: mat_id) + 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, + show_next_bout_button: true + } + ) + end + end end diff --git a/app/views/mats/_current_match.html.erb b/app/views/mats/_current_match.html.erb new file mode 100644 index 0000000..8c9c6a4 --- /dev/null +++ b/app/views/mats/_current_match.html.erb @@ -0,0 +1,37 @@ +<% @mat = mat %> +<% @match = local_assigns[:match] || mat.unfinished_matches.first %> +<% @next_match = local_assigns[:next_match] || mat.unfinished_matches.second %> +<% @show_next_bout_button = local_assigns.key?(:show_next_bout_button) ? local_assigns[:show_next_bout_button] : true %> + +<% @wrestlers = [] %> +<% if @match %> + <% if @match.w1 %> + <% @wrestler1_name = @match.wrestler1.name %> + <% @wrestler1_school_name = @match.wrestler1.school.name %> + <% @wrestler1_last_match = @match.wrestler1.last_match %> + <% @wrestlers.push(@match.wrestler1) %> + <% else %> + <% @wrestler1_name = "Not assigned" %> + <% @wrestler1_school_name = "N/A" %> + <% @wrestler1_last_match = nil %> + <% end %> + + <% if @match.w2 %> + <% @wrestler2_name = @match.wrestler2.name %> + <% @wrestler2_school_name = @match.wrestler2.school.name %> + <% @wrestler2_last_match = @match.wrestler2.last_match %> + <% @wrestlers.push(@match.wrestler2) %> + <% else %> + <% @wrestler2_name = "Not assigned" %> + <% @wrestler2_school_name = "N/A" %> + <% @wrestler2_last_match = nil %> + <% end %> + + <% @tournament = @match.tournament %> +<% end %> + +<% if @match %> + <%= render "matches/matchstats" %> +<% else %> +
No matches assigned to this mat.
+<% end %> diff --git a/app/views/mats/show.html.erb b/app/views/mats/show.html.erb index 9667e41..d767a6d 100644 --- a/app/views/mats/show.html.erb +++ b/app/views/mats/show.html.erb @@ -1,9 +1,12 @@No matches assigned to this mat.
-<% end %> +<%= turbo_stream_from @mat %> +<%= turbo_frame_tag dom_id(@mat, :current_match) do %> + <%= render "mats/current_match", + mat: @mat, + match: @match, + next_match: @next_match, + show_next_bout_button: @show_next_bout_button %> +<% end %> diff --git a/app/views/tournaments/matches.html.erb b/app/views/tournaments/matches.html.erb index 9bbcc3c..65d38a6 100644 --- a/app/views/tournaments/matches.html.erb +++ b/app/views/tournaments/matches.html.erb @@ -33,4 +33,7 @@ <% end %> - \ No newline at end of file + +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 diff --git a/test/models/match_broadcast_test.rb b/test/models/match_broadcast_test.rb new file mode 100644 index 0000000..da51b34 --- /dev/null +++ b/test/models/match_broadcast_test.rb @@ -0,0 +1,68 @@ +require 'test_helper' + +class MatchBroadcastTest < ActiveSupport::TestCase + include ActionView::RecordIdentifier + + test "broadcasts to old and new mats when mat changes" do + create_double_elim_tournament_single_weight_1_6(4) + mat1 = @tournament.mats.create!(name: "Mat 1") + mat2 = @tournament.mats.create!(name: "Mat 2") + @tournament.matches.update_all(mat_id: nil) + match = @tournament.matches.first + + # Set an initial mat + match.update!(mat: mat1) + stream1 = stream_name_for(mat1) + stream2 = stream_name_for(mat2) + + # Clear the stream so we can test changes from this state + clear_streams(stream1, stream2) + + # Update the mat and check the stream + match.update!(mat: mat2) + assert_equal [mat1.id, mat2.id], match.saved_change_to_mat_id + + assert_equal 1, broadcasts_for(stream1).size + assert_equal 1, broadcasts_for(stream2).size + + assert_includes broadcasts_for(stream2).last, dom_id(mat2, :current_match) + end + + test "broadcasts when a match is removed from a mat" do + create_double_elim_tournament_single_weight_1_6(4) + mat = @tournament.mats.create!(name: "Mat 1") + @tournament.matches.update_all(mat_id: nil) + match = @tournament.matches.first + + # Set an initial mat + match.update!(mat: mat) + stream = stream_name_for(mat) + + # Clear the stream so we can test changes from this state + clear_streams(stream) + + # Update the mat and check the stream + match.update!(mat: nil) + assert_equal [mat.id, nil], match.saved_change_to_mat_id + + assert_equal 1, broadcasts_for(stream).size + assert_includes broadcasts_for(stream).last, dom_id(mat, :current_match) + end + + private + + def broadcasts_for(stream) + ActionCable.server.pubsub.broadcasts(stream) + end + + def clear_streams(*streams) + ActionCable.server.pubsub.clear + streams.each do |stream| + broadcasts_for(stream).clear + end + end + + def stream_name_for(streamable) + Turbo::StreamsChannel.send(:stream_name_from, [streamable]) + end +end diff --git a/test/views/mats_current_match_partial_test.rb b/test/views/mats_current_match_partial_test.rb new file mode 100644 index 0000000..8165b93 --- /dev/null +++ b/test/views/mats_current_match_partial_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class MatsCurrentMatchPartialTest < ActionView::TestCase + include ActionView::RecordIdentifier + + test "renders current match contents when assigned" do + create_double_elim_tournament_single_weight_1_6(4) + mat = @tournament.mats.create!(name: "Mat 1") + match = @tournament.matches.first + + match.update!(mat: mat) + + 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 + create_double_elim_tournament_single_weight_1_6(4) + mat = @tournament.mats.create!(name: "Mat 1") + @tournament.matches.update_all(mat_id: nil) + + render partial: "mats/current_match", locals: { mat: mat } + + assert_includes rendered, "No matches assigned to this mat." + end +end