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
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ require 'bundler'
2
+ require 'bundler/setup'
3
+
4
+ require 'appraisal'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ require 'rdoc/task'
9
+ require 'rspec/core/rake_task'
10
+
11
+ desc "Run specs"
12
+ RSpec::Core::RakeTask.new(:spec)
13
+
14
+ desc "Generate code coverage"
15
+ RSpec::Core::RakeTask.new(:coverage) do |t|
16
+ t.rcov = true
17
+ t.rcov_opts = ['--exclude', 'spec']
18
+ end
19
+
20
+ desc 'Generate documentation for plugin.'
21
+ Rake::RDocTask.new(:rdoc) do |rdoc|
22
+ rdoc.rdoc_dir = 'rdoc'
23
+ rdoc.title = 'ValidatesTimeliness'
24
+ rdoc.options << '--line-numbers' << '--inline-source'
25
+ rdoc.rdoc_files.include('README')
26
+ rdoc.rdoc_files.include('lib/**/*.rb')
27
+ end
28
+
29
+ desc 'Default: run specs.'
30
+ task :default => :spec
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'validates_timeliness'
@@ -0,0 +1,16 @@
1
+ module ValidatesTimeliness
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ desc "Copy ValidatesTimeliness default files"
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def copy_initializers
8
+ copy_file 'validates_timeliness.rb', 'config/initializers/validates_timeliness.rb'
9
+ end
10
+
11
+ def copy_locale_file
12
+ copy_file 'en.yml', 'config/locales/validates_timeliness.en.yml'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ invalid_date: "is not a valid date"
5
+ invalid_time: "is not a valid time"
6
+ invalid_datetime: "is not a valid datetime"
7
+ is_at: "must be at %{restriction}"
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
+ validates_timeliness:
13
+ error_value_formats:
14
+ date: '%Y-%m-%d'
15
+ time: '%H:%M:%S'
16
+ datetime: '%Y-%m-%d %H:%M:%S'
@@ -0,0 +1,40 @@
1
+ ValidatesTimeliness.setup do |config|
2
+ # Extend ORM/ODMs for full support (:active_record, :mongoid).
3
+ # config.extend_orms = [ :active_record ]
4
+ #
5
+ # Default timezone
6
+ # config.default_timezone = :utc
7
+ #
8
+ # Set the dummy date part for a time type values.
9
+ # config.dummy_date_for_time_type = [ 2000, 1, 1 ]
10
+ #
11
+ # Ignore errors when restriction options are evaluated
12
+ # config.ignore_restriction_errors = false
13
+ #
14
+ # Re-display invalid values in date/time selects
15
+ # config.enable_date_time_select_extension!
16
+ #
17
+ # Handle multiparameter date/time values strictly
18
+ # config.enable_multiparameter_extension!
19
+ #
20
+ # Shorthand date and time symbols for restrictions
21
+ # config.restriction_shorthand_symbols.update(
22
+ # :now => lambda { Time.current },
23
+ # :today => lambda { Date.current }
24
+ # )
25
+ #
26
+ # Use the plugin date/time parser which is stricter and extendable
27
+ # config.use_plugin_parser = false
28
+ #
29
+ # Add one or more formats making them valid. e.g. add_formats(:date, 'd(st|rd|th) of mmm, yyyy')
30
+ # config.parser.add_formats()
31
+ #
32
+ # Remove one or more formats making them invalid. e.g. remove_formats(:date, 'dd/mm/yyy')
33
+ # config.parser.remove_formats()
34
+ #
35
+ # Change the amiguous year threshold when parsing a 2 digit year
36
+ # config.parser.ambiguous_year_threshold = 30
37
+ #
38
+ # Treat ambiguous dates, such as 01/02/1950, as a Non-US date.
39
+ # config.parser.remove_us_formats
40
+ end
@@ -0,0 +1 @@
1
+ require "validates_timeliness"
@@ -0,0 +1,70 @@
1
+ require 'date'
2
+ require 'active_support/concern'
3
+ require 'active_support/core_ext/module'
4
+ require 'active_support/core_ext/hash/except'
5
+ require 'active_support/core_ext/string/conversions'
6
+ require 'active_support/core_ext/date/acts_like'
7
+ require 'active_support/core_ext/date/conversions'
8
+ require 'active_support/core_ext/time/acts_like'
9
+ require 'active_support/core_ext/time/conversions'
10
+ require 'active_support/core_ext/date_time/acts_like'
11
+ require 'active_support/core_ext/date_time/conversions'
12
+ require 'timeliness'
13
+
14
+ Timeliness.module_eval do
15
+ class << self
16
+ alias :dummy_date_for_time_type :date_for_time_type
17
+ alias :dummy_date_for_time_type= :date_for_time_type=
18
+ alias :remove_us_formats :use_euro_formats
19
+ end
20
+ end
21
+
22
+ module ValidatesTimeliness
23
+ autoload :VERSION, 'validates_timeliness/version'
24
+
25
+ class << self
26
+ delegate :default_timezone, :default_timezone=, :dummy_date_for_time_type, :dummy_date_for_time_type=, :to => Timeliness
27
+
28
+ attr_accessor :extend_orms, :ignore_restriction_errors, :restriction_shorthand_symbols, :use_plugin_parser
29
+ end
30
+
31
+ # Extend ORM/ODMs for full support (:active_record, :mongoid).
32
+ self.extend_orms = []
33
+
34
+ # Ignore errors when restriction options are evaluated
35
+ self.ignore_restriction_errors = false
36
+
37
+ # Shorthand time and date symbols for restrictions
38
+ self.restriction_shorthand_symbols = {
39
+ :now => lambda { Time.current },
40
+ :today => lambda { Date.current }
41
+ }
42
+
43
+ # Use the plugin date/time parser which is stricter and extensible
44
+ self.use_plugin_parser = false
45
+
46
+ # Default timezone
47
+ self.default_timezone = :utc
48
+
49
+ # Set the dummy date part for a time type values.
50
+ self.dummy_date_for_time_type = [ 2000, 1, 1 ]
51
+
52
+ # Setup method for plugin configuration
53
+ def self.setup
54
+ yield self
55
+ load_orms
56
+ end
57
+
58
+ def self.load_orms
59
+ extend_orms.each {|orm| require "validates_timeliness/orm/#{orm}" }
60
+ end
61
+
62
+ def self.parser; Timeliness end
63
+ end
64
+
65
+ require 'validates_timeliness/conversion'
66
+ require 'validates_timeliness/validator'
67
+ require 'validates_timeliness/helper_methods'
68
+ require 'validates_timeliness/attribute_methods'
69
+ require 'validates_timeliness/extensions'
70
+ require 'validates_timeliness/railtie' if defined?(Rails)
@@ -0,0 +1,95 @@
1
+ module ValidatesTimeliness
2
+ module AttributeMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :timeliness_validated_attributes
7
+ self.timeliness_validated_attributes = []
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ public
13
+ # Override in ORM shim
14
+ def timeliness_attribute_timezone_aware?(attr_name)
15
+ false
16
+ end
17
+
18
+ # Override in ORM shim
19
+ def timeliness_attribute_type(attr_name)
20
+ :datetime
21
+ end
22
+
23
+ def undefine_attribute_methods
24
+ super
25
+ undefine_timeliness_attribute_methods
26
+ end
27
+
28
+ protected
29
+
30
+ def define_timeliness_methods(before_type_cast=false)
31
+ return if timeliness_validated_attributes.blank?
32
+ timeliness_validated_attributes.each do |attr_name|
33
+ define_attribute_timeliness_methods(attr_name, before_type_cast)
34
+ end
35
+ end
36
+
37
+ def define_attribute_timeliness_methods(attr_name, before_type_cast=false)
38
+ define_timeliness_write_method(attr_name)
39
+ define_timeliness_before_type_cast_method(attr_name) if before_type_cast
40
+ end
41
+
42
+ def define_timeliness_write_method(attr_name)
43
+ method_body, line = <<-EOV, __LINE__ + 1
44
+ def #{attr_name}=(value)
45
+ original_value = value
46
+ @timeliness_cache ||= {}
47
+ @timeliness_cache["#{attr_name}"] = original_value
48
+ #{ "if value.is_a?(String)\n#{timeliness_type_cast_code(attr_name, 'value')}\nend" if ValidatesTimeliness.use_plugin_parser }
49
+
50
+ super(value)
51
+ end
52
+ EOV
53
+ generated_timeliness_methods.module_eval(method_body, __FILE__, line)
54
+ end
55
+
56
+ def define_timeliness_before_type_cast_method(attr_name)
57
+ method_body, line = <<-EOV, __LINE__ + 1
58
+ def #{attr_name}_before_type_cast
59
+ _timeliness_raw_value_for('#{attr_name}') || begin
60
+ a = @attributes['#{attr_name}']
61
+ a.respond_to?(:value_before_type_cast) ? a.value_before_type_cast : a
62
+ end
63
+ end
64
+ EOV
65
+ generated_timeliness_methods.module_eval(method_body, __FILE__, line)
66
+ end
67
+
68
+ def timeliness_type_cast_code(attr_name, var_name)
69
+ type = timeliness_attribute_type(attr_name)
70
+ timezone_aware = timeliness_attribute_timezone_aware?(attr_name)
71
+ timezone = :current if timezone_aware
72
+
73
+ "#{var_name} = Timeliness::Parser.parse(#{var_name}, :#{type}, :zone => #{timezone.inspect})"
74
+ end
75
+
76
+ def generated_timeliness_methods
77
+ @generated_timeliness_methods ||= Module.new.tap { |m| include(m) }
78
+ end
79
+
80
+ def undefine_timeliness_attribute_methods
81
+ generated_timeliness_methods.module_eval do
82
+ instance_methods.each { |m| undef_method(m) }
83
+ end
84
+ end
85
+ end
86
+
87
+ def _timeliness_raw_value_for(attr_name)
88
+ @timeliness_cache && @timeliness_cache[attr_name]
89
+ end
90
+
91
+ def _clear_timeliness_cache
92
+ @timeliness_cache = {}
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,70 @@
1
+ module ValidatesTimeliness
2
+ module Conversion
3
+
4
+ def type_cast_value(value, type)
5
+ return nil if value.nil? || !value.respond_to?(:to_time)
6
+
7
+ value = value.in_time_zone if value.acts_like?(:time) && @timezone_aware
8
+ value = case type
9
+ when :time
10
+ dummy_time(value)
11
+ when :date
12
+ value.to_date
13
+ when :datetime
14
+ value.is_a?(Time) ? value : value.to_time
15
+ end
16
+ if options[:ignore_usec] && value.is_a?(Time)
17
+ Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if @timezone_aware))
18
+ else
19
+ value
20
+ end
21
+ end
22
+
23
+ def dummy_time(value)
24
+ time = if value.acts_like?(:time)
25
+ value = value.in_time_zone if @timezone_aware
26
+ [value.hour, value.min, value.sec]
27
+ else
28
+ [0,0,0]
29
+ end
30
+ values = ValidatesTimeliness.dummy_date_for_time_type + time
31
+ Timeliness::Parser.make_time(values, (:current if @timezone_aware))
32
+ end
33
+
34
+ def evaluate_option_value(value, record)
35
+ case value
36
+ when Time, Date
37
+ value
38
+ when String
39
+ parse(value)
40
+ when Symbol
41
+ if !record.respond_to?(value) && restriction_shorthand?(value)
42
+ ValidatesTimeliness.restriction_shorthand_symbols[value].call
43
+ else
44
+ evaluate_option_value(record.send(value), record)
45
+ end
46
+ when Proc
47
+ result = value.arity > 0 ? value.call(record) : value.call
48
+ evaluate_option_value(result, record)
49
+ else
50
+ value
51
+ end
52
+ end
53
+
54
+ def restriction_shorthand?(symbol)
55
+ ValidatesTimeliness.restriction_shorthand_symbols.keys.include?(symbol)
56
+ end
57
+
58
+ def parse(value)
59
+ return nil if value.nil?
60
+ if ValidatesTimeliness.use_plugin_parser
61
+ Timeliness::Parser.parse(value, @type, :zone => (:current if @timezone_aware), :format => options[:format], :strict => false)
62
+ else
63
+ @timezone_aware ? Time.zone.parse(value, Time.zone.now) : value.to_time(ValidatesTimeliness.default_timezone)
64
+ end
65
+ rescue ArgumentError, TypeError
66
+ nil
67
+ end
68
+
69
+ end
70
+ end
@@ -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