Merge pull request #35 from turniere/ticket/TURNIERE-147

Implement Group Stage creation Logic
This commit is contained in:
Thor77 2019-05-04 21:04:50 +02:00 committed by GitHub
commit be24b1bc39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 438 additions and 26 deletions

View File

@ -1,4 +1,4 @@
# turniere-backend [![pipeline status](https://gitlab.com/turniere/turniere-backend/badges/master/pipeline.svg)](https://gitlab.com/turniere/turniere-backend/commits/master) [![Coverage Status](https://coveralls.io/repos/gitlab/turniere/turniere-backend/badge.svg?branch=ticket%2FTURNIERE-155)](https://coveralls.io/gitlab/turniere/turniere-backend?branch=ticket%2FTURNIERE-155) # turniere-backend [![Build Status](https://travis-ci.org/turniere/turniere-backend.svg?branch=master)](https://travis-ci.org/turniere/turniere-backend) [![pipeline status](https://gitlab.com/turniere/turniere-backend/badges/master/pipeline.svg)](https://gitlab.com/turniere/turniere-backend/commits/master) [![Coverage Status](https://coveralls.io/repos/gitlab/turniere/turniere-backend/badge.svg?branch=master)](https://coveralls.io/gitlab/turniere/turniere-backend?branch=master) [![](https://img.shields.io/badge/Protected_by-Hound-a873d1.svg)](https://houndci.com)
Ruby on Rails application serving as backend for turnie.re Ruby on Rails application serving as backend for turnie.re
# Installation # Installation

View File

@ -31,25 +31,27 @@ class TournamentsController < ApplicationController
def create def create
params = tournament_params params = tournament_params
params.require(:teams) params.require(:teams)
# convert teams parameter into Team objects group_stage = params.delete(:group_stage)
teams = params.delete('teams').map do |team| teams = params.delete('teams')
if team[:id]
Team.find team[:id]
elsif team[:name]
Team.create name: team[:name]
end
end
# create tournament # create tournament
tournament = current_user.tournaments.new params tournament = current_user.tournaments.new params
# associate provided teams with tournament if group_stage
tournament.teams = teams groups = organize_teams_in_groups(teams)
# add groups to tournament
result = AddGroupStageToTournamentAndSaveTournamentToDatabase.call(tournament: tournament, groups: groups)
else
# convert teams parameter into Team objects
teams = teams.map(&method(:find_or_create_team))
# associate provided teams with tournament
tournament.teams = teams
# add playoff stage to tournament
result = AddPlayoffsToTournamentAndSaveTournamentToDatabase.call(tournament: tournament)
end
# validate tournament # validate tournament
unless tournament.valid? unless tournament.valid?
render json: tournament.errors, status: :unprocessable_entity render json: tournament.errors, status: :unprocessable_entity
return return
end end
# add playoff stage to tournament
result = AddPlayoffsToTournamentAndSaveTournamentToDatabase.call(tournament: tournament)
# return appropriate result # return appropriate result
if result.success? if result.success?
render json: result.tournament, status: :created, location: result.tournament render json: result.tournament, status: :created, location: result.tournament
@ -74,6 +76,24 @@ class TournamentsController < ApplicationController
private private
def organize_teams_in_groups(teams)
# each team gets put into a array of teams depending on the group specified in team[:group]
teams.group_by { |team| team['group'] }.values.map do |group|
group.map do |team|
find_or_create_team(team)
end
end
end
def find_or_create_team(team)
# convert teams parameter into Team objects
if team[:id]
Team.find team[:id]
elsif team[:name]
Team.create name: team[:name]
end
end
def set_tournament def set_tournament
@tournament = Tournament.find(params[:id]) @tournament = Tournament.find(params[:id])
end end
@ -83,7 +103,7 @@ class TournamentsController < ApplicationController
end end
def tournament_params def tournament_params
params.slice(:name, :description, :public, :teams).permit! params.slice(:name, :description, :public, :teams, :group_stage).permit!
end end
def validate_create_params def validate_create_params

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class AddGroupStageToTournament
include Interactor
def call
tournament = context.tournament
groups = context.groups
context.fail! unless tournament.stages.empty?
begin
group_stage = GroupStageService.generate_group_stage(groups)
tournament.stages = [group_stage]
context.tournament = tournament
rescue StandardError
context.fail!
end
end
end

View File

