Skip to content

Commit

Permalink
Add Interactors::ChangeEventCourse and spec; update Interactors::Chan…
Browse files Browse the repository at this point in the history
…geEffortEvent to use standard responses for errors.
  • Loading branch information
moveson committed Feb 14, 2018
1 parent 58d3a03 commit 281257f
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 12 deletions.
18 changes: 10 additions & 8 deletions app/services/interactors/change_effort_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ def initialize(args)
end

def perform!
existing_start_time = effort.start_time
effort.event = new_event
effort.start_time = existing_start_time
split_times.each { |st| st.split = splits_by_distance[st.distance_from_start] }
save_changes
unless errors.present?
existing_start_time = effort.start_time
effort.event = new_event
effort.start_time = existing_start_time
split_times.each { |st| st.split = splits_by_distance[st.distance_from_start] }
save_changes
end
Interactors::Response.new(errors, response_message)
end

Expand Down Expand Up @@ -58,9 +60,9 @@ def response_message
end

def verify_compatibility
raise ArgumentError, "#{effort} cannot be assigned to #{new_event} because distances do not coincide" unless split_times.all? { |st| distances.include?(st.distance_from_start) }
raise ArgumentError, "#{effort} cannot be assigned to #{new_event} because sub splits do not coincide" unless split_times.all? { |st| splits_by_distance[st.distance_from_start].sub_split_bitkeys.include?(st.bitkey) }
raise ArgumentError, "#{effort} cannot be assigned to #{new_event} because laps exceed maximum required" unless split_times.all? { |st| maximum_lap >= st.lap }
errors << distance_mismatch_error(effort, new_event) and return unless split_times.all? { |st| distances.include?(st.distance_from_start) }
errors << sub_split_mismatch_error(effort, new_event) and return unless split_times.all? { |st| splits_by_distance[st.distance_from_start].sub_split_bitkeys.include?(st.bitkey) }
errors << lap_mismatch_error(effort, new_event) unless split_times.all? { |st| maximum_lap >= st.lap }
end
end
end
65 changes: 65 additions & 0 deletions app/services/interactors/change_event_course.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module Interactors
class ChangeEventCourse
include Interactors::Errors

def self.perform!(args)
new(args).perform!
end

def initialize(args)
ArgsValidator.validate(params: args, required: [:event, :new_course], exclusive: [:event, :new_course], class: self.class)
@event = args[:event]
@new_course = args[:new_course]
@old_course ||= event.course
@split_times ||= event.split_times
@errors = []
verify_compatibility
end

def perform!
unless errors.present?
event.course = new_course
event.splits = new_course.splits
split_times.each { |st| st.split = splits_by_distance[st.distance_from_start] }
save_changes
end
Interactors::Response.new(errors, response_message)
end

private

attr_reader :event, :new_course, :split_times, :errors

def save_changes
ActiveRecord::Base.transaction do
event.save(validate: false) if event.changed?
split_times.each { |st| save_split_time(st) }
errors << resource_error_object(event) unless event.valid?
raise ActiveRecord::Rollback if errors.present?
end
end

def save_split_time(st)
if st.changed?
errors << resource_error_object(split_time) unless st.save
end
end

def distances
@distances ||= splits_by_distance.keys.to_set
end

def splits_by_distance
@splits_by_distance ||= new_course.splits.index_by(&:distance_from_start)
end

def response_message
errors.present? ? "#{event.name} could not be changed to #{new_course.name}. " : "#{event.name} was changed to #{new_course.name}. "
end

def verify_compatibility
errors << distance_mismatch_error(event, new_course) and return unless split_times.all? { |st| distances.include?(st.distance_from_start) }
errors << sub_split_mismatch_error(event, new_course) unless split_times.all? { |st| splits_by_distance[st.distance_from_start].sub_split_bitkeys.include?(st.bitkey) }
end
end
end
15 changes: 15 additions & 0 deletions app/services/interactors/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ def active_record_error(exception)
detail: {exception: exception}}
end

def distance_mismatch_error(child, new_parent)
{title: 'Distances do not match',
detail: {messages: ["#{child} cannot be assigned to #{new_parent} because distances do not coincide"]}}
end

def lap_mismatch_error(child, new_parent)
{title: 'Distances do not match',
detail: {messages: ["#{child} cannot be assigned to #{new_parent} because laps exceed maximum required"]}}
end

def sub_split_mismatch_error(child, new_parent)
{title: 'Distances do not match',
detail: {messages: ["#{child} cannot be assigned to #{new_parent} because sub splits do not coincide"]}}
end

def mismatched_organization_error(old_event_group, new_event_group)
{title: 'Event group organizations do not match',
detail: {messages: ["The event cannot be updated because #{old_event_group} is organized under #{old_event_group.organization}, but #{new_event_group} is organized under #{new_event_group.organization}"]}}
Expand Down
14 changes: 10 additions & 4 deletions spec/services/interactors/change_effort_event_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
let(:effort) { build_stubbed(:effort) }
let(:new_event) { build_stubbed(:event) }

