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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +5 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.rails50 +5 -0
  7. data/README.md +16 -2
  8. data/Rakefile +3 -3
  9. data/VERSION +1 -1
  10. data/lib/validates_overlap/locale/es.yml +4 -0
  11. data/lib/validates_overlap/overlap_validator.rb +42 -38
  12. data/spec/dummy/app/models/active_meeting.rb +2 -2
  13. data/spec/dummy/app/models/end_overlap_meeting.rb +2 -2
  14. data/spec/dummy/app/models/meeting.rb +1 -1
  15. data/spec/dummy/app/models/position.rb +4 -4
  16. data/spec/dummy/app/models/secure_meeting.rb +1 -1
  17. data/spec/dummy/app/models/shift.rb +1 -1
  18. data/spec/dummy/app/models/start_end_overlap_meeting.rb +2 -2
  19. data/spec/dummy/app/models/start_overlap_meeting.rb +1 -1
  20. data/spec/dummy/app/models/time_slot.rb +4 -4
  21. data/spec/dummy/app/models/user_meeting.rb +1 -1
  22. data/spec/dummy/config/application.rb +7 -7
  23. data/spec/dummy/config/boot.rb +1 -1
  24. data/spec/dummy/config/environments/development.rb +1 -2
  25. data/spec/dummy/config/environments/production.rb +1 -1
  26. data/spec/dummy/config/initializers/session_store.rb +1 -1
  27. data/spec/dummy/db/schema.rb +52 -54
  28. data/spec/dummy/spec/factories/position.rb +2 -2
  29. data/spec/dummy/spec/factories/user_meeting.rb +2 -2
  30. data/spec/dummy/spec/models/active_meetings_spec.rb +4 -10
  31. data/spec/dummy/spec/models/end_overlap_meeting_spec.rb +26 -29
  32. data/spec/dummy/spec/models/meeting_spec.rb +51 -47
  33. data/spec/dummy/spec/models/position_spec.rb +18 -28
  34. data/spec/dummy/spec/models/secure_meeting_spec.rb +5 -11
  35. data/spec/dummy/spec/models/shift_spec.rb +32 -33
  36. data/spec/dummy/spec/models/start_end_overlap_meeting_spec.rb +26 -29
  37. data/spec/dummy/spec/models/start_overlap_meeting_spec.rb +26 -29
  38. data/spec/dummy/spec/models/time_slot_spec.rb +21 -31
  39. data/spec/dummy/spec/models/user_meeting_spec.rb +16 -19
  40. data/spec/dummy/spec/models/user_spec.rb +3 -5
  41. data/spec/dummy/spec/overlap_validator_spec.rb +12 -14
  42. data/spec/spec_helper.rb +29 -20
  43. data/validates_overlap.gemspec +19 -16
  44. metadata +48 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e861b52a30a5b27d67b83042a658a6cd923025a
4
- data.tar.gz: 242c6bff486745218266e73c6e562995d6ed7e68
3
+ metadata.gz: 483af513bf060127e3608a51a2257f7f9f4e1f3b
4
+ data.tar.gz: da46a701164a966e9f26d21e31aafcc4bf2ddc24
5
5
  SHA512:
6
- metadata.gz: 3f60ffe6700e1e3100931adb078e05d9c34a0344a4eb7221035e894cbbc18d36424fb8c09dbb40b1cd70f63a4506025c4a09ded49b7946e278633b4e7f36c242
7
- data.tar.gz: 87f79459a1ef597b566a7dcccf0d16af964ba4c7229e8cea3bff813e2c9ef4580fba0f99d3b925b09a31176e867d75db63f66dfd46ae462637fa749b5bc41e4d
6
+ metadata.gz: b43a7550f859c43f109cdf62acc9ebcef51985de90c3d54f0d8fbba1ed94fbc19bac6aa5a5696ed3cdf39f18060a0621c751510e6bf2eb83d24da6dbbca88526
7
+ data.tar.gz: d9ac0f6e807ae6198b21c7e804addd35e8fd24dbcdea8d6d7390e708964a4c687da0aa8a00432babad2dc910173059d1182ea3006432525bb1725505c81a05d9
data/.gitignore CHANGED
@@ -8,3 +8,4 @@ spec/dummy/tmp/
8
8
  database.yml
9
9
  Gemfile*.lock
10
10
  .DS_Store
11
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ Style/Encoding:
2
+ Enabled: false
3
+
4
+ Metrics/LineLength:
5
+ Max: 120
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source "https://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
data/Gemfile.rails50 ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rails', '~> 5.0'
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 and 4.
7
+ #### This gem is compatible with Rails 3, 4, 5.
8
8
 
9
9
  #### When this gem should be helpful for you?
