Merge branch 'codex/tur-151-group-next-match-start' into 'master'
TUR-151: group next match start See merge request turniere/turniere-backend!69
This commit is contained in:
commit
49379f4c80
|
|
@ -12,15 +12,18 @@ class TournamentsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
before_action :set_tournament_for_show, only: %i[show]
|
before_action :set_tournament_for_show, only: %i[show]
|
||||||
before_action :set_tournament, only: %i[update destroy set_timer timer sync_state test_sync]
|
before_action :set_tournament,
|
||||||
before_action :authenticate_user!, only: %i[create update destroy set_timer test_sync]
|
only: %i[update destroy set_timer timer sync_state test_sync start_next_group_matches]
|
||||||
before_action -> { require_owner! @tournament.owner }, only: %i[update destroy set_timer test_sync]
|
before_action :authenticate_user!, only: %i[create update destroy set_timer test_sync start_next_group_matches]
|
||||||
|
before_action -> { require_owner! @tournament.owner },
|
||||||
|
only: %i[update destroy set_timer test_sync start_next_group_matches]
|
||||||
before_action :validate_create_params, only: %i[create]
|
before_action :validate_create_params, only: %i[create]
|
||||||
before_action :validate_update_params, only: %i[update]
|
before_action :validate_update_params, only: %i[update]
|
||||||
before_action :validate_set_timer_params, only: %i[set_timer]
|
before_action :validate_set_timer_params, only: %i[set_timer]
|
||||||
before_action :ensure_update_allowed!, only: %i[update]
|
before_action :ensure_update_allowed!, only: %i[update]
|
||||||
before_action :ensure_destroy_allowed!, only: %i[destroy]
|
before_action :ensure_destroy_allowed!, only: %i[destroy]
|
||||||
before_action :ensure_timer_update_allowed!, only: %i[set_timer]
|
before_action :ensure_timer_update_allowed!, only: %i[set_timer]
|
||||||
|
before_action -> { require_writable_tournament!(@tournament) }, only: %i[start_next_group_matches]
|
||||||
before_action :authorize_sync_request!, only: %i[sync_state]
|
before_action :authorize_sync_request!, only: %i[sync_state]
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
||||||
|
|
||||||
|
|
@ -165,6 +168,20 @@ class TournamentsController < ApplicationController
|
||||||
}, status: :bad_gateway
|
}, status: :bad_gateway
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# PATCH /tournaments/:id/start_next_group_matches
|
||||||
|
def start_next_group_matches
|
||||||
|
started_matches = TournamentGroupMatchStarter.start_next!(@tournament, logger:)
|
||||||
|
if started_matches.any?
|
||||||
|
push_sync_if_needed!(@tournament)
|
||||||
|
TournamentLiveBroadcast.call(@tournament.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: ActiveModelSerializers::SerializableResource.new(
|
||||||
|
started_matches,
|
||||||
|
each_serializer: LiveMatchSerializer,
|
||||||
|
include: %w[match_scores.team]
|
||||||
|
).as_json
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TournamentGroupMatchStarter
|
||||||
|
class << self
|
||||||
|
def upcoming_matches(tournament)
|
||||||
|
group_stage = tournament.group_stage
|
||||||
|
return [] if group_stage.nil?
|
||||||
|
|
||||||
|
next_matches = group_stage.groups.filter_map do |group|
|
||||||
|
group.matches.not_started.order(:position).first
|
||||||
|
end
|
||||||
|
lowest_position = next_matches.map(&:position).min
|
||||||
|
return [] if lowest_position.nil?
|
||||||
|
|
||||||
|
next_matches.select { |match| match.position == lowest_position }
|
||||||
|
.sort_by { |match| [match.position, match.group.number, match.id] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_next!(tournament, logger: Rails.logger)
|
||||||
|
started_matches = []
|
||||||
|
|
||||||
|
Match.transaction do
|
||||||
|
started_matches = upcoming_matches(tournament)
|
||||||
|
started_matches.each { |match| match.update!(state: :in_progress) }
|
||||||
|
recalculate_group_scores!(started_matches, logger:)
|
||||||
|
end
|
||||||
|
|
||||||
|
started_matches
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def recalculate_group_scores!(matches, logger:)
|
||||||
|
matches.map(&:group).uniq.each do |group|
|
||||||
|
next if UpdateGroupsGroupScoresAndSave.call(group:).success?
|
||||||
|
|
||||||
|
logger.warn "Updating groups group score failed for #{group}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -79,12 +79,7 @@ class TournamentMatchesPayload
|
||||||
end
|
end
|
||||||
|
|
||||||
def upcoming_group_matches(tournament)
|
def upcoming_group_matches(tournament)
|
||||||
group_stage = tournament.stages.find { |stage| stage.level == -1 }
|
TournamentGroupMatchStarter.upcoming_matches(tournament)
|
||||||
return [] if group_stage.nil?
|
|
||||||
|
|
||||||
group_stage.groups.filter_map do |group|
|
|
||||||
group.matches.select { |match| match.state == 'not_started' }.min_by(&:position)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def upcoming_playoff_matches(tournament)
|
def upcoming_playoff_matches(tournament)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ Rails.application.routes.draw do
|
||||||
get :timer
|
get :timer
|
||||||
patch :timer, action: :set_timer
|
patch :timer, action: :set_timer
|
||||||
patch :sync_state
|
patch :sync_state
|
||||||
|
patch :start_next_group_matches
|
||||||
post :test_sync
|
post :test_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -888,4 +888,54 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
expect(json.dig(:tournament, :sync_last_push_error)).to eq('connection refused')
|
expect(json.dig(:tournament, :sync_last_push_error)).to eq('connection refused')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'PATCH #start_next_group_matches' do
|
||||||
|
let(:tournament) { create(:group_stage_tournament, group_count: 4, match_factory: :filled_group_match) }
|
||||||
|
let(:groups) { tournament.group_stage.groups.sort_by(&:number) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
set_group_match_states(groups[0], %i[finished finished finished in_progress])
|
||||||
|
groups.drop(1).each do |group|
|
||||||
|
set_group_match_states(group, %i[finished finished in_progress not_started])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as owner' do
|
||||||
|
before do
|
||||||
|
apply_authentication_headers_for tournament.owner
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'starts only lowest pending catch-up matches and returns them' do
|
||||||
|
expect(TournamentLiveBroadcast).to receive(:call).with(tournament.id)
|
||||||
|
|
||||||
|
patch :start_next_group_matches, params: { id: tournament.to_param }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
body = deserialize_response(response)
|
||||||
|
expect(body.map { |match| match.dig(:group, :id) }).to match_array(groups.drop(1).map(&:id))
|
||||||
|
expect(body.map { |match| match[:position] }.uniq).to eq([3])
|
||||||
|
expect(body.map { |match| match[:state] }.uniq).to eq(['in_progress'])
|
||||||
|
expect(groups.drop(1).map { |group| group.matches.find_by!(position: 3).reload.state }.uniq)
|
||||||
|
.to eq(['in_progress'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as another user' do
|
||||||
|
before do
|
||||||
|
apply_authentication_headers_for create(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders a forbidden error response' do
|
||||||
|
patch :start_next_group_matches, params: { id: tournament.to_param }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_group_match_states(group, states)
|
||||||
|
group.matches.order(:id).each_with_index do |match, index|
|
||||||
|
match.update!(position: index, state: states.fetch(index))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -376,7 +376,6 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
group = group_stage.fetch(:groups).min_by { |item| item.fetch(:number) }
|
group = group_stage.fetch(:groups).min_by { |item| item.fetch(:number) }
|
||||||
ordered_matches = group.fetch(:matches).sort_by { |item| item.fetch(:position) }
|
ordered_matches = group.fetch(:matches).sort_by { |item| item.fetch(:position) }
|
||||||
first_match = ordered_matches.fetch(0)
|
first_match = ordered_matches.fetch(0)
|
||||||
second_match = ordered_matches.fetch(1)
|
|
||||||
fourth_match = ordered_matches.fetch(3)
|
fourth_match = ordered_matches.fetch(3)
|
||||||
|
|
||||||
upcoming_before = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
upcoming_before = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||||
|
|
@ -408,7 +407,48 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(upcoming_match_for_group(upcoming_after_swap, group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id))
|
expect(upcoming_match_for_group(upcoming_after_swap, group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id))
|
||||||
|
|
||||||
expect(upcoming_after_finish[:status]).to eq(200)
|
expect(upcoming_after_finish[:status]).to eq(200)
|
||||||
expect(upcoming_match_for_group(upcoming_after_finish, group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id))
|
expect(upcoming_match_for_group(upcoming_after_finish, group_id: group.fetch(:id))).to be_nil
|
||||||
|
expect(upcoming_after_finish[:json].map { |match| match.fetch(:position) }.uniq)
|
||||||
|
.to eq([first_match.fetch(:position)])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'starts next group matches at the lowest pending position and returns started matches' do
|
||||||
|
tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Start Next Groups')
|
||||||
|
groups = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||||
|
.fetch(:groups)
|
||||||
|
.sort_by { |group| group.fetch(:number) }
|
||||||
|
matches_by_group = groups.to_h do |group|
|
||||||
|
[group.fetch(:id), group.fetch(:matches).sort_by { |match| match.fetch(:position) }]
|
||||||
|
end
|
||||||
|
|
||||||
|
first_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches")
|
||||||
|
expect(first_start[:status]).to eq(200)
|
||||||
|
expect(first_start[:json].map { |match| match.dig(:group, :id) })
|
||||||
|
.to match_array(groups.map { |group| group.fetch(:id) })
|
||||||
|
expect(first_start[:json].map { |match| match.fetch(:position) }.uniq).to eq([0])
|
||||||
|
|
||||||
|
matches_by_group.fetch(groups.first.fetch(:id))[1..3].each do |match|
|
||||||
|
start_group_match!(match_id: match.fetch(:id))
|
||||||
|
end
|
||||||
|
groups.drop(1).each do |group|
|
||||||
|
matches_by_group.fetch(group.fetch(:id))[1..2].each do |match|
|
||||||
|
start_group_match!(match_id: match.fetch(:id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expected_match_ids = groups.drop(1).map do |group|
|
||||||
|
matches_by_group.fetch(group.fetch(:id)).fetch(3).fetch(:id)
|
||||||
|
end
|
||||||
|
upcoming = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||||
|
catch_up_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches")
|
||||||
|
|
||||||
|
expect(upcoming[:status]).to eq(200)
|
||||||
|
expect(upcoming[:json].map { |match| match.fetch(:id) }).to match_array(expected_match_ids)
|
||||||
|
|
||||||
|
expect(catch_up_start[:status]).to eq(200)
|
||||||
|
expect(catch_up_start[:json].map { |match| match.fetch(:id) }).to match_array(expected_match_ids)
|
||||||
|
expect(catch_up_start[:json].map { |match| match.fetch(:position) }.uniq).to eq([3])
|
||||||
|
expect(catch_up_start[:json].map { |match| match.fetch(:state) }.uniq).to eq(['in_progress'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client(email:, password:)
|
def login_client(email:, password:)
|
||||||
|
|
@ -506,6 +546,11 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(finish_response[:status]).to eq(200)
|
expect(finish_response[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def start_group_match!(match_id:)
|
||||||
|
start_response = owner_client.patch("/matches/#{match_id}", body: { state: 'in_progress' })
|
||||||
|
expect(start_response[:status]).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
def tournament_match_count(tournament)
|
def tournament_match_count(tournament)
|
||||||
tournament.fetch(:stages).sum do |stage|
|
tournament.fetch(:stages).sum do |stage|
|
||||||
stage.fetch(:matches, []).count + stage.fetch(:groups, []).sum { |group| group.fetch(:matches).count }
|
stage.fetch(:matches, []).count + stage.fetch(:groups, []).sum { |group| group.fetch(:matches).count }
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,14 @@ RSpec.describe TournamentsController, type: :routing do
|
||||||
expect(patch: '/tournaments/1/sync_state').to route_to('tournaments#sync_state', id: '1')
|
expect(patch: '/tournaments/1/sync_state').to route_to('tournaments#sync_state', id: '1')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'routes to #start_next_group_matches' do
|
||||||
|
expect(patch: '/tournaments/1/start_next_group_matches').to(
|
||||||
|
route_to('tournaments#start_next_group_matches', id: '1')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'routes to #test_sync' do
|
it 'routes to #test_sync' do
|
||||||
expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1')
|
expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe TournamentGroupMatchStarter do
|
||||||
|
let(:tournament) { create(:group_stage_tournament, group_count: 4, match_factory: :filled_group_match) }
|
||||||
|
let(:groups) { tournament.group_stage.groups.sort_by(&:number) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
set_group_match_states(groups[0], %i[finished finished finished in_progress])
|
||||||
|
groups.drop(1).each do |group|
|
||||||
|
set_group_match_states(group, %i[finished finished in_progress not_started])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.upcoming_matches' do
|
||||||
|
it 'returns only groups whose next unstarted match is at the lowest pending position' do
|
||||||
|
matches = described_class.upcoming_matches(tournament)
|
||||||
|
|
||||||
|
expect(matches.map(&:group)).to match_array(groups.drop(1))
|
||||||
|
expect(matches.map(&:position).uniq).to eq([3])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.start_next!' do
|
||||||
|
it 'starts selected catch-up matches and returns them' do
|
||||||
|
started_matches = described_class.start_next!(tournament, logger: Logger.new(nil))
|
||||||
|
|
||||||
|
expect(started_matches.map(&:group)).to match_array(groups.drop(1))
|
||||||
|
expect(started_matches.map(&:position).uniq).to eq([3])
|
||||||
|
expect(started_matches.map { |match| match.reload.state }.uniq).to eq(['in_progress'])
|
||||||
|
expect(groups.first.matches.find_by!(position: 3).reload).to be_in_progress
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_group_match_states(group, states)
|
||||||
|
group.matches.order(:id).each_with_index do |match, index|
|
||||||
|
match.update!(position: index, state: states.fetch(index))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -49,6 +49,21 @@ RSpec.describe 'Tournament live payloads' do
|
||||||
expect(matches.map(&:group_id)).to match_array(tournament.group_stage.groups.map(&:id))
|
expect(matches.map(&:group_id)).to match_array(tournament.group_stage.groups.map(&:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns only catch-up group matches for upcoming state' do
|
||||||
|
tournament = create(:group_stage_tournament, group_count: 4, match_factory: :filled_group_match)
|
||||||
|
groups = tournament.group_stage.groups.sort_by(&:number)
|
||||||
|
|
||||||
|
set_group_match_states(groups[0], %i[finished finished finished in_progress])
|
||||||
|
groups.drop(1).each do |group|
|
||||||
|
set_group_match_states(group, %i[finished finished in_progress not_started])
|
||||||
|
end
|
||||||
|
|
||||||
|
matches = described_class.matches_for(tournament, state: 'upcoming')
|
||||||
|
|
||||||
|
expect(matches.map(&:group_id)).to match_array(groups.drop(1).map(&:id))
|
||||||
|
expect(matches.map(&:position).uniq).to eq([3])
|
||||||
|
end
|
||||||
|
|
||||||
it 'filters all matches belonging to a stage level' do
|
it 'filters all matches belonging to a stage level' do
|
||||||
tournament = create(:stage_tournament, stage_count: 2)
|
tournament = create(:stage_tournament, stage_count: 2)
|
||||||
stage = tournament.stages.find_by!(level: 1)
|
stage = tournament.stages.find_by!(level: 1)
|
||||||
|
|
@ -106,4 +121,10 @@ RSpec.describe 'Tournament live payloads' do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_group_match_states(group, states)
|
||||||
|
group.matches.order(:id).each_with_index do |match, index|
|
||||||
|
match.update!(position: index, state: states.fetch(index))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
2
tasks.py
2
tasks.py
|
|
@ -537,7 +537,7 @@ def _start_server(command, env, log_path="/tmp/turniere-e2e-server.log"):
|
||||||
def _start_test_server(host=SERVER_HOST, port=SERVER_PORT):
|
def _start_test_server(host=SERVER_HOST, port=SERVER_PORT):
|
||||||
return _start_server(
|
return _start_server(
|
||||||
f"{_bundle_exec('rails', 's')} -e test -b {host} -p {port}",
|
f"{_bundle_exec('rails', 's')} -e test -b {host} -p {port}",
|
||||||
_env(),
|
_env(GIT_COMMIT_SHA=_git_commit_sha()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue