feat(beamers): replace asset URLs with config

Add beamer config JSON, drop obsolete asset URL fields, and narrow display_state to the new beamer API states. Migration maps legacy display states to supported values and initializes config to {}.
This commit is contained in:
Daniel Schädler 2026-04-26 16:13:12 +02:00
parent 4bd1531408
commit acdef9f5b4
16 changed files with 147 additions and 61 deletions

View File

@ -68,7 +68,12 @@ class BeamersController < ApplicationController
end end
def beamer_params def beamer_params
params.permit(:name, :display_state, :qr_code_url, :logo_url) permitted = params.permit(:name, :display_state).to_h
config = params.key?(:config) ? params[:config] : params.dig(:beamer, :config)
return permitted unless config
permitted[:config] = config.respond_to?(:to_unsafe_h) ? config.to_unsafe_h : config
permitted
end end
def push_sync_if_needed! def push_sync_if_needed!

View File

@ -3,14 +3,14 @@
class Beamer < ApplicationRecord class Beamer < ApplicationRecord
DEFAULT_NAME = 'default' DEFAULT_NAME = 'default'
DISPLAY_STATES = %w[ DISPLAY_STATES = %w[
matches_all matches_in_progress matches_upcoming matches_not_started during_game between_games static_image group_tables
matches_finished groups between_games during_game
].freeze ].freeze
belongs_to :tournament belongs_to :tournament
validates :name, presence: true, uniqueness: { scope: :tournament_id } validates :name, presence: true, uniqueness: { scope: :tournament_id }
validates :display_state, presence: true, inclusion: { in: DISPLAY_STATES } validates :display_state, presence: true, inclusion: { in: DISPLAY_STATES }
validate :config_is_hash
delegate :owner, to: :tournament delegate :owner, to: :tournament
@ -34,7 +34,7 @@ class Beamer < ApplicationRecord
BeamerBroadcast.call( BeamerBroadcast.call(
tournament_id: snapshot.fetch(:tournament_id), tournament_id: snapshot.fetch(:tournament_id),
beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq, beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq,
payload: BeamerBroadcastPayload.build(snapshot: snapshot, event: broadcast_event) payload: BeamerBroadcastPayload.build(snapshot:, event: broadcast_event)
) )
ensure ensure
@broadcast_previous_name = nil @broadcast_previous_name = nil
@ -46,4 +46,10 @@ class Beamer < ApplicationRecord
previous_changes.key?('id') ? 'created' : 'updated' previous_changes.key?('id') ? 'created' : 'updated'
end end
def config_is_hash
return if config.is_a?(Hash)
errors.add(:config, 'must be an object')
end
end end

View File

@ -76,7 +76,7 @@ class Tournament < ApplicationRecord
def ensure_default_beamer! def ensure_default_beamer!
beamers.find_or_create_by!(name: Beamer::DEFAULT_NAME) do |beamer| beamers.find_or_create_by!(name: Beamer::DEFAULT_NAME) do |beamer|
beamer.display_state = 'matches_all' beamer.display_state = 'during_game'
end end
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class BeamerSerializer < ApplicationSerializer class BeamerSerializer < ApplicationSerializer
attributes :id, :name, :display_state, :qr_code_url, :logo_url attributes :id, :name, :display_state, :config
delegate :config, to: :object
end end

View File

@ -2,12 +2,12 @@
class BeamerBroadcastPayload class BeamerBroadcastPayload
class << self class << self
def build(beamer: nil, snapshot: nil, event:) def build(event:, beamer: nil, snapshot: nil)
data = snapshot || self.snapshot(beamer) data = snapshot || self.snapshot(beamer)
{ {
type: "beamer.#{event}", type: "beamer.#{event}",
event: event, event:,
tournament_id: data.fetch(:tournament_id), tournament_id: data.fetch(:tournament_id),
beamer: serialized_beamer(data) beamer: serialized_beamer(data)
} }
@ -19,8 +19,7 @@ class BeamerBroadcastPayload
tournament_id: beamer.tournament_id, tournament_id: beamer.tournament_id,
name: beamer.name, name: beamer.name,
display_state: beamer.display_state, display_state: beamer.display_state,
qr_code_url: beamer.qr_code_url, config: beamer.config
logo_url: beamer.logo_url
} }
end end
@ -31,8 +30,7 @@ class BeamerBroadcastPayload
id: snapshot.fetch(:id), id: snapshot.fetch(:id),
name: snapshot.fetch(:name), name: snapshot.fetch(:name),
display_state: snapshot.fetch(:display_state), display_state: snapshot.fetch(:display_state),
qr_code_url: snapshot[:qr_code_url], config: snapshot.fetch(:config)
logo_url: snapshot[:logo_url]
} }
end end
end end

