Sync beamer state to followers
This commit is contained in:
parent
8789c2cb2e
commit
03947ac0ca
|
|
@ -5,6 +5,7 @@ class BeamersController < ApplicationController
|
|||
before_action :set_beamer, only: %i[show update destroy]
|
||||
before_action :authenticate_user!, only: %i[create update destroy]
|
||||
before_action -> { require_owner! @tournament.owner }, only: %i[create update destroy]
|
||||
before_action -> { require_writable_tournament!(@tournament) }, only: %i[create update destroy]
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
||||
|
||||
# GET /tournaments/:tournament_id/beamers
|
||||
|
|
@ -32,6 +33,7 @@ class BeamersController < ApplicationController
|
|||
def create
|
||||
beamer = @tournament.beamers.new(beamer_params)
|
||||
if beamer.save
|
||||
push_sync_if_needed!
|
||||
render json: beamer, status: :created
|
||||
else
|
||||
render json: beamer.errors, status: :unprocessable_entity
|
||||
|
|
@ -41,6 +43,7 @@ class BeamersController < ApplicationController
|
|||
# PATCH /tournaments/:tournament_id/beamers/:id
|
||||
def update
|
||||
if @beamer.update(beamer_params)
|
||||
push_sync_if_needed!
|
||||
render json: @beamer
|
||||
else
|
||||
render json: @beamer.errors, status: :unprocessable_entity
|
||||
|
|
@ -50,6 +53,7 @@ class BeamersController < ApplicationController
|
|||
# DELETE /tournaments/:tournament_id/beamers/:id
|
||||
def destroy
|
||||
@beamer.destroy
|
||||
push_sync_if_needed!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
|
|
@ -66,4 +70,8 @@ class BeamersController < ApplicationController
|
|||
def beamer_params
|
||||
params.permit(:name, :display_state, :qr_code_url, :logo_url)
|
||||
end
|
||||
|
||||
def push_sync_if_needed!
|
||||
TournamentSyncEnqueue.call(@tournament)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
class TournamentSnapshotBuilder
|
||||
ROOT_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Tournament).map(&:to_sym).freeze
|
||||
TEAM_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Team).map(&:to_sym).freeze
|
||||
BEAMER_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Beamer).map(&:to_sym).freeze
|
||||
STAGE_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Stage).map(&:to_sym).freeze
|
||||
GROUP_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Group).map(&:to_sym).freeze
|
||||
MATCH_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Match).map(&:to_sym).freeze
|
||||
|
|
@ -22,6 +23,7 @@ class TournamentSnapshotBuilder
|
|||
payload[:timer_end] = tournament.timer_end&.iso8601
|
||||
payload[:sync_generated_at] = Time.current.iso8601(6)
|
||||
payload[:teams] = teams_payload
|
||||
payload[:beamers] = beamers_payload
|
||||
payload[:stages] = stages_payload
|
||||
payload[:groups] = groups_payload
|
||||
payload[:matches] = matches_payload
|
||||
|
|
@ -46,6 +48,12 @@ class TournamentSnapshotBuilder
|
|||
end
|
||||
end
|
||||
|
||||
def beamers_payload
|
||||
tournament.beamers.order(:id).map do |beamer|
|
||||
beamer.attributes.symbolize_keys.slice(*BEAMER_ATTRIBUTES)
|
||||
end
|
||||
end
|
||||
|
||||
def groups_payload
|
||||
tournament.stages.flat_map(&:groups).sort_by(&:id).map do |group|
|
||||
group.attributes.symbolize_keys.slice(*GROUP_ATTRIBUTES)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class TournamentSnapshotImporter
|
|||
return tournament if stale_snapshot?
|
||||
|
||||
existing_teams = tournament.teams.to_a
|
||||
existing_beamers = tournament.beamers.to_a
|
||||
existing_stages = tournament.stages.to_a
|
||||
existing_groups = existing_stages.flat_map(&:groups)
|
||||
existing_matches = tournament.matches
|
||||
|
|
@ -34,12 +35,14 @@ class TournamentSnapshotImporter
|
|||
|
||||
update_tournament!
|
||||
teams_by_source_id = sync_teams!
|
||||
sync_beamers!
|
||||
stages_by_source_id = sync_stages!
|
||||
groups_by_source_id = sync_groups!(stages_by_source_id)
|
||||
matches_by_source_id = sync_matches!(stages_by_source_id, groups_by_source_id)
|
||||
sync_match_scores!(matches_by_source_id, teams_by_source_id)
|
||||
sync_group_scores!(groups_by_source_id, teams_by_source_id)
|
||||
|
||||
destroy_stale_records(existing_beamers, snapshot.fetch(:beamers, []))
|
||||
destroy_stale_records(existing_group_scores, snapshot.fetch(:group_scores, []))
|
||||
destroy_stale_records(existing_match_scores, snapshot.fetch(:match_scores, []))
|
||||
destroy_stale_records(existing_matches, snapshot.fetch(:matches, []))
|
||||
|
|
@ -87,6 +90,25 @@ class TournamentSnapshotImporter
|
|||
end
|
||||
end
|
||||
|
||||
def sync_beamers!
|
||||
existing_by_source_id = tournament.beamers.index_by(&:sync_source_id)
|
||||
existing_by_name = tournament.beamers.index_by(&:name)
|
||||
|
||||
snapshot.fetch(:beamers, []).each_with_object({}) do |attrs, beamers_by_source_id|
|
||||
source_id = attrs.fetch(:id)
|
||||
beamer = existing_by_source_id[source_id] || existing_by_name[attrs.fetch(:name)] || Beamer.new
|
||||
beamer.sync_source_id = source_id
|
||||
beamer.tournament = tournament
|
||||
beamer.name = attrs.fetch(:name)
|
||||
beamer.display_state = attrs.fetch(:display_state)
|
||||
beamer.is_default = attrs.fetch(:is_default, false)
|
||||
beamer.qr_code_url = attrs[:qr_code_url]
|
||||
beamer.logo_url = attrs[:logo_url]
|
||||
beamer.save!
|
||||
beamers_by_source_id[source_id] = beamer
|
||||
end
|
||||
end
|
||||
|
||||
def sync_groups!(stages_by_source_id)
|
||||
sync_records(
|
||||
model_class: Group,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ class TournamentSyncSchema
|
|||
synced: %w[id name].freeze,
|
||||
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
||||
}.freeze,
|
||||
Beamer => {
|
||||
synced: %w[id name display_state is_default qr_code_url logo_url].freeze,
|
||||
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
||||
}.freeze,
|
||||
Stage => {
|
||||
synced: %w[id level state].freeze,
|
||||
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSyncSourceIdToBeamers < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :beamers, :sync_source_id, :integer
|
||||
end
|
||||
end
|
||||
|
|
@ -164,6 +164,21 @@ RSpec.describe BeamersController, type: :controller do
|
|||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'on read only follower tournament' do
|
||||
before(:each) do
|
||||
@tournament.update!(read_only_mode: true, sync_auth_token: 'shared-secret')
|
||||
apply_authentication_headers_for @owner
|
||||
end
|
||||
|
||||
it 'returns locked' do
|
||||
patch :update, params: {
|
||||
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||
display_state: 'groups'
|
||||
}
|
||||
expect(response).to have_http_status(:locked)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
describe '.import!' 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.beamers.create!(name: 'stream-a', display_state: 'during_game', qr_code_url: 'https://example.com/qr')
|
||||
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
|
||||
|
||||
snapshot = TournamentSnapshotBuilder.build(source)
|
||||
|
|
@ -16,9 +17,11 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
expect(follower.sync_source_tournament_id).to eq(source.id)
|
||||
expect(follower.name).to eq(source.name)
|
||||
expect(follower.teams.count).to eq(snapshot.fetch(:teams).count)
|
||||
expect(follower.beamers.count).to eq(snapshot.fetch(:beamers).count)
|
||||
expect(follower.stages.count).to eq(source.stages.count)
|
||||
expect(follower.matches.count).to eq(source.matches.count)
|
||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) })
|
||||
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map { |beamer| beamer.fetch(:id) })
|
||||
end
|
||||
|
||||
it 'ignores stale snapshots that arrive after a newer one' do
|
||||
|
|
@ -69,6 +72,23 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
instant_finalists_amount: 64,
|
||||
intermediate_round_participants_amount: 0
|
||||
)
|
||||
tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME).update!(
|
||||
display_state: 'groups',
|
||||
qr_code_url: 'https://example.com/default-qr',
|
||||
logo_url: '/logos/default.png'
|
||||
)
|
||||
tournament.beamers.create!(
|
||||
name: 'stream-a',
|
||||
display_state: 'during_game',
|
||||
qr_code_url: 'https://example.com/stream-a',
|
||||
logo_url: '/logos/stream-a.png'
|
||||
)
|
||||
tournament.beamers.create!(
|
||||
name: 'stream-b',
|
||||
display_state: 'between_games',
|
||||
qr_code_url: 'https://example.com/stream-b',
|
||||
logo_url: '/logos/stream-b.png'
|
||||
)
|
||||
|
||||
group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished)
|
||||
advancing_teams = []
|
||||
|
|
@ -160,6 +180,9 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
||||
timer_end: tournament.timer_end&.iso8601,
|
||||
teams: tournament.teams.order(:name).map { |team| team.name },
|
||||
beamers: tournament.beamers.order(:name).map do |beamer|
|
||||
beamer.slice(:name, :display_state, :is_default, :qr_code_url, :logo_url).symbolize_keys
|
||||
end,
|
||||
stages: tournament.stages.order(level: :asc).map do |stage|
|
||||
{
|
||||
level: stage.level,
|
||||
|
|
|
|||
Loading…
Reference in New Issue