10
- If you are developing Rails 3 app, let say some meeting planner and you can't save records which have time overlap.
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 "Run `bundle install` to install missing gems"
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 :default => :spec
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.6.0
1
+ 0.7.0
@@ -0,0 +1,4 @@
1
+ es:
2
+ errors:
3
+ messages:
4
+ overlap: coincide con otro registro
@@ -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(-2147483648).to_datetime
7
- END_OF_UNIX_TIME = Time.at(2147483648).to_datetime
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
- if self.find_crossed(record)
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
- # Check if exists at least one record in DB which is crossed with current record
32
- def find_crossed(record)
36
+ def initialize_query(record, options = {})
33
37
  self.scoped_model = record.class
34
- self.generate_overlap_sql_values(record)
35
- self.generate_overlap_sql_conditions(record)
36
- self.add_attributes(record, options[:scope]) if options && options[:scope].present?
37
- self.add_query_options(options[:query_options]) if options && options[:query_options].present?
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
- return self.scoped_model.exists?([sql_conditions, sql_values])
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
- self.get_assoc_value(record, attr)
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
- raise "Validation of time range must be defined by 2 attributes" unless attributes.size == 2
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 = {:starts_at_value => starts_at_value || BEGIN_OF_UNIX_TIME, :ends_at_value => ends_at_value || END_OF_UNIX_TIME}
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(".").last) ? "<" : "<="
123
- ends_at_sign = except_option.include?(ends_at_attr.to_s.split(".").last) ? ">" : ">="
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(" AND ")
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| self.add_attribute(record, 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
- self.add_attribute(record, attr_name, value)
144
+ add_attribute(record, attr_name, value)
139
145
  end
140
146
  else
141
- self.add_attribute(record, attrs)
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
- " IS NULL"
151
- elsif _value.is_a?(Array)
152
- " IN (:%s)"
153
- else
154
- " = :%s"
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
- self.sql_values.merge!({:"#{value_attribute_name(attr_name)}" => _value})
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?(".") ? attr_name.to_s.gsub(".", "_") : attr_name
163
- name + "_value"
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 = self.scoped_model.send(method_name.to_sym, *params)
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, :overlap => {:query_options => {:active => nil}}
3
- scope :active, -> { where(:is_active => true) }
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, :overlap => {:exclude_edges => :ends_at}
3
- end
2
+ validates :starts_at, :ends_at, overlap: { exclude_edges: :ends_at }
3
+ end
@@ -1,3 +1,3 @@
1
1
  class Meeting < ActiveRecord::Base
2
- validates :starts_at, :ends_at, :overlap => true
2
+ validates :starts_at, :ends_at, overlap: { load_overlapped: true }
3
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
- :overlap => {
6
- :query_options => {:includes => :time_slot},
7
- :scope => { "positions.user_id" => proc{|position| position.user_id} }
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 SecureMeeting < ActiveRecord::Base
2
- validates :starts_at, :ends_at, :overlap => true
2
+ validates :starts_at, :ends_at, overlap: true
3
3
  end
@@ -1,3 +1,3 @@
1
1
  class Shift < ActiveRecord::Base
2
- validates :starts_at, :ends_at, :overlap => {:start_shift => -1.day, :end_shift => 1.day}
2
+ validates :starts_at, :ends_at, overlap: { start_shift: -1.day, end_shift: 1.day }
3
3
  end
@@ -1,3 +1,3 @@
1
1
  class StartEndOverlapMeeting < ActiveRecord::Base
2
- validates :starts_at, :ends_at, :overlap => {:exclude_edges => [:starts_at, :ends_at]}
3
- end
2
+ validates :starts_at, :ends_at, overlap: { exclude_edges: [:starts_at, :ends_at] }
3
+ end
@@ -1,3 +1,3 @@
1
1
  class StartOverlapMeeting < ActiveRecord::Base
2
- validates :starts_at, :ends_at, :overlap => {:exclude_edges => :starts_at}
2
+ validates :starts_at, :ends_at, overlap: { exclude_edges: :starts_at }
3
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
- :overlap => {
5
- :query_options => {:includes => :positions},
6
- :scope => {"positions.user_id" => proc{|time_slot| time_slot.positions.map(&:user_id)} }
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,3 +1,3 @@
1
1
  class UserMeeting < ActiveRecord::Base
2
- validates :starts_at, :ends_at, :overlap => {:scope => "user_id"}
2
+ validates :starts_at, :ends_at, overlap: { scope: 'user_id' }
3
3
  end
@@ -1,13 +1,13 @@
1
1
  require File.expand_path('../boot', __FILE__)
2
2
 
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"
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 "validates_overlap"
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 = "utf-8"
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]
@@ -7,4 +7,4 @@ if File.exist?(gemfile)
7
7
  Bundler.setup
8
8
  end
9
9
 
10
- $:.unshift File.expand_path('../../../../lib', __FILE__)
10
+ $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
@@ -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 = "X-Sendfile"
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, :key => '_dummy_session'
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