View File

@ -109,8 +109,7 @@ class TournamentSnapshotImporter
beamer.name = attrs.fetch(:name) beamer.name = attrs.fetch(:name)
beamer.display_state = attrs.fetch(:display_state) beamer.display_state = attrs.fetch(:display_state)
beamer.is_default = attrs.fetch(:is_default, false) beamer.is_default = attrs.fetch(:is_default, false)
beamer.qr_code_url = attrs[:qr_code_url] beamer.config = attrs.fetch(:config, {})
beamer.logo_url = attrs[:logo_url]
beamer.save! beamer.save!
beamers_by_source_id[source_id] = beamer beamers_by_source_id[source_id] = beamer
end end

View File

@ -33,7 +33,7 @@ class TournamentSyncSchema
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
}.freeze, }.freeze,
Beamer => { Beamer => {
synced: %w[id name display_state is_default qr_code_url logo_url].freeze, synced: %w[id name display_state is_default config].freeze,
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
}.freeze, }.freeze,
Stage => { Stage => {

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class ReplaceBeamerAssetFieldsWithConfig < ActiveRecord::Migration[7.0]
def up
add_column :beamers, :config, :json, null: false, default: {}
execute <<~SQL.squish
UPDATE beamers
SET display_state = CASE display_state
WHEN 'groups' THEN 'group_tables'
WHEN 'between_games' THEN 'between_games'
WHEN 'during_game' THEN 'during_game'
ELSE 'during_game'
END
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
end
def down
add_column :beamers, :qr_code_url, :string
add_column :beamers, :logo_url, :string
change_column_default :beamers, :display_state, from: 'during_game', to: 'matches_all'
remove_column :beamers, :config, :json
end
end

View File

@ -10,17 +10,16 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2026_04_24_120000) do ActiveRecord::Schema[7.0].define(version: 2026_04_26_120000) do
create_table "beamers", force: :cascade do |t| create_table "beamers", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "display_state", default: "matches_all", null: false t.string "display_state", default: "during_game", null: false
t.boolean "is_default", default: false t.boolean "is_default", default: false
t.string "qr_code_url"
t.string "logo_url"
t.integer "tournament_id", null: false t.integer "tournament_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "sync_source_id" t.integer "sync_source_id"
t.json "config", default: {}, null: false
t.index ["tournament_id", "name"], name: "index_beamers_on_tournament_id_and_name", unique: true t.index ["tournament_id", "name"], name: "index_beamers_on_tournament_id_and_name", unique: true
t.index ["tournament_id"], name: "index_beamers_on_tournament_id" t.index ["tournament_id"], name: "index_beamers_on_tournament_id"
end end

View File

@ -30,9 +30,10 @@ Initial subscribe snapshot:
"beamer": { "beamer": {
"id": 34, "id": 34,
"name": "default", "name": "default",
"display_state": "matches_all", "display_state": "during_game",
"qr_code_url": null, "config": {
"logo_url": null "match_filter": "state:in_progress"
}
} }
} }
``` ```

View File

