Merge pull request #61 from turniere/ticket/TURNIERE-234
Implement Group Stage end
This commit is contained in:
commit
687bec10a1
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StagesController < ApplicationController
|
||||
before_action :set_stage, only: %i[show update]
|
||||
before_action :authenticate_user!, only: %i[update]
|
||||
before_action -> { require_owner! @stage.owner }, only: %i[update]
|
||||
|
||||
# GET /stages/1
|
||||
def show
|
||||
render json: @stage, include: '**'
|
||||
end
|
||||
|
||||
# PUT /stages/1
|
||||
def update
|
||||
if stage_params[:state] == 'finished'
|
||||
unless @stage.state == 'in_progress'
|
||||
render json: { error: 'Only running group stages can be finished' }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
Stage.transaction do
|
||||
if @stage.update(stage_params)
|
||||
handle_group_stage_end
|
||||
|
||||
render json: @stage
|
||||
else
|
||||
render json: @stage.errors, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
else
|
||||
render json: {
|
||||
error: 'The state attribute may only be changed to finished'
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_group_stage_end
|
||||
unless @stage.over?
|
||||
render json: {
|
||||
error: 'Group Stage still has some matches that are not over yet. Finish them to generate playoffs'
|
||||
}, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
return if AddPlayoffsToTournamentAndSave.call(tournament: @stage.tournament,
|
||||
teams: GroupStageService.get_advancing_teams(@stage)).success?
|
||||
|
||||
render json: { error: 'Generating group stage failed' }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
def set_stage
|
||||
@stage = Stage.find(params[:id])
|
||||
end
|
||||
|
||||
def stage_params
|
||||
params.slice(:state).permit!
|
||||
end
|
||||
end
|
||||
|
|
@ -49,7 +49,7 @@ class TournamentsController < ApplicationController
|
|||
# associate provided teams with tournament
|
||||
tournament.teams = teams
|
||||
# add playoff stage to tournament
|
||||
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament)
|
||||
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: tournament.teams)
|
||||
end
|
||||
# validate tournament
|
||||
unless tournament.valid?
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class AddPlayoffsToTournament
|
|||
def call
|
||||
tournament = context.tournament
|
||||
context.fail! if tournament.stages.size > 1
|
||||
if (playoff_stages = PlayoffStageService.generate_playoff_stages_from_tournament(tournament))
|
||||
if (playoff_stages = PlayoffStageService.generate_playoff_stages(context.teams, context.randomize_matches))
|
||||
if tournament.stages.empty?
|
||||
tournament.stages = playoff_stages
|
||||
else
|
||||
|
|
|
|||
|
|
@ -3,4 +3,8 @@
|
|||
class GroupScore < ApplicationRecord
|
||||
belongs_to :team
|
||||
belongs_to :group
|
||||
|
||||
def difference_in_points
|
||||
scored_points - received_points
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,4 +18,14 @@ class Stage < ApplicationRecord
|
|||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def over?
|
||||
return matches.find { |m| m.state != 'finished' }.nil? unless matches.size.zero?
|
||||
|
||||
unless groups.size.zero? && groups.map(&:matches).flatten.size.zero?
|
||||
return groups.map(&:matches).flatten.find { |m| m.state != 'finished' }.nil?
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -55,5 +55,38 @@ class GroupStageService
|
|||
end
|
||||
changed_group_scores
|
||||
end
|
||||
|
||||
# Returns a list of the teams in the group sorted by their group_points, difference_in_points, scored_points
|
||||
#
|
||||
# @param group Group the group to get the teams from
|
||||
# @return [Array] of teams
|
||||
def teams_sorted_by_group_scores(group)
|
||||
group.teams.sort do |a, b|
|
||||
group_score_a = group.group_scores.find_by(team: a)
|
||||
group_score_b = group.group_scores.find_by(team: b)
|
||||
|
||||
[group_score_b.group_points,
|
||||
group_score_b.difference_in_points,
|
||||
group_score_b.scored_points] <=>
|
||||
[group_score_a.group_points,
|
||||
group_score_a.difference_in_points,
|
||||
group_score_a.scored_points]
|
||||
end
|
||||
end
|
||||
|
||||
# Returns all teams advancing to playoff stage from given group stage
|
||||
# They are ordered in such a way, that PlayoffStageService will correctly match the teams
|
||||
#
|
||||
# @param group_stage GroupStage the group stage to get all advancing teams from
|
||||
# @return [Array] the teams advancing from that group stage
|
||||
def get_advancing_teams(group_stage)
|
||||
advancing_teams = []
|
||||
group_winners = group_stage.groups.map(&method(:teams_sorted_by_group_scores))
|
||||
(group_stage.tournament.instant_finalists_amount + group_stage.tournament.intermediate_round_participants_amount)
|
||||
.times do |i|
|
||||
advancing_teams << group_winners[i % group_stage.groups.size].shift
|
||||
end
|
||||
advancing_teams
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ class PlayoffStageService
|
|||
#
|
||||
# @param teams [Array] The teams to generate the playoff stages with
|
||||
# @return [Array] the generated playoff stages
|
||||
def generate_playoff_stages(teams)
|
||||
def generate_playoff_stages(teams, randomize_matches)
|
||||
playoffs = []
|
||||
stage_count = calculate_required_stage_count(teams.size)
|
||||
# initial_matches are the matches in the first stage; this is the only stage filled with teams from the start on
|
||||
initial_matches = MatchService.generate_matches(teams)
|
||||
initial_matches = initial_matches.shuffle.each_with_index { |m, i| m.position = i } if randomize_matches
|
||||
initial_stage = Stage.new level: stage_count - 1, matches: initial_matches
|
||||
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
||||
playoffs << initial_stage
|
||||
|
|
@ -20,14 +21,6 @@ class PlayoffStageService
|
|||
playoffs
|
||||
end
|
||||
|
||||
# Generates the playoff stage given the tournament
|
||||
#
|
||||
# @param tournament [Tournament] The tournament to generate the playoff stages from
|
||||
# @return [Array] the generated playoff stages
|
||||
def generate_playoff_stages_from_tournament(tournament)
|
||||
generate_playoff_stages tournament.teams
|
||||
end
|
||||
|
||||
# Generates given number of empty stages
|
||||
#
|
||||
# @param stage_count [Integer] number of stages to generate
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ Rails.application.routes.draw do
|
|||
resources :matches, only: %i[show update] do
|
||||
resources :bets, only: %i[index create]
|
||||
end
|
||||
resources :stages, only: %i[show update]
|
||||
resources :teams, only: %i[show update]
|
||||
resources :tournaments do
|
||||
resources :statistics, only: %i[index]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe StagesController, type: :controller do
|
||||
let(:stage) do
|
||||
create(:playoff_stage)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'returns a success response' do
|
||||
get :show, params: { id: stage.to_param }
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it 'should return the correct stage' do
|
||||
get :show, params: { id: stage.to_param }
|
||||
body = deserialize_response response
|
||||
expect(Stage.find_by(id: body[:id])).to eq(stage)
|
||||
expect(body[:level]).to eq(stage.level)
|
||||
expect(body[:state]).to eq(stage.state)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:finished) do
|
||||
{ state: 'finished' }
|
||||
end
|
||||
|
||||
context 'group_stage with matches that are done' do
|
||||
let(:running_group_stage) do
|
||||
create(:group_stage, match_factory: :finished_group_match)
|
||||
end
|
||||
|
||||
it 'doesn\'t have any other stages besides it before update' do
|
||||
expect(running_group_stage.tournament.stages.size).to eq(1)
|
||||
end
|
||||
|
||||
context 'as owner' do
|
||||
before(:each) do
|
||||
apply_authentication_headers_for running_group_stage.owner
|
||||
end
|
||||
|
||||
before do
|
||||
put :update, params: { id: running_group_stage.to_param }.merge(finished)
|
||||
running_group_stage.reload
|
||||
end
|
||||
|
||||
it 'succeeds' do
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it 'stops the stage' do
|
||||
expect(running_group_stage.state).to eq('finished')
|
||||
end
|
||||
|
||||
it 'adds new stages to the tournament' do
|
||||
expect(running_group_stage.tournament.stages.size).to be > 1
|
||||
end
|
||||
|
||||
it 'adds the right teams' do
|
||||
expect(running_group_stage.tournament.stages.max_by(&:level).teams)
|
||||
.to match_array(GroupStageService.get_advancing_teams(running_group_stage))
|
||||
end
|
||||
end
|
||||
|
||||
context 'as another user' do
|
||||
before(:each) do
|
||||
apply_authentication_headers_for create(:user)
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
put :update, params: { id: stage.to_param }.merge(finished)
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'trying to finish a group stage with unfinished matches' do
|
||||
let(:group_stage) do
|
||||
create(:group_stage)
|
||||
end
|
||||
|
||||
before do
|
||||
apply_authentication_headers_for group_stage.owner
|
||||
put :update, params: { id: group_stage.to_param }.merge(finished)
|
||||
end
|
||||
|
||||
it 'it returns unprocessable entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns the correct error' do
|
||||
expect(deserialize_response(response)[:error])
|
||||
.to eq('Group Stage still has some matches that are not over yet. Finish them to generate playoffs')
|
||||
end
|
||||
end
|
||||
|
||||
context 'already finished group stage' do
|
||||
let(:finished_group_stage) do
|
||||
group_stage = create(:group_stage, match_factory: :finished_group_match)
|
||||
group_stage.finished!
|
||||
group_stage.save!
|
||||
group_stage
|
||||
end
|
||||
|
||||
before do
|
||||
apply_authentication_headers_for finished_group_stage.owner
|
||||
put :update, params: { id: finished_group_stage.to_param }.merge(finished)
|
||||
end
|
||||
|
||||
it 'it returns unprocessable entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns the correct error' do
|
||||
expect(deserialize_response(response)[:error]).to eq('Only running group stages can be finished')
|
||||
end
|
||||
end
|
||||
|
||||
context 'trying to change the state to something other than :finished' do
|
||||
let(:group_stage) do
|
||||
create(:group_stage)
|
||||
end
|
||||
|
||||
before do
|
||||
apply_authentication_headers_for group_stage.owner
|
||||
put :update, params: { id: group_stage.to_param }.merge(state: 'in_progress')
|
||||
end
|
||||
|
||||
it 'it returns unprocessable entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns the correct error' do
|
||||
expect(deserialize_response(response)[:error]).to eq('The state attribute may only be changed to finished')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,5 +4,9 @@ FactoryBot.define do
|
|||
factory :group_score do
|
||||
team
|
||||
group
|
||||
|
||||
group_points { rand 5 }
|
||||
scored_points { rand 5 }
|
||||
received_points { rand 5 }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ FactoryBot.define do
|
|||
match.match_scores = create_list(:match_score, evaluator.match_scores_count)
|
||||
end
|
||||
state { :in_progress }
|
||||
factory :finished_playoff_match do
|
||||
state { :finished }
|
||||
end
|
||||
end
|
||||
|
||||
factory :single_team_match do
|
||||
|
|
@ -53,6 +56,10 @@ FactoryBot.define do
|
|||
after(:create) do |match, evaluator|
|
||||
match.match_scores = create_list(:match_score, evaluator.match_scores_count)
|
||||
end
|
||||
|
||||
factory :finished_group_match do
|
||||
state { :finished }
|
||||
end
|
||||
end
|
||||
|
||||
factory :undecided_group_match do
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
RSpec.describe AddPlayoffsToTournament, type: :interactor do
|
||||
let(:group_stage_tournament_context) do
|
||||
AddPlayoffsToTournament.call(tournament: @group_stage_tournament)
|
||||
AddPlayoffsToTournament.call(tournament: @group_stage_tournament, teams: @group_stage_tournament.teams)
|
||||
end
|
||||
|
||||
let(:playoff_stage_tournament_context) do
|
||||
AddPlayoffsToTournament.call(tournament: @playoff_stage_tournament)
|
||||
AddPlayoffsToTournament.call(tournament: @playoff_stage_tournament, teams: @playoff_stage_tournament.teams)
|
||||
end
|
||||
|
||||
let(:full_tournament_context) do
|
||||
AddPlayoffsToTournament.call(tournament: @full_tournament)
|
||||
AddPlayoffsToTournament.call(tournament: @full_tournament, teams: @full_tournament.teams)
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
@ -23,7 +23,7 @@ RSpec.describe AddPlayoffsToTournament, type: :interactor do
|
|||
context 'PlayoffStageService mocked' do
|
||||
before do
|
||||
expect(class_double('PlayoffStageService').as_stubbed_const(transfer_nested_constants: true))
|
||||
.to receive(:generate_playoff_stages_from_tournament)
|
||||
.to receive(:generate_playoff_stages)
|
||||
.and_return(@stages)
|
||||
end
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ RSpec.describe AddPlayoffsToTournament, type: :interactor do
|
|||
context 'playoff generation fails' do
|
||||
before do
|
||||
expect(class_double('PlayoffStageService').as_stubbed_const(transfer_nested_constants: true))
|
||||
.to receive(:generate_playoff_stages_from_tournament)
|
||||
.to receive(:generate_playoff_stages)
|
||||
.and_return(nil)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -39,5 +39,65 @@ RSpec.describe Stage, type: :model do
|
|||
expect(teams).to match_array(@teams)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty stage' do
|
||||
it 'returns an empty Array' do
|
||||
expect(create(:stage).teams).to match_array([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#over?' do
|
||||
context 'group stage' do
|
||||
context 'with unfinished matches' do
|
||||
it 'returns false' do
|
||||
expect(create(:group_stage).over?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with all matches finished' do
|
||||
let(:finished_group_stage) do
|
||||
group_stage = create(:group_stage)
|
||||
group_stage.groups.map(&:matches).flatten.each do |m|
|
||||
m.state = :finished
|
||||
m.save!
|
||||
end
|
||||
group_stage
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(finished_group_stage.over?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'playoff stage' do
|
||||
context 'with unfinished matches' do
|
||||
it 'returns false' do
|
||||
expect(create(:playoff_stage).over?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with all matches finished' do
|
||||
let(:finished_playoff_stage) do
|
||||
playoff_stage = create(:playoff_stage)
|
||||
playoff_stage.matches.each do |m|
|
||||
m.state = :finished
|
||||
m.save!
|
||||
end
|
||||
playoff_stage
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(finished_playoff_stage.over?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty stage' do
|
||||
it 'returns false' do
|
||||
expect(create(:stage).over?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -156,4 +156,38 @@ RSpec.describe GroupStageService do
|
|||
it_should_behave_like 'only_return_group_scores'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#teams_sorted_by_group_scores' do
|
||||
before do
|
||||
@group_to_sort = create(:group, match_count: 10, match_factory: :filled_group_match)
|
||||
end
|
||||
|
||||
let(:sorted_teams) do
|
||||
GroupStageService.teams_sorted_by_group_scores(@group_to_sort)
|
||||
end
|
||||
|
||||
let(:sorted_teams_grouped_by_group_points) do
|
||||
sorted_teams.group_by { |t| @group_to_sort.group_scores.find_by(team: t).group_points }.values
|
||||
end
|
||||
|
||||
it 'sorts the teams after group_scores first' do
|
||||
i = 0
|
||||
while i < (sorted_teams.size - 1)
|
||||
expect(@group_to_sort.group_scores.find_by(team: sorted_teams[i]).group_points)
|
||||
.to be >= @group_to_sort.group_scores.find_by(team: sorted_teams[i + 1]).group_points
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
it 'sorts the teams after difference_in_points second' do
|
||||
sorted_teams_grouped_by_group_points.each do |teams|
|
||||
i = 0
|
||||
while i < (teams.size - 1)
|
||||
expect(@group_to_sort.group_scores.find_by(team: teams[i]).difference_in_points)
|
||||
.to be >= @group_to_sort.group_scores.find_by(team: teams[i + 1]).difference_in_points
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ RSpec.describe PlayoffStageService do
|
|||
amount_of_teams = parameters[:team_size]
|
||||
expected_amount_of_playoff_stages = parameters[:expected_amount_of_playoff_stages]
|
||||
teams = build_list(:team, amount_of_teams)
|
||||
stages = PlayoffStageService.generate_playoff_stages(teams)
|
||||
stages = PlayoffStageService.generate_playoff_stages(teams, false)
|
||||
expect(stages.size).to eq(expected_amount_of_playoff_stages)
|
||||
stages.each_index do |i|
|
||||
stage = stages[i]
|
||||
|
|
@ -82,7 +82,7 @@ RSpec.describe PlayoffStageService do
|
|||
|
||||
describe 'number of teams isn\'t a power of two' do
|
||||
let(:generated_stages) do
|
||||
PlayoffStageService.generate_playoff_stages(create_list(:team, 12))
|
||||
PlayoffStageService.generate_playoff_stages(create_list(:team, 12), false)
|
||||
end
|
||||
|
||||
let(:intermediate_stage) do
|
||||
|
|
@ -102,7 +102,7 @@ RSpec.describe PlayoffStageService do
|
|||
|
||||
describe 'number of teams is a power of two' do
|
||||
let(:generated_stages) do
|
||||
PlayoffStageService.generate_playoff_stages(create_list(:team, 16))
|
||||
PlayoffStageService.generate_playoff_stages(create_list(:team, 16), false)
|
||||
end
|
||||
|
||||
it 'generates only normal playoff_stage state stages' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue