jc-validates_timeliness 3.1.0

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