sp-validates_timeliness 3.1.2

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +183 -0
  3. data/LICENSE +20 -0
  4. data/README.md +323 -0
  5. data/Rakefile +30 -0
  6. data/init.rb +1 -0
  7. data/lib/generators/validates_timeliness/install_generator.rb +16 -0
  8. data/lib/generators/validates_timeliness/templates/en.yml +16 -0
  9. data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +40 -0
  10. data/lib/jc-validates_timeliness.rb +1 -0
  11. data/lib/validates_timeliness.rb +70 -0
  12. data/lib/validates_timeliness/attribute_methods.rb +95 -0
  13. data/lib/validates_timeliness/conversion.rb +70 -0
  14. data/lib/validates_timeliness/extensions.rb +14 -0
  15. data/lib/validates_timeliness/extensions/date_time_select.rb +61 -0
  16. data/lib/validates_timeliness/extensions/multiparameter_handler.rb +80 -0
  17. data/lib/validates_timeliness/helper_methods.rb +23 -0
  18. data/lib/validates_timeliness/orm/active_record.rb +53 -0
  19. data/lib/validates_timeliness/orm/mongoid.rb +63 -0
  20. data/lib/validates_timeliness/railtie.rb +15 -0
  21. data/lib/validates_timeliness/validator.rb +117 -0
  22. data/lib/validates_timeliness/version.rb +3 -0
  23. data/spec/spec_helper.rb +109 -0
  24. data/spec/support/config_helper.rb +36 -0
  25. data/spec/support/model_helpers.rb +27 -0
  26. data/spec/support/tag_matcher.rb +35 -0
  27. data/spec/support/test_model.rb +59 -0
  28. data/spec/validates_timeliness/attribute_methods_spec.rb +86 -0
  29. data/spec/validates_timeliness/conversion_spec.rb +234 -0
  30. data/spec/validates_timeliness/extensions/date_time_select_spec.rb +163 -0
  31. data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +44 -0
  32. data/spec/validates_timeliness/helper_methods_spec.rb +30 -0
  33. data/spec/validates_timeliness/orm/active_record_spec.rb +244 -0
  34. data/spec/validates_timeliness/orm/mongoid_spec.rb +189 -0
  35. data/spec/validates_timeliness/validator/after_spec.rb +57 -0
  36. data/spec/validates_timeliness/validator/before_spec.rb +57 -0
  37. data/spec/validates_timeliness/validator/is_at_spec.rb +61 -0
  38. data/spec/validates_timeliness/validator/on_or_after_spec.rb +57 -0
  39. data/spec/validates_timeliness/validator/on_or_before_spec.rb +57 -0
  40. data/spec/validates_timeliness/validator_spec.rb +246 -0
  41. data/spec/validates_timeliness_spec.rb +43 -0
  42. data/validates_timeliness.gemspec +25 -0
  43. metadata +157 -0
@@ -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.2'
3
+ end
@@ -0,0 +1,109 @@
1
+ require 'rspec'
2
+ require 'rspec/collection_matchers'
3
+
4
+ # Coveralls
5
+ require "coveralls"
6
+
7
+ Coveralls.wear! do
8
+ add_filter 'spec'
9
+ end
10
+
11
+ require 'active_model'
12
+ require 'active_model/validations'
13
+ require 'active_record'
14
+ require 'action_view'
15
+ require 'timecop'
16
+
17
+ require 'validates_timeliness'
18
+
19
+ require 'support/test_model'
20
+ require 'support/model_helpers'
21
+ require 'support/config_helper'
22
+ require 'support/tag_matcher'
23
+
24
+ ValidatesTimeliness.setup do |c|
25
+ c.extend_orms = [ :active_record ]
26
+ c.enable_date_time_select_extension!
27
+ c.enable_multiparameter_extension!
28
+ c.default_timezone = :utc
29
+ end
30
+
31
+ Time.zone = 'Australia/Melbourne'
32
+
33
+ LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml')
34
+ I18n.load_path.unshift(LOCALE_PATH)
35
+
36
+ # Extend TestModel as you would another ORM/ODM module
37
+ module TestModelShim
38
+ extend ActiveSupport::Concern
39
+ include ValidatesTimeliness::AttributeMethods
40
+
41
+ module ClassMethods
42
+ # Hook method for attribute method generation
43
+ def define_attribute_methods(attr_names)
44
+ super
45
+ define_timeliness_methods
46
+ end
47
+
48
+ # Hook into native time zone handling check, if any
49
+ def timeliness_attribute_timezone_aware?(attr_name)
50
+ false
51
+ end
52
+ end
53
+ end
54
+
55
+ class Person
56
+ include TestModel
57
+ attribute :birth_date, :date
58
+ attribute :birth_time, :time
59
+ attribute :birth_datetime, :datetime
60
+
61
+ define_attribute_methods model_attributes.keys
62
+ end
63
+
64
+ class PersonWithShim < Person
65
+ include TestModelShim
66
+ end
67
+
68
+ I18n.enforce_available_locales = false
69
+
70
+ ActiveRecord::Base.default_timezone = :utc
71
+ ActiveRecord::Base.time_zone_aware_attributes = true
72
+ ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
73
+ ActiveRecord::Migration.verbose = false
74
+ ActiveRecord::Schema.define(:version => 1) do
75
+ create_table :employees, :force => true do |t|
76
+ t.string :first_name
77
+ t.string :last_name
78
+ t.date :birth_date
79
+ t.time :birth_time
80
+ t.datetime :birth_datetime
81
+ end
82
+ end
83
+
84
+ class Employee < ActiveRecord::Base
85
+ attr_accessor :redefined_birth_date_called
86
+ validates_date :birth_date, :allow_nil => true
87
+ validates_date :birth_time, :allow_nil => true
88
+ validates_date :birth_datetime, :allow_nil => true
89
+
90
+ def birth_date=(value)
91
+ self.redefined_birth_date_called = true
92
+ super
93
+ end
94
+ end
95
+
96
+ RSpec.configure do |c|
97
+ c.mock_with :rspec
98
+ c.include(ModelHelpers)
99
+ c.include(ConfigHelper)
100
+ c.include(TagMatcher)
101
+ c.before do
102
+ reset_validation_setup_for(Person)
103
+ reset_validation_setup_for(PersonWithShim)
104
+ end
105
+
106
+ c.filter_run_excluding :active_record => lambda {|version|
107
+ !(::ActiveRecord::VERSION::STRING.to_s =~ /^#{version.to_s}/)
108
+ }
109
+ end
@@ -0,0 +1,36 @@
1
+ module ConfigHelper
2
+ extend ActiveSupport::Concern
3
+
4
+ # Justin French tip
5
+ def with_config(preference_name, temporary_value)
6
+ old_value = ValidatesTimeliness.send(preference_name)
7
+ ValidatesTimeliness.send(:"#{preference_name}=", temporary_value)
8
+ yield
9
+ ensure
10
+ ValidatesTimeliness.send(:"#{preference_name}=", old_value)
11
+ end
12
+
13
+ def reset_validation_setup_for(model_class)
14
+ model_class.reset_callbacks(:validate)
15
+ model_class._validators.clear
16
+ model_class.timeliness_validated_attributes = [] if model_class.respond_to?(:timeliness_validated_attributes)
17
+ model_class.undefine_attribute_methods
18
+ # This is a hack to avoid a disabled super method error message after an undef
19
+ model_class.instance_variable_set(:@generated_attribute_methods, nil)
20
+ model_class.instance_variable_set(:@generated_timeliness_methods, nil)
21
+ end
22
+
23
+ module ClassMethods
24
+ def with_config(preference_name, temporary_value)
25
+ original_config_value = ValidatesTimeliness.send(preference_name)
26
+
27
+ before(:all) do
28
+ ValidatesTimeliness.send(:"#{preference_name}=", temporary_value)
29
+ end
30
+
31
+ after(:all) do
32
+ ValidatesTimeliness.send(:"#{preference_name}=", original_config_value)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ module ModelHelpers
2
+
3
+ # Some test helpers from Rails source
4
+ def invalid!(attr_name, values, error = nil)
5
+ with_each_person_value(attr_name, values) do |record, value|
6
+ expect(record).to be_invalid
7
+ expect(record.errors[attr_name].size).to be >= 1
8
+ expect(record.errors[attr_name].first).to eq(error) if error
9
+ end
10
+ end
11
+
12
+ def valid!(attr_name, values)
13
+ with_each_person_value(attr_name, values) do |record, value|
14
+ expect(record).to be_valid
15
+ end
16
+ end
17
+
18
+ def with_each_person_value(attr_name, values)
19
+ record = Person.new
20
+ values = [values] unless values.is_a?(Array)
21
+ values.each do |value|
22
+ record.send("#{attr_name}=", value)
23
+ yield record, value
24
+ end
25
+ end
26
+
27
+ end