jc-validates_timeliness 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +14 -0
  3. data/CHANGELOG.rdoc +183 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +301 -0
  6. data/Rakefile +30 -0
  7. data/gemfiles/mongoid_2_1.gemfile +16 -0
  8. data/gemfiles/mongoid_2_2.gemfile +16 -0
  9. data/gemfiles/mongoid_2_3.gemfile +16 -0
  10. data/gemfiles/mongoid_2_4.gemfile +16 -0
  11. data/gemfiles/rails_3_0.gemfile +15 -0
  12. data/gemfiles/rails_3_1.gemfile +15 -0
  13. data/gemfiles/rails_3_2.gemfile +15 -0
  14. data/init.rb +1 -0
  15. data/lib/generators/validates_timeliness/install_generator.rb +16 -0
  16. data/lib/generators/validates_timeliness/templates/en.yml +16 -0
  17. data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +40 -0
  18. data/lib/validates_timeliness.rb +70 -0
  19. data/lib/validates_timeliness/attribute_methods.rb +92 -0
  20. data/lib/validates_timeliness/conversion.rb +70 -0
  21. data/lib/validates_timeliness/extensions.rb +14 -0
  22. data/lib/validates_timeliness/extensions/date_time_select.rb +61 -0
  23. data/lib/validates_timeliness/extensions/multiparameter_handler.rb +80 -0
  24. data/lib/validates_timeliness/helper_methods.rb +23 -0
  25. data/lib/validates_timeliness/orm/active_record.rb +53 -0
  26. data/lib/validates_timeliness/orm/mongoid.rb +63 -0
  27. data/lib/validates_timeliness/railtie.rb +15 -0
  28. data/lib/validates_timeliness/validator.rb +117 -0
  29. data/lib/validates_timeliness/version.rb +3 -0
  30. data/spec/spec_helper.rb +100 -0
  31. data/spec/support/config_helper.rb +36 -0
  32. data/spec/support/model_helpers.rb +27 -0
  33. data/spec/support/tag_matcher.rb +35 -0
  34. data/spec/support/test_model.rb +60 -0
  35. data/spec/validates_timeliness/attribute_methods_spec.rb +86 -0
  36. data/spec/validates_timeliness/conversion_spec.rb +234 -0
  37. data/spec/validates_timeliness/extensions/date_time_select_spec.rb +163 -0
  38. data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +44 -0
  39. data/spec/validates_timeliness/helper_methods_spec.rb +30 -0
  40. data/spec/validates_timeliness/orm/active_record_spec.rb +244 -0
  41. data/spec/validates_timeliness/orm/mongoid_spec.rb +189 -0
  42. data/spec/validates_timeliness/validator/after_spec.rb +57 -0
  43. data/spec/validates_timeliness/validator/before_spec.rb +57 -0
  44. data/spec/validates_timeliness/validator/is_at_spec.rb +61 -0
  45. data/spec/validates_timeliness/validator/on_or_after_spec.rb +57 -0
  46. data/spec/validates_timeliness/validator/on_or_before_spec.rb +57 -0
  47. data/spec/validates_timeliness/validator_spec.rb +246 -0
  48. data/spec/validates_timeliness_spec.rb +43 -0
  49. data/validates_timeliness.gemspec +20 -0
  50. metadata +128 -0