@ -4,4 +4,8 @@ class Group < ApplicationRecord
belongs_to :stage belongs_to :stage
has_many :matches, dependent: :destroy has_many :matches, dependent: :destroy
has_many :group_scores, dependent: :destroy has_many :group_scores, dependent: :destroy
def teams
matches.map(&:teams).flatten.uniq
end
end end

View File

@ -11,6 +11,10 @@ class Match < ApplicationRecord
validate :stage_xor_group validate :stage_xor_group
def teams
match_scores.map(&:team).flatten.uniq
end
private private
def stage_xor_group def stage_xor_group

View File

@ -4,4 +4,10 @@ class Stage < ApplicationRecord
belongs_to :tournament belongs_to :tournament
has_many :matches, dependent: :destroy has_many :matches, dependent: :destroy
has_many :groups, dependent: :destroy has_many :groups, dependent: :destroy
def teams
return matches.map(&:teams).flatten.uniq unless matches.size.zero?
groups.map(&:teams).flatten.uniq unless groups.size.zero?
end
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddGroupStageToTournamentAndSaveTournamentToDatabase
include Interactor::Organizer
organize AddGroupStageToTournament, SaveTournamentToDatabase
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class GroupStageService
def self.generate_group_stage(groups)
raise 'Cannot generate group stage without groups' if groups.length.zero?
# raise an error if the average group size is not a whole number
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
end
def self.get_group_object_from(team_array)
Group.new matches: generate_all_matches_between(team_array)
end
def self.generate_all_matches_between(teams)
matches = []
teams.combination(2).to_a # = matchups
.each_with_index do |matchup, i|
match = Match.new state: :not_started,
position: i,
match_scores: [
MatchScore.new(team: matchup.first),
MatchScore.new(team: matchup.second)
]
matches << match
end
matches
end
end

View File

@ -9,6 +9,8 @@ RSpec.describe TournamentsController, type: :controller do
@another_user = create(:user) @another_user = create(:user)
@private_tournament = create(:tournament, user: @another_user, public: false) @private_tournament = create(:tournament, user: @another_user, public: false)
@teams = create_list(:detached_team, 4) @teams = create_list(:detached_team, 4)
@teams16 = create_list(:detached_team, 16)
@groups = create_list(:group, 4)
end end
describe 'GET #index' do describe 'GET #index' do
@ -103,7 +105,7 @@ RSpec.describe TournamentsController, type: :controller do
end end
describe 'POST #create' do describe 'POST #create' do
let(:create_data) do let(:create_playoff_tournament_data) do
{ {
name: Faker::Creature::Dog.name, name: Faker::Creature::Dog.name,
description: Faker::Lorem.sentence, description: Faker::Lorem.sentence,
@ -112,9 +114,20 @@ RSpec.describe TournamentsController, type: :controller do
} }
end end
let(:create_group_tournament_data) do
teams_with_groups = @teams16.each_with_index.map { |team, i| { id: team.id, group: (i / 4).floor } }
{
name: Faker::TvShows::FamilyGuy.character,
description: Faker::Movies::HarryPotter.quote,
public: false,
group_stage: true,
teams: teams_with_groups
}
end
context 'without authentication headers' do context 'without authentication headers' do
it 'renders an unauthorized error response' do it 'renders an unauthorized error response' do
put :create, params: create_data put :create, params: create_playoff_tournament_data
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@ -127,40 +140,58 @@ RSpec.describe TournamentsController, type: :controller do
context 'with existing teams' do context 'with existing teams' do
it 'creates a new Tournament' do it 'creates a new Tournament' do
expect do expect do
post :create, params: create_data post :create, params: create_playoff_tournament_data
end.to change(Tournament, :count).by(1) end.to change(Tournament, :count).by(1)
end end
it 'associates the new tournament with the authenticated user' do it 'associates the new tournament with the authenticated user' do
expect do expect do
post :create, params: create_data post :create, params: create_playoff_tournament_data
end.to change(@user.tournaments, :count).by(1) end.to change(@user.tournaments, :count).by(1)
end end
it 'associates the given teams with the created tournament' do it 'associates the given teams with the created tournament' do
post :create, params: create_data post :create, params: create_playoff_tournament_data
body = deserialize_response response body = deserialize_response response
tournament = Tournament.find(body[:id]) tournament = Tournament.find(body[:id])
expect(tournament.teams).to match_array(@teams) expect(tournament.teams).to match_array(@teams)
end end
it 'generates a playoff stage' do it 'generates a playoff stage' do
post :create, params: create_data post :create, params: create_playoff_tournament_data
body = deserialize_response response body = deserialize_response response
tournament = Tournament.find(body[:id]) tournament = Tournament.find(body[:id])
expect(tournament.stages.first).to be_a(Stage) expect(tournament.stages.first).to be_a(Stage)
end end
it 'generates a playoff stage with all given teams' do it 'generates a playoff stage with all given teams' do
post :create, params: create_data post :create, params: create_playoff_tournament_data
body = deserialize_response response body = deserialize_response response
tournament = Tournament.find(body[:id]) tournament = Tournament.find(body[:id])
included_teams = tournament.stages.first.matches.map { |m| m.match_scores.map(&:team) }.flatten.uniq included_teams = tournament.stages.first.matches.map { |m| m.match_scores.map(&:team) }.flatten.uniq
expect(included_teams).to match_array(@teams) expect(included_teams).to match_array(@teams)
end end
context 'with parameter group_stage=true' do
before do
post :create, params: create_group_tournament_data
body = deserialize_response response
@group_stage_tournament = Tournament.find(body[:id])
end
it 'generates a group stage with all teams given in parameters' do
included_teams = @group_stage_tournament.stages.find_by(level: -1).teams
expect(included_teams).to match_array(@teams16)
end
it 'generates a group stage' do
group_stage = @group_stage_tournament.stages.find_by(level: -1)
expect(group_stage).to be_a(Stage)
end
end
it 'renders a JSON response with the new tournament' do it 'renders a JSON response with the new tournament' do
post :create, params: create_data post :create, params: create_playoff_tournament_data
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
expect(response.content_type).to eq('application/json') expect(response.content_type).to eq('application/json')
expect(response.location).to eq(tournament_url(Tournament.last)) expect(response.location).to eq(tournament_url(Tournament.last))
@ -169,16 +200,25 @@ RSpec.describe TournamentsController, type: :controller do
context 'with missing teams' do context 'with missing teams' do
it 'returns an error response' do it 'returns an error response' do
data = create_data data = create_playoff_tournament_data
data[:teams] << { id: Team.last.id + 1 } data[:teams] << { id: Team.last.id + 1 }
post :create, params: data post :create, params: data
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
end end
end end
context 'with unequal group sizes' do
it 'returns an error response' do
data = create_group_tournament_data
data[:teams].pop
post :create, params: data
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'with team names' do context 'with team names' do
it 'creates teams for given names' do it 'creates teams for given names' do
data = create_data data = create_playoff_tournament_data
data.delete :teams data.delete :teams
data[:teams] = (1..12).collect { { name: Faker::Creature::Dog.name } } data[:teams] = (1..12).collect { { name: Faker::Creature::Dog.name } }
expect do expect do
@ -193,6 +233,15 @@ RSpec.describe TournamentsController, type: :controller do
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_entity)
end end
end end
context 'with empty team objects' do
it 'renders an unprocessable entity response' do
data = create_group_tournament_data
data[:teams] = [{ group: 1 }, { group: 1 }, { group: 2 }, { group: 2 }]
post :create, params: data
expect(response).to have_http_status(:unprocessable_entity)
end
end
end end
end end

View File

@ -2,7 +2,15 @@
FactoryBot.define do FactoryBot.define do
factory :group do factory :group do
number { 0 } transient do
match_count { 4 }
end
sequence(:number)
stage stage
after(:create) do |group, evaluator|
create_list(:group_match, evaluator.match_count, group: group)
end
end end
end end

View File

@ -16,5 +16,14 @@ FactoryBot.define do
factory :group_match, class: Match do factory :group_match, class: Match do
group group
factory :running_group_match do
transient do
match_scores_count { 2 }
end
after(:create) do |match, evaluator|
match.match_scores = create_list(:match_score, evaluator.match_scores_count)
end
state { 3 }
end
end end
end end

View File

@ -3,5 +3,24 @@
FactoryBot.define do FactoryBot.define do
factory :stage do factory :stage do
tournament tournament
factory :group_stage do
level { -1 }
transient do
group_count { 4 }
end
after(:create) do |stage, evaluator|
stage.groups = create_list(:group, evaluator.group_count)
end
end
factory :playoff_stage do
level { rand(10) }
transient do
match_count { 4 }
end
after(:create) do |stage, evaluator|
stage.matches = create_list(:match, evaluator.match_count)
end
end
end end
end end

