adzap-validates_timeliness 1.1.1

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 +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