@@ -0,0 +1,14 @@
1
+ module ValidatesTimeliness
2
+ module Extensions
3
+ autoload :DateTimeSelect, 'validates_timeliness/extensions/date_time_select'
4
+ autoload :MultiparameterHandler, 'validates_timeliness/extensions/multiparameter_handler'
5
+ end
6
+
7
+ def self.enable_date_time_select_extension!
8
+ ::ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::Extensions::DateTimeSelect)
9
+ end
10
+
11
+ def self.enable_multiparameter_extension!
12
+ ::ActiveRecord::Base.send(:include, ValidatesTimeliness::Extensions::MultiparameterHandler)
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ module ValidatesTimeliness
2
+ module Extensions
3
+ module DateTimeSelect
4
+ extend ActiveSupport::Concern
5
+
6
+ # Intercepts the date and time select helpers to reuse the values from
7
+ # the params rather than the parsed value. This allows invalid date/time
8
+ # values to be redisplayed instead of blanks to aid correction by the user.
9
+ # It's a minor usability improvement which is rarely an issue for the user.
10
+
11
+ included do
12
+ alias_method_chain :datetime_selector, :timeliness
13
+ alias_method_chain :value, :timeliness
14
+ end
15
+
16
+ class TimelinessDateTime
17
+ attr_accessor :year, :month, :day, :hour, :min, :sec
18
+
19
+ def initialize(year, month, day, hour, min, sec)
20
+ @year, @month, @day, @hour, @min, @sec = year, month, day, hour, min, sec
21
+ end
22
+
23
+ # adapted from activesupport/lib/active_support/core_ext/date_time/calculations.rb, line 36 (3.0.7)
24
+ def change(options)
25
+ TimelinessDateTime.new(
26
+ options[:year] || year,
27
+ options[:month] || month,
28
+ options[:day] || day,
29
+ options[:hour] || hour,
30
+ options[:min] || (options[:hour] ? 0 : min),
31
+ options[:sec] || ((options[:hour] || options[:min]) ? 0 : sec)
32
+ )
33
+ end
34
+ end
35
+
36
+ def datetime_selector_with_timeliness(*args)
37
+ @timeliness_date_or_time_tag = true
38
+ datetime_selector_without_timeliness(*args)
39
+ end
40
+
41
+ def value_with_timeliness(object)
42
+ unless @timeliness_date_or_time_tag && @template_object.params[@object_name]
43
+ return value_without_timeliness(object)
44
+ end
45
+
46
+ @template_object.params[@object_name]
47
+
48
+ pairs = @template_object.params[@object_name].select {|k,v| k =~ /^#{@method_name}\(/ }
49
+ return value_without_timeliness(object) if pairs.empty?
50
+
51
+ values = [nil] * 6
52
+ pairs.map do |(param, value)|
53
+ position = param.scan(/\((\d+)\w+\)/).first.first
54
+ values[position.to_i-1] = value.to_i
55
+ end
56
+
57
+ TimelinessDateTime.new(*values)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,80 @@
1
+ module ValidatesTimeliness
2
+ module Extensions
3
+ module MultiparameterHandler
4
+ extend ActiveSupport::Concern
5
+
6
+ # Stricter handling of date and time values from multiparameter
7
+ # assignment from the date/time select view helpers
8
+
9
+ included do
10
+ alias_method_chain :instantiate_time_object, :timeliness
11
+ alias_method :execute_callstack_for_multiparameter_attributes, :execute_callstack_for_multiparameter_attributes_with_timeliness
12
+ alias_method :read_value_from_parameter, :read_value_from_parameter_with_timeliness
13
+ end
14
+
15
+ private
16
+
17
+ def invalid_multiparameter_date_or_time_as_string(values)
18
+ value = [values[0], *values[1..2].map {|s| s.to_s.rjust(2,"0")} ].join("-")
19
+ value += ' ' + values[3..5].map {|s| s.to_s.rjust(2, "0") }.join(":") unless values[3..5].empty?
20
+ value
21
+ end
22
+
23
+ def instantiate_time_object_with_timeliness(name, values)
24
+ validate_multiparameter_date_values(values) {
25
+ instantiate_time_object_without_timeliness(name, values)
26
+ }
27
+ end
28
+
29
+ def instantiate_date_object(name, values)
30
+ validate_multiparameter_date_values(values) {
31
+ Date.new(*values)
32
+ }
33
+ end
34
+
35
+ # Yield if date values are valid
36
+ def validate_multiparameter_date_values(values)
37
+ if values[0..2].all?{ |v| v.present? } && Date.valid_civil?(*values[0..2])
38
+ yield
39
+ else
40
+ invalid_multiparameter_date_or_time_as_string(values)
41
+ end
42
+ end
43
+
44
+ def read_value_from_parameter_with_timeliness(name, values_from_param)
45
+ klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
46
+ values = values_from_param.is_a?(Hash) ? values_from_param.to_a.sort_by(&:first).map(&:last) : values_from_param
47
+
48
+ if values.empty? || values.all?{ |v| v.nil? }
49
+ nil
50
+ elsif klass == Time
51
+ instantiate_time_object(name, values)
52
+ elsif klass == Date
53
+ instantiate_date_object(name, values)
54
+ else
55
+ if respond_to?(:read_other_parameter_value)
56
+ read_date_parameter_value(name, values_from_param)
57
+ else
58
+ klass.new(*values)
59
+ end
60
+ end
61
+ end
62
+
63
+ def execute_callstack_for_multiparameter_attributes_with_timeliness(callstack)
64
+ errors = []
65
+ callstack.each do |name, values_with_empty_parameters|
66
+ begin
67
+ send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
68
+ rescue => ex
69
+ values = values_with_empty_parameters.is_a?(Hash) ? values_with_empty_parameters.values : values_with_empty_parameters
70
+ errors << ActiveRecord::AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
71
+ end
72
+ end
73
+ unless errors.empty?
74
+ raise ActiveRecord::MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module ValidatesTimeliness
2
+ VERSION = '3.1.0'
3
+ end
@@ -0,0 +1,100 @@
1
+ require 'rspec'
2
+ require 'rspec/collection_matchers'
3
+
4
+ require 'active_model'
5
+ require 'active_model/validations'
6
+ require 'active_record'
7
+ require 'action_view'
8
+ require 'timecop'
9
+
10
+ require 'validates_timeliness'
11
+
12
+ require 'support/test_model'
13
+ require 'support/model_helpers'
14
+ require 'support/config_helper'
15
+ require 'support/tag_matcher'
16
+
17
+ ValidatesTimeliness.setup do |c|
18
+ c.extend_orms = [ :active_record ]
19
+ c.enable_date_time_select_extension!
20
+ c.enable_multiparameter_extension!
21
+ c.default_timezone = :utc
22
+ end
23
+
24
+ Time.zone = 'Australia/Melbourne'
25
+
26
+ LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml')
27
+ I18n.load_path.unshift(LOCALE_PATH)
28
+
29
+ # Extend TestModel as you would another ORM/ODM module
30
+ module TestModelShim
31
+ extend ActiveSupport::Concern
32
+ include ValidatesTimeliness::AttributeMethods
33
+
34
+ module ClassMethods
35
+ # Hook method for attribute method generation
36
+ def define_attribute_methods(attr_names)
37
+ super
38
+ define_timeliness_methods
39
+ end
40
+
41
+ # Hook into native time zone handling check, if any
42
+ def timeliness_attribute_timezone_aware?(attr_name)
43
+ false
44
+ end
45
+ end
46
+ end
47
+
48
+ class Person
49
+ include TestModel
50
+ attribute :birth_date, :date
51
+ attribute :birth_time, :time
52
+ attribute :birth_datetime, :datetime
53
+
54
+ define_attribute_methods model_attributes.keys
55
+ end
56
+
57
+ class PersonWithShim < Person
58
+ include TestModelShim
59
+ end
60
+
61
+ ActiveRecord::Base.default_timezone = :utc
62
+ ActiveRecord::Base.time_zone_aware_attributes = true
63
+ ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
64
+ ActiveRecord::Migration.verbose = false
65
+ ActiveRecord::Schema.define(:version => 1) do
66
+ create_table :employees, :force => true do |t|
67
+ t.string :first_name
68
+ t.string :last_name
69
+ t.date :birth_date
70
+ t.time :birth_time
71
+ t.datetime :birth_datetime
72
+ end
73
+ end
74
+
75
+ class Employee < ActiveRecord::Base
76
+ attr_accessor :redefined_birth_date_called
77
+ validates_date :birth_date, :allow_nil => true
78
+ validates_date :birth_time, :allow_nil => true
79
+ validates_date :birth_datetime, :allow_nil => true
80
+
81
+ def birth_date=(value)
82
+ self.redefined_birth_date_called = true
83
+ super
84
+ end
85
+ end
86
+
87
+ RSpec.configure do |c|
88
+ c.mock_with :rspec
89
+ c.include(ModelHelpers)
90
+ c.include(ConfigHelper)
91
+ c.include(TagMatcher)
92
+ c.before do
93
+ reset_validation_setup_for(Person)
94
+ reset_validation_setup_for(PersonWithShim)
95
+ end
96
+
97
+ c.filter_run_excluding :active_record => lambda {|version|
98
+ !(::ActiveRecord::VERSION::STRING.to_s =~ /^#{version.to_s}/)
99
+ }
100
+ end