validates_overlap 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +55 -8
- data/lib/validates_overlap/overlap_validator.rb +81 -43
- data/lib/validates_overlap/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22d9351114cf974a1843daead1ff7fb57503951d2cfecd9bfe19c2041bd4dd01
|
|
4
|
+
data.tar.gz: 95e4e59f48f05f22ad589a06e77224fd1d065414ddb22a0d68f17e23005c0168
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8483ac32c24e082f364de7e090dd01b6d14183657bbf6e002770b1d256904eed24e56fcf19707bfa586d79ca9be24b1b7e5e5f9d09de9f2f18d404cc0dec840b
|
|
7
|
+
data.tar.gz: 4b2d9a55f399584ba014ca1633bfa4e278ad42a89db9ff5db4828da9c71d1bba731ceb9394ee5cb9903f2ffa51c76f542efea40037a4e99e7ed6bcea2498f73d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# ValidatesOverlap 1.x Change Log
|
|
2
2
|
|
|
3
|
+
## 1.2.0 (2026-08-11)
|
|
4
|
+
|
|
5
|
+
RSpec tests: **85 → 126** (+41 tests)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- 🎉 the validator is now stateless and thread-safe 🎉 — fixes [Issue #50](https://github.com/tilo/validates_overlap/issues/50): concurrent validations of the same model class could corrupt each other's query, because Rails shares one validator instance per class and the query lived on it as instance state (intermittent `ActiveRecord::PreparedStatementInvalid`, or silently wrong validation results). Thanks to [Jorge Santos](https://github.com/jsantos) for the report
|
|
10
|
+
- string range columns raised `TypeError` because a default shift of `0` was added even when no shift was configured — shifts are now only applied when set
|
|
11
|
+
- open-ended (nil) endpoints produced wrong results in several cases: an endless range failed to conflict with records after January 2038 (the nil endpoint was substituted with a Unix-time sentinel), and open-ended integer or string ranges could silently never conflict at all. A nil endpoint now simply drops its comparison from the query — type-independent and exact
|
|
12
|
+
- the record's primary key is now passed to the database as a bind value when a persisted record is excluded from the comparison — it was interpolated into the SQL, which broke string keys containing a quote
|
|
13
|
+
|
|
14
|
+
### Improvements
|
|
15
|
+
|
|
16
|
+
- documented in the README that `start_shift` / `end_shift` work in both directions: widening the range enforces a minimum gap, shrinking it tolerates a specified amount of overlap — now locked in by specs
|
|
17
|
+
- the overlap check works on any linearly orderable column type — now covered by specs for date, datetime, timestamp, integer, decimal, and string range columns (including open-ended ranges and integer gap/tolerance shifts) and documented in the README ("non-date ranges")
|
|
18
|
+
- `:time` range columns now raise `OverlapValidator::UnsupportedColumnType` — time-of-day is a cyclic domain, where a wraparound window is indistinguishable from accidentally swapped fields; the validator refuses loudly instead of answering wrong (see the README note for cyclic domains)
|
|
19
|
+
- test coverage: real UUID/string primary key test restored (lost in a 2019 refactor), new tests for `:scoped_model`, literal scope values, and the two-attributes requirement; the long-disabled endless-objects test was fixed and re-enabled — the suite has no pending tests
|
|
20
|
+
|
|
21
|
+
### Internal
|
|
22
|
+
|
|
23
|
+
- removed the accessors `sql_conditions`, `sql_values`, and `scoped_model` from `OverlapValidator` — they were the shared state causing thread-safety issues; the query-building methods now take and return their inputs
|
|
24
|
+
- removed the constants `OverlapValidator::BEGIN_OF_UNIX_TIME` and `END_OF_UNIX_TIME` — the sentinel substitution is gone; an open-ended boundary simply contributes no comparison to the query
|
|
25
|
+
|
|
3
26
|
## 1.1.0 (2026-08-07)
|
|
4
27
|
|
|
5
28
|
RSpec tests: **81 → 85** (+4 tests)
|
data/README.md
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
# ValidatesOverlap
|
|
2
2
|
|
|
3
|
-
 [ [](https://github.com/tilo/validates_overlap/actions/workflows/ruby.yml) [](https://app.codecov.io/gh/tilo/validates_overlap/tree/main) [](https://rubygems.org/gems/validates_overlap) [](https://rubygems.org/gems/validates_overlap) [](https://www.ruby-toolbox.com/projects/validates_overlap)
|
|
4
4
|
|
|
5
|
-
`validates_overlap`
|
|
6
|
-
Ideal solution for booking applications where you want to make sure, that one place can be booked only once in specific time period.
|
|
5
|
+
`validates_overlap` provides an ActiveRecord validator for resources that must not overlap, e.g. in datetime. Think rentals, meetings, bookings, work shifts, or assignments where the same resource cannot be assigned to multiple people or entities during overlapping time periods. But it also works for other domains than datetime (see below).
|
|
7
6
|
|
|
8
|
-
You
|
|
7
|
+
You specify two attributes defining a datetime range, such as `starts_at` and `ends_at`, and the validator checks with a single SQL query whether another record overlaps that range — no records are loaded for the comparison. If one does, the record receives a normal validation error.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
It also supports scoped validation (per user, room, resource, etc.), open-ended ranges (a nil start or end counts as extending forever), ranges that may touch at their boundaries (`exclude_edges`), required gaps between ranges or a tolerated amount of overlap (`start_shift` / `end_shift`), associations, and retrieving the conflicting records.
|
|
11
10
|
|
|
12
|
-
The
|
|
11
|
+
The range columns don't have to be dates or times: any linearly orderable column type works, such as integer ranges (ticket number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions).
|
|
13
12
|
|
|
14
|
-
##
|
|
13
|
+
## Note: Other Domains
|
|
14
|
+
|
|
15
|
+
Other domains / types can be checked for overlap, as long as they can be compared linearly.
|
|
16
|
+
e.g. The overlap check runs on plain SQL comparisons, so any linearly orderable column type works — for example integer ranges (no two records may claim overlapping number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions). A nil endpoint means the range is open-ended on that side, for these types too, and the shifts work for numeric ranges as well (e.g. an integer gap or overlap tolerance). The test suite covers `date`, `datetime`, `timestamp`, `integer`, `decimal`, and `string` range columns.
|
|
17
|
+
|
|
18
|
+
## ⚠️ Note: Cyclic Domains can NOT be validated for overlap
|
|
19
|
+
|
|
20
|
+
Overlap validation requires a linear domain: every range must satisfy `start <= end`. On a cyclic (wrap-around) domain, like time, every pair of values denotes *some* valid range (`11:00..10:00` is simply the 23-hour complement of `10:00..11:00`), so a wraparound range is indistinguishable from accidentally swapped fields — no validation can tell intent from typo. This is a mathematical property of circular domains, not an implementation gap.
|
|
21
|
+
|
|
22
|
+
The validator therefore refuses `:time` range columns and raises `OverlapValidator::UnsupportedColumnType` — use datetime columns instead, or split windows that cross midnight into two records.
|
|
23
|
+
|
|
24
|
+
But cyclicity is a property of the domain, not the column type — ⚠️ user-encoded cyclic domains hide inside perfectly linear columns, where no guard can see them:
|
|
25
|
+
|
|
26
|
+
- day-of-week as integer (0..6): a Friday-to-Monday shift range `5..1` wraps — same pathology as `22:00..02:00`, stored in an innocent `:integer` column
|
|
27
|
+
- month numbers (1..12): a November-to-February season range `11..2`
|
|
28
|
+
- ISO week numbers: a range from week 52 to week 2 across New Year
|
|
29
|
+
- angles / compass headings (0..360): a heading sector `350..10`
|
|
30
|
+
- longitude (−180°..+180°)
|
|
31
|
+
- hour-of-day as integer — people re-implement `:time` in an int column all the time
|
|
32
|
+
- time-of-day (24-hour clock values without a date component)
|
|
33
|
+
|
|
34
|
+
If your domain is cyclic, the validator will silently give wrong answers for wrapping ranges. Restructure the data instead: split wrapping ranges into two linear records, or lift the values into a linear domain (e.g. datetime instead of time-of-day).
|
|
35
|
+
|
|
36
|
+
To catch inverted ranges loudly instead of silently (for any column type), pair the overlap validation with an order check on your model, e.g. `validates :ends_at, comparison: { greater_than: :starts_at }`.
|
|
37
|
+
|
|
38
|
+
## Ruby / Rails Compatibility
|
|
15
39
|
|
|
16
40
|
Every combination below is verified on every push by the [CI matrix](https://github.com/tilo/validates_overlap/actions):
|
|
17
41
|
|
|
@@ -26,6 +50,8 @@ Every combination below is verified on every push by the [CI matrix](https://git
|
|
|
26
50
|
|
|
27
51
|
The gemspec requires `activerecord >= 6.0`. Rails 6.0 is not part of the test matrix, but no incompatibilities are known. The previous version 0.8.6 was compatible with Rails 3, 4, and 5.
|
|
28
52
|
|
|
53
|
+
Note for MySQL users: use `DATETIME` (not `TIMESTAMP`) columns for your range attributes — MySQL's `TIMESTAMP` type cannot store dates after January 2038, which matters for long-running or far-future ranges. PostgreSQL and SQLite date/time types have no such limit.
|
|
54
|
+
|
|
29
55
|
## Usage
|
|
30
56
|
|
|
31
57
|
Add to your gemfile
|
|
@@ -57,8 +83,29 @@ validates :starts_at, :ends_at, :overlap => {:exclude_edges => ["starts_at", "en
|
|
|
57
83
|
|
|
58
84
|
#### shift edges
|
|
59
85
|
|
|
86
|
+
The shifts move the record's own range edges before the overlap check, so you can require a gap between records — or tolerate a bounded overlap:
|
|
87
|
+
|
|
60
88
|
```ruby
|
|
89
|
+
# widen the range: records must be at least 1 day apart (gap enforced)
|
|
61
90
|
validates :starts_at, :ends_at, :overlap => {:start_shift => -1.day, :end_shift => 1.day}
|
|
91
|
+
|
|
92
|
+
# shrink the range: up to 2 days of overlap are accepted
|
|
93
|
+
validates :starts_at, :ends_at, :overlap => {:start_shift => 2.days, :end_shift => -2.days}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### non-date ranges
|
|
97
|
+
|
|
98
|
+
The overlap check runs on plain SQL comparisons, so any orderable column type works — for example integer ranges (no two records may claim overlapping number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions). A nil endpoint means the range is open-ended on that side, for these types too, and the shifts work for numeric ranges as well (e.g. an integer gap or overlap tolerance). The test suite covers `date`, `datetime`, `timestamp`, `integer`, `decimal`, and `string` range columns.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
class TicketBlock < ActiveRecord::Base
|
|
102
|
+
validates :number_start, :number_end, :overlap => true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
TicketBlock.create!(number_start: 100, number_end: 199)
|
|
106
|
+
TicketBlock.new(number_start: 150, number_end: 250).valid? # => false (overlaps)
|
|
107
|
+
TicketBlock.new(number_start: 150, number_end: nil).valid? # => false (open-ended, overlaps)
|
|
108
|
+
TicketBlock.new(number_start: 200, number_end: 299).valid? # => true
|
|
62
109
|
```
|
|
63
110
|
|
|
64
111
|
#### define custom validation key(s) and message
|
|
@@ -70,7 +117,7 @@ validates :starts_at, :ends_at, :overlap => {:message_title => [:start_at, :end_
|
|
|
70
117
|
|
|
71
118
|
#### with complicated relations
|
|
72
119
|
|
|
73
|
-
Example describes
|
|
120
|
+
Example describes validation of user, positions and time slots.
|
|
74
121
|
User can't be assigned 2 times on position which is under time slot with time overlap.
|
|
75
122
|
|
|
76
123
|
```ruby
|
|
@@ -3,12 +3,9 @@ require 'active_support/i18n'
|
|
|
3
3
|
I18n.load_path << File.dirname(__FILE__) + '/locale/en.yml'
|
|
4
4
|
|
|
5
5
|
class OverlapValidator < ActiveModel::EachValidator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
attr_accessor :sql_conditions
|
|
10
|
-
attr_accessor :sql_values
|
|
11
|
-
attr_accessor :scoped_model
|
|
6
|
+
# Raised when a range attribute uses a column type the validator cannot
|
|
7
|
+
# operate on (e.g. :time — a cyclic domain, see README)
|
|
8
|
+
class UnsupportedColumnType < ArgumentError; end
|
|
12
9
|
|
|
13
10
|
def initialize(args)
|
|
14
11
|
attributes_are_range(args[:attributes])
|
|
@@ -16,11 +13,15 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
|
16
13
|
super
|
|
17
14
|
end
|
|
18
15
|
|
|
16
|
+
# NOTE: Rails registers ONE validator instance per model class, shared by every
|
|
17
|
+
# validation of that class (including concurrent ones) — so the query being
|
|
18
|
+
# built must never be stored on the validator itself (issue #50)
|
|
19
19
|
def validate(record)
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
reject_unsupported_column_types(record)
|
|
21
|
+
relation, sql_conditions, sql_values = initialize_query(record, options)
|
|
22
|
+
if overlapped_exists?(relation, sql_conditions, sql_values)
|
|
22
23
|
if options[:load_overlapped]
|
|
23
|
-
record.instance_variable_set(:@overlapped_records, get_overlapped)
|
|
24
|
+
record.instance_variable_set(:@overlapped_records, get_overlapped(relation, sql_conditions, sql_values))
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
if record.respond_to? attributes.first
|
|
@@ -39,22 +40,41 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
|
39
40
|
|
|
40
41
|
protected
|
|
41
42
|
|
|
43
|
+
# Time-of-day is a cyclic domain: every pair of values denotes some valid
|
|
44
|
+
# range there, so wraparound intent is indistinguishable from accidentally
|
|
45
|
+
# swapped fields — refuse loudly instead of answering wrong (see README).
|
|
46
|
+
# Checked at validate time, not at class-definition time, because column
|
|
47
|
+
# metadata must not be touched while migrations may still be pending.
|
|
48
|
+
def reject_unsupported_column_types(record)
|
|
49
|
+
return unless record.class.respond_to?(:columns_hash)
|
|
50
|
+
attributes.each do |attr|
|
|
51
|
+
next if attr.to_s.include?('.')
|
|
52
|
+
column = record.class.columns_hash[attr.to_s]
|
|
53
|
+
next unless column && column.type == :time
|
|
54
|
+
raise UnsupportedColumnType, "#{record.class.name}##{attr} is a :time column; time-of-day is a cyclic domain and cannot be validated for overlap — use datetime columns, or split ranges that cross midnight (see README)"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build the complete overlap query for this record.
|
|
59
|
+
# return array in form [relation, sql_conditions, sql_values]
|
|
42
60
|
def initialize_query(record, options = {})
|
|
43
61
|
scoped_model = options[:scoped_model].present? ? options[:scoped_model].constantize : record.class
|
|
44
|
-
|
|
45
|
-
generate_overlap_sql_values(record)
|
|
46
|
-
generate_overlap_sql_conditions(record)
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
relation = scoped_model.default_scoped
|
|
63
|
+
sql_values = generate_overlap_sql_values(record)
|
|
64
|
+
sql_conditions, primary_key_values = generate_overlap_sql_conditions(record, sql_values)
|
|
65
|
+
sql_values = sql_values.merge(primary_key_values)
|
|
66
|
+
sql_conditions, sql_values = add_attributes(record, options[:scope], sql_conditions, sql_values) if options && options[:scope].present?
|
|
67
|
+
relation = add_query_options(relation, options[:query_options]) if options && options[:query_options].present?
|
|
68
|
+
[relation, sql_conditions, sql_values]
|
|
49
69
|
end
|
|
50
70
|
|
|
51
71
|
# Check if exists at least one record in DB which is overlapped with current record
|
|
52
|
-
def overlapped_exists?
|
|
53
|
-
|
|
72
|
+
def overlapped_exists?(relation, sql_conditions, sql_values)
|
|
73
|
+
relation.exists?([sql_conditions, sql_values])
|
|
54
74
|
end
|
|
55
75
|
|
|
56
|
-
def get_overlapped
|
|
57
|
-
|
|
76
|
+
def get_overlapped(relation, sql_conditions, sql_values)
|
|
77
|
+
relation.where([sql_conditions, sql_values])
|
|
58
78
|
end
|
|
59
79
|
|
|
60
80
|
# Resolve attributes values from record to use in sql conditions
|
|
@@ -108,55 +128,70 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
|
108
128
|
record.send(primary_key_name)
|
|
109
129
|
end
|
|
110
130
|
|
|
111
|
-
# Generate sql condition for time range cross
|
|
112
|
-
|
|
131
|
+
# Generate sql condition for time range cross; a persisted record is excluded
|
|
132
|
+
# from the comparison by its primary key, passed as a bind value
|
|
133
|
+
# return array in form [sql_conditions, sql_values]
|
|
134
|
+
def generate_overlap_sql_conditions(record, sql_values)
|
|
113
135
|
starts_at_attr, ends_at_attr = attributes_to_sql(record)
|
|
114
|
-
main_condition = condition_string(starts_at_attr, ends_at_attr)
|
|
115
|
-
primary_key_name = primary_key(record)
|
|
116
|
-
key = primary_key_value(primary_key_name, record)
|
|
136
|
+
main_condition = condition_string(starts_at_attr, ends_at_attr, sql_values)
|
|
117
137
|
if record.new_record?
|
|
118
|
-
|
|
138
|
+
[main_condition, {}]
|
|
119
139
|
else
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
key = primary_key_value(primary_key(record), record)
|
|
141
|
+
["#{main_condition} AND #{record_table_name(record)}.#{primary_key(record)} != :record_primary_key_value", { record_primary_key_value: key }]
|
|
122
142
|
end
|
|
123
143
|
end
|
|
124
144
|
|
|
125
|
-
# Return hash of values for overlap sql condition
|
|
145
|
+
# Return hash of values for overlap sql condition; a nil endpoint means the
|
|
146
|
+
# record's range is open-ended on that side — no value is emitted for it and
|
|
147
|
+
# condition_string drops the corresponding comparison
|
|
148
|
+
# NOTE: shifts are only applied when configured — unconditionally adding a
|
|
149
|
+
# default of 0 would raise a TypeError for non-numeric types such as String
|
|
126
150
|
def generate_overlap_sql_values(record)
|
|
127
151
|
starts_at_value, ends_at_value = resolve_values_from_attributes(record)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
start_shift = options && options[:start_shift]
|
|
153
|
+
end_shift = options && options[:end_shift]
|
|
154
|
+
starts_at_value += start_shift if starts_at_value && start_shift
|
|
155
|
+
ends_at_value += end_shift if ends_at_value && end_shift
|
|
156
|
+
sql_values = {}
|
|
157
|
+
sql_values[:starts_at_value] = starts_at_value if starts_at_value
|
|
158
|
+
sql_values[:ends_at_value] = ends_at_value if ends_at_value
|
|
159
|
+
sql_values
|
|
131
160
|
end
|
|
132
161
|
|
|
133
162
|
# Return the condition string depend on exclude_edges option.
|
|
134
|
-
|
|
163
|
+
# A comparison is only emitted for endpoints the record actually has: an
|
|
164
|
+
# open-ended side matches every other record by definition, so its clause is
|
|
165
|
+
# dropped (a record with both endpoints nil overlaps everything)
|
|
166
|
+
def condition_string(starts_at_attr, ends_at_attr, sql_values)
|
|
135
167
|
except_option = Array(options[:exclude_edges]).map(&:to_s)
|
|
136
168
|
starts_at_sign = except_option.include?(starts_at_attr.to_s.split('.').last) ? '<' : '<='
|
|
137
169
|
ends_at_sign = except_option.include?(ends_at_attr.to_s.split('.').last) ? '>' : '>='
|
|
138
170
|
query = []
|
|
139
|
-
query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)"
|
|
140
|
-
query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)"
|
|
141
|
-
query.join(' AND ')
|
|
171
|
+
query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)" if sql_values.key?(:starts_at_value)
|
|
172
|
+
query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)" if sql_values.key?(:ends_at_value)
|
|
173
|
+
query.empty? ? '1 = 1' : query.join(' AND ')
|
|
142
174
|
end
|
|
143
175
|
|
|
144
176
|
# Add attributes and values to sql conditions.
|
|
145
177
|
# helps to use with scope options, so scope can be added as this forms :scope => "user_id" or :scope => ["user_id", "place_id"]
|
|
146
|
-
|
|
178
|
+
# return array in form [sql_conditions, sql_values]
|
|
179
|
+
def add_attributes(record, attrs, sql_conditions, sql_values)
|
|
147
180
|
if attrs.is_a?(Array)
|
|
148
|
-
attrs.each { |attr| add_attribute(record, attr) }
|
|
181
|
+
attrs.each { |attr| sql_conditions, sql_values = add_attribute(record, attr, sql_conditions, sql_values) }
|
|
149
182
|
elsif attrs.is_a?(Hash)
|
|
150
183
|
attrs.each do |attr_name, value|
|
|
151
|
-
add_attribute(record, attr_name, value)
|
|
184
|
+
sql_conditions, sql_values = add_attribute(record, attr_name, sql_conditions, sql_values, value)
|
|
152
185
|
end
|
|
153
186
|
else
|
|
154
|
-
add_attribute(record, attrs)
|
|
187
|
+
sql_conditions, sql_values = add_attribute(record, attrs, sql_conditions, sql_values)
|
|
155
188
|
end
|
|
189
|
+
[sql_conditions, sql_values]
|
|
156
190
|
end
|
|
157
191
|
|
|
158
192
|
# Add attribute and his value to sql condition
|
|
159
|
-
|
|
193
|
+
# return array in form [sql_conditions, sql_values]
|
|
194
|
+
def add_attribute(record, attr_name, sql_conditions, sql_values, value = nil)
|
|
160
195
|
_value = resolve_attribute_value(record, attr_name, value)
|
|
161
196
|
operator = if _value.nil?
|
|
162
197
|
' IS NULL'
|
|
@@ -166,8 +201,9 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
|
166
201
|
' = :%s'
|
|
167
202
|
end
|
|
168
203
|
|
|
169
|
-
|
|
170
|
-
sql_values.merge
|
|
204
|
+
sql_conditions += " AND #{attribute_to_sql(attr_name, record)} #{operator}" % value_attribute_name(attr_name)
|
|
205
|
+
sql_values = sql_values.merge(:"#{value_attribute_name(attr_name)}" => _value)
|
|
206
|
+
[sql_conditions, sql_values]
|
|
171
207
|
end
|
|
172
208
|
|
|
173
209
|
def value_attribute_name(attr_name)
|
|
@@ -200,9 +236,11 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
|
200
236
|
# Allow to use scope, joins, includes methods before querying
|
|
201
237
|
# == Example:
|
|
202
238
|
# validates_overlap :date_from, :date_to, :query_options => {:includes => "visits"}
|
|
203
|
-
|
|
239
|
+
# return the relation with the query options applied
|
|
240
|
+
def add_query_options(relation, methods)
|
|
204
241
|
methods.each do |method_name, params|
|
|
205
|
-
|
|
242
|
+
relation = relation.send(method_name.to_sym, *params)
|
|
206
243
|
end
|
|
244
|
+
relation
|
|
207
245
|
end
|
|
208
246
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: validates_overlap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robin Bortlik
|
|
@@ -164,8 +164,9 @@ dependencies:
|
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
166
|
version: '0'
|
|
167
|
-
description:
|
|
168
|
-
|
|
167
|
+
description: Adds ActiveRecord validations that prevent overlapping date/time ranges
|
|
168
|
+
— bookings, reservations, meetings, shifts. One SQL query; supports scoping and
|
|
169
|
+
open-ended ranges
|
|
169
170
|
email:
|
|
170
171
|
- robinbortlik@gmail.com
|
|
171
172
|
- tilo.sloboda@gmail.com
|
|
@@ -189,7 +190,7 @@ homepage: https://github.com/tilo/validates_overlap
|
|
|
189
190
|
licenses:
|
|
190
191
|
- MIT
|
|
191
192
|
metadata:
|
|
192
|
-
source_code_uri: https://github.com/tilo/validates_overlap/tree/v1.
|
|
193
|
+
source_code_uri: https://github.com/tilo/validates_overlap/tree/v1.2.0
|
|
193
194
|
bug_tracker_uri: https://github.com/tilo/validates_overlap/issues
|
|
194
195
|
changelog_uri: https://github.com/tilo/validates_overlap/blob/main/CHANGELOG.md
|
|
195
196
|
rubygems_mfa_required: 'true'
|