class ActsAsSpan::NoOverlapValidator

Validator that checks whether a record is overlapping with others

Takes options `:instance_scope` (optional) and `:scope` (required):

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.

Public Instance Methods

temporally_overlapping_for(record) click to toggle source

TODO: add back condition for start_date nil TODO: add support for multiple spans (currently only checks :default)

# File lib/acts_as_span/no_overlap_validator.rb, line 60
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[end_field].gteq(start_date)
      .or(arel_table[end_field].eq(nil))
      )
    )
  else
    scope.where(
      arel_table[end_field].gteq(start_date)
      .or(arel_table[end_field].eq(nil))
    )
  end
end
validate(record) click to toggle source
# File lib/acts_as_span/no_overlap_validator.rb, line 35
def validate(record)
  overlapping_records = temporally_overlapping_for(record)
  instance_scope = if options[:instance_scope].is_a? Proc
                     record.instance_eval(&options[:instance_scope])
                   else
                     true
                   end

  return unless overlapping_records.any? && instance_scope

  error_message = options[:message] || :no_overlap
  record.errors.add(
    :base,
    error_message,
    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