View File

@ -11,6 +11,7 @@ FactoryBot.define do
after(:create) do |tournament, evaluator| after(:create) do |tournament, evaluator|
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament) tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
end end
factory :stage_tournament do factory :stage_tournament do
transient do transient do
stage_count { 1 } stage_count { 1 }
@ -19,5 +20,11 @@ FactoryBot.define do
tournament.stages = create_list(:stage, evaluator.stage_count) tournament.stages = create_list(:stage, evaluator.stage_count)
end end
end end
factory :group_stage_tournament do
after(:create) do |tournament, _evaluator|
tournament.stages = create_list(:group_stage, 1)
end
end
end end
end end

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
RSpec.describe AddGroupStageToTournament do
let(:empty_tournament_context) do
AddGroupStageToTournament.call(tournament: @empty_tournament, groups: @groups)
end
let(:group_stage_tournament_context) do
AddGroupStageToTournament.call(tournament: @group_stage_tournament, groups: @groups)
end
before do
@empty_tournament = create(:stage_tournament, stage_count: 0)
@group_stage_tournament = create(:group_stage_tournament)
@group_stage = create(:group_stage)
@groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values
end
context 'GroupStageService mocked' do
before do
expect(class_double('GroupStageService').as_stubbed_const(transfer_nested_constants: true))
.to receive(:generate_group_stage).with(@groups)
.and_return(@group_stage)
end
context 'empty tournament' do
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)
end
end
end
context 'empty groups' do
before do
expect(class_double('GroupStageService').as_stubbed_const(transfer_nested_constants: true))
.to receive(:generate_group_stage).with(@groups)
.and_raise('Cannot generate group stage without groups')
end
it 'playoff generation fails' do
expect(empty_tournament_context).to be_a_failure
end
end
context 'unequal group sizes' do
before do
expect(class_double('GroupStageService').as_stubbed_const(transfer_nested_constants: true))
.to receive(:generate_group_stage).with(@groups)
.and_raise('Groups need to be equal size')
end
it 'playoff generation fails' do
expect(empty_tournament_context).to be_a_failure
end
end
context 'tournament where group stage is already generated' do
it 'does not add group stage' do
expect(group_stage_tournament_context).to be_a_failure
end
end
end

View File

@ -20,7 +20,7 @@ RSpec.describe AddPlayoffsToTournament do
@stages = create_list(:stage, 5) @stages = create_list(:stage, 5)
end end
context 'ez lyfe' do context 'PlayoffStageService mocked' do
before do before do
expect(class_double('PlayoffStageService').as_stubbed_const(transfer_nested_constants: true)) expect(class_double('PlayoffStageService').as_stubbed_const(transfer_nested_constants: true))
.to receive(:generate_playoff_stages_from_tournament) .to receive(:generate_playoff_stages_from_tournament)

View File

@ -12,4 +12,19 @@ RSpec.describe Group, type: :model do
it 'has a valid factory' do it 'has a valid factory' do
expect(build(:group)).to be_valid expect(build(:group)).to be_valid
end end
describe '#teams' do
before do
@group = create(:group, match_count: 1) # this is getting stubbed anyways
@teams = create_list(:team, 4)
expect_any_instance_of(Match)
.to receive(:teams)
.and_return(@teams)
end
it 'returns all teams from the matches within the matches below' do
teams = @group.teams
expect(teams).to match_array(@teams)
end
end
end end

View File

@ -43,7 +43,43 @@ RSpec.describe Match, type: :model do
end end
end end
context '#teams' do
before do
@playoff_match = create(:running_playoff_match)
@group_match = create(:running_group_match)
@teams = create_list(:team, 2)
@match_scores = create_list(:match_score, 2)
@match_scores.each_with_index { |match, i| match.team = @teams[i] }
@playoff_match.match_scores = @match_scores
@group_match.match_scores = @match_scores
end
context 'called on group match' do
let(:call_teams_on_group_match) do
@group_match.teams
end
it 'returns 2 team objects' do
teams = call_teams_on_group_match
expect(teams).to match_array(@teams)
end
end
context 'called on playoff match' do
let(:call_teams_on_playoff_match) do
@playoff_match.teams
end
it 'returns 2 team objects' do
teams = call_teams_on_playoff_match
expect(teams).to match_array(@teams)
end
end
end
it 'has a valid factory' do it 'has a valid factory' do
expect(build(:match)).to be_valid expect(build(:match)).to be_valid
expect(build(:running_playoff_match)).to be_valid
expect(build(:group_match)).to be_valid
end end
end end

View File

@ -11,5 +11,38 @@ RSpec.describe Stage, type: :model do
it 'has a valid factory' do it 'has a valid factory' do
expect(build(:stage)).to be_valid expect(build(:stage)).to be_valid
expect(build(:group_stage)).to be_valid
end
describe '#teams' do
context 'group stage' do
before do
@stage = create(:group_stage, group_count: 1) # this is getting stubbed anyways
@teams = create_list(:team, 4)
expect_any_instance_of(Group)
.to receive(:teams)
.and_return(@teams)
end
it 'returns all teams from the matches within the groups below' do
teams = @stage.teams
expect(teams).to match_array(@teams)
end
end
context 'playoff stage' do
before do
@stage = create(:playoff_stage, match_count: 1) # this is getting stubbed anyways
@teams = create_list(:team, 4)
expect_any_instance_of(Match)
.to receive(:teams)
.and_return(@teams)
end
it 'returns all teams from the matches within the matches below' do
teams = @stage.teams
expect(teams).to match_array(@teams)
end
end
end end
end end

View File

@ -15,5 +15,6 @@ RSpec.describe Team, type: :model do
it 'has a valid factory' do it 'has a valid factory' do
expect(build(:team)).to be_valid expect(build(:team)).to be_valid
expect(build(:detached_team)).to be_valid
end end
end end

View File

@ -33,5 +33,7 @@ RSpec.describe Tournament, type: :model do
it 'has valid factory' do it 'has valid factory' do
expect(build(:tournament)).to be_valid expect(build(:tournament)).to be_valid
expect(build(:stage_tournament)).to be_valid
expect(build(:group_stage_tournament)).to be_valid
end end
end end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
RSpec.describe GroupStageService do
before do
@stage = create(:stage)
@teams1 = create_list(:team, 4)
@teams2 = create_list(:team, 4)
@groups = Hash[1 => @teams1, 2 => @teams2].values
end
describe '#generate_group_stage method' do
it 'returns a stage object' do
group_stage = GroupStageService.generate_group_stage(@groups)
expect(group_stage).to be_a(Stage)
end
it 'returns a stage object with level -1' do
group_stage_level = GroupStageService.generate_group_stage(@groups).level
expect(group_stage_level).to be(-1)
end
it 'adds the provided groups to the stage' do
group_stage_teams = GroupStageService.generate_group_stage(@groups).teams
expect(group_stage_teams).to match_array(@groups.flatten)
end
it 'raises exception when given different sizes of groups' do
unequal_groups = @groups
unequal_groups.first.pop
expect { GroupStageService.generate_group_stage(unequal_groups) }
.to raise_exception 'Groups need to be equal size'
end
it 'raises exception when given no groups' do
expect { GroupStageService.generate_group_stage([]) }
.to raise_exception 'Cannot generate group stage without groups'
end
end
describe '#get_group_object_from' do
it 'returns a group' do
group = GroupStageService.get_group_object_from(@teams1)
expect(group).to be_a(Group)
end
end
describe '#generate_all_matches_between' do
it 'generates a list of not started matches' do
matches = GroupStageService.generate_all_matches_between(@teams2)
matches.each do |match|
expect(match).to be_a(Match)
match_state = match.state
expect(match_state).to eq('not_started')
end
end
it 'generates the right amount of matches' do
matches = GroupStageService.generate_all_matches_between(@teams2)
# (1..@teams2.size-1).sum -> 1. Team has to play against n-1 teams; second against n-2 ....
expect(matches.size).to be((1..@teams2.size - 1).sum)
end
it 'gives matches exclusive positions' do
matches = GroupStageService.generate_all_matches_between(@teams2)
match_positions = matches.map(&:position)
expect(match_positions.length).to eq(match_positions.uniq.length)
end
it 'doesn\'t match a team against itself' do
matches = GroupStageService.generate_all_matches_between(@teams1)
matches.each do |match|
expect(match.teams.first).to_not eq(match.teams.second)
end
end
end
end