Skip to content

Commit

Permalink
Fix NoOverlapValidator
Browse files Browse the repository at this point in the history
The fix is on line 38 of lib/no_overlap_validator.rb
The missing space between `record.isntance_eval` and `&options...` was
causing an error when instance_eval was used.

Also adds specs that act as a health check for the instance_scope
feature.
  • Loading branch information
izzergh committed Apr 1, 2020
1 parent 279edb8 commit 42cdc56
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 21 deletions.
72 changes: 52 additions & 20 deletions lib/acts_as_span/no_overlap_validator.rb
Original file line number Diff line number Diff line change
@@ -1,51 +1,83 @@
# frozen_string_literal: true

require 'active_model'

module ActsAsSpan
# Validator that checks whether a record is overlapping with others
#
# Takes options `:instance_scope` (optional) and `:scope` (required):
# * `instance_scope` is a proc which, when evaluated by the record, returns
# a boolean value. When false, the validatior will not check for overlap.
# When true, the validator checks normally.
# * `scope` is also a proc. This is must return an ActiveRecord Relation that
# determines which records' spans to compare.
#
# Usage:
# Given a record with `siblings` defined, the most basic use case is:
# ```
# validates_with ActsAsSpan::NoOverlapValidator,
# scope: proc { siblings }
# ```
# When this record is validated, every record in the ActiveRecord relation
# `record.siblings` is checked for mutual overlap with `record`.
#
# Use `instance_scope` if there is some condition where a record oughtn't be
# validated for whatever reason:
# ```
# validates_with ActsAsSpan::NoOverlapValidator,
# scope: proc { siblings }, instance_scope: proc { favorite? }
# ```
# Now, when this record is validated, if `record.favorite?` is `true`,
# `record` must pass the overlap check with its siblings.
# If `record.favorite?` is `false`, it is under less scrutiny.
#
class NoOverlapValidator < ActiveModel::Validator
def validate(record)
overlapping_records = temporally_overlapping_for(record)
instance_scope = if options[:instance_scope].is_a? Proc
record.instance_eval&options[:instance_scope]
record.instance_eval(&options[:instance_scope])
else
true
end

if overlapping_records.any? && instance_scope

record.errors.add(
:base,
:no_overlap,
model_name: record.class.model_name.human,
model_name_plural: record.class.model_name.plural.humanize,
start_date: record.span.start_date,
end_date: record.span.end_date,
count: overlapping_records.size,
overlapping_records_s: overlapping_records.join(",")
)
end
return unless overlapping_records.any? && instance_scope

record.errors.add(
:base,
:no_overlap,
model_name: record.class.model_name.human,
model_name_plural: record.class.model_name.plural.humanize,
start_date: record.span.start_date,
end_date: record.span.end_date,
count: overlapping_records.size,
overlapping_records_s: overlapping_records.join(', ')
)
end

#TODO add back condition for start_date nil
#TODO add support for multiple spans (currently only checks :default)
# TODO: add back condition for start_date nil
# TODO: add support for multiple spans (currently only checks :default)
def temporally_overlapping_for(record)
scope = record.instance_eval(&options[:scope])

start_date = record.span.start_date || Date.current

end_date = record.span.end_date
end_field = record.span.end_field

arel_table = record.class.arel_table

if end_date
scope.where(
arel_table[record.span.start_field].lteq(end_date)
.and(
arel_table[record.span.end_field].gteq(start_date)
.or(arel_table[record.span.end_field].eq(nil))
arel_table[end_field].gteq(start_date)
.or(arel_table[end_field].eq(nil))
)
)
else
scope.where(
arel_table[record.span.end_field].gteq(start_date)
.or(arel_table[record.span.end_field].eq(nil))
arel_table[end_field].gteq(start_date)
.or(arel_table[end_field].eq(nil))
)
end
end
Expand Down
23 changes: 23 additions & 0 deletions spec/lib/no_overlap_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@

let(:all_siblings) { [brother, sister, brother_from_another_mother] }

describe 'instance_scope' do
let(:child_class) { OneParentChild }

let(:new_child_start_date) { Date.current - 2 }
let(:new_child_end_date) { Date.current + 3 }

before { new_child.favorite = favorite }

context 'when instance_scope evaluates to false' do
let(:favorite) { false }
it 'skips validation on the record for which instance_scope is false' do
expect(new_child).to be_valid
end
end

context 'when instance_scope evaluates to true' do
let(:favorite) { true }
it 'validates normally' do
expect(new_child).not_to be_valid
end
end
end

describe 'an object with a single parent' do
let(:child_class) { OneParentChild }

Expand Down
10 changes: 9 additions & 1 deletion spec/spec_models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,22 @@

t.date :start_date
t.date :end_date

# every one-parent child is a favorite! ...by default
t.boolean :favorite, default: true
end

acts_as_span

def favorite?
favorite
end

belongs_to :mama
has_siblings through: [:mama]

validates_with ActsAsSpan::NoOverlapValidator, scope: proc { siblings }
validates_with ActsAsSpan::NoOverlapValidator,
scope: proc { siblings }, instance_scope: proc { favorite? }
validates_with ActsAsSpan::WithinParentDateSpanValidator, parents: [:mama]
end

Expand Down

0 comments on commit 42cdc56

Please sign in to comment.