@ -66,7 +66,11 @@ RSpec.describe BeamersController, type: :controller do
describe 'POST #create' do describe 'POST #create' do
let(:valid_params) do let(:valid_params) do
{ name: 'new-beamer', display_state: 'groups' } {
name: 'new-beamer',
display_state: 'during_game',
config: { match_filter: 'state:in_progress' }
}
end end
context 'as owner' do context 'as owner' do
@ -83,7 +87,8 @@ RSpec.describe BeamersController, type: :controller do
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params) post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
body = deserialize_response response body = deserialize_response response
expect(body[:name]).to eq('new-beamer') expect(body[:name]).to eq('new-beamer')
expect(body[:display_state]).to eq('groups') expect(body[:display_state]).to eq('during_game')
expect(body[:config]).to eq(match_filter: 'state:in_progress')
end end
it 'rejects invalid display_state' do it 'rejects invalid display_state' do
@ -95,7 +100,7 @@ RSpec.describe BeamersController, type: :controller do
it 'rejects duplicate name in same tournament' do it 'rejects duplicate name in same tournament' do
post :create, params: { post :create, params: {
tournament_id: @tournament.to_param, name: @beamer.name, display_state: 'matches_all' tournament_id: @tournament.to_param, name: @beamer.name, display_state: 'during_game'
} }
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_entity)
end end
@ -133,15 +138,18 @@ RSpec.describe BeamersController, type: :controller do
expect(@beamer.reload.display_state).to eq('during_game') expect(@beamer.reload.display_state).to eq('during_game')
end end
it 'updates optional fields' do it 'replaces config' do
@beamer.update!(config: { match_filter: 'state:upcoming', image_key: 'old-logo' })
patch :update, params: { patch :update, params: {
tournament_id: @tournament.to_param, id: @beamer.to_param, tournament_id: @tournament.to_param, id: @beamer.to_param,
qr_code_url: 'https://example.com/qr', logo_url: '/static/logo.png' config: { image_key: 'bpwstr_logo' }
} }
expect(response).to be_successful expect(response).to be_successful
body = deserialize_response response
expect(body[:config]).to eq(image_key: 'bpwstr_logo')
@beamer.reload @beamer.reload
expect(@beamer.qr_code_url).to eq('https://example.com/qr') expect(@beamer.config).to eq('image_key' => 'bpwstr_logo')
expect(@beamer.logo_url).to eq('/static/logo.png')
end end
it 'rejects invalid display_state' do it 'rejects invalid display_state' do
@ -159,7 +167,7 @@ RSpec.describe BeamersController, type: :controller do
it 'returns forbidden' do it 'returns forbidden' do
patch :update, params: { patch :update, params: {
tournament_id: @tournament.to_param, id: @beamer.to_param, tournament_id: @tournament.to_param, id: @beamer.to_param,
display_state: 'groups' display_state: 'group_tables'
} }
expect(response).to have_http_status(:forbidden) expect(response).to have_http_status(:forbidden)
end end
@ -174,7 +182,7 @@ RSpec.describe BeamersController, type: :controller do
it 'returns locked' do it 'returns locked' do
patch :update, params: { patch :update, params: {
tournament_id: @tournament.to_param, id: @beamer.to_param, tournament_id: @tournament.to_param, id: @beamer.to_param,
display_state: 'groups' display_state: 'group_tables'
} }
expect(response).to have_http_status(:locked) expect(response).to have_http_status(:locked)
end end

View File

@ -26,7 +26,10 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
tournament = create_tournament(name_prefix: 'Ws Beamer') tournament = create_tournament(name_prefix: 'Ws Beamer')
create_beamer = client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: { create_beamer = client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
name: 'projector-a', name: 'projector-a',
display_state: 'matches_all' display_state: 'during_game',
config: {
match_filter: 'state:in_progress'
}
}) })
expect(create_beamer[:status]).to eq(201) expect(create_beamer[:status]).to eq(201)
beamer = create_beamer.fetch(:json) beamer = create_beamer.fetch(:json)
@ -37,22 +40,25 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
initial_payload = cable_client.wait_for_beamer_payload! initial_payload = cable_client.wait_for_beamer_payload!
expect(initial_payload.fetch('type')).to eq('beamer.snapshot') expect(initial_payload.fetch('type')).to eq('beamer.snapshot')
expect(initial_payload.dig('beamer', 'id')).to eq(beamer.fetch(:id)) expect(initial_payload.dig('beamer', 'id')).to eq(beamer.fetch(:id))
expect(initial_payload.dig('beamer', 'display_state')).to eq('matches_all') expect(initial_payload.dig('beamer', 'display_state')).to eq('during_game')
expect(initial_payload.dig('beamer', 'config', 'match_filter')).to eq('state:in_progress')
update = client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: { update = client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
display_state: 'during_game', display_state: 'static_image',
qr_code_url: 'https://example.com/qr', config: {
logo_url: '/static/logo.png' image_key: 'bpwstr_logo'
}
}) })
expect(update[:status]).to eq(200) expect(update[:status]).to eq(200)
updated_payload = wait_for_beamer_payload!( updated_payload = wait_for_beamer_payload!(
cable_client, cable_client,
event_type: 'beamer.updated', event_type: 'beamer.updated',
display_state: 'during_game' display_state: 'static_image'
) )
expect(updated_payload.dig('beamer', 'qr_code_url')).to eq('https://example.com/qr') expect(updated_payload.dig('beamer', 'config')).to eq('image_key' => 'bpwstr_logo')
expect(updated_payload.dig('beamer', 'logo_url')).to eq('/static/logo.png') expect(updated_payload['beamer']).not_to have_key('qr_code_url')
expect(updated_payload['beamer']).not_to have_key('logo_url')
delete = client.delete("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}") delete = client.delete("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}")
expect(delete[:status]).to eq(204) expect(delete[:status]).to eq(204)
@ -60,7 +66,7 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
deleted_payload = wait_for_beamer_payload!( deleted_payload = wait_for_beamer_payload!(
cable_client, cable_client,
event_type: 'beamer.deleted', event_type: 'beamer.deleted',
display_state: 'during_game' display_state: 'static_image'
) )
expect(deleted_payload.dig('beamer', 'name')).to eq('projector-a') expect(deleted_payload.dig('beamer', 'name')).to eq('projector-a')
ensure ensure

View File

@ -32,7 +32,7 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
follower = create_follower_tournament(name_prefix: 'Ws Beamer Follower') follower = create_follower_tournament(name_prefix: 'Ws Beamer Follower')
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: { configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state", sync_target_url: follower_sync_url(follower),
sync_auth_token: sync_token sync_auth_token: sync_token
}) })
expect(configure_sync[:status]).to eq(200) expect(configure_sync[:status]).to eq(200)
@ -40,21 +40,35 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id)) follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id))
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url) cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id), beamer_name: follower_default_beamer.fetch(:name)) cable_client.subscribe_beamer!(
tournament_id: follower.fetch(:id),
beamer_name: follower_default_beamer.fetch(:name)
)
cable_client.wait_for_beamer_payload! cable_client.wait_for_beamer_payload!
source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id), beamer_name: DEFAULT_BEAMER_NAME) source_default_beamer = fetch_beamer(
update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: { base_url: source_base_url,
display_state: 'groups' tournament_id: source.fetch(:id),
}) beamer_name: DEFAULT_BEAMER_NAME
)
update = source_client.patch(
"/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}",
body: {
display_state: 'static_image',
config: {
image_key: 'bpwstr_logo'
}
}
)
expect(update[:status]).to eq(200) expect(update[:status]).to eq(200)
follower_payload = wait_for_beamer_payload!( follower_payload = wait_for_beamer_payload!(
cable_client, cable_client,
event_type: 'beamer.updated', event_type: 'beamer.updated',
display_state: 'groups' display_state: 'static_image'
) )
expect(follower_payload.dig('beamer', 'name')).to eq(DEFAULT_BEAMER_NAME) expect(follower_payload.dig('beamer', 'name')).to eq(DEFAULT_BEAMER_NAME)
expect(follower_payload.dig('beamer', 'config')).to eq('image_key' => 'bpwstr_logo')
ensure ensure
cable_client&.close cable_client&.close
end end
@ -99,6 +113,10 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
response.fetch(:json) response.fetch(:json)
end end
def follower_sync_url(follower)
"#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state"
end
def fetch_beamer(base_url:, tournament_id:, beamer_name:) def fetch_beamer(base_url:, tournament_id:, beamer_name:)
client = TurniereE2E::ApiClient.new(base_url:) client = TurniereE2E::ApiClient.new(base_url:)
response = client.get("/tournaments/#{tournament_id}/beamers/by_name", params: { name: beamer_name }) response = client.get("/tournaments/#{tournament_id}/beamers/by_name", params: { name: beamer_name })

View File

@ -3,7 +3,8 @@
FactoryBot.define do FactoryBot.define do
factory :beamer do factory :beamer do
sequence(:name) { |n| "beamer-#{n}" } sequence(:name) { |n| "beamer-#{n}" }
display_state { 'matches_all' } display_state { 'during_game' }
config { {} }
tournament tournament
factory :default_beamer do factory :default_beamer do

View File

