validates_overlap 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|