Merge pull request #55 from turniere/ticket/TURNIERE-234

Implement variables relevant for group stage to playoff transition
This commit is contained in:
Daniel Schädler 2019-06-13 22:52:34 +02:00 committed by GitHub
commit 33c7ce9695
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 265 additions and 26 deletions

View File

@ -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
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

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class StageSerializer < ApplicationSerializer
attributes :level
attributes :level, :state
has_many :matches
has_many :groups

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)]

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe SaveApplicationRecordObject do
RSpec.describe SaveApplicationRecordObject, type: :interactor do
before do
@tournament = create(:tournament)
end

View File

@ -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)

View File

@ -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

View File

@ -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