sp-validates_timeliness 3.1.2
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 +7 -0
- data/CHANGELOG.rdoc +183 -0
- data/LICENSE +20 -0
- data/README.md +323 -0
- data/Rakefile +30 -0
- data/init.rb +1 -0
- data/lib/generators/validates_timeliness/install_generator.rb +16 -0
- data/lib/generators/validates_timeliness/templates/en.yml +16 -0
- data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +40 -0
- data/lib/jc-validates_timeliness.rb +1 -0
- data/lib/validates_timeliness.rb +70 -0
- data/lib/validates_timeliness/attribute_methods.rb +95 -0
- data/lib/validates_timeliness/conversion.rb +70 -0
- data/lib/validates_timeliness/extensions.rb +14 -0
- data/lib/validates_timeliness/extensions/date_time_select.rb +61 -0
- data/lib/validates_timeliness/extensions/multiparameter_handler.rb +80 -0
- data/lib/validates_timeliness/helper_methods.rb +23 -0
- data/lib/validates_timeliness/orm/active_record.rb +53 -0
- data/lib/validates_timeliness/orm/mongoid.rb +63 -0
- data/lib/validates_timeliness/railtie.rb +15 -0
- data/lib/validates_timeliness/validator.rb +117 -0
- data/lib/validates_timeliness/version.rb +3 -0
- data/spec/spec_helper.rb +109 -0
- data/spec/support/config_helper.rb +36 -0
- data/spec/support/model_helpers.rb +27 -0
- data/spec/support/tag_matcher.rb +35 -0
- data/spec/support/test_model.rb +59 -0
- data/spec/validates_timeliness/attribute_methods_spec.rb +86 -0
- data/spec/validates_timeliness/conversion_spec.rb +234 -0
- data/spec/validates_timeliness/extensions/date_time_select_spec.rb +163 -0
- data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +44 -0
- data/spec/validates_timeliness/helper_methods_spec.rb +30 -0
- data/spec/validates_timeliness/orm/active_record_spec.rb +244 -0
- data/spec/validates_timeliness/orm/mongoid_spec.rb +189 -0
- data/spec/validates_timeliness/validator/after_spec.rb +57 -0
- data/spec/validates_timeliness/validator/before_spec.rb +57 -0
- data/spec/validates_timeliness/validator/is_at_spec.rb +61 -0
- data/spec/validates_timeliness/validator/on_or_after_spec.rb +57 -0
- data/spec/validates_timeliness/validator/on_or_before_spec.rb +57 -0
- data/spec/validates_timeliness/validator_spec.rb +246 -0
- data/spec/validates_timeliness_spec.rb +43 -0
- data/validates_timeliness.gemspec +25 -0
- metadata +157 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module Validations
|
3
|
+
|
4
|
+
module HelperMethods
|
5
|
+
def validates_date(*attr_names)
|
6
|
+
timeliness_validation_for attr_names, :date
|
7
|
+
end
|
8
|
+
|
9
|
+
def validates_time(*attr_names)
|
10
|
+
timeliness_validation_for attr_names, :time
|
11
|
+
end
|
12
|
+
|
13
|
+
def validates_datetime(*attr_names)
|
14
|
+
timeliness_validation_for attr_names, :datetime
|
15
|
+
end
|
16
|
+
|
17
|
+
def timeliness_validation_for(attr_names, type)
|
18
|
+
validates_with TimelinessValidator, _merge_attributes(attr_names).merge(:type => type)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ValidatesTimeliness
|
2
|
+
module ORM
|
3
|
+
module ActiveRecord
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
public
|
8
|
+
|
9
|
+
def timeliness_attribute_timezone_aware?(attr_name)
|
10
|
+
create_time_zone_conversion_attribute?(attr_name, timeliness_column_for_attribute(attr_name))
|
11
|
+
end
|
12
|
+
|
13
|
+
def timeliness_attribute_type(attr_name)
|
14
|
+
timeliness_column_for_attribute(attr_name).type
|
15
|
+
end
|
16
|
+
|
17
|
+
def timeliness_column_for_attribute(attr_name)
|
18
|
+
columns_hash.fetch(attr_name.to_s) do |attr_name|
|
19
|
+
validation_type = _validators[attr_name.to_sym].find {|v| v.kind == :timeliness }.type
|
20
|
+
::ActiveRecord::ConnectionAdapters::Column.new(attr_name, nil, validation_type.to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def define_attribute_methods
|
25
|
+
super.tap do |attribute_methods_generated|
|
26
|
+
define_timeliness_methods true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def timeliness_type_cast_code(attr_name, var_name)
|
33
|
+
type = timeliness_attribute_type(attr_name)
|
34
|
+
|
35
|
+
method_body = super
|
36
|
+
method_body << "\n#{var_name} = #{var_name}.to_date if #{var_name}" if type == :date
|
37
|
+
method_body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reload(*args)
|
42
|
+
_clear_timeliness_cache
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ActiveRecord::Base
|
51
|
+
include ValidatesTimeliness::AttributeMethods
|
52
|
+
include ValidatesTimeliness::ORM::ActiveRecord
|
53
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ValidatesTimeliness
|
2
|
+
module ORM
|
3
|
+
module Mongoid
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
# You need define the fields before you define the validations.
|
6
|
+
# It is best to use the plugin parser to avoid errors on a bad
|
7
|
+
# field value in Mongoid. Parser will return nil rather than error.
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
public
|
11
|
+
|
12
|
+
# Mongoid has no bulk attribute method definition hook. It defines
|
13
|
+
# them with each field definition. So we likewise define them after
|
14
|
+
# each validation is defined.
|
15
|
+
#
|
16
|
+
def timeliness_validation_for(attr_names, type)
|
17
|
+
super
|
18
|
+
attr_names.each { |attr_name| define_timeliness_write_method(attr_name) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def timeliness_attribute_type(attr_name)
|
22
|
+
{
|
23
|
+
Date => :date,
|
24
|
+
Time => :time,
|
25
|
+
DateTime => :datetime
|
26
|
+
}[fields[attr_name.to_s].type] || :datetime
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def timeliness_type_cast_code(attr_name, var_name)
|
32
|
+
type = timeliness_attribute_type(attr_name)
|
33
|
+
|
34
|
+
"#{var_name} = Timeliness::Parser.parse(value, :#{type})"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
module Reload
|
40
|
+
def reload(*args)
|
41
|
+
_clear_timeliness_cache
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module Mongoid::Document
|
50
|
+
include ValidatesTimeliness::AttributeMethods
|
51
|
+
include ValidatesTimeliness::ORM::Mongoid
|
52
|
+
|
53
|
+
# Pre-2.3 reload
|
54
|
+
if (instance_methods & ['reload', :reload]).present?
|
55
|
+
def reload_with_timeliness
|
56
|
+
_clear_timeliness_cache
|
57
|
+
reload_without_timeliness
|
58
|
+
end
|
59
|
+
alias_method_chain :reload, :timeliness
|
60
|
+
else
|
61
|
+
include ValidatesTimeliness::ORM::Mongoid::Reload
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ValidatesTimeliness
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer "validates_timeliness.initialize_active_record", :after => 'active_record.initialize_timezone' do
|
4
|
+
ActiveSupport.on_load(:active_record) do
|
5
|
+
ValidatesTimeliness.default_timezone = ActiveRecord::Base.default_timezone
|
6
|
+
ValidatesTimeliness.extend_orms << :active_record
|
7
|
+
ValidatesTimeliness.load_orms
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer "validates_timeliness.initialize_restriction_errors" do
|
12
|
+
ValidatesTimeliness.ignore_restriction_errors = !Rails.env.test?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_model/validator'
|
3
|
+
|
4
|
+
module ValidatesTimeliness
|
5
|
+
class Validator < ActiveModel::EachValidator
|
6
|
+
include Conversion
|
7
|
+
|
8
|
+
attr_reader :type
|
9
|
+
|
10
|
+
RESTRICTIONS = {
|
11
|
+
:is_at => :==,
|
12
|
+
:before => :<,
|
13
|
+
:after => :>,
|
14
|
+
:on_or_before => :<=,
|
15
|
+
:on_or_after => :>=,
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
DEFAULT_ERROR_VALUE_FORMATS = {
|
19
|
+
:date => '%Y-%m-%d',
|
20
|
+
:time => '%H:%M:%S',
|
21
|
+
:datetime => '%Y-%m-%d %H:%M:%S'
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
RESTRICTION_ERROR_MESSAGE = "Error occurred validating %s for %s restriction:\n%s"
|
25
|
+
|
26
|
+
# Prior to version 4.1, Rails will call `#setup`, if defined. This method is deprecated in Rails 4.1 and removed
|
27
|
+
# altogether in 4.2.
|
28
|
+
SETUP_DEPRECATED = ActiveModel.respond_to?(:version) && ActiveModel.version >= Gem::Version.new('4.1')
|
29
|
+
|
30
|
+
def self.kind
|
31
|
+
:timeliness
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(options)
|
35
|
+
@type = options.delete(:type) || :datetime
|
36
|
+
@allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank)
|
37
|
+
|
38
|
+
if range = options.delete(:between)
|
39
|
+
raise ArgumentError, ":between must be a Range or an Array" unless range.is_a?(Range) || range.is_a?(Array)
|
40
|
+
options[:on_or_after] = range.first
|
41
|
+
if range.is_a?(Range) && range.exclude_end?
|
42
|
+
options[:before] = range.last
|
43
|
+
else
|
44
|
+
options[:on_or_before] = range.last
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
@restrictions_to_check = RESTRICTIONS.keys & options.keys
|
49
|
+
super
|
50
|
+
setup_timeliness_validated_attributes(options[:class]) if options[:class]
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_timeliness_validated_attributes(model)
|
54
|
+
if model.respond_to?(:timeliness_validated_attributes)
|
55
|
+
model.timeliness_validated_attributes ||= []
|
56
|
+
model.timeliness_validated_attributes |= @attributes
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Provide backwards compatibility for Rails < 4.1, which expects `#setup` to be defined.
|
61
|
+
alias_method :setup, :setup_timeliness_validated_attributes unless SETUP_DEPRECATED
|
62
|
+
|
63
|
+
def validate_each(record, attr_name, value)
|
64
|
+
raw_value = attribute_raw_value(record, attr_name) || value
|
65
|
+
return if (@allow_nil && raw_value.nil?) || (@allow_blank && raw_value.blank?)
|
66
|
+
|
67
|
+
@timezone_aware = timezone_aware?(record, attr_name)
|
68
|
+
value = parse(raw_value) if value.is_a?(String) || options[:format]
|
69
|
+
value = type_cast_value(value, @type)
|
70
|
+
|
71
|
+
add_error(record, attr_name, :"invalid_#{@type}") and return if value.blank?
|
72
|
+
|
73
|
+
validate_restrictions(record, attr_name, value)
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_restrictions(record, attr_name, value)
|
77
|
+
@restrictions_to_check.each do |restriction|
|
78
|
+
begin
|
79
|
+
restriction_value = type_cast_value(evaluate_option_value(options[restriction], record), @type)
|
80
|
+
unless value.send(RESTRICTIONS[restriction], restriction_value)
|
81
|
+
add_error(record, attr_name, restriction, restriction_value) and break
|
82
|
+
end
|
83
|
+
rescue => e
|
84
|
+
unless ValidatesTimeliness.ignore_restriction_errors
|
85
|
+
message = RESTRICTION_ERROR_MESSAGE % [ attr_name, restriction.inspect, e.message ]
|
86
|
+
add_error(record, attr_name, message) and break
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_error(record, attr_name, message, value=nil)
|
93
|
+
value = format_error_value(value) if value
|
94
|
+
message_options = { :message => options[:"#{message}_message"], :restriction => value }
|
95
|
+
record.errors.add(attr_name, message, message_options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def format_error_value(value)
|
99
|
+
format = I18n.t(@type, :default => DEFAULT_ERROR_VALUE_FORMATS[@type], :scope => 'validates_timeliness.error_value_formats')
|
100
|
+
value.strftime(format)
|
101
|
+
end
|
102
|
+
|
103
|
+
def attribute_raw_value(record, attr_name)
|
104
|
+
record.respond_to?(:_timeliness_raw_value_for) &&
|
105
|
+
record._timeliness_raw_value_for(attr_name.to_s)
|
106
|
+
end
|
107
|
+
|
108
|
+
def timezone_aware?(record, attr_name)
|
109
|
+
record.class.respond_to?(:timeliness_attribute_timezone_aware?) &&
|
110
|
+
record.class.timeliness_attribute_timezone_aware?(attr_name)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Compatibility with ActiveModel validates method which matches option keys to their validator class
|
117
|
+
ActiveModel::Validations::TimelinessValidator = ValidatesTimeliness::Validator
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'rspec/collection_matchers'
|
3
|
+
|
4
|
+
# Coveralls
|
5
|
+
require "coveralls"
|
6
|
+
|
7
|
+
Coveralls.wear! do
|
8
|
+
add_filter 'spec'
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'active_model'
|
12
|
+
require 'active_model/validations'
|
13
|
+
require 'active_record'
|
14
|
+
require 'action_view'
|
15
|
+
require 'timecop'
|
16
|
+
|
17
|
+
require 'validates_timeliness'
|
18
|
+
|
19
|
+
require 'support/test_model'
|
20
|
+
require 'support/model_helpers'
|
21
|
+
require 'support/config_helper'
|
22
|
+
require 'support/tag_matcher'
|
23
|
+
|
24
|
+
ValidatesTimeliness.setup do |c|
|
25
|
+
c.extend_orms = [ :active_record ]
|
26
|
+
c.enable_date_time_select_extension!
|
27
|
+
c.enable_multiparameter_extension!
|
28
|
+
c.default_timezone = :utc
|
29
|
+
end
|
30
|
+
|
31
|
+
Time.zone = 'Australia/Melbourne'
|
32
|
+
|
33
|
+
LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml')
|
34
|
+
I18n.load_path.unshift(LOCALE_PATH)
|
35
|
+
|
36
|
+
# Extend TestModel as you would another ORM/ODM module
|
37
|
+
module TestModelShim
|
38
|
+
extend ActiveSupport::Concern
|
39
|
+
include ValidatesTimeliness::AttributeMethods
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
# Hook method for attribute method generation
|
43
|
+
def define_attribute_methods(attr_names)
|
44
|
+
super
|
45
|
+
define_timeliness_methods
|
46
|
+
end
|
47
|
+
|
48
|
+
# Hook into native time zone handling check, if any
|
49
|
+
def timeliness_attribute_timezone_aware?(attr_name)
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Person
|
56
|
+
include TestModel
|
57
|
+
attribute :birth_date, :date
|
58
|
+
attribute :birth_time, :time
|
59
|
+
attribute :birth_datetime, :datetime
|
60
|
+
|
61
|
+
define_attribute_methods model_attributes.keys
|
62
|
+
end
|
63
|
+
|
64
|
+
class PersonWithShim < Person
|
65
|
+
include TestModelShim
|
66
|
+
end
|
67
|
+
|
68
|
+
I18n.enforce_available_locales = false
|
69
|
+
|
70
|
+
ActiveRecord::Base.default_timezone = :utc
|
71
|
+
ActiveRecord::Base.time_zone_aware_attributes = true
|
72
|
+
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
|
73
|
+
ActiveRecord::Migration.verbose = false
|
74
|
+
ActiveRecord::Schema.define(:version => 1) do
|
75
|
+
create_table :employees, :force => true do |t|
|
76
|
+
t.string :first_name
|
77
|
+
t.string :last_name
|
78
|
+
t.date :birth_date
|
79
|
+
t.time :birth_time
|
80
|
+
t.datetime :birth_datetime
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Employee < ActiveRecord::Base
|
85
|
+
attr_accessor :redefined_birth_date_called
|
86
|
+
validates_date :birth_date, :allow_nil => true
|
87
|
+
validates_date :birth_time, :allow_nil => true
|
88
|
+
validates_date :birth_datetime, :allow_nil => true
|
89
|
+
|
90
|
+
def birth_date=(value)
|
91
|
+
self.redefined_birth_date_called = true
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
RSpec.configure do |c|
|
97
|
+
c.mock_with :rspec
|
98
|
+
c.include(ModelHelpers)
|
99
|
+
c.include(ConfigHelper)
|
100
|
+
c.include(TagMatcher)
|
101
|
+
c.before do
|
102
|
+
reset_validation_setup_for(Person)
|
103
|
+
reset_validation_setup_for(PersonWithShim)
|
104
|
+
end
|
105
|
+
|
106
|
+
c.filter_run_excluding :active_record => lambda {|version|
|
107
|
+
!(::ActiveRecord::VERSION::STRING.to_s =~ /^#{version.to_s}/)
|
108
|
+
}
|
109
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ConfigHelper
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
# Justin French tip
|
5
|
+
def with_config(preference_name, temporary_value)
|
6
|
+
old_value = ValidatesTimeliness.send(preference_name)
|
7
|
+
ValidatesTimeliness.send(:"#{preference_name}=", temporary_value)
|
8
|
+
yield
|
9
|
+
ensure
|
10
|
+
ValidatesTimeliness.send(:"#{preference_name}=", old_value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset_validation_setup_for(model_class)
|
14
|
+
model_class.reset_callbacks(:validate)
|
15
|
+
model_class._validators.clear
|
16
|
+
model_class.timeliness_validated_attributes = [] if model_class.respond_to?(:timeliness_validated_attributes)
|
17
|
+
model_class.undefine_attribute_methods
|
18
|
+
# This is a hack to avoid a disabled super method error message after an undef
|
19
|
+
model_class.instance_variable_set(:@generated_attribute_methods, nil)
|
20
|
+
model_class.instance_variable_set(:@generated_timeliness_methods, nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def with_config(preference_name, temporary_value)
|
25
|
+
original_config_value = ValidatesTimeliness.send(preference_name)
|
26
|
+
|
27
|
+
before(:all) do
|
28
|
+
ValidatesTimeliness.send(:"#{preference_name}=", temporary_value)
|
29
|
+
end
|
30
|
+
|
31
|
+
after(:all) do
|
32
|
+
ValidatesTimeliness.send(:"#{preference_name}=", original_config_value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ModelHelpers
|
2
|
+
|
3
|
+
# Some test helpers from Rails source
|
4
|
+
def invalid!(attr_name, values, error = nil)
|
5
|
+
with_each_person_value(attr_name, values) do |record, value|
|
6
|
+
expect(record).to be_invalid
|
7
|
+
expect(record.errors[attr_name].size).to be >= 1
|
8
|
+
expect(record.errors[attr_name].first).to eq(error) if error
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid!(attr_name, values)
|
13
|
+
with_each_person_value(attr_name, values) do |record, value|
|
14
|
+
expect(record).to be_valid
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_each_person_value(attr_name, values)
|
19
|
+
record = Person.new
|
20
|
+
values = [values] unless values.is_a?(Array)
|
21
|
+
values.each do |value|
|
22
|
+
record.send("#{attr_name}=", value)
|
23
|
+
yield record, value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|