diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index d11667d..9fa7f18 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -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 diff --git a/app/interactors/add_group_stage_to_tournament.rb b/app/interactors/add_group_stage_to_tournament.rb index d1d28e2..23e81de 100644 --- a/app/interactors/add_group_stage_to_tournament.rb +++ b/app/interactors/add_group_stage_to_tournament.rb @@ -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! diff --git a/app/models/stage.rb b/app/models/stage.rb index 9246352..1f36b21 100644 --- a/app/models/stage.rb +++ b/app/models/stage.rb @@ -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 diff --git a/app/models/tournament.rb b/app/models/tournament.rb index 3813f08..ec71609 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -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 diff --git a/app/serializers/stage_serializer.rb b/app/serializers/stage_serializer.rb index dc76a83..56cb6d0 100644 --- a/app/serializers/stage_serializer.rb +++ b/app/serializers/stage_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StageSerializer < ApplicationSerializer - attributes :level + attributes :level, :state has_many :matches has_many :groups diff --git a/app/serializers/tournament_serializer.rb b/app/serializers/tournament_serializer.rb index 9805797..25582e1 100644 --- a/app/serializers/tournament_serializer.rb +++ b/app/serializers/tournament_serializer.rb @@ -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 diff --git a/app/services/group_stage_service.rb b/app/services/group_stage_service.rb index 592f6ad..504ff24 100644 --- a/app/services/group_stage_service.rb +++ b/app/services/group_stage_service.rb @@ -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) diff --git a/app/services/tournament_service.rb b/app/services/tournament_service.rb new file mode 100644 index 0000000..b01ef02 --- /dev/null +++ b/app/services/tournament_service.rb @@ -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 diff --git a/db/migrate/0000_create_schema.rb b/db/migrate/0000_create_schema.rb index f1d4a3f..61bba1f 100644 --- a/db/migrate/0000_create_schema.rb +++ b/db/migrate/0000_create_schema.rb @@ -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 diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index c3313bd..06e05dc 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -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 diff --git a/spec/factories/stages.rb b/spec/factories/stages.rb index c2d1e2b..8027cd7 100644 --- a/spec/factories/stages.rb +++ b/spec/factories/stages.rb @@ -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 } diff --git a/spec/factories/tournaments.rb b/spec/factories/tournaments.rb index b4a522a..9a8a7d3 100644 --- a/spec/factories/tournaments.rb +++ b/spec/factories/tournaments.rb @@ -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 diff --git a/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb b/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb index 6e009df..1b4f90b 100644 --- a/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb +++ b/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb @@ -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 diff --git a/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb b/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb index ca60cf9..0ee2cbc 100644 --- a/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb +++ b/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb @@ -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 diff --git a/spec/interactors/populate_match_below_interactor_spec.rb b/spec/interactors/populate_match_below_interactor_spec.rb index 8b751e0..6f87038 100644 --- a/spec/interactors/populate_match_below_interactor_spec.rb +++ b/spec/interactors/populate_match_below_interactor_spec.rb @@ -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)] diff --git a/spec/interactors/save_application_record_object_to_database_interactor_spec.rb b/spec/interactors/save_application_record_object_to_database_interactor_spec.rb index ce2edb3..46f6d72 100644 --- a/spec/interactors/save_application_record_object_to_database_interactor_spec.rb +++ b/spec/interactors/save_application_record_object_to_database_interactor_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe SaveApplicationRecordObject do +RSpec.describe SaveApplicationRecordObject, type: :interactor do before do @tournament = create(:tournament) end diff --git a/spec/interactors/update_groups_group_scores_interactor_spec.rb b/spec/interactors/update_groups_group_scores_interactor_spec.rb index 83d1068..6db8ba9 100644 --- a/spec/interactors/update_groups_group_scores_interactor_spec.rb +++ b/spec/interactors/update_groups_group_scores_interactor_spec.rb @@ -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) diff --git a/spec/services/group_stage_service_spec.rb b/spec/services/group_stage_service_spec.rb index c7a850e..2a41658 100644 --- a/spec/services/group_stage_service_spec.rb +++ b/spec/services/group_stage_service_spec.rb @@ -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 diff --git a/spec/services/tournament_service_spec.rb b/spec/services/tournament_service_spec.rb new file mode 100644 index 0000000..65abd35 --- /dev/null +++ b/spec/services/tournament_service_spec.rb @@ -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