diff --git a/app/models/beamer.rb b/app/models/beamer.rb index 3170cf0..90405af 100644 --- a/app/models/beamer.rb +++ b/app/models/beamer.rb @@ -2,6 +2,7 @@ class Beamer < ApplicationRecord DEFAULT_NAME = 'default' + DEFAULT_CONFIG = { 'match_filter' => 'state:in_progress' }.freeze DISPLAY_STATES = %w[ during_game between_games static_image group_tables ].freeze diff --git a/app/models/tournament.rb b/app/models/tournament.rb index b91176a..3debf27 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -67,27 +67,32 @@ class Tournament < ApplicationRecord end def sync_configuration_is_complete - return if sync_target_url.blank? && sync_auth_token.blank? - return if sync_target_url.present? && sync_auth_token.present? - return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank? + return if sync_configuration_blank? + return if sync_configuration_complete? + return if follower_sync_token_only? errors.add(:base, 'sync_target_url and sync_auth_token must be configured together') end + def sync_configuration_blank? + sync_target_url.blank? && sync_auth_token.blank? + end + + def sync_configuration_complete? + sync_target_url.present? && sync_auth_token.present? + end + + def follower_sync_token_only? + read_only_mode? && sync_auth_token.present? && sync_target_url.blank? + end + def ensure_default_beamer! beamers.find_or_create_by!(name: Beamer::DEFAULT_NAME) do |beamer| beamer.display_state = 'during_game' + beamer.config = Beamer::DEFAULT_CONFIG end end - def sync_configuration_is_complete - return if sync_target_url.blank? && sync_auth_token.blank? - return if sync_target_url.present? && sync_auth_token.present? - return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank? - - errors.add(:base, 'sync_target_url and sync_auth_token must be configured together') - end - def clear_follower_sync_token_when_disabling_read_only_mode return if read_only_mode? return if sync_target_url.present? diff --git a/db/migrate/20260426120000_replace_beamer_asset_fields_with_config.rb b/db/migrate/20260426120000_replace_beamer_asset_fields_with_config.rb index f9b8f99..c726149 100644 --- a/db/migrate/20260426120000_replace_beamer_asset_fields_with_config.rb +++ b/db/migrate/20260426120000_replace_beamer_asset_fields_with_config.rb @@ -14,6 +14,12 @@ class ReplaceBeamerAssetFieldsWithConfig < ActiveRecord::Migration[7.0] END SQL + execute <<~SQL.squish + UPDATE beamers + SET config = '{"match_filter":"state:in_progress"}' + WHERE name = 'default' + SQL + change_column_default :beamers, :display_state, from: 'matches_all', to: 'during_game' remove_column :beamers, :qr_code_url, :string remove_column :beamers, :logo_url, :string diff --git a/spec/e2e/http/tournament_beamers_spec.rb b/spec/e2e/http/tournament_beamers_spec.rb index 268dded..a5f0625 100644 --- a/spec/e2e/http/tournament_beamers_spec.rb +++ b/spec/e2e/http/tournament_beamers_spec.rb @@ -38,20 +38,26 @@ RSpec.describe 'Tournament beamer HTTP E2E' do expect(default_beamer[:status]).to eq(200) expect(default_beamer.dig(:json, :name)).to eq('default') - expect(default_beamer.dig(:json, :display_state)).to eq('matches_all') + expect(default_beamer.dig(:json, :display_state)).to eq('during_game') + expect(default_beamer.dig(:json, :config)).to eq(match_filter: 'state:in_progress') expect(default_show[:status]).to eq(200) + expect(default_show.dig(:json, :config)).to eq(match_filter: 'state:in_progress') create_response = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: { name: 'hall-a', - display_state: 'groups', - qr_code_url: 'https://example.com/hall-a', - logo_url: '/logos/hall-a.png' + display_state: 'group_tables', + config: { + match_filter: 'stage:groups', + table_view: 'compact' + } }) expect(create_response[:status]).to eq(201) beamer = create_response.fetch(:json) expect(beamer.fetch(:name)).to eq('hall-a') - expect(beamer.fetch(:display_state)).to eq('groups') - expect(beamer.fetch(:qr_code_url)).to eq('https://example.com/hall-a') + expect(beamer.fetch(:display_state)).to eq('group_tables') + expect(beamer.fetch(:config)).to eq(match_filter: 'stage:groups', table_view: 'compact') + expect(beamer).not_to have_key(:qr_code_url) + expect(beamer).not_to have_key(:logo_url) index_response = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers") show_response = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}") @@ -65,34 +71,41 @@ RSpec.describe 'Tournament beamer HTTP E2E' do expect(by_name_response.dig(:json, :id)).to eq(beamer.fetch(:id)) update_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: { - display_state: 'during_game', - qr_code_url: 'https://example.com/live', - logo_url: '/logos/live.png' + display_state: 'static_image', + config: { + image_key: 'bpwstr_logo' + } }) expect(update_response[:status]).to eq(200) - expect(update_response.dig(:json, :display_state)).to eq('during_game') - expect(update_response.dig(:json, :qr_code_url)).to eq('https://example.com/live') - expect(update_response.dig(:json, :logo_url)).to eq('/logos/live.png') + expect(update_response.dig(:json, :display_state)).to eq('static_image') + expect(update_response.dig(:json, :config)).to eq(image_key: 'bpwstr_logo') + expect(update_response[:json]).not_to have_key(:qr_code_url) + expect(update_response[:json]).not_to have_key(:logo_url) invalid_update = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: { display_state: 'bogus' }) + invalid_config_update = owner_client.patch( + "/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", + body: { config: ['not-an-object'] } + ) duplicate_create = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: { name: 'hall-a', - display_state: 'matches_all' + display_state: 'during_game' }) forbidden_update = other_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: { - display_state: 'groups' + display_state: 'group_tables' }) unauthenticated_create = anonymous_client.post( "/tournaments/#{tournament.fetch(:id)}/beamers", body: { name: 'anon', - display_state: 'groups' + display_state: 'group_tables' } ) expect(invalid_update[:status]).to eq(422) + expect(invalid_config_update[:status]).to eq(422) expect(duplicate_create[:status]).to eq(422) expect(forbidden_update[:status]).to eq(403) expect(unauthenticated_create[:status]).to eq(401) @@ -108,7 +121,7 @@ RSpec.describe 'Tournament beamer HTTP E2E' do ) locked_update = owner_client.patch( beamer_path(read_only_tournament.fetch(:id), read_only_beamer.dig(:json, :id)), - body: { display_state: 'groups' } + body: { display_state: 'group_tables' } ) expect(locked_update[:status]).to eq(423) diff --git a/spec/e2e/http/tournament_follow_sync_beamers_spec.rb b/spec/e2e/http/tournament_follow_sync_beamers_spec.rb index 4095fda..f421e15 100644 --- a/spec/e2e/http/tournament_follow_sync_beamers_spec.rb +++ b/spec/e2e/http/tournament_follow_sync_beamers_spec.rb @@ -41,7 +41,8 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do follower_default = wait_for_follower_beamer!( tournament_id: follower.fetch(:id), beamer_name: 'default', - display_state: source_default.fetch(:display_state) + display_state: source_default.fetch(:display_state), + config: source_default.fetch(:config) ) expect(follower_default.fetch(:name)).to eq('default') @@ -49,8 +50,10 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do beamer_path(source.fetch(:id), source_default.fetch(:id)), body: { display_state: 'between_games', - qr_code_url: 'https://example.com/leader-default', - logo_url: '/logos/default.png' + config: { + match_filter: 'state:queued', + rotation_seconds: 10 + } } ) expect(leader_default_update[:status]).to eq(200) @@ -60,16 +63,22 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do tournament_id: follower.fetch(:id), beamer_name: 'default', display_state: 'between_games', - qr_code_url: 'https://example.com/leader-default', - logo_url: '/logos/default.png' + config: { + match_filter: 'state:queued', + rotation_seconds: 10 + } ) expect(mirrored_default.fetch(:name)).to eq('default') + expect(mirrored_default).not_to have_key(:qr_code_url) + expect(mirrored_default).not_to have_key(:logo_url) leader_create = source_client.post("/tournaments/#{source.fetch(:id)}/beamers", body: { name: 'stage-left', - display_state: 'groups', - qr_code_url: 'https://example.com/stage-left', - logo_url: '/logos/stage-left.png' + display_state: 'group_tables', + config: { + match_filter: 'stage:groups', + table_view: 'compact' + } }) expect(leader_create[:status]).to eq(201) sync_now_if_requested!(source.fetch(:id)) @@ -77,17 +86,23 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do mirrored_created = wait_for_follower_beamer!( tournament_id: follower.fetch(:id), beamer_name: 'stage-left', - display_state: 'groups', - qr_code_url: 'https://example.com/stage-left', - logo_url: '/logos/stage-left.png' + display_state: 'group_tables', + config: { + match_filter: 'stage:groups', + table_view: 'compact' + } ) expect(mirrored_created.fetch(:name)).to eq('stage-left') + expect(mirrored_created).not_to have_key(:qr_code_url) + expect(mirrored_created).not_to have_key(:logo_url) leader_update = source_client.patch( beamer_path(source.fetch(:id), leader_create.dig(:json, :id)), body: { - display_state: 'during_game', - qr_code_url: 'https://example.com/stage-left-live' + display_state: 'static_image', + config: { + image_key: 'stage-left-live' + } } ) expect(leader_update[:status]).to eq(200) @@ -96,14 +111,17 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do mirrored_updated = wait_for_follower_beamer!( tournament_id: follower.fetch(:id), beamer_name: 'stage-left', - display_state: 'during_game', - qr_code_url: 'https://example.com/stage-left-live' + display_state: 'static_image', + config: { + image_key: 'stage-left-live' + } ) - expect(mirrored_updated.fetch(:logo_url)).to eq('/logos/stage-left.png') + expect(mirrored_updated).not_to have_key(:qr_code_url) + expect(mirrored_updated).not_to have_key(:logo_url) locked_follower_update = follower_client.patch( beamer_path(follower.fetch(:id), mirrored_updated.fetch(:id)), - body: { display_state: 'matches_finished' } + body: { display_state: 'during_game' } ) expect(locked_follower_update[:status]).to eq(423) @@ -150,7 +168,7 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do response = follower_read_client.get(beamer_by_name_path(tournament_id), params: { name: beamer_name }) if response[:status] == 200 && response.dig(:json, :name) == beamer_name beamer = response.fetch(:json) - return beamer if expected_attrs.all? { |key, value| beamer.fetch(key) == value } + return beamer if expected_attrs.all? { |key, value| beamer[key] == value } end raise "follower beamer #{beamer_name} not synced within #{timeout}s" if Time.now.utc >= deadline diff --git a/spec/factories/beamers.rb b/spec/factories/beamers.rb index 354c43c..1d8b01e 100644 --- a/spec/factories/beamers.rb +++ b/spec/factories/beamers.rb @@ -9,9 +9,10 @@ FactoryBot.define do factory :default_beamer do name { Beamer::DEFAULT_NAME } + config { Beamer::DEFAULT_CONFIG } initialize_with do - tournament.beamers.find_or_initialize_by(name: name) + tournament.beamers.find_or_initialize_by(name:) end end end diff --git a/spec/models/tournament_spec.rb b/spec/models/tournament_spec.rb index 62fc7a4..c613d59 100644 --- a/spec/models/tournament_spec.rb +++ b/spec/models/tournament_spec.rb @@ -35,8 +35,11 @@ RSpec.describe Tournament, type: :model do describe 'default beamer' do it 'creates a default beamer automatically' do tournament = create(:tournament) + default_beamer = tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME) expect(tournament.beamers.pluck(:name)).to include(Beamer::DEFAULT_NAME) + expect(default_beamer.display_state).to eq('during_game') + expect(default_beamer.config).to eq(Beamer::DEFAULT_CONFIG) end end