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

Implement Group Stage end
This commit is contained in:
Daniel Schädler 2019-06-18 22:46:44 +02:00 committed by GitHub
commit 687bec10a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 367 additions and 19 deletions

View File

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

View File

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

View File

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

View File

@ -3,4 +3,8 @@
class GroupScore < ApplicationRecord
belongs_to :team
belongs_to :group
def difference_in_points
scored_points - received_points
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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