validates_timeliness 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/CHANGELOG +43 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +320 -0
  4. data/Rakefile +58 -0
  5. data/TODO +8 -0
  6. data/lib/validates_timeliness.rb +66 -0
  7. data/lib/validates_timeliness/action_view/instance_tag.rb +45 -0
  8. data/lib/validates_timeliness/active_record/attribute_methods.rb +157 -0
  9. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +64 -0
  10. data/lib/validates_timeliness/core_ext/date.rb +13 -0
  11. data/lib/validates_timeliness/core_ext/date_time.rb +13 -0
  12. data/lib/validates_timeliness/core_ext/time.rb +13 -0
  13. data/lib/validates_timeliness/formats.rb +310 -0
  14. data/lib/validates_timeliness/locale/en.yml +11 -0
  15. data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +121 -0
  16. data/lib/validates_timeliness/validation_methods.rb +82 -0
  17. data/lib/validates_timeliness/validator.rb +120 -0
  18. data/spec/action_view/instance_tag_spec.rb +38 -0
  19. data/spec/active_record/attribute_methods_spec.rb +204 -0
  20. data/spec/active_record/multiparameter_attributes_spec.rb +48 -0
  21. data/spec/core_ext/dummy_time_spec.rb +31 -0
  22. data/spec/formats_spec.rb +274 -0
  23. data/spec/ginger_scenarios.rb +19 -0
  24. data/spec/resources/application.rb +2 -0
  25. data/spec/resources/person.rb +3 -0
  26. data/spec/resources/schema.rb +10 -0
  27. data/spec/resources/sqlite_patch.rb +19 -0
  28. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +178 -0
  29. data/spec/spec_helper.rb +54 -0
  30. data/spec/time_travel/MIT-LICENSE +20 -0
  31. data/spec/time_travel/time_extensions.rb +33 -0
  32. data/spec/time_travel/time_travel.rb +12 -0
  33. data/spec/validation_methods_spec.rb +61 -0
  34. data/spec/validator_spec.rb +438 -0
  35. metadata +105 -0
@@ -0,0 +1,11 @@
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}}"
@@ -0,0 +1,121 @@
1
+ module Spec
2
+ module Rails
3
+ module Matchers
4
+ class ValidateTimeliness
5
+ cattr_accessor :test_values
6
+
7
+ @@test_values = {
8
+ :date => {:pass => '2000-01-01', :fail => '2000-01-32'},
9
+ :time => {:pass => '12:00', :fail => '25:00'},
10
+ :datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'}
11
+ }
12
+
13
+ def initialize(attribute, options)
14
+ @expected, @options = attribute, options
15
+ @validator = ValidatesTimeliness::Validator.new(options)
16
+ compile_error_messages
17
+ end
18
+
19
+ def compile_error_messages
20
+ messages = validator.send(:error_messages)
21
+ @messages = messages.inject({}) {|h, (k, v)| h[k] = v.gsub(/ (\%s|\{\{\w*\}\})/, ''); h }
22
+ end
23
+
24
+ def matches?(record)
25
+ @record = record
26
+ type = options[:type]
27
+
28
+ invalid_value = @@test_values[type][:fail]
29
+ valid_value = parse_and_cast(@@test_values[type][:pass])
30
+ valid = error_matching(invalid_value, /#{messages["invalid_#{type}".to_sym]}/) &&
31
+ no_error_matching(valid_value, /#{messages["invalid_#{type}".to_sym]}/)
32
+
33
+ valid = test_option(:before, :-) if options[:before] && valid
34
+ valid = test_option(:after, :+) if options[:after] && valid
35
+
36
+ valid = test_option(:on_or_before, :+, :modify_on => :invalid) if options[:on_or_before] && valid
37
+ valid = test_option(:on_or_after, :-, :modify_on => :invalid) if options[:on_or_after] && valid
38
+
39
+ return valid
40
+ end
41
+
42
+ def failure_message
43
+ "expected model to validate #{options[:type]} attribute #{expected.inspect} with #{last_failure}"
44
+ end
45
+
46
+ def negative_failure_message
47
+ "expected not to validate #{options[:type]} attribute #{expected.inspect}"
48
+ end
49
+
50
+ def description
51
+ "have validated #{options[:type]} attribute #{expected.inspect}"
52
+ end
53
+
54
+ private
55
+ attr_reader :actual, :expected, :record, :options, :messages, :last_failure, :validator
56
+
57
+ def test_option(option, modifier, settings={})
58
+ settings.reverse_merge!(:modify_on => :valid)
59
+ boundary = parse_and_cast(options[option])
60
+
61
+ valid_value, invalid_value = if settings[:modify_on] == :valid
62
+ [ boundary.send(modifier, 1), boundary ]
63
+ else
64
+ [ boundary, boundary.send(modifier, 1) ]
65
+ end
66
+
67
+ message = messages[option]
68
+ error_matching(invalid_value, /#{message}/) &&
69
+ no_error_matching(valid_value, /#{message}/)
70
+ end
71
+
72
+ def parse_and_cast(value)
73
+ value = validator.send(:restriction_value, value, record)
74
+ validator.send(:type_cast_value, value)
75
+ end
76
+
77
+ def error_matching(value, match)
78
+ record.send("#{expected}=", value)
79
+ record.valid?
80
+ errors = record.errors.on(expected)
81
+ pass = [ errors ].flatten.any? {|error| match === error }
82
+ @last_failure = "error matching #{match.inspect} when value is #{format_value(value)}" unless pass
83
+ pass
84
+ end
85
+
86
+ def no_error_matching(value, match)
87
+ pass = !error_matching(value, match)
88
+ @last_failure = "no error matching #{match.inspect} when value is #{format_value(value)}" unless pass
89
+ pass
90
+ end
91
+
92
+ def format_value(value)
93
+ return value if value.is_a?(String)
94
+ value.strftime(ValidatesTimeliness::Validator.error_value_formats[options[:type]])
95
+ end
96
+ end
97
+
98
+ def validate_date(attribute, options={})
99
+ options[:type] = :date
100
+ validate_timeliness_of(attribute, options)
101
+ end
102
+
103
+ def validate_time(attribute, options={})
104
+ options[:type] = :time
105
+ validate_timeliness_of(attribute, options)
106
+ end
107
+
108
+ def validate_datetime(attribute, options={})
109
+ options[:type] = :datetime
110
+ validate_timeliness_of(attribute, options)
111
+ end
112
+
113
+ private
114
+
115
+ def validate_timeliness_of(attribute, options={})
116
+ ValidateTimeliness.new(attribute, options)
117
+ end
118
+
119
+ end
120
+ end
121
+ 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,120 @@
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
+ attr_reader :configuration, :type
15
+
16
+ def initialize(configuration)
17
+ defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
18
+ @configuration = defaults.merge(configuration)
19
+ @type = @configuration.delete(:type)
20
+ end
21
+
22
+ def call(record, attr_name)
23
+ value = record.send(attr_name)
24
+ value = record.class.parse_date_time(value, type, false) if value.is_a?(String)
25
+ raw_value = raw_value(record, attr_name)
26
+
27
+ return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
28
+
29
+ add_error(record, attr_name, :blank) and return if raw_value.blank?
30
+
31
+ add_error(record, attr_name, "invalid_#{type}".to_sym) and return unless value
32
+
33
+ validate_restrictions(record, attr_name, value)
34
+ end
35
+
36
+ private
37
+
38
+ def raw_value(record, attr_name)
39
+ record.send("#{attr_name}_before_type_cast")
40
+ end
41
+
42
+ def validate_restrictions(record, attr_name, value)
43
+ restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='}
44
+
45
+ display = self.class.error_value_formats[type]
46
+
47
+ value = type_cast_value(value)
48
+
49
+ restriction_methods.each do |option, method|
50
+ next unless restriction = configuration[option]
51
+ begin
52
+ compare = restriction_value(restriction, record)
53
+ next if compare.nil?
54
+ compare = type_cast_value(compare)
55
+
56
+ unless value.send(method, compare)
57
+ add_error(record, attr_name, option, :restriction => compare.strftime(display))
58
+ end
59
+ rescue
60
+ unless self.class.ignore_restriction_errors
61
+ add_error(record, attr_name, "restriction '#{option}' value was invalid")
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def add_error(record, attr_name, message, interpolate={})
68
+ if Rails::VERSION::STRING < '2.2'
69
+ message = error_messages[message] if message.is_a?(Symbol)
70
+ message = message % interpolate.values unless interpolate.empty?
71
+ record.errors.add(attr_name, message)
72
+ else
73
+ # use i18n support in AR for message or use custom message passed to validation method
74
+ custom = custom_error_messages[message]
75
+ record.errors.add(attr_name, custom || message, interpolate)
76
+ end
77
+ end
78
+
79
+ def error_messages
80
+ return @error_messages if defined?(@error_messages)
81
+ @error_messages = ValidatesTimeliness.default_error_messages.merge(custom_error_messages)
82
+ end
83
+
84
+ def custom_error_messages
85
+ return @custom_error_messages if defined?(@custom_error_messages)
86
+ @custom_error_messages = configuration.inject({}) {|h, (k, v)| h[$1.to_sym] = v if k.to_s =~ /(.*)_message$/;h }
87
+ end
88
+
89
+ def restriction_value(restriction, record)
90
+ case restriction
91
+ when Time, Date, DateTime
92
+ restriction
93
+ when Symbol
94
+ restriction_value(record.send(restriction), record)
95
+ when Proc
96
+ restriction_value(restriction.call(record), record)
97
+ else
98
+ record.class.parse_date_time(restriction, type, false)
99
+ end
100
+ end
101
+
102
+ def type_cast_value(value)
103
+ case type
104
+ when :time
105
+ value.to_dummy_time
106
+ when :date
107
+ value.to_date
108
+ when :datetime
109
+ if value.is_a?(DateTime) || value.is_a?(Time)
110
+ value.to_time
111
+ else
112
+ value.to_time(ValidatesTimelines.default_timezone)
113
+ end
114
+ else
115
+ nil
116
+ end
117
+ end
118
+
119
+ end
120
+ 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
@@ -0,0 +1,204 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
4
+ include ValidatesTimeliness::ActiveRecord::AttributeMethods
5
+ include ValidatesTimeliness::ValidationMethods
6
+
7
+ before do
8
+ @person = Person.new
9
+ end
10
+
11
+ it "should call write_date_time_attribute when date attribute assigned value" do
12
+ @person.should_receive(:write_date_time_attribute)
13
+ @person.birth_date = "2000-01-01"
14
+ end
15
+
16
+ it "should call write_date_time_attribute when time attribute assigned value" do
17
+ @person.should_receive(:write_date_time_attribute)
18
+ @person.birth_time = "12:00"
19
+ end
20
+
21
+ it "should call write_date_time_attribute when datetime attribute assigned value" do
22
+ @person.should_receive(:write_date_time_attribute)
23
+ @person.birth_date_and_time = "2000-01-01 12:00"
24
+ end
25
+
26
+ it "should call parser on write for datetime attribute" do
27
+ @person.class.should_receive(:parse_date_time).once
28
+ @person.birth_date_and_time = "2000-01-01 02:03:04"
29
+ end
30
+
31
+ it "should call parser on write for date attribute" do
32
+ @person.class.should_receive(:parse_date_time).once
33
+ @person.birth_date = "2000-01-01"
34
+ end
35
+
36
+ it "should call parser on write for time attribute" do
37
+ @person.class.should_receive(:parse_date_time).once
38
+ @person.birth_time = "12:00"
39
+ end
40
+
41
+ it "should return raw string value for attribute_before_type_cast when written as string" do
42
+ time_string = "2000-01-01 02:03:04"
43
+ @person.birth_date_and_time = time_string
44
+ @person.birth_date_and_time_before_type_cast.should == time_string
45
+ end
46
+
47
+ it "should return Time object for attribute_before_type_cast when written as Time" do
48
+ @person.birth_date_and_time = Time.mktime(2000, 1, 1, 2, 3, 4)
49
+ @person.birth_date_and_time_before_type_cast.should be_kind_of(Time)
50
+ end
51
+
52
+ it "should return Time object for datetime attribute read method when assigned Time object" do
53
+ @person.birth_date_and_time = Time.now
54
+ @person.birth_date_and_time.should be_kind_of(Time)
55
+ end
56
+
57
+ it "should return Time object for datetime attribute read method when assigned string" do
58
+ @person.birth_date_and_time = "2000-01-01 02:03:04"
59
+ @person.birth_date_and_time.should be_kind_of(Time)
60
+ end
61
+
62
+ it "should return Date object for date attribute read method when assigned Date object" do
63
+ @person.birth_date = Date.today
64
+ @person.birth_date.should be_kind_of(Date)
65
+ end
66
+
67
+ it "should return Date object for date attribute read method when assigned string" do
68
+ @person.birth_date = '2000-01-01'
69
+ @person.birth_date.should be_kind_of(Date)
70
+ end
71
+
72
+ it "should return nil when time is invalid" do
73
+ @person.birth_date_and_time = "2000-01-32 02:03:04"
74
+ @person.birth_date_and_time.should be_nil
75
+ end
76
+
77
+ it "should not save invalid date value to database" do
78
+ time_string = "2000-01-32 02:03:04"
79
+ @person = Person.new
80
+ @person.birth_date_and_time = time_string
81
+ @person.save
82
+ @person.reload
83
+ @person.birth_date_and_time_before_type_cast.should be_nil
84
+ end
85
+
86
+ unless RAILS_VER < '2.1'
87
+ it "should return stored time string as Time with correct timezone" do
88
+ Time.zone = 'Melbourne'
89
+ time_string = "2000-06-01 02:03:04"
90
+ @person.birth_date_and_time = time_string
91
+ @person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z %z').should == time_string + ' EST +1000'
92
+ end
93
+
94
+ it "should return time object from database in correct timezone" do
95
+ Time.zone = 'Melbourne'
96
+ time_string = "2000-06-01 09:00:00"
97
+ @person = Person.new
98
+ @person.birth_date_and_time = time_string
99
+ @person.save
100
+ @person.reload
101
+ @person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z %z').should == time_string + ' EST +1000'
102
+ end
103
+
104
+ describe "dirty attributes" do
105
+
106
+ it "should return true for attribute changed? when value updated" do
107
+ time_string = "2000-01-01 02:03:04"
108
+ @person.birth_date_and_time = time_string
109
+ @person.birth_date_and_time_changed?.should be_true
110
+ end
111
+
112
+ it "should show changes when time attribute changed from nil to Time object" do
113
+ time_string = "2000-01-01 02:03:04"
114
+ @person.birth_date_and_time = time_string
115
+ time = @person.birth_date_and_time
116
+ @person.changes.should == {"birth_date_and_time" => [nil, time]}
117
+ end
118
+
119
+ it "should show changes when time attribute changed from Time object to nil" do
120
+ time_string = "2020-01-01 02:03:04"
121
+ @person.birth_date_and_time = time_string
122
+ @person.save false
123
+ @person.reload
124
+ time = @person.birth_date_and_time
125
+ @person.birth_date_and_time = nil
126
+ @person.changes.should == {"birth_date_and_time" => [time, nil]}
127
+ end
128
+
129
+ it "should show no changes when assigned same value as Time object" do
130
+ time_string = "2020-01-01 02:03:04"
131
+ @person.birth_date_and_time = time_string
132
+ @person.save false
133
+ @person.reload
134
+ time = @person.birth_date_and_time
135
+ @person.birth_date_and_time = time
136
+ @person.changes.should == {}
137
+ end
138
+
139
+ it "should show no changes when assigned same value as time string" do
140
+ time_string = "2020-01-01 02:03:04"
141
+ @person.birth_date_and_time = time_string
142
+ @person.save false
143
+ @person.reload
144
+ @person.birth_date_and_time = time_string
145
+ @person.changes.should == {}
146
+ end
147
+
148
+ end
149
+ else
150
+
151
+ it "should return time object from database in default timezone" do
152
+ ActiveRecord::Base.default_timezone = :utc
153
+ time_string = "2000-01-01 09:00:00"
154
+ @person = Person.new
155
+ @person.birth_date_and_time = time_string
156
+ @person.save
157
+ @person.reload
158
+ @person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z').should == time_string + ' GMT'
159
+ end
160
+
161
+ end
162
+
163
+ it "should return same time object on repeat reads on existing object" do
164
+ Time.zone = 'Melbourne' unless RAILS_VER < '2.1'
165
+ time_string = "2000-01-01 09:00:00"
166
+ @person = Person.new
167
+ @person.birth_date_and_time = time_string
168
+ @person.save!
169
+ @person.reload
170
+ time = @person.birth_date_and_time
171
+ @person.birth_date_and_time.should == time
172
+ end
173
+
174
+ it "should return same date object on repeat reads on existing object" do
175
+ date_string = Date.today
176
+ @person = Person.new
177
+ @person.birth_date = date_string
178
+ @person.save!
179
+ @person.reload
180
+ date = @person.birth_date
181
+ @person.birth_date.should == date
182
+ end
183
+
184
+ it "should return correct date value after new value assigned" do
185
+ today = Date.today
186
+ tomorrow = Date.today + 1.day
187
+ @person = Person.new
188
+ @person.birth_date = today
189
+ @person.birth_date.should == today
190
+ @person.birth_date = tomorrow
191
+ @person.birth_date.should == tomorrow
192
+ end
193
+
194
+ it "should update date attribute on existing object" do
195
+ today = Date.today
196
+ tomorrow = Date.today + 1.day
197
+ @person = Person.create(:birth_date => today)
198
+ @person.birth_date = tomorrow
199
+ @person.save!
200
+ @person.reload
201
+ @person.birth_date.should == tomorrow
202
+ end
203
+
204
+ end