adzap-validates_timeliness 1.1.1
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.
- data/CHANGELOG +49 -0
- data/LICENSE +20 -0
- data/README.rdoc +329 -0
- data/Rakefile +58 -0
- data/TODO +7 -0
- data/lib/validates_timeliness.rb +67 -0
- data/lib/validates_timeliness/action_view/instance_tag.rb +45 -0
- data/lib/validates_timeliness/active_record/attribute_methods.rb +157 -0
- data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +64 -0
- data/lib/validates_timeliness/core_ext/date.rb +13 -0
- data/lib/validates_timeliness/core_ext/date_time.rb +13 -0
- data/lib/validates_timeliness/core_ext/time.rb +13 -0
- data/lib/validates_timeliness/formats.rb +309 -0
- data/lib/validates_timeliness/locale/en.yml +12 -0
- data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +157 -0
- data/lib/validates_timeliness/validation_methods.rb +82 -0
- data/lib/validates_timeliness/validator.rb +163 -0
- data/spec/action_view/instance_tag_spec.rb +38 -0
- data/spec/active_record/attribute_methods_spec.rb +204 -0
- data/spec/active_record/multiparameter_attributes_spec.rb +48 -0
- data/spec/core_ext/dummy_time_spec.rb +31 -0
- data/spec/formats_spec.rb +274 -0
- data/spec/ginger_scenarios.rb +19 -0
- data/spec/resources/application.rb +2 -0
- data/spec/resources/person.rb +3 -0
- data/spec/resources/schema.rb +10 -0
- data/spec/resources/sqlite_patch.rb +19 -0
- data/spec/spec/rails/matchers/validate_timeliness_spec.rb +206 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/time_travel/MIT-LICENSE +20 -0
- data/spec/time_travel/time_extensions.rb +33 -0
- data/spec/time_travel/time_travel.rb +12 -0
- data/spec/validation_methods_spec.rb +61 -0
- data/spec/validator_spec.rb +475 -0
- metadata +105 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
errors:
|
4
|
+
messages:
|
5
|
+
invalid_date: "is not a valid date"
|
6
|
+
invalid_time: "is not a valid time"
|
7
|
+
invalid_datetime: "is not a valid datetime"
|
8
|
+
before: "must be before {{restriction}}"
|
9
|
+
on_or_before: "must be on or before {{restriction}}"
|
10
|
+
after: "must be after {{restriction}}"
|
11
|
+
on_or_after: "must be on or after {{restriction}}"
|
12
|
+
between: "must be between {{earliest}} and {{latest}}"
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Spec
|
2
|
+
module Rails
|
3
|
+
module Matchers
|
4
|
+
class ValidateTimeliness
|
5
|
+
|
6
|
+
VALIDITY_TEST_VALUES = {
|
7
|
+
:date => {:pass => '2000-01-01', :fail => '2000-01-32'},
|
8
|
+
:time => {:pass => '12:00', :fail => '25:00'},
|
9
|
+
:datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'}
|
10
|
+
}
|
11
|
+
|
12
|
+
OPTION_TEST_SETTINGS = {
|
13
|
+
:before => { :method => :-, :modify_on => :valid },
|
14
|
+
:after => { :method => :+, :modify_on => :valid },
|
15
|
+
:on_or_before => { :method => :+, :modify_on => :invalid },
|
16
|
+
:on_or_after => { :method => :-, :modify_on => :invalid }
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(attribute, options)
|
20
|
+
@expected, @options = attribute, options
|
21
|
+
@validator = ValidatesTimeliness::Validator.new(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def matches?(record)
|
25
|
+
@record = record
|
26
|
+
@type = @options[:type]
|
27
|
+
|
28
|
+
valid = test_validity
|
29
|
+
|
30
|
+
valid = test_option(:before) if @options[:before] && valid
|
31
|
+
valid = test_option(:after) if @options[:after] && valid
|
32
|
+
|
33
|
+
valid = test_option(:on_or_before) if @options[:on_or_before] && valid
|
34
|
+
valid = test_option(:on_or_after) if @options[:on_or_after] && valid
|
35
|
+
|
36
|
+
valid = test_between if @options[:between] && valid
|
37
|
+
|
38
|
+
return valid
|
39
|
+
end
|
40
|
+
|
41
|
+
def failure_message
|
42
|
+
"expected model to validate #{@type} attribute #{@expected.inspect} with #{@last_failure}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def negative_failure_message
|
46
|
+
"expected not to validate #{@type} attribute #{@expected.inspect}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def description
|
50
|
+
"have validated #{@type} attribute #{@expected.inspect}"
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def test_validity
|
56
|
+
invalid_value = VALIDITY_TEST_VALUES[@type][:fail]
|
57
|
+
valid_value = parse_and_cast(VALIDITY_TEST_VALUES[@type][:pass])
|
58
|
+
error_matching(invalid_value, "invalid_#{@type}".to_sym) &&
|
59
|
+
no_error_matching(valid_value, "invalid_#{@type}".to_sym)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_option(option)
|
63
|
+
settings = OPTION_TEST_SETTINGS[option]
|
64
|
+
boundary = parse_and_cast(@options[option])
|
65
|
+
|
66
|
+
method = settings[:method]
|
67
|
+
|
68
|
+
valid_value, invalid_value = if settings[:modify_on] == :valid
|
69
|
+
[ boundary.send(method, 1), boundary ]
|
70
|
+
else
|
71
|
+
[ boundary, boundary.send(method, 1) ]
|
72
|
+
end
|
73
|
+
|
74
|
+
error_matching(invalid_value, option) &&
|
75
|
+
no_error_matching(valid_value, option)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_before
|
79
|
+
before = parse_and_cast(@options[:before])
|
80
|
+
|
81
|
+
error_matching(before - 1, :before) &&
|
82
|
+
no_error_matching(before, :before)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_between
|
86
|
+
between = parse_and_cast(@options[:between])
|
87
|
+
|
88
|
+
error_matching(between.first - 1, :between) &&
|
89
|
+
error_matching(between.last + 1, :between) &&
|
90
|
+
no_error_matching(between.first, :between) &&
|
91
|
+
no_error_matching(between.last, :between)
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_and_cast(value)
|
95
|
+
value = @validator.send(:restriction_value, value, @record)
|
96
|
+
@validator.send(:type_cast_value, value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def error_matching(value, option)
|
100
|
+
match = error_message_for(option)
|
101
|
+
@record.send("#{@expected}=", value)
|
102
|
+
@record.valid?
|
103
|
+
errors = @record.errors.on(@expected)
|
104
|
+
pass = [ errors ].flatten.any? {|error| /#{match}/ === error }
|
105
|
+
@last_failure = "error matching '#{match}' when value is #{format_value(value)}" unless pass
|
106
|
+
pass
|
107
|
+
end
|
108
|
+
|
109
|
+
def no_error_matching(value, option)
|
110
|
+
pass = !error_matching(value, option)
|
111
|
+
unless pass
|
112
|
+
error = error_message_for(option)
|
113
|
+
@last_failure = "no error matching '#{error}' when value is #{format_value(value)}"
|
114
|
+
end
|
115
|
+
pass
|
116
|
+
end
|
117
|
+
|
118
|
+
def error_message_for(option)
|
119
|
+
msg = @validator.send(:error_messages)[option]
|
120
|
+
restriction = @validator.send(:restriction_value, @validator.configuration[option], @record)
|
121
|
+
|
122
|
+
if restriction
|
123
|
+
restriction = [restriction] unless restriction.is_a?(Array)
|
124
|
+
restriction.map! {|r| @validator.send(:type_cast_value, r) }
|
125
|
+
interpolate = @validator.send(:interpolation_values, option, restriction )
|
126
|
+
if defined?(I18n)
|
127
|
+
msg = @record.errors.generate_message(@expected, option, interpolate)
|
128
|
+
else
|
129
|
+
msg = msg % interpolate
|
130
|
+
end
|
131
|
+
end
|
132
|
+
msg
|
133
|
+
end
|
134
|
+
|
135
|
+
def format_value(value)
|
136
|
+
return value if value.is_a?(String)
|
137
|
+
value.strftime(ValidatesTimeliness::Validator.error_value_formats[@type])
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_date(attribute, options={})
|
142
|
+
options[:type] = :date
|
143
|
+
ValidateTimeliness.new(attribute, options)
|
144
|
+
end
|
145
|
+
|
146
|
+
def validate_time(attribute, options={})
|
147
|
+
options[:type] = :time
|
148
|
+
ValidateTimeliness.new(attribute, options)
|
149
|
+
end
|
150
|
+
|
151
|
+
def validate_datetime(attribute, options={})
|
152
|
+
options[:type] = :datetime
|
153
|
+
ValidateTimeliness.new(attribute, options)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module ValidatesTimeliness
|
2
|
+
module ValidationMethods
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def parse_date_time(raw_value, type, strict=true)
|
11
|
+
return nil if raw_value.blank?
|
12
|
+
return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
|
13
|
+
|
14
|
+
time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict)
|
15
|
+
raise if time_array.nil?
|
16
|
+
|
17
|
+
# Rails dummy time date part is defined as 2000-01-01
|
18
|
+
time_array[0..2] = 2000, 1, 1 if type == :time
|
19
|
+
|
20
|
+
# Date.new enforces days per month, unlike Time
|
21
|
+
date = Date.new(*time_array[0..2]) unless type == :time
|
22
|
+
|
23
|
+
return date if type == :date
|
24
|
+
|
25
|
+
# Create time object which checks time part, and return time object
|
26
|
+
make_time(time_array)
|
27
|
+
rescue
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def validates_time(*attr_names)
|
32
|
+
configuration = attr_names.extract_options!
|
33
|
+
configuration[:type] = :time
|
34
|
+
validates_timeliness_of(attr_names, configuration)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validates_date(*attr_names)
|
38
|
+
configuration = attr_names.extract_options!
|
39
|
+
configuration[:type] = :date
|
40
|
+
validates_timeliness_of(attr_names, configuration)
|
41
|
+
end
|
42
|
+
|
43
|
+
def validates_datetime(*attr_names)
|
44
|
+
configuration = attr_names.extract_options!
|
45
|
+
configuration[:type] = :datetime
|
46
|
+
validates_timeliness_of(attr_names, configuration)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def validates_timeliness_of(attr_names, configuration)
|
52
|
+
validator = ValidatesTimeliness::Validator.new(configuration)
|
53
|
+
|
54
|
+
# bypass handling of allow_nil and allow_blank to validate raw value
|
55
|
+
configuration.delete(:allow_nil)
|
56
|
+
configuration.delete(:allow_blank)
|
57
|
+
validates_each(attr_names, configuration) do |record, attr_name, value|
|
58
|
+
validator.call(record, attr_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Time.zone. Rails 2.0 should be default_timezone.
|
63
|
+
def make_time(time_array)
|
64
|
+
if Time.respond_to?(:zone) && time_zone_aware_attributes
|
65
|
+
Time.zone.local(*time_array)
|
66
|
+
else
|
67
|
+
begin
|
68
|
+
Time.send(::ActiveRecord::Base.default_timezone, *time_array)
|
69
|
+
rescue ArgumentError, TypeError
|
70
|
+
zone_offset = ::ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0
|
71
|
+
time_array.pop # remove microseconds
|
72
|
+
DateTime.civil(*(time_array << zone_offset))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
ActiveRecord::Base.send(:include, ValidatesTimeliness::ValidationMethods)
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module ValidatesTimeliness
|
2
|
+
|
3
|
+
class Validator
|
4
|
+
cattr_accessor :ignore_restriction_errors
|
5
|
+
cattr_accessor :error_value_formats
|
6
|
+
|
7
|
+
self.ignore_restriction_errors = false
|
8
|
+
self.error_value_formats = {
|
9
|
+
:time => '%H:%M:%S',
|
10
|
+
:date => '%Y-%m-%d',
|
11
|
+
:datetime => '%Y-%m-%d %H:%M:%S'
|
12
|
+
}
|
13
|
+
|
14
|
+
RESTRICTION_METHODS = {
|
15
|
+
:before => :<,
|
16
|
+
:after => :>,
|
17
|
+
:on_or_before => :<=,
|
18
|
+
:on_or_after => :>=,
|
19
|
+
:between => lambda {|v, r| (r.first..r.last).include?(v) }
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_reader :configuration, :type
|
23
|
+
|
24
|
+
def initialize(configuration)
|
25
|
+
defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
|
26
|
+
@configuration = defaults.merge(configuration)
|
27
|
+
@type = @configuration.delete(:type)
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(record, attr_name)
|
31
|
+
value = record.send(attr_name)
|
32
|
+
value = record.class.parse_date_time(value, type, false) if value.is_a?(String)
|
33
|
+
raw_value = raw_value(record, attr_name)
|
34
|
+
|
35
|
+
return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
|
36
|
+
|
37
|
+
add_error(record, attr_name, :blank) and return if raw_value.blank?
|
38
|
+
|
39
|
+
add_error(record, attr_name, "invalid_#{type}".to_sym) and return unless value
|
40
|
+
|
41
|
+
validate_restrictions(record, attr_name, value)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def raw_value(record, attr_name)
|
47
|
+
record.send("#{attr_name}_before_type_cast")
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_restrictions(record, attr_name, value)
|
51
|
+
value = type_cast_value(value)
|
52
|
+
|
53
|
+
RESTRICTION_METHODS.each do |option, method|
|
54
|
+
next unless restriction = configuration[option]
|
55
|
+
begin
|
56
|
+
restriction = restriction_value(restriction, record)
|
57
|
+
next if restriction.nil?
|
58
|
+
restriction = type_cast_value(restriction)
|
59
|
+
|
60
|
+
unless evaluate_restriction(restriction, value, method)
|
61
|
+
add_error(record, attr_name, option, interpolation_values(option, restriction))
|
62
|
+
end
|
63
|
+
rescue
|
64
|
+
unless self.class.ignore_restriction_errors
|
65
|
+
add_error(record, attr_name, "restriction '#{option}' value was invalid")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def interpolation_values(option, restriction)
|
72
|
+
format = self.class.error_value_formats[type]
|
73
|
+
restriction = [restriction] unless restriction.is_a?(Array)
|
74
|
+
|
75
|
+
if defined?(I18n)
|
76
|
+
message = custom_error_messages[option] || I18n.translate('activerecord.errors.messages')[option]
|
77
|
+
subs = message.scan(/\{\{([^\}]*)\}\}/)
|
78
|
+
interpolations = {}
|
79
|
+
subs.each_with_index {|s, i| interpolations[s[0].to_sym] = restriction[i].strftime(format) }
|
80
|
+
interpolations
|
81
|
+
else
|
82
|
+
restriction.map {|r| r.strftime(format) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def evaluate_restriction(restriction, value, comparator)
|
87
|
+
return true if restriction.nil?
|
88
|
+
|
89
|
+
case comparator
|
90
|
+
when Symbol
|
91
|
+
value.send(comparator, restriction)
|
92
|
+
when Proc
|
93
|
+
comparator.call(value, restriction)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_error(record, attr_name, message, interpolate=nil)
|
98
|
+
if defined?(I18n)
|
99
|
+
# use i18n support in AR for message or use custom message passed to validation method
|
100
|
+
custom = custom_error_messages[message]
|
101
|
+
record.errors.add(attr_name, custom || message, interpolate || {})
|
102
|
+
else
|
103
|
+
message = error_messages[message] if message.is_a?(Symbol)
|
104
|
+
message = message % interpolate
|
105
|
+
record.errors.add(attr_name, message)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def error_messages
|
110
|
+
return @error_messages if defined?(@error_messages)
|
111
|
+
@error_messages = ValidatesTimeliness.default_error_messages.merge(custom_error_messages)
|
112
|
+
end
|
113
|
+
|
114
|
+
def custom_error_messages
|
115
|
+
return @custom_error_messages if defined?(@custom_error_messages)
|
116
|
+
@custom_error_messages = configuration.inject({}) {|msgs, (k, v)|
|
117
|
+
if md = /(.*)_message$/.match(k.to_s)
|
118
|
+
msgs[md[0].to_sym] = v
|
119
|
+
end
|
120
|
+
msgs
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def restriction_value(restriction, record)
|
125
|
+
case restriction
|
126
|
+
when Time, Date, DateTime
|
127
|
+
restriction
|
128
|
+
when Symbol
|
129
|
+
restriction_value(record.send(restriction), record)
|
130
|
+
when Proc
|
131
|
+
restriction_value(restriction.call(record), record)
|
132
|
+
when Array
|
133
|
+
restriction.map {|r| restriction_value(r, record) }.sort
|
134
|
+
when Range
|
135
|
+
restriction_value([restriction.first, restriction.last], record)
|
136
|
+
else
|
137
|
+
record.class.parse_date_time(restriction, type, false)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def type_cast_value(value)
|
142
|
+
if value.is_a?(Array)
|
143
|
+
value.map {|v| type_cast_value(v) }
|
144
|
+
else
|
145
|
+
case type
|
146
|
+
when :time
|
147
|
+
value.to_dummy_time
|
148
|
+
when :date
|
149
|
+
value.to_date
|
150
|
+
when :datetime
|
151
|
+
if value.is_a?(DateTime) || value.is_a?(Time)
|
152
|
+
value.to_time
|
153
|
+
else
|
154
|
+
value.to_time(ValidatesTimeliness.default_timezone)
|
155
|
+
end
|
156
|
+
else
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe ValidatesTimeliness::ActionView::InstanceTag, :type => :helper do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@person = Person.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should display invalid datetime as datetime_select values" do
|
10
|
+
@person.birth_date_and_time = "2008-02-30 12:00:22"
|
11
|
+
output = datetime_select(:person, :birth_date_and_time, :include_blank => true, :include_seconds => true)
|
12
|
+
|
13
|
+
output.should have_tag('select[id=person_birth_date_and_time_1i]') do
|
14
|
+
with_tag('option[selected=selected]', '2008')
|
15
|
+
end
|
16
|
+
output.should have_tag('select[id=person_birth_date_and_time_2i]') do
|
17
|
+
with_tag('option[selected=selected]', 'February')
|
18
|
+
end
|
19
|
+
output.should have_tag('select[id=person_birth_date_and_time_3i]') do
|
20
|
+
with_tag('option[selected=selected]', '30')
|
21
|
+
end
|
22
|
+
output.should have_tag('select[id=person_birth_date_and_time_4i]') do
|
23
|
+
with_tag('option[selected=selected]', '12')
|
24
|
+
end
|
25
|
+
output.should have_tag('select[id=person_birth_date_and_time_5i]') do
|
26
|
+
with_tag('option[selected=selected]', '00')
|
27
|
+
end
|
28
|
+
output.should have_tag('select[id=person_birth_date_and_time_6i]') do
|
29
|
+
with_tag('option[selected=selected]', '22')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should display datetime_select when datetime value is nil" do
|
34
|
+
@person.birth_date_and_time = nil
|
35
|
+
output = datetime_select(:person, :birth_date_and_time, :include_blank => true, :include_seconds => true)
|
36
|
+
output.should have_tag('select', 6)
|
37
|
+
end
|
38
|
+
end
|