it 'initializes when provided with an effort and a new event_id' do
it 'initializes when provided with an effort and a new_event' do
expect { subject }.not_to raise_error
end

Expand Down Expand Up @@ -103,20 +103,26 @@
split = new_event.ordered_splits.second
split.update(distance_from_start: split.distance_from_start - 1)
new_event.reload
expect { subject }.to raise_error(/distances do not coincide/)
response = subject.perform!
expect(response).not_to be_successful
expect(response.errors.first[:detail][:messages]).to include(/distances do not coincide/)
end

it 'raises an error if sub_splits do not coincide' do
split = new_event.ordered_splits.second
split.update(sub_split_bitmap: 1)
new_event.reload
expect { subject }.to raise_error(/sub splits do not coincide/)
response = subject.perform!
expect(response).not_to be_successful
expect(response.errors.first[:detail][:messages]).to include(/sub splits do not coincide/)
end

it 'raises an error if laps are out of range' do
split_time = effort.ordered_split_times.last
split_time.update(lap: 2)
expect { subject }.to raise_error(/laps exceed maximum required/)
response = subject.perform!
expect(response).not_to be_successful
expect(response.errors.first[:detail][:messages]).to include(/laps exceed maximum required/)
end
end

Expand Down
105 changes: 105 additions & 0 deletions spec/services/interactors/change_event_course_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
require 'rails_helper'

RSpec.describe Interactors::ChangeEventCourse do
subject { Interactors::ChangeEventCourse.new(event: event, new_course: new_course) }

describe '#initialization' do
let(:event) { build_stubbed(:event) }
let(:new_course) { build_stubbed(:course) }

it 'initializes when provided with an event and a new course_id' do
expect { subject }.not_to raise_error
end

context 'if no event is provided' do
let(:event) { nil }

it 'raises an error' do
expect { subject }.to raise_error(/must include event/)
end
end

context 'if no new_course is provided' do
let(:new_course) { nil }

it 'raises an error' do
expect { subject }.to raise_error(/must include new_course/)
end
end
end

describe '#perform!' do
let(:event) { create(:event, course: old_course) }
let!(:old_course) { create(:course, splits: old_splits) }
let(:old_split_1) { create(:start_split) }
let(:old_split_2) { create(:split, distance_from_start: 10000) }
let(:old_split_3) { create(:split, distance_from_start: 20000) }
let(:old_splits) { [old_split_1, old_split_2, old_split_3] }
let!(:efforts) { create_list(:effort, 2, event: event) }

context 'when the new course has splits with the same distances as the old' do
let(:new_course) { create(:course, splits: new_splits) }
let(:new_split_1) { create(:start_split) }
let(:new_split_2) { create(:split, distance_from_start: old_course.ordered_splits.second.distance_from_start) }
let(:new_split_3) { create(:split, distance_from_start: old_course.ordered_splits.third.distance_from_start) }
let(:new_split_4) { create(:split, distance_from_start: new_split_3.distance_from_start + 10000) }
let(:new_splits) { [new_split_1, new_split_2, new_split_3, new_split_4] }

before do
FactoryBot.reload
old_course.reload
new_course.reload
event.splits << old_course.splits
create_split_times_for_event
end

it 'updates the event course_id to the id of the provided course' do
expect(event.course_id).not_to eq(new_course.id)
response = subject.perform!
expect(event.course_id).to eq(new_course.id)
expect(response).to be_successful
expect(response.message).to match(/was changed to/)
end

it 'changes the split_ids of event split_times to the corresponding split_ids of the new course' do
sub_splits = new_course.sub_splits.first(efforts.first.split_times.size)
efforts.each do |effort|
effort.reload
expect(effort.split_times.map(&:sub_split)).not_to match_array(sub_splits)
end
subject.perform!
efforts.each do |effort|
effort.reload
expect(effort.split_times.map(&:sub_split)).to match_array(sub_splits)
end
end

it 'returns an unsuccessful response with errors if distances do not coincide' do
split = new_course.ordered_splits.second
split.update(distance_from_start: split.distance_from_start - 1)
new_course.reload
response = subject.perform!
expect(response).not_to be_successful
expect(response.errors.first[:detail][:messages]).to include(/distances do not coincide/)
end

it 'raises an error if sub_splits do not coincide' do
split = new_course.ordered_splits.second
split.update(sub_split_bitmap: 1)
new_course.reload
response = subject.perform!
expect(response).not_to be_successful
expect(response.errors.first[:detail][:messages]).to include(/sub splits do not coincide/)
end
end

def create_split_times_for_event
time_points = event.required_time_points
efforts.each do |effort|
time_points.each_with_index do |time_point, i|
create(:split_time, time_point: time_point, effort: effort, time_from_start: i * 1000)
end
end
end
end
end

0 comments on commit 281257f

Please sign in to comment.