@ -24,6 +24,12 @@ RSpec.describe Beamer, type: :model do
expect(beamer).not_to be_valid expect(beamer).not_to be_valid
end end
it 'requires config to be an object' do
beamer = build(:beamer, config: ['not-an-object'])
expect(beamer).not_to be_valid
expect(beamer.errors[:config]).to include('must be an object')
end
Beamer::DISPLAY_STATES.each do |state| Beamer::DISPLAY_STATES.each do |state|
it "accepts display_state '#{state}'" do it "accepts display_state '#{state}'" do
beamer = build(:beamer, display_state: state) beamer = build(:beamer, display_state: state)
@ -54,10 +60,15 @@ RSpec.describe Beamer, type: :model do
expect(args.fetch(:beamer_names)).to eq(['projector']) expect(args.fetch(:beamer_names)).to eq(['projector'])
end end
tournament.beamers.create!(name: 'projector', display_state: 'groups') tournament.beamers.create!(
name: 'projector',
display_state: 'group_tables',
config: { 'match_filter' => 'state:in_progress' }
)
expect(payload[:type]).to eq('beamer.created') expect(payload[:type]).to eq('beamer.created')
expect(payload.dig(:beamer, :display_state)).to eq('groups') expect(payload.dig(:beamer, :display_state)).to eq('group_tables')
expect(payload.dig(:beamer, :config)).to eq('match_filter' => 'state:in_progress')
end end
it 'broadcasts update payload to previous and current beamer names when renamed' do it 'broadcasts update payload to previous and current beamer names when renamed' do
@ -69,15 +80,17 @@ RSpec.describe Beamer, type: :model do
expect(args.fetch(:beamer_names)).to contain_exactly('old-name', 'new-name') expect(args.fetch(:beamer_names)).to contain_exactly('old-name', 'new-name')
end end
beamer.update!(name: 'new-name', display_state: 'during_game') beamer.update!(name: 'new-name', display_state: 'during_game', config: { 'image_key' => 'bpwstr_logo' })
expect(payload[:type]).to eq('beamer.updated') expect(payload[:type]).to eq('beamer.updated')
expect(payload.dig(:beamer, :name)).to eq('new-name') expect(payload.dig(:beamer, :name)).to eq('new-name')
expect(payload.dig(:beamer, :display_state)).to eq('during_game') expect(payload.dig(:beamer, :display_state)).to eq('during_game')
expect(payload.dig(:beamer, :config)).to eq('image_key' => 'bpwstr_logo')
end end
it 'broadcasts delete payload with last persisted snapshot' do it 'broadcasts delete payload with last persisted snapshot' do
beamer = create(:beamer, name: 'projector', display_state: 'between_games') beamer = create(:beamer, name: 'projector', display_state: 'between_games',
config: { 'match_filter' => 'state:in_progress' })
payload = nil payload = nil
expect(BeamerBroadcast).to receive(:call) do |args| expect(BeamerBroadcast).to receive(:call) do |args|
@ -90,6 +103,7 @@ RSpec.describe Beamer, type: :model do
expect(payload[:type]).to eq('beamer.deleted') expect(payload[:type]).to eq('beamer.deleted')
expect(payload.dig(:beamer, :name)).to eq('projector') expect(payload.dig(:beamer, :name)).to eq('projector')
expect(payload.dig(:beamer, :display_state)).to eq('between_games') expect(payload.dig(:beamer, :display_state)).to eq('between_games')
expect(payload.dig(:beamer, :config)).to eq('match_filter' => 'state:in_progress')
end end
end end
end end

View File

@ -6,7 +6,11 @@ RSpec.describe TournamentSnapshotImporter do
describe '.import!' do describe '.import!' do
it 'replaces follower tournament graph using source ids as stable mapping keys' do it 'replaces follower tournament graph using source ids as stable mapping keys' do
source = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match) source = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
source.beamers.create!(name: 'stream-a', display_state: 'during_game', qr_code_url: 'https://example.com/qr') source.beamers.create!(
name: 'stream-a',
display_state: 'during_game',
config: { match_filter: 'state:in_progress' }
)
list = create(:team_action_list, tournament: source) list = create(:team_action_list, tournament: source)
create(:team_action_item, team_action_list: list, team: source.teams.first, completed: true) create(:team_action_item, team_action_list: list, team: source.teams.first, completed: true)
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret') follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
@ -78,21 +82,18 @@ RSpec.describe TournamentSnapshotImporter do
intermediate_round_participants_amount: 0 intermediate_round_participants_amount: 0
) )
tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME).update!( tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME).update!(
display_state: 'groups', display_state: 'group_tables',
qr_code_url: 'https://example.com/default-qr', config: { match_filter: 'state:in_progress' }
logo_url: '/logos/default.png'
) )
tournament.beamers.create!( tournament.beamers.create!(
name: 'stream-a', name: 'stream-a',
display_state: 'during_game', display_state: 'during_game',
qr_code_url: 'https://example.com/stream-a', config: { match_filter: 'state:in_progress' }
logo_url: '/logos/stream-a.png'
) )
tournament.beamers.create!( tournament.beamers.create!(
name: 'stream-b', name: 'stream-b',
display_state: 'between_games', display_state: 'between_games',
qr_code_url: 'https://example.com/stream-b', config: { image_key: 'bpwstr_logo' }
logo_url: '/logos/stream-b.png'
) )
group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished) group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished)
@ -202,7 +203,7 @@ RSpec.describe TournamentSnapshotImporter do
} }
end, end,
beamers: tournament.beamers.order(:name).map do |beamer| beamers: tournament.beamers.order(:name).map do |beamer|
beamer.slice(:name, :display_state, :is_default, :qr_code_url, :logo_url).symbolize_keys beamer.slice(:name, :display_state, :is_default, :config).symbolize_keys
end, end,
stages: tournament.stages.order(level: :asc).map do |stage| stages: tournament.stages.order(level: :asc).map do |stage|
{ {