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.
Files changed (35) hide show
  1. data/CHANGELOG +49 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +329 -0
  4. data/Rakefile +58 -0
  5. data/TODO +7 -0
  6. data/lib/validates_timeliness.rb +67 -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 +309 -0
  14. data/lib/validates_timeliness/locale/en.yml +12 -0
  15. data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +157 -0
  16. data/lib/validates_timeliness/validation_methods.rb +82 -0
  17. data/lib/validates_timeliness/validator.rb +163 -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 +206 -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 +475 -0
  35. 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