Skip to content

Instantly share code, notes, and snippets.

@kevinansfield
Last active August 29, 2015 14:22
Show Gist options
  • Save kevinansfield/5ac5c612921418ea874a to your computer and use it in GitHub Desktop.
Save kevinansfield/5ac5c612921418ea874a to your computer and use it in GitHub Desktop.
class Competition < ActiveRecord::Base
belongs_to :discipline_class
has_one :discipline, through: :discipline_class
belongs_to :judge
has_many :moves, -> { order('position ASC') }, dependent: :destroy
has_many :competition_entries, dependent: :destroy
has_many :awards, through: :competition_entries
has_many :competitors, through: :competition_entries
has_one :example_video, class_name: 'Video', as: :videoable,
dependent: :destroy
end
class Competition < ActiveRecord::Base
require_dependency 'competition/operations/give_awards'
class Create < Trailblazer::Operation
include CRUD
model Competition, :create
contract do
extend CarrierWave::Mount
include Reform::Form::ModelReflections
include Reform::Form::MultiParameterAttributes
include Disposable::Twin::Collection::Semantics
property :discipline_class_id
property :judge_id
property :name
property :description
property :start_at
property :end_at
property :pattern
mount_uploader :pattern, PatternUploader
validates :discipline_class_id, presence: true
validates :judge_id, presence: true
validates :name, presence: true,
uniqueness: { scope: :discipline_class_id, case_sensitive: false }
validates :start_at, presence: true
validates :end_at, presence: true
validate :end_at_later_than_start_at
validate :start_at_does_not_overlap_existing_competition
validate :end_at_does_not_overlap_existing_competition
# TODO: Need to validate that we have at least one maneuver
property :example_video, populate_if_empty: Video do
property :video_file
end
collection :moves, populate_if_empty: Move, skip_if: :all_blank do
delegate :new_record?, to: :model
property :name
property :position
validates :name, presence: true
end
private
def end_at_later_than_start_at
if end_at && start_at && end_at <= start_at
errors.add(:end_at, 'must be later than start date')
end
end
def start_at_does_not_overlap_existing_competition
if discipline_class_id && !start_at.blank?
if other_competitions_in_class.running_at(start_at).exists?
errors.add(:start_at, 'overlaps existing competition')
end
end
end
def end_at_does_not_overlap_existing_competition
if discipline_class_id && !start_at.blank? && !end_at.blank?
if other_competitions_in_class.running_between(start_at, end_at).exists?
errors.add(:end_at, 'overlaps existing competition')
end
end
end
def other_competitions_in_class
Competition.
where(discipline_class_id: discipline_class_id).
where.not(id: model.id)
end
end
def process(params)
validate(params[:competition]) do |f|
f.save
end
end
end
class Update < Create
action :update
contract do
collection :moves, inherit: true, skip_if: :skip_move? do
property :id, writeable: false
property :_destroy, virtual: true
end
private
def skip_move?(fragment, options)
return true if fragment['_destroy'] == '1' && moves.delete(moves.find { |m| m.id.to_s == fragment['id'] })
return true if fragment['name'].blank?
end
end
def process(params)
validate(params[:competition]) do |f|
f.save
end
end
end
class Destroy < Trailblazer::Operation
include CRUD
model Competition, :update
def process(params)
model.destroy
end
end
end
require 'test_helper'
class CompetitionCrudTest < ActiveSupport::TestCase
# TODO: Replace fixtures with operations + factories
let(:discipline_class) { discipline_classes(:reining) }
let(:judge) { users(:judge) }
let(:create_params) {{
competition: {
discipline_class_id: discipline_class.id,
judge_id: judge.id,
name: 'New Competition',
description: 'I am a description.',
start_at: Time.zone.parse('2015-06-12 00:00:00'),
end_at: Time.zone.parse('2015-06-19 23:59:59'),
example_video: {
'video_file' => ''
},
moves: [
{ 'position' => 3, 'name' => 'Last' },
{ 'position' => 1, 'name' => 'First' },
{ 'position' => 2, 'name' => 'Middle' }
]
}
}}
describe 'Create' do
it 'persists valid' do
res, op = Competition::Create.run(create_params)
res.must_equal true
model = op.model
model.must_be :persisted?
model.discipline_class.must_equal discipline_class
model.judge.must_equal judge
model.name.must_equal 'New Competition'
model.description.must_equal 'I am a description.'
model.start_at.must_equal Time.zone.parse('2015-06-12 00:00:00')
model.end_at.must_equal Time.zone.parse('2015-06-19 23:59:59')
model.example_video.wont_be_nil
model.moves.count.must_equal 3
model.moves.pluck(:name).must_equal ['First', 'Middle', 'Last']
end
it 'validates attribute presence' do
missing_attributes = create_params
missing_attributes[:competition][:discipline_class_id] = ''
missing_attributes[:competition][:judge_id] = ''
missing_attributes[:competition][:name] = ''
missing_attributes[:competition][:start_at] = ''
missing_attributes[:competition][:end_at] = ''
missing_attributes[:competition][:moves].first[:name] = ''
res, op = Competition::Create.run(missing_attributes)
op.errors[:discipline_class_id].must_include "can't be blank"
op.errors[:judge_id].must_include "can't be blank"
op.errors[:name].must_include "can't be blank"
op.errors[:start_at].must_include "can't be blank"
op.errors[:end_at].must_include "can't be blank"
op.errors[:'moves.name'].must_include "can't be blank"
end
it 'validates it ends after it starts' do
invalid_date = create_params
invalid_date[:competition][:end_at] = Time.zone.parse('2015-06-11 23:59:59')
res, op = Competition::Create.run(invalid_date)
op.errors[:end_at].must_equal ['must be later than start date']
end
describe 'overlap validations' do
let(:params) {{
discipline_class_id: discipline_class.id,
judge_id: judge.id
}}
it 'checks no others running on start date' do
existing_params = params.merge(
start_at: Time.zone.parse('2015-06-01 00:00'),
end_at: Time.zone.parse('2015-06-14 23:59')
)
Competition::Create[competition: FactoryGirl.build(:competition, existing_params)]
new_params = params.merge(
start_at: Time.zone.parse('2015-06-02 00:00'),
end_at: Time.zone.parse('2015-06-15 23:59')
)
res, op = Competition::Create.run(competition: FactoryGirl.build(:competition, new_params))
op.errors[:start_at].must_equal ['overlaps existing competition']
end
it 'checks no others running on end date' do
existing_params = params.merge(
start_at: Time.zone.parse('2015-06-14 00:00'),
end_at: Time.zone.parse('2015-06-21 23:59')
)
Competition::Create[competition: FactoryGirl.build(:competition, existing_params)]
new_params = params.merge(
start_at: Time.zone.parse('2015-06-01 00:00'),
end_at: Time.zone.parse('2015-06-15 23:59')
)
res, op = Competition::Create.run(competition: FactoryGirl.build(:competition, new_params))
op.errors[:end_at].must_equal ['overlaps existing competition']
end
it 'checks no others running between start and end date' do
existing_params = params.merge(
start_at: Time.zone.parse('2015-06-02 00:00'),
end_at: Time.zone.parse('2015-06-13 23:59')
)
Competition::Create[competition: FactoryGirl.build(:competition, existing_params)]
new_params = params.merge(
start_at: Time.zone.parse('2015-06-01 00:00'),
end_at: Time.zone.parse('2015-06-14 23:59')
)
res, op = Competition::Create.run(competition: FactoryGirl.build(:competition, new_params))
op.errors[:end_at].must_equal ['overlaps existing competition']
end
it 'allows overlapping competitions across discipline classes' do
existing_params = params.merge(
# TODO: replace fixture with operation/factory
discipline_class_id: discipline_classes(:finished).id,
start_at: Time.zone.parse('2014-06-02 00:00'),
end_at: Time.zone.parse('2014-06-13 23:59')
)
Competition::Create[competition: FactoryGirl.build(:competition, existing_params)]
new_params = params.merge(
start_at: Time.zone.parse('2014-06-01 00:00'),
end_at: Time.zone.parse('2014-06-14 23:59')
)
res, op = Competition::Create.run(competition: FactoryGirl.build(:competition, new_params))
res.must_equal true
end
end
end
describe 'Update' do
let(:competition) { Competition::Create.(create_params).model }
let(:update_params) {{
id: competition.id,
competition: {
name: 'Updated Competition',
description: 'I am a new description.',
start_at: Time.zone.parse('2015-07-01 00:00:00'),
end_at: Time.zone.parse('2015-07-30 23:59:59'),
example_video: {
'video_file' => ''
},
'moves' => [
{ 'id' => competition.moves.all[0].id.to_s, 'position' => 1, 'name' => 'First Changed' },
{ 'id' => competition.moves.all[1].id.to_s, 'position' => 2, 'name' => 'Middle Changed' },
{ 'id' => competition.moves.all[2].id.to_s, 'position' => 3, 'name' => 'Last Changed' }
]
}
}}
it 'persists valid' do
res, op = Competition::Update.run(update_params)
res.must_equal true
model = op.model
model.reload # needed to ensure we get updated move records
model.must_be :persisted?
model.discipline_class.must_equal discipline_class
model.judge.must_equal judge
model.name.must_equal 'Updated Competition'
model.description.must_equal 'I am a new description.'
model.start_at.must_equal Time.zone.parse('2015-07-01 00:00:00')
model.end_at.must_equal Time.zone.parse('2015-07-30 23:59:59')
model.example_video.wont_be_nil
model.moves.count.must_equal 3
model.moves.pluck(:name).must_equal ['First Changed', 'Middle Changed', 'Last Changed']
end
it 'allows move deletion' do
deleted_move = update_params
deleted_move[:competition]['moves'][0]['_destroy'] = '1'
original_moves = competition.moves.all.to_a
original_move_ids = competition.moves.pluck(:id)
res, op = Competition::Update.run(deleted_move)
res.must_equal true
model = op.model
model.reload # needed to ensure we get updated move records
pp original_moves
# [#<Move:0x007f9aca9992a8
# id: 908005741,
# name: "First",
# position: 1,
# competition_id: 802290397,
# created_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00,
# updated_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00>,
# #<Move:0x007f9aca998df8
# id: 908005742,
# name: "Middle",
# position: 2,
# competition_id: 802290397,
# created_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00,
# updated_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00>,
# #<Move:0x007f9aca998b00
# id: 908005740,
# name: "Last",
# position: 3,
# competition_id: 802290397,
# created_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00,
# updated_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00>
# ]
pp model.moves.all.to_a
# [#<Move:0x007f9ad3b6ca68
# id: 908005742,
# name: "Middle",
# position: 2,
# competition_id: 802290397,
# created_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00,
# updated_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00>,
# #<Move:0x007f9ad3b6c900
# id: 908005740,
# name: "Middle Changed",
# position: 2,
# competition_id: 802290397,
# created_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00,
# updated_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00>,
# #<Move:0x007f9ad3b6c798
# id: 908005743,
# name: "Last Changed",
# position: 3,
# competition_id: 802290397,
# created_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00,
# updated_at: Sat, 13 Jun 2015 12:17:03 UTC +00:00>
# ]
model.moves.pluck(:id).wont_equal original_move_ids
model.moves.count.must_equal 2
model.moves.pluck(:name).must_equal ['Last Changed', 'Middle Changed']
end
end
describe 'Destroy' do
it 'destroys correct record' do
competition = Competition::Create.(create_params).model
assert_difference 'Competition.count', -1 do
Competition::Destroy.(id: competition.id)
end
Competition.exists?(id: competition.id).must_equal false
end
end
end
class Move < ActiveRecord::Base
belongs_to :competition
has_many :scorecard_moves
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment