Merge pull request #50 from turniere/ticket/TURNIERE-216

Implement Group Score Update on match_score points change
This commit is contained in:
Daniel Schädler 2019-06-05 10:24:19 +02:00 committed by GitHub
commit fc13634769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 405 additions and 64 deletions

View File

@ -13,6 +13,7 @@ class MatchScoresController < ApplicationController
# PATCH/PUT /scores/1 # PATCH/PUT /scores/1
def update def update
if @match_score.update(match_score_params) if @match_score.update(match_score_params)
UpdateGroupsGroupScoresAndSave.call(group: @match_score.match.group) if @match_score.part_of_group_match?
render json: @match_score render json: @match_score
else else
render json: @match_score.errors, status: :unprocessable_entity render json: @match_score.errors, status: :unprocessable_entity

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class UpdateGroupsGroupScores
include Interactor
def call
context.object_to_save = GroupStageService.update_group_scores(context.group)
rescue StandardError
context.fail!
end
end

View File

@ -3,7 +3,4 @@
class GroupScore < ApplicationRecord class GroupScore < ApplicationRecord
belongs_to :team belongs_to :team
belongs_to :group belongs_to :group
# :)
alias_attribute :received_points, :recieved_points
end end

View File

@ -19,17 +19,41 @@ class Match < ApplicationRecord
stage ? stage.owner : group.owner stage ? stage.owner : group.owner
end end
def winner def current_leading_team
return nil unless finished?
return nil if match_scores.first.points == match_scores.second.points return nil if match_scores.first.points == match_scores.second.points
match_scores.max_by(&:points).team match_scores.max_by(&:points).team
end end
def winner
finished? ? current_leading_team : nil
end
def group_match? def group_match?
group.present? group.present?
end end
def scored_points_of(team)
teams.include?(team) ? match_scores.find_by(team: team).points : 0
end
def received_points_of(team)
teams.include?(team) ? match_scores.find { |ms| ms.team != team }.points : 0
end
def group_points_of(team)
return 0 unless (finished? || in_progress?) && teams.include?(team)
case current_leading_team
when team
3
when nil
1
else
0
end
end
private private
def stage_xor_group def stage_xor_group

View File

@ -5,4 +5,8 @@ class MatchScore < ApplicationRecord
belongs_to :team belongs_to :team
delegate :owner, to: :match delegate :owner, to: :match
def part_of_group_match?
match.group_match?
end
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class UpdateGroupsGroupScoresAndSave
include Interactor::Organizer
organize UpdateGroupsGroupScores, SaveApplicationRecordObject
end

View File

@ -1,32 +1,59 @@
# frozen_string_literal: true # frozen_string_literal: true
class GroupStageService class GroupStageService
def self.generate_group_stage(groups) class << self
raise 'Cannot generate group stage without groups' if groups.length.zero? def 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 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? 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)) groups = groups.map(&method(:get_group_object_from))
Stage.new level: -1, groups: groups Stage.new level: -1, groups: groups
end end
def self.get_group_object_from(team_array) def get_group_object_from(team_array)
Group.new matches: generate_all_matches_between(team_array) Group.new matches: generate_all_matches_between(team_array),
end group_scores: team_array.map { |team| GroupScore.new team: team }
end
def self.generate_all_matches_between(teams)
matches = [] def generate_all_matches_between(teams)
teams.combination(2).to_a # = matchups matches = []
.each_with_index do |matchup, i| teams.combination(2).to_a # = matchups
match = Match.new state: :not_started, .each_with_index do |matchup, i|
position: i, match = Match.new state: :not_started,
match_scores: [ position: i,
MatchScore.new(team: matchup.first), match_scores: [
MatchScore.new(team: matchup.second) MatchScore.new(team: matchup.first),
] MatchScore.new(team: matchup.second)
matches << match ]
matches << match
end
matches
end
# Updates all group_scores of the given group
#
# @param group Group the group to update the group_scores in
# @return [Array] the changed group_scores that need to be saved
def update_group_scores(group)
changed_group_scores = []
group.teams.each do |team|
group_score = group.group_scores.find_by(team: team)
matches = group.matches.select { |match| match.teams.include? team }
# reset previous values
group_score.group_points = 0
group_score.scored_points = 0
group_score.received_points = 0
matches.each do |match|
# calculate points for every match
group_score.group_points += match.group_points_of team
group_score.scored_points += match.scored_points_of team
group_score.received_points += match.received_points_of team
end
changed_group_scores << group_score
end
changed_group_scores
end end
matches
end end
end end

View File

@ -104,7 +104,7 @@ class CreateSchema < ActiveRecord::Migration[5.2]
create_table :group_scores do |t| create_table :group_scores do |t|
t.integer :group_points, default: 0 t.integer :group_points, default: 0
t.integer :scored_points, default: 0 t.integer :scored_points, default: 0
t.integer :recieved_points, default: 0 t.integer :received_points, default: 0
t.belongs_to :team, index: true, foreign_key: { on_delete: :cascade }, null: false t.belongs_to :team, index: true, foreign_key: { on_delete: :cascade }, null: false
t.belongs_to :group, index: true, foreign_key: { on_delete: :cascade }, null: false t.belongs_to :group, index: true, foreign_key: { on_delete: :cascade }, null: false

View File

@ -34,18 +34,32 @@ RSpec.describe MatchScoresController, type: :controller do
before(:each) do before(:each) do
apply_authentication_headers_for @owner apply_authentication_headers_for @owner
end end
context 'when match_score update succeeds' do
it 'updates the requested score' do
put :update, params: { id: @match_score.to_param }.merge(valid_update)
@match_score.reload
expect(@match_score.points).to eq(valid_update[:points])
end
it 'updates the requested score' do it 'renders a response with the updated team' do
put :update, params: { id: @match_score.to_param }.merge(valid_update) put :update, params: { id: @match_score.to_param }.merge(valid_update)
@match_score.reload expect(response).to be_successful
expect(@match_score.points).to eq(valid_update[:points]) body = deserialize_response response
expect(body[:points]).to eq(valid_update[:points])
end
end end
it 'renders a response with the updated team' do context 'when match_score update fails' do
put :update, params: { id: @match_score.to_param }.merge(valid_update) before do
expect(response).to be_successful allow_any_instance_of(MatchScore)
body = deserialize_response response .to receive(:update)
expect(body[:points]).to eq(valid_update[:points]) .and_return(false)
end
it 'returns unprocessable entity' do
put :update, params: { id: @match_score.to_param }.merge(valid_update)
expect(response).to have_http_status(:unprocessable_entity)
end
end end
end end
@ -61,4 +75,45 @@ RSpec.describe MatchScoresController, type: :controller do
end end
end end
end end
describe 'on a real tournament' do
before do
@owner = create(:user)
@tournament = create(:group_stage_tournament, stage_count: 0, match_factory: :filled_group_match, owner: @owner)
@group = @tournament.stages.first.groups.first
@match_score = @group.matches.first.match_scores.first
end
let(:valid_update) do
{
points: 42
}
end
describe 'updating a match_score' do
before(:each) do
apply_authentication_headers_for @owner
expect(UpdateGroupsGroupScoresAndSave).to receive(:call).once.with(group: @group).and_return(context)
end
shared_examples_for 'successful_update_of_match_score' do
it 'returns a 200 status code' do
put :update, params: { id: @match_score.to_param }.merge(valid_update)
expect(response).to be_successful
end
end
context 'when group_score calculation succeeds' do
let(:context) { double(:context, success?: true) }
it_should_behave_like 'successful_update_of_match_score'
end
context 'when group_score calculation fails' do
let(:context) { double(:context, success?: false) }
it_should_behave_like 'successful_update_of_match_score'
end
end
end
end end

