Merge pull request #50 from turniere/ticket/TURNIERE-216
Implement Group Score Update on match_score points change
This commit is contained in:
commit
fc13634769
|
|
@ -13,6 +13,7 @@ class MatchScoresController < ApplicationController
|
|||
# PATCH/PUT /scores/1
|
||||
def update
|
||||
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
|
||||
else
|
||||
render json: @match_score.errors, status: :unprocessable_entity
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -3,7 +3,4 @@
|
|||
class GroupScore < ApplicationRecord
|
||||
belongs_to :team
|
||||
belongs_to :group
|
||||
|
||||
# :)
|
||||
alias_attribute :received_points, :recieved_points
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,17 +19,41 @@ class Match < ApplicationRecord
|
|||
stage ? stage.owner : group.owner
|
||||
end
|
||||
|
||||
def winner
|
||||
return nil unless finished?
|
||||
def current_leading_team
|
||||
return nil if match_scores.first.points == match_scores.second.points
|
||||
|
||||
match_scores.max_by(&:points).team
|
||||
end
|
||||
|
||||
def winner
|
||||
finished? ? current_leading_team : nil
|
||||
end
|
||||
|
||||
def group_match?
|
||||
group.present?
|
||||
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
|
||||
|
||||
def stage_xor_group
|
||||
|
|
|
|||
|
|
@ -5,4 +5,8 @@ class MatchScore < ApplicationRecord
|
|||
belongs_to :team
|
||||
|
||||
delegate :owner, to: :match
|
||||
|
||||
def part_of_group_match?
|
||||
match.group_match?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateGroupsGroupScoresAndSave
|
||||
include Interactor::Organizer
|
||||
|
||||
organize UpdateGroupsGroupScores, SaveApplicationRecordObject
|
||||
end
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GroupStageService
|
||||
def self.generate_group_stage(groups)
|
||||
class << self
|
||||
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
|
||||
|
|
@ -11,11 +12,12 @@ class GroupStageService
|
|||
Stage.new level: -1, groups: groups
|
||||
end
|
||||
|
||||
def self.get_group_object_from(team_array)
|
||||
Group.new matches: generate_all_matches_between(team_array)
|
||||
def get_group_object_from(team_array)
|
||||
Group.new matches: generate_all_matches_between(team_array),
|
||||
group_scores: team_array.map { |team| GroupScore.new team: team }
|
||||
end
|
||||
|
||||
def self.generate_all_matches_between(teams)
|
||||
def generate_all_matches_between(teams)
|
||||
matches = []
|
||||
teams.combination(2).to_a # = matchups
|
||||
.each_with_index do |matchup, i|
|
||||
|
|
@ -29,4 +31,29 @@ class GroupStageService
|
|||
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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class CreateSchema < ActiveRecord::Migration[5.2]
|
|||
create_table :group_scores do |t|
|
||||
t.integer :group_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 :group, index: true, foreign_key: { on_delete: :cascade }, null: false
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ RSpec.describe MatchScoresController, type: :controller do
|
|||
before(:each) do
|
||||
apply_authentication_headers_for @owner
|
||||
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
|
||||
|
|
@ -49,6 +49,20 @@ RSpec.describe MatchScoresController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when match_score update fails' do
|
||||
before do
|
||||
allow_any_instance_of(MatchScore)
|
||||
.to receive(:update)
|
||||
.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
|
||||
|
||||
context 'as another user' do
|
||||
before(:each) do
|
||||
apply_authentication_headers_for create(:user)
|
||||
|
|
@ -61,4 +75,45 @@ RSpec.describe MatchScoresController, type: :controller do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe StatisticsController, type: :controller do
|
|||
|
||||
context 'tournament with a group stage' 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)
|
||||
@most_dominant_score = create(:group_score,
|
||||
team: @tournament.teams.first,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@ FactoryBot.define do
|
|||
factory :group do
|
||||
transient do
|
||||
match_count { 4 }
|
||||
match_factory { :group_match }
|
||||
end
|
||||
|
||||
sequence(:number)
|
||||
stage
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -34,14 +34,18 @@ FactoryBot.define do
|
|||
factory :group_match, class: Match do
|
||||
group
|
||||
position { 0 }
|
||||
factory :running_group_match do
|
||||
factory :filled_group_match do
|
||||
transient do
|
||||
match_scores_count { 2 }
|
||||
end
|
||||
|
||||
factory :running_group_match do
|
||||
state { :in_progress }
|
||||
end
|
||||
|
||||
after(:create) do |match, evaluator|
|
||||
match.match_scores = create_list(:match_score, evaluator.match_scores_count)
|
||||
end
|
||||
state { :in_progress }
|
||||
end
|
||||
|
||||
factory :undecided_group_match do
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ FactoryBot.define do
|
|||
level { -1 }
|
||||
transient do
|
||||
group_count { 4 }
|
||||
match_factory { :group_match }
|
||||
end
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,6 @@ FactoryBot.define do
|
|||
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
|
||||
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
|
||||
transient do
|
||||
stage_count { 1 }
|
||||
|
|
@ -44,8 +35,14 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
factory :group_stage_tournament do
|
||||
after(:create) do |tournament, _evaluator|
|
||||
tournament.stages << create(:group_stage)
|
||||
transient do
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ RSpec.describe AddGroupStageToTournament do
|
|||
|
||||
before do
|
||||
@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)
|
||||
@groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.describe AddPlayoffsToTournament do
|
|||
end
|
||||
|
||||
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)
|
||||
@full_tournament = create(:dummy_stage_tournament)
|
||||
@stages = create_list(:stage, 3)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -7,4 +7,14 @@ RSpec.describe MatchScore, type: :model do
|
|||
it { should belong_to :match }
|
||||
it { should belong_to :team }
|
||||
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
|
||||
|
|
|
|||
|
|
@ -90,4 +90,86 @@ RSpec.describe Match, type: :model do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -2,29 +2,34 @@
|
|||
|
||||
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
|
||||
@prepared_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)
|
||||
group_stage = GroupStageService.generate_group_stage(@prepared_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
|
||||
group_stage_level = GroupStageService.generate_group_stage(@prepared_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)
|
||||
group_stage_teams = GroupStageService.generate_group_stage(@prepared_groups).teams
|
||||
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
|
||||
|
||||
it 'raises exception when given different sizes of groups' do
|
||||
unequal_groups = @groups
|
||||
unequal_groups = @prepared_groups
|
||||
unequal_groups.first.pop
|
||||
expect { GroupStageService.generate_group_stage(unequal_groups) }
|
||||
.to raise_exception 'Groups need to be equal size'
|
||||
|
|
@ -72,4 +77,72 @@ RSpec.describe GroupStageService do
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue