├── .document ├── .gemtest ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE ├── Rakefile ├── Readme.md ├── config └── locales │ ├── af.yml │ ├── ar.yml │ ├── ca.yml │ ├── de.yml │ ├── en.yml │ ├── es.yml │ ├── fr.yml │ ├── it.yml │ ├── ja.yml │ ├── nl.yml │ ├── pl.yml │ ├── pt-BR.yml │ ├── ru.yml │ ├── tr.yml │ └── zh-CN.yml ├── date_validator.gemspec ├── lib ├── active_model │ └── validations │ │ └── date_validator.rb ├── date_validator.rb └── date_validator │ ├── engine.rb │ └── version.rb └── test ├── date_validator_test.rb └── test_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegram/date_validator/2ec1621d82231ba64f7dd7450d0c0018d7b4994d/.gemtest -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | env: 12 | CI: "true" 13 | 14 | jobs: 15 | main: 16 | name: Tests 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | ruby: [2.2, 2.3, 2.4, 2.5, 2.6, 2.7] 21 | activemodel: [3.2, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2, 6.0, 6.1] 22 | include: 23 | - ruby: 3.0 24 | activemodel: 6.0 25 | - ruby: 3.0 26 | activemodel: 6.1 27 | exclude: 28 | - ruby: 2.2 29 | activemodel: 6.0 30 | - ruby: 2.3 31 | activemodel: 6.0 32 | - ruby: 2.4 33 | activemodel: 6.0 34 | - ruby: 2.2 35 | activemodel: 6.1 36 | - ruby: 2.3 37 | activemodel: 6.1 38 | - ruby: 2.4 39 | activemodel: 6.1 40 | fail-fast: true 41 | env: 42 | ACTIVE_MODEL_VERSION: ${{ matrix.activemodel }} 43 | steps: 44 | - uses: actions/checkout@v2.0.0 45 | with: 46 | fetch-depth: 1 47 | - uses: ruby/setup-ruby@master 48 | with: 49 | ruby-version: ${{ matrix.ruby }} 50 | - run: bundle install --jobs 4 --retry 3 51 | name: Install Ruby deps 52 | - run: bundle exec rake 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | *.rbc 7 | doc/* 8 | .yardoc/* 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | active_model_opts = 6 | case version = ENV['ACTIVE_MODEL_VERSION'] || "master" 7 | when 'master' then { github: 'rails/rails' } 8 | when 'default' then '~> 3' 9 | else "~> #{version}" 10 | end 11 | 12 | gem 'activemodel', active_model_opts 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Codegram 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake/testtask' 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | t.verbose = true 9 | end 10 | 11 | begin 12 | require 'yard' 13 | YARD::Rake::YardocTask.new(:docs) do |t| 14 | t.files = ['lib/**/*.rb'] 15 | t.options = ['-m', 'markdown', '--no-private', '-r', 'Readme.md', '--title', 'Date Validator documentation'] 16 | end 17 | 18 | site = 'doc' 19 | source_branch = 'master' 20 | deploy_branch = 'gh-pages' 21 | 22 | desc "generate and deploy documentation website to github pages" 23 | multitask :pages do 24 | puts ">>> Deploying #{deploy_branch} branch to Github Pages <<<" 25 | require 'git' 26 | repo = Git.open('.') 27 | puts "\n>>> Checking out #{deploy_branch} branch <<<\n" 28 | repo.branch("#{deploy_branch}").checkout 29 | (Dir["*"] - [site]).each { |f| rm_rf(f) } 30 | Dir["#{site}/*"].each {|f| mv(f, "./")} 31 | rm_rf(site) 32 | puts "\n>>> Moving generated site files <<<\n" 33 | Dir["**/*"].each {|f| repo.add(f) } 34 | repo.status.deleted.each {|f, s| repo.remove(f)} 35 | puts "\n>>> Commiting: Site updated at #{Time.now.utc} <<<\n" 36 | message = ENV["MESSAGE"] || "Site updated at #{Time.now.utc}" 37 | repo.commit(message) 38 | puts "\n>>> Pushing generated site to #{deploy_branch} branch <<<\n" 39 | repo.push 40 | puts "\n>>> Github Pages deploy complete <<<\n" 41 | repo.branch("#{source_branch}").checkout 42 | end 43 | 44 | task doc: [:docs] 45 | rescue LoadError 46 | end 47 | 48 | task default: :test 49 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # date_validator [![Build Status](https://travis-ci.org/codegram/date_validator.png?branch=master)](https://travis-ci.org/codegram/date_validator) 2 | 3 | 4 | A simple date validator for Rails. Should be compatible with all latest Rubies (>2.2, includes Ruby 3.0). 5 | 6 | 7 | ```shell 8 | $ gem install date_validator 9 | ``` 10 | 11 | And I mean simple. In your model: 12 | 13 | ```ruby 14 | validates :expiration_date, date: true 15 | ``` 16 | 17 | or with some options, such as: 18 | 19 | ```ruby 20 | validates :expiration_date, 21 | date: { after: Proc.new { Time.now }, 22 | before: Proc.new { Time.now + 1.year } } 23 | # Using Proc.new prevents production cache issues 24 | ``` 25 | 26 | If you want to check the date against another attribute, you can pass it 27 | a Symbol instead of a block: 28 | 29 | ```ruby 30 | # Ensure the expiration date is after the packaging date 31 | validates :expiration_date, 32 | date: { after: :packaging_date } 33 | ``` 34 | 35 | or access attributes via the object being validated directly (the input to the Proc): 36 | 37 | ```ruby 38 | validates :due_date, 39 | date: { after_or_equal_to: Proc.new { |obj| obj.created_at.to_date } 40 | # The object being validated is available in the Proc 41 | ``` 42 | 43 | For now the available options you can use are `:after`, `:before`, 44 | `:after_or_equal_to`, `:before_or_equal_to` and `:equal_to`. 45 | 46 | If you want to specify a custom message, you can do so in the options hash: 47 | 48 | ```ruby 49 | validates :start_date, 50 | date: { after: Proc.new { Date.today }, message: 'must be after today' }, 51 | on: :create 52 | ``` 53 | 54 | Pretty much self-explanatory! :) 55 | 56 | If you want to make sure an attribute is before/after another attribute, use: 57 | 58 | ```ruby 59 | validates :start_date, date: { before: :end_date } 60 | ``` 61 | 62 | If you want to allow an empty date, use: 63 | 64 | ```ruby 65 | validates :optional_date, date: { allow_blank: true } 66 | ``` 67 | ## Note on Patches/Pull Requests 68 | 69 | * Fork the project. 70 | * Make your feature addition or bug fix. 71 | * Add tests for it. This is important so I don't break it in a 72 | future version unintentionally. 73 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 74 | * Send us a pull request. Bonus points for topic branches. 75 | 76 | ## Copyright 77 | 78 | Copyright (c) 2013 Codegram. See LICENSE for details. 79 | -------------------------------------------------------------------------------- /config/locales/af.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for Afrikaans. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails/locale for starting points. 3 | af: 4 | errors: 5 | messages: 6 | not_a_date: "is nie 'n datum nie" 7 | date_after: "moet na %{date} wees" 8 | date_after_or_equal_to: "moet na of op %{date} wees" 9 | date_before: "moet voor %{date} wees" 10 | date_before_or_equal_to: "moet voor of op %{date} wees" 11 | date_equal_to: "moet gelyk aan %{date} wees" 12 | cannot_be_in_the_future: "kan nie in die toekoms wees nie" 13 | -------------------------------------------------------------------------------- /config/locales/ar.yml: -------------------------------------------------------------------------------- 1 | ar: 2 | errors: 3 | messages: 4 | not_a_date: "ليس بتاريخ" 5 | date_after: "يجب أن يكون بعد %{date}" 6 | date_after_or_equal_to: "يجب أن يكون مساوي أو بعد %{date}" 7 | date_before: "يجب أن يكون قبل %{date}" 8 | date_before_or_equal_to: "يجب أن يكون مساوي أو قبل %{date}" 9 | date_equal_to: "يجب أن يساوي %{date}" 10 | -------------------------------------------------------------------------------- /config/locales/ca.yml: -------------------------------------------------------------------------------- 1 | ca: 2 | errors: 3 | messages: 4 | not_a_date: "no és una data" 5 | date_after: "ha de ser posterior a %{date}" 6 | date_after_or_equal_to: "ha de ser posterior o igual a %{date}" 7 | date_before: "ha de ser abans de %{date}" 8 | date_before_or_equal_to: "ha de ser abans de o igual a %{date}" 9 | date_equal_to: "ha de ser igual a %{date}" 10 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | errors: 3 | messages: 4 | not_a_date: "ist kein Datum" 5 | date_after: "muss nach %{date} sein" 6 | date_after_or_equal_to: "muss nach oder gleich %{date} sein" 7 | date_before: "muss vor %{date} sein" 8 | date_before_or_equal_to: "muss vor oder gleich %{date} sein" 9 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | not_a_date: "is not a date" 5 | date_after: "must be after %{date}" 6 | date_after_or_equal_to: "must be after or equal to %{date}" 7 | date_before: "must be before %{date}" 8 | date_before_or_equal_to: "must be before or equal to %{date}" 9 | date_equal_to: "must be equal to %{date}" 10 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | errors: 3 | messages: 4 | not_a_date: "no es una fecha" 5 | date_after: "tiene que ser posterior a %{date}" 6 | date_after_or_equal_to: "tiene que ser posterior o igual a %{date}" 7 | date_before: "tiene que ser antes de %{date}" 8 | date_before_or_equal_to: "tiene que ser antes o igual a %{date}" 9 | date_equal_to: "tiene que ser igual a %{date}" 10 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | fr: 5 | errors: 6 | messages: 7 | not_a_date: "n'est pas une date" 8 | date_after: "doit être après %{date}" 9 | date_after_or_equal_to: "doit être supérieur ou égal à %{date}" 10 | date_before: "doit être avant %{date}" 11 | date_before_or_equal_to: "doit être inférieur ou égal à %{date}" 12 | -------------------------------------------------------------------------------- /config/locales/it.yml: -------------------------------------------------------------------------------- 1 | it: 2 | errors: 3 | messages: 4 | not_a_date: "non è una data" 5 | date_after: "deve essere successiva a %{date}" 6 | date_after_or_equal_to: "deve essere successiva o uguale a %{date}" 7 | date_before: "deve essere antecedente a %{date}" 8 | date_before_or_equal_to: "deve essere antecedente o uguale a %{date}" 9 | date_equal_to: "deve essere uguale a %{date}" 10 | -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | errors: 3 | messages: 4 | not_a_date: "は日付ではありません" 5 | date_after: "は%{date}より後に設定してください" 6 | date_after_or_equal_to: "は%{date}以後に設定してください" 7 | date_before: "は%{date}より前に設定してください" 8 | date_before_or_equal_to: "は%{date}以前に設定してください" 9 | date_equal_to: "は%{date}にしてください" 10 | -------------------------------------------------------------------------------- /config/locales/nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | errors: 3 | messages: 4 | not_a_date: "is geen datum" 5 | date_after: "moet na %{date} liggen" 6 | date_after_or_equal_to: "moet gelijk zijn aan of na %{date} liggen" 7 | date_before: "moet voor %{date} liggen" 8 | date_before_or_equal_to: "moet gelijk zijn of voor %{date} liggen" 9 | -------------------------------------------------------------------------------- /config/locales/pl.yml: -------------------------------------------------------------------------------- 1 | pl: 2 | errors: 3 | messages: 4 | not_a_date: "nie jest datą" 5 | date_after: "musi być po %{date}" 6 | date_after_or_equal_to: "musi być po lub równa %{date}" 7 | date_before: "musi być przed %{date}" 8 | date_before_or_equal_to: "musi być przed lub równa %{date}" 9 | -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | errors: 3 | messages: 4 | not_a_date: "não é uma data válida" 5 | date_after: "deve ser após %{date}" 6 | date_after_or_equal_to: "deve ser após ou igual a %{date}" 7 | date_before: "deve ser antes de %{date}" 8 | date_before_or_equal_to: "deve ser antes ou igual a %{date}" 9 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | errors: 3 | messages: 4 | not_a_date: "не является датой" 5 | date_after: "должна быть позднее чем %{date}" 6 | date_after_or_equal_to: "должна равняться или быть позднее чем %{date}" 7 | date_before: "должна быть ранее чем %{date}" 8 | date_before_or_equal_to: "должна равняться или быть ранее чем %{date}" 9 | -------------------------------------------------------------------------------- /config/locales/tr.yml: -------------------------------------------------------------------------------- 1 | tr: 2 | errors: 3 | messages: 4 | not_a_date: "geçerli tarih değil" 5 | date_after: "%{date} den ileri bir tarih olmalı" 6 | date_after_or_equal_to: "%{date} ile aynı veya ileri bir tarih olmalı" 7 | date_before: "%{date} den öncesi bir tarih olmalı" 8 | date_before_or_equal_to: "%{date} ile aynı veya öncesi bir tarih olmalı" 9 | date_equal_to: "%{date} ile aynı tarihte olmalı" 10 | -------------------------------------------------------------------------------- /config/locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | errors: 3 | messages: 4 | not_a_date: "不是一个日期" 5 | date_after: "必须晚于 %{date}" 6 | date_after_or_equal_to: "必须早于或等于 %{date}" 7 | date_before: "必须早于 %{date}" 8 | date_before_or_equal_to: "必须晚于或等于 %{date}" 9 | date_equal_to: "必须等于 %{date}" 10 | -------------------------------------------------------------------------------- /date_validator.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "date_validator/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "date_validator" 7 | s.version = DateValidator::VERSION 8 | s.authors = ["Oriol Gual", "Josep M. Bach", "Josep Jaume Rey"] 9 | s.email = ["info@codegram.com"] 10 | s.homepage = "http://github.com/codegram/date_validator" 11 | s.license = "MIT" 12 | s.summary = %q{A simple, ORM agnostic, Ruby >=2.2 compatible date validator for Rails 3+, based on ActiveModel.} 13 | s.description = %q{A simple, ORM agnostic, Ruby >=2.2 compatible date validator for Rails 3+, based on ActiveModel. Currently supporting :after, :before, :after_or_equal_to and :before_or_equal_to options.} 14 | 15 | s.rubyforge_project = "date_validator" 16 | 17 | s.add_runtime_dependency 'activemodel', '>= 3' 18 | s.add_runtime_dependency 'activesupport', '>= 3' 19 | 20 | s.add_development_dependency 'minitest' 21 | s.add_development_dependency 'rake', '>= 12.3.3' 22 | s.add_development_dependency 'tzinfo' 23 | 24 | s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) 25 | 26 | s.files = `git ls-files`.split("\n") 27 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 28 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 29 | s.require_paths = ["lib"] 30 | end 31 | -------------------------------------------------------------------------------- /lib/active_model/validations/date_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validations' 2 | require 'active_support/core_ext/date_time/conversions' 3 | require 'active_support/core_ext/hash/conversions' 4 | 5 | # ActiveModel Rails module. 6 | module ActiveModel 7 | 8 | # ActiveModel::Validations Rails module. Contains all the default validators. 9 | module Validations 10 | 11 | # Date Validator. Inherits from ActiveModel::EachValidator. 12 | # 13 | # Responds to the regular validator API methods `#check_validity` and 14 | # `#validate_each`. 15 | class DateValidator < ActiveModel::EachValidator 16 | 17 | # Implemented checks and their associated operators. 18 | CHECKS = { after: :>, after_or_equal_to: :>=, 19 | before: :<, before_or_equal_to: :<=, 20 | equal_to: :== 21 | }.freeze 22 | 23 | # Call `#initialize` on the superclass, adding a default 24 | # `allow_nil: false` option. 25 | def initialize(options) 26 | super(options.reverse_merge(allow_nil: false)) 27 | end 28 | 29 | # Validates the arguments passed to the validator. 30 | # 31 | # They must be either any kind of Time, a Proc, or a Symbol. 32 | def check_validity! 33 | keys = CHECKS.keys 34 | options.slice(*keys).each do |option, value| 35 | next if is_time?(value) || value.is_a?(Proc) || value.is_a?(Symbol) || (defined?(ActiveSupport::TimeWithZone) and value.is_a? ActiveSupport::TimeWithZone) 36 | raise ArgumentError, ":#{option} must be a time, a date, a time_with_zone, a symbol or a proc" 37 | end 38 | end 39 | 40 | # Overridden because standard allow_nil and allow_blank checks don't work with 41 | # string expressions that cannot be type cast to dates. We have to validate 42 | # the pre-type cast values. 43 | def validate(record) 44 | attributes.each do |attribute| 45 | value = record.read_attribute_for_validation(attribute) 46 | validate_each(record, attribute, value) 47 | end 48 | end 49 | 50 | # The actual validator method. It is called when ActiveRecord iterates 51 | # over all the validators. 52 | def validate_each(record, attr_name, value) 53 | before_type_cast = :"#{attr_name}_before_type_cast" 54 | 55 | value_before_type_cast = if record.respond_to?(before_type_cast) 56 | record.send(before_type_cast) 57 | else 58 | nil 59 | end 60 | 61 | if value_before_type_cast.present? && value.nil? 62 | record.errors.add(attr_name, :not_a_date, **options) 63 | return 64 | end 65 | 66 | return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 67 | 68 | unless value 69 | record.errors.add(attr_name, :not_a_date, **options) 70 | return 71 | end 72 | 73 | unless is_time?(value) 74 | record.errors.add(attr_name, :not_a_date, **options) 75 | return 76 | end 77 | 78 | options.slice(*CHECKS.keys).each do |option, option_value| 79 | option_value = option_value.call(record) if option_value.is_a?(Proc) 80 | option_value = record.send(option_value) if option_value.is_a?(Symbol) 81 | 82 | original_value = value 83 | original_option_value = option_value 84 | 85 | # To enable to_i conversion, these types must be converted to Datetimes 86 | if defined?(ActiveSupport::TimeWithZone) 87 | option_value = option_value.to_datetime if option_value.is_a?(ActiveSupport::TimeWithZone) 88 | value = value.to_datetime if value.is_a?(ActiveSupport::TimeWithZone) 89 | end 90 | 91 | if defined?(Date) 92 | option_value = option_value.to_datetime if option_value.is_a?(Date) 93 | value = value.to_datetime if value.is_a?(Date) 94 | end 95 | 96 | unless is_time?(option_value) && value.to_i.send(CHECKS[option], option_value.to_i) 97 | record.errors.add(attr_name, :"date_#{option}", **options.merge( 98 | value: original_value, 99 | date: (I18n.localize(original_option_value) rescue original_option_value) 100 | )) 101 | end 102 | end 103 | end 104 | 105 | private 106 | 107 | def is_time?(object) 108 | object.is_a?(Time) || (defined?(Date) and object.is_a?(Date)) || (defined?(ActiveSupport::TimeWithZone) and object.is_a?(ActiveSupport::TimeWithZone)) 109 | end 110 | end 111 | 112 | module HelperMethods 113 | # Validates whether the value of the specified attribute is a validate Date 114 | # 115 | # class Person < ActiveRecord::Base 116 | # validates_date_of :payment_date, after: :packaging_date 117 | # validates_date_of :expiration_date, before: Proc.new { Time.now } 118 | # end 119 | # 120 | # Configuration options: 121 | # * :after - check that a Date is after the specified one. 122 | # * :before - check that a Date is before the specified one. 123 | # * :after_or_equal_to - check that a Date is after or equal to the specified one. 124 | # * :before_or_equal_to - check that a Date is before or equal to the specified one. 125 | # * :equal_to - check that a Date is equal to the specified one. 126 | def validates_date_of(*attr_names) 127 | validates_with DateValidator, _merge_attributes(attr_names) 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/date_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validations/date_validator' 2 | require 'active_support/i18n' 3 | require 'date_validator/engine' if defined?(Rails) 4 | 5 | # A simple date validator for Rails 3+. 6 | # 7 | # @example 8 | # validates :expiration_date, 9 | # date: { after: Proc.new { Time.now }, 10 | # before: Proc.new { Time.now + 1.year } } 11 | # # Using Proc.new prevents production cache issues 12 | # 13 | module DateValidator 14 | end 15 | -------------------------------------------------------------------------------- /lib/date_validator/engine.rb: -------------------------------------------------------------------------------- 1 | module DateValidator 2 | class Engine < Rails::Engine 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/date_validator/version.rb: -------------------------------------------------------------------------------- 1 | module DateValidator 2 | # The version number. 3 | VERSION = "0.12.0" 4 | end 5 | -------------------------------------------------------------------------------- /test/date_validator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module ActiveModel 4 | module Validations 5 | 6 | describe DateValidator do 7 | 8 | before do 9 | TestRecord.reset_callbacks(:validate) 10 | end 11 | 12 | it "checks validity of the arguments" do 13 | [3, "foo", 1..6].each do |wrong_argument| 14 | proc { 15 | TestRecord.validates(:expiration_date, date: { before: wrong_argument }) 16 | }.must_raise(ArgumentError, ":before must be a time, a date, a time_with_zone, a symbol or a proc") 17 | end 18 | end 19 | 20 | it "complains when no options are provided" do 21 | I18n.backend.reload! 22 | TestRecord.validates :expiration_date, 23 | date: { before: Time.now } 24 | 25 | model = TestRecord.new(nil) 26 | model.valid?.must_equal false 27 | model.errors[:expiration_date].must_equal(["is not a date"]) 28 | end 29 | 30 | it "works with helper methods" do 31 | time = Time.now 32 | TestRecord.validates_date_of :expiration_date, before: time 33 | model = TestRecord.new(time + 20000) 34 | model.valid?.must_equal false 35 | end 36 | 37 | [:valid,:invalid].each do |must_be| 38 | _context = must_be == :valid ? 'when value validates correctly' : 'when value does not match validation requirements' 39 | 40 | describe _context do 41 | [:after, :before, :after_or_equal_to, :before_or_equal_to, :equal_to].each do |check| 42 | now = Time.now.to_datetime 43 | 44 | model_date = case check 45 | when :after then must_be == :valid ? now + 21000 : now - 1 46 | when :before then must_be == :valid ? now - 21000 : now + 1 47 | when :after_or_equal_to then must_be == :valid ? now : now - 21000 48 | when :before_or_equal_to then must_be == :valid ? now : now + 21000 49 | when :equal_to then must_be == :valid ? now : now + 21000 50 | end 51 | 52 | it "ensures that an attribute is #{must_be} when #{must_be == :valid ? 'respecting' : 'offending' } the #{check} check" do 53 | TestRecord.validates :expiration_date, 54 | date: {:"#{check}" => now} 55 | 56 | model = TestRecord.new(model_date) 57 | must_be == :valid ? model.valid?.must_equal(true) : model.valid?.must_equal(false) 58 | end 59 | 60 | if _context == 'when value does not match validation requirements' 61 | it "yields a default error message indicating that value must be #{check} validation requirements" do 62 | TestRecord.validates :expiration_date, 63 | date: {:"#{check}" => now} 64 | 65 | model = TestRecord.new(model_date) 66 | model.valid?.must_equal false 67 | model.errors[:expiration_date].must_equal(["must be " + check.to_s.gsub('_',' ') + " #{I18n.localize(now)}"]) 68 | end 69 | end 70 | end 71 | 72 | if _context == 'when value does not match validation requirements' 73 | now = Time.now.to_datetime 74 | 75 | it "allows for a custom validation message" do 76 | TestRecord.validates :expiration_date, 77 | date: { before_or_equal_to: now, 78 | message: 'must be after Christmas' } 79 | 80 | model = TestRecord.new(now + 21000) 81 | model.valid?.must_equal false 82 | model.errors[:expiration_date].must_equal(["must be after Christmas"]) 83 | end 84 | 85 | it "allows custom validation message to be handled by I18n" do 86 | custom_message = 'Custom Date Message' 87 | 88 | I18n.backend.eager_load! if I18n.backend.respond_to?(:eager_load!) 89 | I18n.backend.store_translations('en', { errors: { messages: { not_a_date: custom_message }}}) 90 | 91 | TestRecord.validates :expiration_date, date: true 92 | 93 | model = TestRecord.new(nil) 94 | model.valid?.must_equal false 95 | model.errors[:expiration_date].must_equal([custom_message]) 96 | end 97 | end 98 | 99 | end 100 | end 101 | 102 | extra_types = [:proc, :symbol] 103 | extra_types.push(:date) if defined?(Date) and defined?(DateTime) 104 | extra_types.push(:time_with_zone) if defined?(ActiveSupport::TimeWithZone) 105 | 106 | extra_types.each do |type| 107 | it "accepts a #{type} as an argument to a check" do 108 | case type 109 | when :proc then 110 | TestRecord.validates(:expiration_date, date: { after: Proc.new {Time.now + 21000} }).must_be_kind_of Hash 111 | when :symbol then 112 | TestRecord.send(:define_method, :min_date, lambda { Time.now + 21000 }) 113 | TestRecord.validates(:expiration_date, date: { after: :min_date }).must_be_kind_of Hash 114 | when :date then 115 | TestRecord.validates(:expiration_date, date: { after: Time.now.to_date }).must_be_kind_of Hash 116 | when :time_with_zone then 117 | Time.zone = "Hawaii" 118 | TestRecord.validates(:expiration_date, date: { before: Time.zone.parse((Time.now + 21000).to_s, Time.now) }).must_be_kind_of Hash 119 | end 120 | end 121 | end 122 | 123 | it "gracefully handles an unexpected result from a proc argument evaluation" do 124 | TestRecord.validates :expiration_date, 125 | date: { after: Proc.new { nil } } 126 | 127 | TestRecord.new(Time.now).valid?.must_equal false 128 | end 129 | 130 | it "gracefully handles an unexpected result from a symbol argument evaluation" do 131 | TestRecord.send(:define_method, :min_date, lambda { nil }) 132 | TestRecord.validates :expiration_date, 133 | date: { after: :min_date } 134 | 135 | TestRecord.new(Time.now).valid?.must_equal false 136 | end 137 | 138 | describe "with type cast attributes" do 139 | before do 140 | TestRecord.send(:define_method, :expiration_date_before_type_cast, lambda { 'last year' }) 141 | end 142 | 143 | it "should detect invalid date expressions when nil is allowed" do 144 | TestRecord.validates(:expiration_date, date: true, allow_nil: true) 145 | TestRecord.new(nil).valid?.must_equal false 146 | end 147 | 148 | it "should detect invalid date expressions when blank is allowed" do 149 | TestRecord.validates(:expiration_date, date: true, allow_blank: true) 150 | TestRecord.new(nil).valid?.must_equal false 151 | end 152 | end 153 | 154 | describe 'with garbage input' do 155 | it 'is invalid' do 156 | TestRecord.validates(:expiration_date, date: true, allow_nil: true) 157 | TestRecord.new('not a date').valid?.must_equal false 158 | end 159 | end 160 | end 161 | 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'simplecov' 3 | SimpleCov.start do 4 | add_group "Lib", 'lib' 5 | end 6 | rescue LoadError 7 | end 8 | 9 | begin; require 'turn'; rescue LoadError; end 10 | 11 | gem 'minitest' 12 | require 'minitest/autorun' 13 | 14 | require 'active_model' 15 | require 'active_support/core_ext/time/zones' 16 | require 'date_validator' 17 | 18 | I18n.load_path += Dir[File.expand_path(File.join(File.dirname(__FILE__), '../config/locales', '*.yml')).to_s] 19 | 20 | class TestRecord 21 | include ActiveModel::Validations 22 | attr_accessor :expiration_date 23 | 24 | def initialize(expiration_date) 25 | @expiration_date = expiration_date 26 | end 27 | end 28 | --------------------------------------------------------------------------------