View File

@ -13,7 +13,7 @@ RSpec.describe StatisticsController, type: :controller do
context 'tournament with a group stage' do context 'tournament with a group stage' do
before do before do
@tournament = create(:group_stage_only_tournament) @tournament = create(:group_stage_tournament, stage_count: 0)
@group_stage = @tournament.stages.find_by(level: -1) @group_stage = @tournament.stages.find_by(level: -1)
@most_dominant_score = create(:group_score, @most_dominant_score = create(:group_score,
team: @tournament.teams.first, team: @tournament.teams.first,

View File

@ -4,13 +4,17 @@ FactoryBot.define do
factory :group do factory :group do
transient do transient do
match_count { 4 } match_count { 4 }
match_factory { :group_match }
end end
sequence(:number) sequence(:number)
stage stage
after(:create) do |group, evaluator| after(:create) do |group, evaluator|
create_list(:group_match, evaluator.match_count, group: group) create_list(evaluator.match_factory, evaluator.match_count, group: group)
group.group_scores = group.teams.map do |team|
create(:group_score, team: team, group: group)
end
end end
end end
end end

View File

@ -34,14 +34,18 @@ FactoryBot.define do
factory :group_match, class: Match do factory :group_match, class: Match do
group group
position { 0 } position { 0 }
factory :running_group_match do factory :filled_group_match do
transient do transient do
match_scores_count { 2 } match_scores_count { 2 }
end end
factory :running_group_match do
state { :in_progress }
end
after(:create) do |match, evaluator| after(:create) do |match, evaluator|
match.match_scores = create_list(:match_score, evaluator.match_scores_count) match.match_scores = create_list(:match_score, evaluator.match_scores_count)
end end
state { :in_progress }
end end
factory :undecided_group_match do factory :undecided_group_match do

View File

@ -7,9 +7,10 @@ FactoryBot.define do
level { -1 } level { -1 }
transient do transient do
group_count { 4 } group_count { 4 }
match_factory { :group_match }
end end
after(:create) do |stage, evaluator| after(:create) do |stage, evaluator|
stage.groups = create_list(:group, evaluator.group_count) stage.groups = create_list(:group, evaluator.group_count, match_factory: evaluator.match_factory)
end end
end end

View File

@ -12,15 +12,6 @@ FactoryBot.define do
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament) tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
end end
factory :group_stage_only_tournament do
transient do
group_count { 2 }
end
after(:create) do |tournament, evaluator|
tournament.stages << create(:group_stage, group_count: evaluator.group_count)
end
end
factory :stage_tournament do factory :stage_tournament do
transient do transient do
stage_count { 1 } stage_count { 1 }
@ -44,8 +35,14 @@ FactoryBot.define do
end end
factory :group_stage_tournament do factory :group_stage_tournament do
after(:create) do |tournament, _evaluator| transient do
tournament.stages << create(:group_stage) group_count { 2 }
match_factory { :group_match }
end
after(:create) do |tournament, evaluator|
tournament.stages << create(:group_stage,
match_factory: evaluator.match_factory,
group_count: evaluator.group_count)
end end
end end
end end

View File

@ -11,7 +11,7 @@ RSpec.describe AddGroupStageToTournament do
before do before do
@empty_tournament = create(:stageless_tournament) @empty_tournament = create(:stageless_tournament)
@group_stage_tournament = create(:group_stage_only_tournament, group_count: 0) @group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
@group_stage = create(:group_stage) @group_stage = create(:group_stage)
@groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values @groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values
end end

View File

@ -14,7 +14,7 @@ RSpec.describe AddPlayoffsToTournament do
end end
before do before do
@group_stage_tournament = create(:group_stage_only_tournament, group_count: 0) @group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
@playoff_stage_tournament = create(:stageless_tournament) @playoff_stage_tournament = create(:stageless_tournament)
@full_tournament = create(:dummy_stage_tournament) @full_tournament = create(:dummy_stage_tournament)
@stages = create_list(:stage, 3) @stages = create_list(:stage, 3)

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
RSpec.describe UpdateGroupsGroupScores do
before do
@group = create(:group)
@group_scores = create_list(:group_score, 2)
end
context 'save succeeds' do
let(:context) do
UpdateGroupsGroupScores.call(group: @group)
end
before do
allow(GroupStageService)
.to receive(:update_group_scores).with(@group)
.and_return(@group_scores)
end
it 'succeeds' do
expect(context).to be_a_success
end
it 'provides the objects to save' do
expect(context.object_to_save).to eq(@group_scores)
end
end
context 'exception is thrown' do
let(:context) do
UpdateGroupsGroupScores.call(group: @group)
end
before do
allow(GroupStageService)
.to receive(:update_group_scores).with(@group)
.and_throw('This failed :(')
end
it 'fails' do
test = context.failure?
expect(test).to eq(true)
end
end
end

View File

@ -7,4 +7,14 @@ RSpec.describe MatchScore, type: :model do
it { should belong_to :match } it { should belong_to :match }
it { should belong_to :team } it { should belong_to :team }
end end
describe '#part_of_group_match?' do
it 'is part of a group match' do
expect(create(:running_group_match).match_scores.first.part_of_group_match?).to eq(true)
end
it 'isn\'t part of a group match' do
expect(create(:running_playoff_match).match_scores.first.part_of_group_match?).to eq(false)
end
end
end end

View File

@ -90,4 +90,86 @@ RSpec.describe Match, type: :model do
end end
end end
end end
context '#points_of' do
before do
@match = create(:running_group_match)
teams = @match.teams
@team1 = teams.first
@team2 = teams.second
@uninvolved_team = create(:team)
end
context 'even match' do
before do
@match.match_scores.each do |ms|
ms.points = 34
ms.save!
end
end
it 'returns correct group_points' do
expect(@match.group_points_of(@team1)).to be(1)
expect(@match.group_points_of(@team2)).to be(1)
expect(@match.group_points_of(@uninvolved_team)).to be(0)
end
it 'returns correct scored_points' do
expect(@match.scored_points_of(@team1)).to be(34)
expect(@match.scored_points_of(@team2)).to be(34)
expect(@match.scored_points_of(@uninvolved_team)).to be(0)
end
it 'returns correct received_points' do
expect(@match.received_points_of(@team1)).to be(34)
expect(@match.received_points_of(@team2)).to be(34)
expect(@match.received_points_of(@uninvolved_team)).to be(0)
end
end
context 'not started match' do
before do
@not_started_match = create(:running_group_match, state: :not_started)
@team1 = @not_started_match.teams.first
end
it 'returns correct group_points' do
expect(@not_started_match.group_points_of(@team1)).to be(0)
end
it 'returns correct scored_points' do
expect(@match.scored_points_of(@team1)).to be(0)
end
it 'returns correct received_points' do
expect(@match.received_points_of(@team1)).to be(0)
end
end
context 'uneven match' do
before do
@match.match_scores.each do |ms|
ms.points = ms.team == @team1 ? 42 : 17
ms.save!
end
end
it 'returns correct group_points' do
expect(@match.group_points_of(@team1)).to be(3)
expect(@match.group_points_of(@team2)).to be(0)
expect(@match.group_points_of(@uninvolved_team)).to be(0)
end
it 'returns correct scored_points' do
expect(@match.scored_points_of(@team1)).to be(42)
expect(@match.scored_points_of(@team2)).to be(17)
expect(@match.scored_points_of(@uninvolved_team)).to be(0)
end
it 'returns correct received_points' do
expect(@match.received_points_of(@team1)).to be(17)
expect(@match.received_points_of(@team2)).to be(42)
expect(@match.received_points_of(@uninvolved_team)).to be(0)
end
end
end
end end

View File

@ -2,29 +2,34 @@
RSpec.describe GroupStageService do RSpec.describe GroupStageService do
before do before do
@stage = create(:stage)
@teams1 = create_list(:team, 4) @teams1 = create_list(:team, 4)
@teams2 = create_list(:team, 4) @teams2 = create_list(:team, 4)
@groups = Hash[1 => @teams1, 2 => @teams2].values @prepared_groups = Hash[1 => @teams1, 2 => @teams2].values
end end
describe '#generate_group_stage method' do describe '#generate_group_stage method' do
it 'returns a stage object' do it 'returns a stage object' do
group_stage = GroupStageService.generate_group_stage(@groups) group_stage = GroupStageService.generate_group_stage(@prepared_groups)
expect(group_stage).to be_a(Stage) expect(group_stage).to be_a(Stage)
end end
it 'returns a stage object with level -1' do it 'returns a stage object with level -1' do
group_stage_level = GroupStageService.generate_group_stage(@groups).level group_stage_level = GroupStageService.generate_group_stage(@prepared_groups).level
expect(group_stage_level).to be(-1) expect(group_stage_level).to be(-1)
end end
it 'adds the provided groups to the stage' do it 'adds the provided groups to the stage' do
group_stage_teams = GroupStageService.generate_group_stage(@groups).teams group_stage_teams = GroupStageService.generate_group_stage(@prepared_groups).teams
expect(group_stage_teams).to match_array(@groups.flatten) expect(group_stage_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)
end end
it 'raises exception when given different sizes of groups' do it 'raises exception when given different sizes of groups' do
unequal_groups = @groups unequal_groups = @prepared_groups
unequal_groups.first.pop unequal_groups.first.pop
expect { GroupStageService.generate_group_stage(unequal_groups) } expect { GroupStageService.generate_group_stage(unequal_groups) }
.to raise_exception 'Groups need to be equal size' .to raise_exception 'Groups need to be equal size'
@ -72,4 +77,72 @@ RSpec.describe GroupStageService do
end end
end end
end end
describe '#update_group_scores' do
shared_examples_for 'only_return_group_scores' do
it 'only returns group_scores' do
@changed_group_scores.each do |gs|
expect(gs).to be_a(GroupScore)
end
end
end
context 'with only undecided matches' do
before do
@group = create(:group, match_factory: :running_group_match)
@group.matches.each do |match|
match.match_scores.each do |ms|
ms.points = 42
ms.save!
end
end
@changed_group_scores = GroupStageService.update_group_scores(@group)
end
it 'assigns one point to every team' do
@changed_group_scores.map(&:group_points).each do |points|
expect(points).to be(1)
end
end
it 'returns correct values for received_points' do
@changed_group_scores.map(&:received_points).each do |points|
expect(points).to be(42)
end
end
it 'returns correct values for scored_points' do
@changed_group_scores.map(&:scored_points).each do |points|
expect(points).to be(42)
end
end
it_should_behave_like 'only_return_group_scores'
end
context 'with only decided matches' do
before do
@group = create(:group, match_factory: :running_group_match)
@group.matches.each_with_index do |match, i|
match.match_scores.each_with_index do |ms, j|
match_score_number = i + j
ms.points = match_score_number
ms.save!
end
end
@changed_group_scores = GroupStageService.update_group_scores(@group)
end
it 'assigns the right amount of points' do
winning_teams = @changed_group_scores.select { |gs| gs.group_points == 3 }
losing_teams = @changed_group_scores.select { |gs| gs.group_points == 0 }
# Assure that half of the teams won and got 3 points
expect(winning_teams.size).to be(@changed_group_scores.size / 2)
# and half of them lost and got 0
expect(losing_teams.size).to be(@changed_group_scores.size / 2)
end
it_should_behave_like 'only_return_group_scores'
end
end
end end