validates_overlap 0.6.0 → 0.7.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/.gitignore +1 -0
- data/.rspec +2 -0
- data/.rubocop.yml +5 -0
- data/Gemfile +1 -1
- data/Gemfile.rails50 +5 -0
- data/README.md +16 -2
- data/Rakefile +3 -3
- data/VERSION +1 -1
- data/lib/validates_overlap/locale/es.yml +4 -0
- data/lib/validates_overlap/overlap_validator.rb +42 -38
- data/spec/dummy/app/models/active_meeting.rb +2 -2
- data/spec/dummy/app/models/end_overlap_meeting.rb +2 -2
- data/spec/dummy/app/models/meeting.rb +1 -1
- data/spec/dummy/app/models/position.rb +4 -4
- data/spec/dummy/app/models/secure_meeting.rb +1 -1
- data/spec/dummy/app/models/shift.rb +1 -1
- data/spec/dummy/app/models/start_end_overlap_meeting.rb +2 -2
- data/spec/dummy/app/models/start_overlap_meeting.rb +1 -1
- data/spec/dummy/app/models/time_slot.rb +4 -4
- data/spec/dummy/app/models/user_meeting.rb +1 -1
- data/spec/dummy/config/application.rb +7 -7
- data/spec/dummy/config/boot.rb +1 -1
- data/spec/dummy/config/environments/development.rb +1 -2
- data/spec/dummy/config/environments/production.rb +1 -1
- data/spec/dummy/config/initializers/session_store.rb +1 -1
- data/spec/dummy/db/schema.rb +52 -54
- data/spec/dummy/spec/factories/position.rb +2 -2
- data/spec/dummy/spec/factories/user_meeting.rb +2 -2
- data/spec/dummy/spec/models/active_meetings_spec.rb +4 -10
- data/spec/dummy/spec/models/end_overlap_meeting_spec.rb +26 -29
- data/spec/dummy/spec/models/meeting_spec.rb +51 -47
- data/spec/dummy/spec/models/position_spec.rb +18 -28
- data/spec/dummy/spec/models/secure_meeting_spec.rb +5 -11
- data/spec/dummy/spec/models/shift_spec.rb +32 -33
- data/spec/dummy/spec/models/start_end_overlap_meeting_spec.rb +26 -29
- data/spec/dummy/spec/models/start_overlap_meeting_spec.rb +26 -29
- data/spec/dummy/spec/models/time_slot_spec.rb +21 -31
- data/spec/dummy/spec/models/user_meeting_spec.rb +16 -19
- data/spec/dummy/spec/models/user_spec.rb +3 -5
- data/spec/dummy/spec/overlap_validator_spec.rb +12 -14
- data/spec/spec_helper.rb +29 -20
- data/validates_overlap.gemspec +19 -16
- metadata +48 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 483af513bf060127e3608a51a2257f7f9f4e1f3b
|
4
|
+
data.tar.gz: da46a701164a966e9f26d21e31aafcc4bf2ddc24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b43a7550f859c43f109cdf62acc9ebcef51985de90c3d54f0d8fbba1ed94fbc19bac6aa5a5696ed3cdf39f18060a0621c751510e6bf2eb83d24da6dbbca88526
|
7
|
+
data.tar.gz: d9ac0f6e807ae6198b21c7e804addd35e8fd24dbcdea8d6d7390e708964a4c687da0aa8a00432babad2dc910173059d1182ea3006432525bb1725505c81a05d9
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
CHANGED
data/Gemfile.rails50
ADDED
data/README.md
CHANGED
@@ -4,10 +4,10 @@
|
|
4
4
|
|
5
5
|
This project rocks and uses MIT-LICENSE.
|
6
6
|
|
7
|
-
#### This gem is compatible with Rails 3
|
7
|
+
#### This gem is compatible with Rails 3, 4, 5.
|
8
8
|
|
9
9
|
#### When this gem should be helpful for you?
|
10
|
-
|
10
|
+
Ideal solution for booking applications where you want to make sure, that one place can be booked only once in specific time period.
|
11
11
|
|
12
12
|
#### Using
|
13
13
|
|
@@ -76,6 +76,20 @@ class ActiveMeeting < ActiveRecord::Base
|
|
76
76
|
end
|
77
77
|
```
|
78
78
|
|
79
|
+
#### Overlapped records
|
80
|
+
If you need to know what records are in conflict, pass the `{load_overlapped: true }` as validator option and validator will set instance variable `@overlapped_records` to the validated object.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class ActiveMeeting < ActiveRecord::Base
|
84
|
+
validates :starts_at, :ends_at, :overlap => {:load_overlapped => true}
|
85
|
+
|
86
|
+
def overlapped_records
|
87
|
+
@overlapped_records || []
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
```
|
92
|
+
|
79
93
|
## Rails 4.1 update
|
80
94
|
|
81
95
|
If you just upgraded your application to rails 4.1 you can discover some issue with custom scopes. In older versions we suggest to use definition like
|
data/Rakefile
CHANGED
@@ -10,16 +10,16 @@ begin
|
|
10
10
|
Bundler.setup(:default, :development)
|
11
11
|
rescue Bundler::BundlerError => e
|
12
12
|
$stderr.puts e.message
|
13
|
-
$stderr.puts
|
13
|
+
$stderr.puts 'Run `bundle install` to install missing gems'
|
14
14
|
exit e.status_code
|
15
15
|
end
|
16
16
|
|
17
17
|
RSpec::Core::RakeTask.new(:spec)
|
18
18
|
|
19
|
-
task :
|
19
|
+
task default: :spec
|
20
20
|
|
21
21
|
Rake::RDocTask.new do |rdoc|
|
22
|
-
version = File.exist?('VERSION') ? File.read('VERSION') :
|
22
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ''
|
23
23
|
rdoc.rdoc_dir = 'rdoc'
|
24
24
|
rdoc.title = "validates_overlap #{version}"
|
25
25
|
rdoc.rdoc_files.include('README*')
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.7.0
|
@@ -3,8 +3,8 @@ require 'active_support/i18n'
|
|
3
3
|
I18n.load_path << File.dirname(__FILE__) + '/locale/en.yml'
|
4
4
|
|
5
5
|
class OverlapValidator < ActiveModel::EachValidator
|
6
|
-
BEGIN_OF_UNIX_TIME = Time.at(-
|
7
|
-
END_OF_UNIX_TIME = Time.at(
|
6
|
+
BEGIN_OF_UNIX_TIME = Time.at(-2_147_483_648).to_datetime
|
7
|
+
END_OF_UNIX_TIME = Time.at(2_147_483_648).to_datetime
|
8
8
|
|
9
9
|
attr_accessor :sql_conditions
|
10
10
|
attr_accessor :sql_values
|
@@ -12,11 +12,17 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
12
12
|
|
13
13
|
def initialize(args)
|
14
14
|
attributes_are_range(args[:attributes])
|
15
|
+
|
15
16
|
super
|
16
17
|
end
|
17
18
|
|
18
19
|
def validate(record)
|
19
|
-
|
20
|
+
initialize_query(record, options)
|
21
|
+
if overlapped_exists?
|
22
|
+
if options[:load_overlapped]
|
23
|
+
record.instance_variable_set(:@overlapped_records, get_overlapped)
|
24
|
+
end
|
25
|
+
|
20
26
|
if record.respond_to? attributes.first
|
21
27
|
record.errors.add(options[:message_title] || attributes.first, options[:message_content] || :overlap)
|
22
28
|
else
|
@@ -25,27 +31,31 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
25
31
|
end
|
26
32
|
end
|
27
33
|
|
28
|
-
|
29
34
|
protected
|
30
35
|
|
31
|
-
|
32
|
-
def find_crossed(record)
|
36
|
+
def initialize_query(record, options = {})
|
33
37
|
self.scoped_model = record.class
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
+
generate_overlap_sql_values(record)
|
39
|
+
generate_overlap_sql_conditions(record)
|
40
|
+
add_attributes(record, options[:scope]) if options && options[:scope].present?
|
41
|
+
add_query_options(options[:query_options]) if options && options[:query_options].present?
|
42
|
+
end
|
38
43
|
|
39
|
-
|
44
|
+
# Check if exists at least one record in DB which is overlapped with current record
|
45
|
+
def overlapped_exists?
|
46
|
+
scoped_model.exists?([sql_conditions, sql_values])
|
40
47
|
end
|
41
48
|
|
49
|
+
def get_overlapped
|
50
|
+
scoped_model.where([sql_conditions, sql_values])
|
51
|
+
end
|
42
52
|
|
43
53
|
# Resolve attributes values from record to use in sql conditions
|
44
54
|
# return array in form ['2011-01-10', '2011-02-20']
|
45
55
|
def resolve_values_from_attributes(record)
|
46
56
|
attributes.map do |attr|
|
47
|
-
if attr.to_s.include?(
|
48
|
-
|
57
|
+
if attr.to_s.include?('.')
|
58
|
+
get_assoc_value(record, attr)
|
49
59
|
else
|
50
60
|
record.send(attr.to_sym)
|
51
61
|
end
|
@@ -53,7 +63,7 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
53
63
|
end
|
54
64
|
|
55
65
|
def get_assoc_value(record, attr)
|
56
|
-
assoc, attr_name = attr.to_s.split(
|
66
|
+
assoc, attr_name = attr.to_s.split('.')
|
57
67
|
assoc_name = assoc.singularize.to_sym
|
58
68
|
assoc_obj = record.send(assoc_name) if record.respond_to?(assoc_name)
|
59
69
|
(assoc_obj || record).send(attr_name.to_sym)
|
@@ -64,26 +74,23 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
64
74
|
attributes.map { |attr| attribute_to_sql(attr, record) }
|
65
75
|
end
|
66
76
|
|
67
|
-
|
68
77
|
# Prepare attribute name to use in sql conditions created in form 'table_name.attribute_name'
|
69
78
|
def attribute_to_sql(attr, record)
|
70
|
-
if attr.to_s.include?(
|
79
|
+
if attr.to_s.include?('.')
|
71
80
|
attr
|
72
81
|
else
|
73
82
|
"#{record_table_name(record)}.#{attr}"
|
74
83
|
end
|
75
84
|
end
|
76
85
|
|
77
|
-
|
78
86
|
# Get the table name for the record
|
79
87
|
def record_table_name(record)
|
80
88
|
record.class.table_name
|
81
89
|
end
|
82
90
|
|
83
|
-
|
84
91
|
# Check if the validation of time range is defined by 2 attributes
|
85
92
|
def attributes_are_range(attributes)
|
86
|
-
|
93
|
+
fail 'Validation of time range must be defined by 2 attributes' unless attributes.size == 2
|
87
94
|
end
|
88
95
|
|
89
96
|
def primary_key(record)
|
@@ -113,54 +120,52 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
113
120
|
starts_at_value, ends_at_value = resolve_values_from_attributes(record)
|
114
121
|
starts_at_value += options.fetch(:start_shift) { 0 } if starts_at_value && options
|
115
122
|
ends_at_value += options.fetch(:end_shift) { 0 } if ends_at_value && options
|
116
|
-
self.sql_values = {:
|
123
|
+
self.sql_values = { starts_at_value: starts_at_value || BEGIN_OF_UNIX_TIME, ends_at_value: ends_at_value || END_OF_UNIX_TIME }
|
117
124
|
end
|
118
125
|
|
119
126
|
# Return the condition string depend on exclude_edges option.
|
120
127
|
def condition_string(starts_at_attr, ends_at_attr)
|
121
128
|
except_option = Array(options[:exclude_edges]).map(&:to_s)
|
122
|
-
starts_at_sign = except_option.include?(starts_at_attr.to_s.split(
|
123
|
-
ends_at_sign = except_option.include?(ends_at_attr.to_s.split(
|
129
|
+
starts_at_sign = except_option.include?(starts_at_attr.to_s.split('.').last) ? '<' : '<='
|
130
|
+
ends_at_sign = except_option.include?(ends_at_attr.to_s.split('.').last) ? '>' : '>='
|
124
131
|
query = []
|
125
132
|
query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)"
|
126
133
|
query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)"
|
127
|
-
query.join(
|
134
|
+
query.join(' AND ')
|
128
135
|
end
|
129
136
|
|
130
|
-
|
131
137
|
# Add attributes and values to sql conditions.
|
132
138
|
# helps to use with scope options, so scope can be added as this forms :scope => "user_id" or :scope => ["user_id", "place_id"]
|
133
139
|
def add_attributes(record, attrs)
|
134
140
|
if attrs.is_a?(Array)
|
135
|
-
attrs.each { |attr|
|
141
|
+
attrs.each { |attr| add_attribute(record, attr) }
|
136
142
|
elsif attrs.is_a?(Hash)
|
137
143
|
attrs.each do |attr_name, value|
|
138
|
-
|
144
|
+
add_attribute(record, attr_name, value)
|
139
145
|
end
|
140
146
|
else
|
141
|
-
|
147
|
+
add_attribute(record, attrs)
|
142
148
|
end
|
143
149
|
end
|
144
150
|
|
145
|
-
|
146
151
|
# Add attribute and his value to sql condition
|
147
152
|
def add_attribute(record, attr_name, value = nil)
|
148
153
|
_value = resolve_scope_value(record, attr_name, value)
|
149
154
|
operator = if _value.nil?
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
+
' IS NULL'
|
156
|
+
elsif _value.is_a?(Array)
|
157
|
+
' IN (:%s)'
|
158
|
+
else
|
159
|
+
' = :%s'
|
155
160
|
end
|
156
161
|
|
157
162
|
self.sql_conditions += " AND #{attribute_to_sql(attr_name, record)} #{operator}" % value_attribute_name(attr_name)
|
158
|
-
|
163
|
+
sql_values.merge!(:"#{value_attribute_name(attr_name)}" => _value)
|
159
164
|
end
|
160
165
|
|
161
166
|
def value_attribute_name(attr_name)
|
162
|
-
name = attr_name.to_s.include?(
|
163
|
-
name +
|
167
|
+
name = attr_name.to_s.include?('.') ? attr_name.to_s.gsub('.', '_') : attr_name.to_s
|
168
|
+
name + '_value'
|
164
169
|
end
|
165
170
|
|
166
171
|
def resolve_scope_value(record, attr_name, value = nil)
|
@@ -176,8 +181,7 @@ class OverlapValidator < ActiveModel::EachValidator
|
|
176
181
|
# validates_overlap :date_from, :date_to, :query_options => {:includes => "visits"}
|
177
182
|
def add_query_options(methods)
|
178
183
|
methods.each do |method_name, params|
|
179
|
-
self.scoped_model =
|
184
|
+
self.scoped_model = scoped_model.send(method_name.to_sym, *params)
|
180
185
|
end
|
181
186
|
end
|
182
|
-
|
183
187
|
end
|
@@ -1,4 +1,4 @@
|
|
1
1
|
class ActiveMeeting < ActiveRecord::Base
|
2
|
-
validates :starts_at, :ends_at, :
|
3
|
-
scope :active, -> { where(:
|
2
|
+
validates :starts_at, :ends_at, overlap: { query_options: { active: nil } }
|
3
|
+
scope :active, -> { where(is_active: true) }
|
4
4
|
end
|
@@ -1,3 +1,3 @@
|
|
1
1
|
class EndOverlapMeeting < ActiveRecord::Base
|
2
|
-
validates :starts_at, :ends_at, :
|
3
|
-
end
|
2
|
+
validates :starts_at, :ends_at, overlap: { exclude_edges: :ends_at }
|
3
|
+
end
|
@@ -2,8 +2,8 @@ class Position < ActiveRecord::Base
|
|
2
2
|
belongs_to :time_slot
|
3
3
|
belongs_to :user
|
4
4
|
validates :"time_slots.starts_at", :"time_slots.ends_at",
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
overlap: {
|
6
|
+
query_options: { includes: :time_slot },
|
7
|
+
scope: { 'positions.user_id' => proc { |position| position.user_id } }
|
8
|
+
}
|
9
9
|
end
|
@@ -1,3 +1,3 @@
|
|
1
1
|
class StartEndOverlapMeeting < ActiveRecord::Base
|
2
|
-
validates :starts_at, :ends_at, :
|
3
|
-
end
|
2
|
+
validates :starts_at, :ends_at, overlap: { exclude_edges: [:starts_at, :ends_at] }
|
3
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
class TimeSlot < ActiveRecord::Base
|
2
2
|
has_many :positions
|
3
3
|
validates :"time_slots.starts_at", :"time_slots.ends_at",
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
overlap: {
|
5
|
+
query_options: { includes: :positions },
|
6
|
+
scope: { 'positions.user_id' => proc { |time_slot| time_slot.positions.map(&:user_id) } }
|
7
|
+
}
|
8
8
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
require File.expand_path('../boot', __FILE__)
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
3
|
+
require 'active_model/railtie'
|
4
|
+
require 'active_record/railtie'
|
5
|
+
require 'action_controller/railtie'
|
6
|
+
require 'action_view/railtie'
|
7
|
+
require 'action_mailer/railtie'
|
8
8
|
|
9
9
|
Bundler.require
|
10
|
-
require
|
10
|
+
require 'validates_overlap'
|
11
11
|
|
12
12
|
module Dummy
|
13
13
|
class Application < Rails::Application
|
@@ -37,7 +37,7 @@ module Dummy
|
|
37
37
|
# config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
|
38
38
|
|
39
39
|
# Configure the default encoding used in templates for Ruby 1.9.
|
40
|
-
config.encoding =
|
40
|
+
config.encoding = 'utf-8'
|
41
41
|
|
42
42
|
# Configure sensitive parameters which will be filtered from the log file.
|
43
43
|
config.filter_parameters += [:password]
|
data/spec/dummy/config/boot.rb
CHANGED
@@ -11,7 +11,7 @@ Dummy::Application.configure do
|
|
11
11
|
|
12
12
|
# Show full error reports and disable caching
|
13
13
|
config.consider_all_requests_local = true
|
14
|
-
#config.action_view.debug_rjs = true
|
14
|
+
# config.action_view.debug_rjs = true
|
15
15
|
config.action_controller.perform_caching = false
|
16
16
|
|
17
17
|
# Don't care if the mailer can't send
|
@@ -23,4 +23,3 @@ Dummy::Application.configure do
|
|
23
23
|
# Only use best-standards-support built into browsers
|
24
24
|
config.action_dispatch.best_standards_support = :builtin
|
25
25
|
end
|
26
|
-
|
@@ -10,7 +10,7 @@ Dummy::Application.configure do
|
|
10
10
|
config.action_controller.perform_caching = true
|
11
11
|
|
12
12
|
# Specifies the header that your server uses for sending files
|
13
|
-
config.action_dispatch.x_sendfile_header =
|
13
|
+
config.action_dispatch.x_sendfile_header = 'X-Sendfile'
|
14
14
|
|
15
15
|
# For nginx:
|
16
16
|
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# Be sure to restart your server when you modify this file.
|
2
2
|
|
3
|
-
Dummy::Application.config.session_store :cookie_store, :
|
3
|
+
Dummy::Application.config.session_store :cookie_store, key: '_dummy_session'
|
4
4
|
|
5
5
|
# Use the database for sessions instead of the cookie-based default,
|
6
6
|
# which shouldn't be used to store highly confidential information
|