Merge pull request #55 from turniere/ticket/TURNIERE-234
Implement variables relevant for group stage to playoff transition
This commit is contained in:
commit
33c7ce9695
|
|
@ -5,6 +5,7 @@ class TournamentsController < ApplicationController
|
|||
before_action :authenticate_user!, only: %i[create update destroy]
|
||||
before_action -> { require_owner! @tournament.owner }, only: %i[update destroy]
|
||||
before_action :validate_create_params, only: %i[create]
|
||||
before_action :validate_update_params, only: %i[update]
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
||||
|
||||
# GET /tournaments
|
||||
|
|
@ -63,10 +64,20 @@ class TournamentsController < ApplicationController
|
|||
|
||||
# PATCH/PUT /tournaments/1
|
||||
def update
|
||||
if @tournament.update(tournament_params)
|
||||
render json: @tournament
|
||||
else
|
||||
render json: @tournament.errors, status: :unprocessable_entity
|
||||
Tournament.transaction do
|
||||
if only_playoff_teams_amount_changed
|
||||
@tournament.instant_finalists_amount, @tournament.intermediate_round_participants_amount =
|
||||
TournamentService.calculate_default_amount_of_teams_advancing(
|
||||
params['playoff_teams_amount'].to_i,
|
||||
@tournament.stages.find_by(level: -1).groups.size
|
||||
)
|
||||
end
|
||||
if @tournament.update(tournament_params)
|
||||
render json: @tournament
|
||||
else
|
||||
render json: @tournament.errors, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -113,4 +124,26 @@ class TournamentsController < ApplicationController
|
|||
|
||||
render json: { error: 'Invalid teams array' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def only_playoff_teams_amount_changed
|
||||
params['playoff_teams_amount'] &&
|
||||
params['instant_finalists_amount'].nil? &&
|
||||
params['intermediate_round_participants_amount'].nil?
|
||||
end
|
||||
|
||||
def validate_update_params
|
||||
return if only_playoff_teams_amount_changed
|
||||
|
||||
playoff_teams_amount = params['playoff_teams_amount'].to_i || @tournament.playoff_teams_amount
|
||||
instant_finalists_amount = params['instant_finalists_amount'].to_i || @tournament.instant_finalists_amount
|
||||
intermediate_round_participants_amount = params['intermediate_round_participants_amount'].to_i ||
|
||||
@tournament.intermediate_round_participants_amount
|
||||
|
||||
return if instant_finalists_amount + (intermediate_round_participants_amount / 2) ==
|
||||
playoff_teams_amount
|
||||
|
||||
render json: {
|
||||
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class AddGroupStageToTournament
|
|||
begin
|
||||
group_stage = GroupStageService.generate_group_stage(groups)
|
||||
tournament.stages = [group_stage]
|
||||
tournament.instant_finalists_amount, tournament.intermediate_round_participants_amount =
|
||||
TournamentService.calculate_default_amount_of_teams_advancing(tournament.playoff_teams_amount,
|
||||
group_stage.groups.size)
|
||||
context.object_to_save = tournament
|
||||
rescue StandardError
|
||||
context.fail!
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Stage < ApplicationRecord
|
||||
enum state: %i[playoff_stage in_progress finished]
|
||||
|
||||
belongs_to :tournament
|
||||
has_many :matches, dependent: :destroy
|
||||
has_many :groups, dependent: :destroy
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class Tournament < ApplicationRecord
|
|||
validates :name, presence: true
|
||||
validates :code, presence: true, uniqueness: true
|
||||
|
||||
validate :playoff_teams_amount_is_positive_power_of_two
|
||||
|
||||
alias_attribute :owner, :user
|
||||
|
||||
after_initialize :generate_code
|
||||
|
|
@ -24,4 +26,11 @@ class Tournament < ApplicationRecord
|
|||
break if errors['code'].blank?
|
||||
end
|
||||
end
|
||||
|
||||
def playoff_teams_amount_is_positive_power_of_two
|
||||
return if (Utils.po2?(playoff_teams_amount) && playoff_teams_amount.positive?) || playoff_teams_amount.zero?
|
||||
|
||||
errors.add(:playoff_teams_amount,
|
||||
'playoff_teams_amount needs to be a positive power of two')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StageSerializer < ApplicationSerializer
|
||||
attributes :level
|
||||
attributes :level, :state
|
||||
|
||||
has_many :matches
|
||||
has_many :groups
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TournamentSerializer < SimpleTournamentSerializer
|
||||
attributes :description
|
||||
attributes :description, :playoff_teams_amount,
|
||||
:instant_finalists_amount, :intermediate_round_participants_amount
|
||||
has_many :stages
|
||||
has_many :teams
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class GroupStageService
|
|||
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length.to_f % 1).zero?
|
||||
|
||||
groups = groups.map(&method(:get_group_object_from))
|
||||
Stage.new level: -1, groups: groups
|
||||
Stage.new level: -1, groups: groups, state: :in_progress
|
||||
end
|
||||
|
||||
def get_group_object_from(team_array)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TournamentService
|
||||
class << self
|
||||
def calculate_default_amount_of_teams_advancing(playoff_teams_amount, amount_of_groups)
|
||||
# the amount of whole places that advance in a group (e. g. all 1rst places of every group instantly go through)
|
||||
instant_finalists_amount = (playoff_teams_amount.floor / amount_of_groups.floor) * amount_of_groups.floor
|
||||
# the amount of teams that still need to play an intermediate round before advancing to playoffs
|
||||
intermediate_round_participants_amount = (playoff_teams_amount - instant_finalists_amount) * 2
|
||||
|
||||
[instant_finalists_amount, intermediate_round_participants_amount]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -51,7 +51,9 @@ class CreateSchema < ActiveRecord::Migration[5.2]
|
|||
t.string :code, null: false, index: { unique: true }
|
||||
t.string :description
|
||||
t.boolean :public, default: true
|
||||
t.integer :playoff_teams_amount
|
||||
t.integer :playoff_teams_amount, default: 0
|
||||
t.integer :instant_finalists_amount, default: 0
|
||||
t.integer :intermediate_round_participants_amount, default: 0
|
||||
|
||||
# relation to owner
|
||||
t.belongs_to :user, index: true, null: false, foreign_key: { on_delete: :cascade }
|
||||
|
|
@ -61,6 +63,7 @@ class CreateSchema < ActiveRecord::Migration[5.2]
|
|||
|
||||
create_table :stages do |t|
|
||||
t.integer :level
|
||||
t.integer :state, default: 0
|
||||
|
||||
t.belongs_to :tournament, index: true, foreign_key: { on_delete: :cascade }, null: false
|
||||
|
||||
|
|
|
|||
|
|
@ -194,6 +194,42 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
expect(@group_stage_tournament.playoff_teams_amount)
|
||||
.to eq(create_group_tournament_data[:playoff_teams_amount])
|
||||
end
|
||||
|
||||
context 'playoff_teams_amount unacceptable' do
|
||||
shared_examples_for 'wrong playoff_teams_amount' do
|
||||
it 'fails' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
it 'returns the correct error message' do
|
||||
expect(deserialize_response(response)[:playoff_teams_amount].first)
|
||||
.to eq('playoff_teams_amount needs to be a positive power of two')
|
||||
end
|
||||
end
|
||||
|
||||
context 'is not a power of two' do
|
||||
before do
|
||||
post :create, params: create_group_tournament_data.merge(playoff_teams_amount: 18)
|
||||
end
|
||||
|
||||
it_should_behave_like 'wrong playoff_teams_amount'
|
||||
end
|
||||
|
||||
context 'isn\'t positive' do
|
||||
before do
|
||||
post :create, params: create_group_tournament_data.merge(playoff_teams_amount: -16)
|
||||
end
|
||||
|
||||
it_should_behave_like 'wrong playoff_teams_amount'
|
||||
end
|
||||
|
||||
context 'isn\'t positive nor a power of two' do
|
||||
before do
|
||||
post :create, params: create_group_tournament_data.merge(playoff_teams_amount: -42)
|
||||
end
|
||||
|
||||
it_should_behave_like 'wrong playoff_teams_amount'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders a JSON response with the new tournament' do
|
||||
|
|
@ -282,6 +318,100 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
expect(response).to have_http_status(:ok)
|
||||
expect(response.content_type).to eq('application/json')
|
||||
end
|
||||
|
||||
context 'any variable relevant for group stage to playoff transition changed' do
|
||||
before(:each) do
|
||||
@filled_tournament = create(:group_stage_tournament)
|
||||
apply_authentication_headers_for @filled_tournament.owner
|
||||
end
|
||||
|
||||
it 'fails when only instant_finalists_amount is changed' do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(instant_finalists_amount: 29)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'fails when only intermediate_round_participants_amount is changed' do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(intermediate_round_participants_amount: 29)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'fails when parameters don\'t match' do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(intermediate_round_participants_amount: 29,
|
||||
instant_finalists_amount: 32)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'succeeds when all three are changed correctly' do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(intermediate_round_participants_amount: 2,
|
||||
instant_finalists_amount: 1,
|
||||
playoff_teams_amount: 2)
|
||||
end
|
||||
|
||||
context 'only playoff_teams_amount is changed reasonably but update fails' do
|
||||
before do
|
||||
allow_any_instance_of(Tournament)
|
||||
.to receive(:update)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
it 'returns unprocessable entity' do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(playoff_teams_amount: 8)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'doesn\'t change playoff_teams_amount' do
|
||||
expect do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(playoff_teams_amount: 8)
|
||||
@filled_tournament.reload
|
||||
end
|
||||
.to_not(change { @filled_tournament.playoff_teams_amount })
|
||||
end
|
||||
|
||||
it 'doesn\'t change instant_finalists_amount' do
|
||||
expect do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(playoff_teams_amount: 8)
|
||||
@filled_tournament.reload
|
||||
end
|
||||
.to_not(change { @filled_tournament.instant_finalists_amount })
|
||||
end
|
||||
|
||||
it 'doesn\'t change intermediate_round_participants_amount' do
|
||||
expect do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(playoff_teams_amount: 8)
|
||||
@filled_tournament.reload
|
||||
end
|
||||
.to_not(change { @filled_tournament.intermediate_round_participants_amount })
|
||||
end
|
||||
end
|
||||
|
||||
context 'only playoff_teams_amount is changed to something reasonable' do
|
||||
before do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(playoff_teams_amount: 8)
|
||||
@filled_tournament.reload
|
||||
end
|
||||
|
||||
it 'succeeds' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes playoff_teams_amount' do
|
||||
expect(@filled_tournament.playoff_teams_amount).to eq(8)
|
||||
end
|
||||
|
||||
it 'adapts instant_finalists_amount' do
|
||||
expect(@filled_tournament.instant_finalists_amount).to eq(8)
|
||||
end
|
||||
|
||||
it 'adapts intermediate_round_participants_amount' do
|
||||
expect(@filled_tournament.intermediate_round_participants_amount).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'fails when playoff_teams_amount is higher than the amount of teams participating' do
|
||||
put :update, params: { id: @filled_tournament.to_param }.merge(playoff_teams_amount: 783)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as another user' do
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ FactoryBot.define do
|
|||
tournament
|
||||
factory :group_stage do
|
||||
level { -1 }
|
||||
state { :in_progress }
|
||||
transient do
|
||||
group_count { 4 }
|
||||
match_factory { :group_match }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ FactoryBot.define do
|
|||
end
|
||||
after(:create) do |tournament, evaluator|
|
||||
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
|
||||
tournament.playoff_teams_amount = (tournament.teams.size / 2)
|
||||
tournament.instant_finalists_amount = (tournament.playoff_teams_amount / 2)
|
||||
tournament.intermediate_round_participants_amount = ((tournament.playoff_teams_amount -
|
||||
tournament.instant_finalists_amount) * 2)
|
||||
tournament.save!
|
||||
end
|
||||
|
||||
factory :stage_tournament do
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe AddGroupStageToTournament do
|
||||
RSpec.describe AddGroupStageToTournament, type: :interactor do
|
||||
let(:empty_tournament_context) do
|
||||
AddGroupStageToTournament.call(tournament: @empty_tournament, groups: @groups)
|
||||
end
|
||||
|
|
@ -14,6 +14,7 @@ RSpec.describe AddGroupStageToTournament do
|
|||
@group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
|
||||
@group_stage = create(:group_stage)
|
||||
@groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values
|
||||
@tournament_service_defaults = [78_345, 2_387]
|
||||
end
|
||||
|
||||
context 'GroupStageService mocked' do
|
||||
|
|
@ -24,13 +25,28 @@ RSpec.describe AddGroupStageToTournament do
|
|||
end
|
||||
|
||||
context 'empty tournament' do
|
||||
before do
|
||||
allow(class_double('TournamentService').as_stubbed_const(transfer_nested_constants: true))
|
||||
.to receive(:calculate_default_amount_of_teams_advancing)
|
||||
.with(@empty_tournament.playoff_teams_amount, @group_stage.groups.size)
|
||||
.and_return(@tournament_service_defaults)
|
||||
end
|
||||
|
||||
it 'succeeds' do
|
||||
expect(empty_tournament_context).to be_a_success
|
||||
end
|
||||
|
||||
it 'adds group stage to the tournament' do
|
||||
test = empty_tournament_context.tournament.stages.first
|
||||
expect(test).to eq(@group_stage)
|
||||
expect(empty_tournament_context.tournament.stages.first).to eq(@group_stage)
|
||||
end
|
||||
|
||||
it 'sets default for instant_finalists_amount' do
|
||||
expect(empty_tournament_context.tournament.instant_finalists_amount).to eq(@tournament_service_defaults.first)
|
||||
end
|
||||
|
||||
it 'sets default for intermediate_round_participants_amount' do
|
||||
expect(empty_tournament_context.tournament.intermediate_round_participants_amount)
|
||||
.to eq(@tournament_service_defaults.second)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe AddPlayoffsToTournament do
|
||||
RSpec.describe AddPlayoffsToTournament, type: :interactor do
|
||||
let(:group_stage_tournament_context) do
|
||||
AddPlayoffsToTournament.call(tournament: @group_stage_tournament)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe PopulateMatchBelow do
|
||||
RSpec.describe PopulateMatchBelow, type: :interactor do
|
||||
before do
|
||||
@match = create(:match)
|
||||
@objects_to_save = [create(:match), create_list(:match_score, 2)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe SaveApplicationRecordObject do
|
||||
RSpec.describe SaveApplicationRecordObject, type: :interactor do
|
||||
before do
|
||||
@tournament = create(:tournament)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe UpdateGroupsGroupScores do
|
||||
RSpec.describe UpdateGroupsGroupScores, type: :interactor do
|
||||
before do
|
||||
@group = create(:group)
|
||||
@group_scores = create_list(:group_score, 2)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe GroupStageService do
|
||||
RSpec.describe GroupStageService, focus: true do
|
||||
before do
|
||||
@teams1 = create_list(:team, 4)
|
||||
@teams2 = create_list(:team, 4)
|
||||
@prepared_groups = Hash[1 => @teams1, 2 => @teams2].values
|
||||
end
|
||||
describe '#generate_group_stage method' do
|
||||
describe '#generate_group_stage' do
|
||||
let(:prepared_groups_groupstage) do
|
||||
GroupStageService.generate_group_stage(@prepared_groups)
|
||||
end
|
||||
|
||||
it 'returns a stage object' do
|
||||
group_stage = GroupStageService.generate_group_stage(@prepared_groups)
|
||||
expect(group_stage).to be_a(Stage)
|
||||
expect(prepared_groups_groupstage).to be_a(Stage)
|
||||
end
|
||||
|
||||
it 'assigns the correct state' do
|
||||
expect(prepared_groups_groupstage.state).to eq('in_progress')
|
||||
end
|
||||
|
||||
it 'returns a stage object with level -1' do
|
||||
group_stage_level = GroupStageService.generate_group_stage(@prepared_groups).level
|
||||
expect(group_stage_level).to be(-1)
|
||||
expect(prepared_groups_groupstage.level).to be(-1)
|
||||
end
|
||||
|
||||
it 'adds the provided groups to the stage' do
|
||||
group_stage_teams = GroupStageService.generate_group_stage(@prepared_groups).teams
|
||||
expect(group_stage_teams).to match_array(@prepared_groups.flatten)
|
||||
expect(prepared_groups_groupstage.teams).to match_array(@prepared_groups.flatten)
|
||||
end
|
||||
|
||||
it 'adds GroupScore objects for every team present in the group' do
|
||||
group_stage = GroupStageService.generate_group_stage(@prepared_groups)
|
||||
teams_in_group_scores = group_stage.groups.map { |g| g.group_scores.map(&:team) }.flatten
|
||||
expect(teams_in_group_scores).to match_array(@prepared_groups.flatten)
|
||||
expect(prepared_groups_groupstage.groups.map { |g| g.group_scores.map(&:team) }.flatten)
|
||||
.to match_array(@prepared_groups.flatten)
|
||||
end
|
||||
|
||||
it 'raises exception when given different sizes of groups' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe TournamentService do
|
||||
describe '#calculate_default_amount_of_teams_advancing' do
|
||||
before do
|
||||
@instant_finalists_amount, @intermediate_round_participants_amount =
|
||||
TournamentService.calculate_default_amount_of_teams_advancing(32, 5)
|
||||
end
|
||||
|
||||
it 'accurately calculates @instant_finalists_amount' do
|
||||
expect(@instant_finalists_amount).to eq(30)
|
||||
end
|
||||
|
||||
it 'accurately calculates @intermediate_round_participants_amount' do
|
||||
expect(@intermediate_round_participants_amount).to eq(4)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue