diff --git a/app/controllers/match_scores_controller.rb b/app/controllers/match_scores_controller.rb index 80c18ae..92f7dc0 100644 --- a/app/controllers/match_scores_controller.rb +++ b/app/controllers/match_scores_controller.rb @@ -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 diff --git a/app/interactors/update_groups_group_scores.rb b/app/interactors/update_groups_group_scores.rb new file mode 100644 index 0000000..3ea49e2 --- /dev/null +++ b/app/interactors/update_groups_group_scores.rb @@ -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 diff --git a/app/models/group_score.rb b/app/models/group_score.rb index 75ba75e..d7686d3 100644 --- a/app/models/group_score.rb +++ b/app/models/group_score.rb @@ -3,7 +3,4 @@ class GroupScore < ApplicationRecord belongs_to :team belongs_to :group - - # :) - alias_attribute :received_points, :recieved_points end diff --git a/app/models/match.rb b/app/models/match.rb index 2779fa7..bc80977 100644 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -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 diff --git a/app/models/match_score.rb b/app/models/match_score.rb index b2a3f0c..5bf815a 100644 --- a/app/models/match_score.rb +++ b/app/models/match_score.rb @@ -5,4 +5,8 @@ class MatchScore < ApplicationRecord belongs_to :team delegate :owner, to: :match + + def part_of_group_match? + match.group_match? + end end diff --git a/app/organizers/update_groups_group_scores_and_save.rb b/app/organizers/update_groups_group_scores_and_save.rb new file mode 100644 index 0000000..41d641f --- /dev/null +++ b/app/organizers/update_groups_group_scores_and_save.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class UpdateGroupsGroupScoresAndSave + include Interactor::Organizer + + organize UpdateGroupsGroupScores, SaveApplicationRecordObject +end diff --git a/app/services/group_stage_service.rb b/app/services/group_stage_service.rb index ea03168..592f6ad 100644 --- a/app/services/group_stage_service.rb +++ b/app/services/group_stage_service.rb @@ -1,32 +1,59 @@ # frozen_string_literal: true class GroupStageService - def self.generate_group_stage(groups) - raise 'Cannot generate group stage without groups' if groups.length.zero? + 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 - raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length.to_f % 1).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 + groups = groups.map(&method(:get_group_object_from)) + Stage.new level: -1, groups: groups + end + + 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 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 + + # 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 - matches end end diff --git a/db/migrate/0000_create_schema.rb b/db/migrate/0000_create_schema.rb index 600a2a2..29d606a 100644 --- a/db/migrate/0000_create_schema.rb +++ b/db/migrate/0000_create_schema.rb @@ -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 diff --git a/spec/controllers/match_scores_controller_spec.rb b/spec/controllers/match_scores_controller_spec.rb index 67f9b1a..8e34133 100644 --- a/spec/controllers/match_scores_controller_spec.rb +++ b/spec/controllers/match_scores_controller_spec.rb @@ -34,18 +34,32 @@ 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 + expect(@match_score.points).to eq(valid_update[:points]) + end - 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]) + it 'renders a response with the updated team' do + put :update, params: { id: @match_score.to_param }.merge(valid_update) + expect(response).to be_successful + body = deserialize_response response + expect(body[:points]).to eq(valid_update[:points]) + end end - it 'renders a response with the updated team' do - put :update, params: { id: @match_score.to_param }.merge(valid_update) - expect(response).to be_successful - body = deserialize_response response - expect(body[:points]).to eq(valid_update[:points]) + 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 @@ -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 diff --git a/spec/controllers/statistics_controller_spec.rb b/spec/controllers/statistics_controller_spec.rb index d516098..bd105e1 100644 --- a/spec/controllers/statistics_controller_spec.rb +++ b/spec/controllers/statistics_controller_spec.rb @@ -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, diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 839c230..ba375c5 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -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 diff --git a/spec/factories/matches.rb b/spec/factories/matches.rb index 43fb55d..b200839 100644 --- a/spec/factories/matches.rb +++ b/spec/factories/matches.rb @@ -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 diff --git a/spec/factories/stages.rb b/spec/factories/stages.rb index b5736cd..c2d1e2b 100644 --- a/spec/factories/stages.rb +++ b/spec/factories/stages.rb @@ -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 diff --git a/spec/factories/tournaments.rb b/spec/factories/tournaments.rb index 637564c..b4a522a 100644 --- a/spec/factories/tournaments.rb +++ b/spec/factories/tournaments.rb @@ -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 diff --git a/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb b/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb index d7a3208..6e009df 100644 --- a/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb +++ b/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb @@ -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 diff --git a/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb b/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb index 884f0e8..ca60cf9 100644 --- a/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb +++ b/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb @@ -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) diff --git a/spec/interactors/update_groups_group_scores_interactor_spec.rb b/spec/interactors/update_groups_group_scores_interactor_spec.rb new file mode 100644 index 0000000..83d1068 --- /dev/null +++ b/spec/interactors/update_groups_group_scores_interactor_spec.rb @@ -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 diff --git a/spec/models/match_score_spec.rb b/spec/models/match_score_spec.rb index b6f7bfc..343cecd 100644 --- a/spec/models/match_score_spec.rb +++ b/spec/models/match_score_spec.rb @@ -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 diff --git a/spec/models/match_spec.rb b/spec/models/match_spec.rb index 0d4656c..1491bfa 100644 --- a/spec/models/match_spec.rb +++ b/spec/models/match_spec.rb @@ -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 diff --git a/spec/services/group_stage_service_spec.rb b/spec/services/group_stage_service_spec.rb index f88f598..c7a850e 100644 --- a/spec/services/group_stage_service_spec.rb +++ b/spec/services/group_stage_service_spec.rb @@ -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