diff options
175 files changed, 4775 insertions, 3921 deletions
diff --git a/.gitignore b/.gitignore index d86af64569..5357fbe481 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ activesupport/doc activeresource/pkg activerecord/pkg actionpack/pkg +activemodel/test/fixtures/fixture_database.sqlite3 actionmailer/pkg activesupport/pkg railties/pkg diff --git a/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb b/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb index 897a5065cf..2d0cd5c124 100644 --- a/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb +++ b/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb @@ -1 +1 @@ -Have a lovely picture, from me. Enjoy!
\ No newline at end of file +Have some dots. Enjoy!
\ No newline at end of file diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb index 919ab5de94..30d1b836c4 100644 --- a/actionmailer/test/mail_service_test.rb +++ b/actionmailer/test/mail_service_test.rb @@ -6,10 +6,10 @@ class FunkyPathMailer < ActionMailer::Base def multipart_with_template_path_with_dots(recipient) recipients recipient - subject "Have a lovely picture" + subject "This path has dots" from "Chad Fowler <chad@chadfowler.com>" - attachment :content_type => "image/jpeg", - :body => "not really a jpeg, we're only testing, after all" + attachment :content_type => "text/plain", + :body => "dots dots dots..." end end @@ -939,7 +939,8 @@ EOF def test_multipart_with_template_path_with_dots mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient) assert_equal 2, mail.parts.length - assert mail.parts.any? {|part| part.content_type == "text/plain" && part.charset == "utf-8"} + assert "text/plain", mail.parts[1].content_type + assert "utf-8", mail.parts[1].charset end def test_custom_content_type_attributes diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index e3ad19558e..e758983d7a 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,8 @@ *Edge* +* Ruby 1.9: ERB template encoding using a magic comment at the top of the file. [Jeremy Kemper] + <%# encoding: utf-8 %> + * Change integration test helpers to accept Rack environment instead of just HTTP Headers [Pratik Naik] Before : get '/path', {}, 'Accept' => 'text/javascript' diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 9ce897aae8..5f5614e58f 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -59,24 +59,21 @@ end desc 'Old Controller Tests on New Base' Rake::TestTask.new(:test_new_base_on_old_tests) do |t| t.libs << "test/new_base" << "test/lib" - # layout - # Dir.glob( "test/{dispatch,template}/**/*_test.rb" ).sort + + t.verbose = true # ==== Not ported # * filters - # * integration - # * test - # * view_paths - t.test_files = %w( + + t.test_files = Dir.glob( "test/{dispatch,template}/**/*_test.rb" ).sort + %w( action_pack_assertions addresses_render assert_select base benchmark caching capture content_type cookie dispatcher filter_params flash helper http_basic_authentication - http_digest_authentication layout logging mime_responds + http_digest_authentication integration layout logging mime_responds record_identifier redirect render render_js render_json render_other render_xml request_forgery_protection rescue - resources routing selector send_file url_rewriter verification webservice + resources routing selector send_file test url_rewriter + verification view_paths webservice ).map { |name| "test/controller/#{name}_test.rb" } - t.verbose = true end # Genereate the RDoc documentation diff --git a/actionpack/examples/minimal.rb b/actionpack/examples/minimal.rb index 0fc527445e..9eb92cd8e7 100644 --- a/actionpack/examples/minimal.rb +++ b/actionpack/examples/minimal.rb @@ -7,12 +7,6 @@ require 'action_controller' require 'action_controller/new_base' if ENV['NEW'] require 'benchmark' -class BaseController < ActionController::Base - def index - render :text => '' - end -end - class Runner def initialize(app) @app = app @@ -29,14 +23,36 @@ class Runner response[2].each { |part| out.puts part } out.puts '---' end + + def self.run(app, n, label = nil) + puts '=' * label.size, label, '=' * label.size if label + env = { 'n' => n, 'rack.input' => StringIO.new(''), 'rack.errors' => $stdout } + t = Benchmark.realtime { new(app).call(env) } + puts "%d ms / %d req = %.1f usec/req" % [10**3 * t, n, 10**6 * t / n] + puts + end +end + + +N = (ENV['N'] || 1000).to_i + +class BasePostController < ActionController::Base + def index + render :text => '' + end end -n = (ENV['N'] || 1000).to_i -input = StringIO.new('') +OK = [200, {}, []] +MetalPostController = lambda { OK } -elapsed = Benchmark.realtime do - Runner.new(BaseController.action(:index)). - call('n' => n, 'rack.input' => input, 'rack.errors' => $stdout) +if ActionController.const_defined?(:Http) + class HttpPostController < ActionController::Http + def index + self.response_body = '' + end + end end -puts "%dms elapsed, %d req/sec, %.2f msec/req" % - [1000 * elapsed, n / elapsed, 1000 * elapsed / n] + +Runner.run(MetalPostController, N, 'metal') +Runner.run(HttpPostController.action(:index), N, 'http') if defined? HttpPostController +Runner.run(BasePostController.action(:index), N, 'base') diff --git a/actionpack/lib/action_controller/abstract.rb b/actionpack/lib/action_controller/abstract.rb index d6b7a44f5f..f46b91627f 100644 --- a/actionpack/lib/action_controller/abstract.rb +++ b/actionpack/lib/action_controller/abstract.rb @@ -11,4 +11,4 @@ module AbstractController autoload :Renderer, "action_controller/abstract/renderer" # === Exceptions autoload :ActionNotFound, "action_controller/abstract/exceptions" -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/base.rb b/actionpack/lib/action_controller/abstract/base.rb index 0e4803388a..87083a4d79 100644 --- a/actionpack/lib/action_controller/abstract/base.rb +++ b/actionpack/lib/action_controller/abstract/base.rb @@ -2,28 +2,27 @@ require 'active_support/core_ext/module/attr_internal' module AbstractController class Error < StandardError; end - + class DoubleRenderError < Error DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"." def initialize(message = nil) super(message || DEFAULT_MESSAGE) end - end - + end + class Base - attr_internal :response_body attr_internal :response_obj attr_internal :action_name class << self - attr_reader :abstract - + attr_reader :abstract + def abstract! @abstract = true end - + alias_method :abstract?, :abstract def inherited(klass) @@ -34,13 +33,13 @@ module AbstractController def subclasses @subclasses ||= [] end - + def internal_methods controller = self controller = controller.superclass until controller.abstract? controller.public_instance_methods(true) end - + def process(action) new.process(action.to_s) end @@ -48,7 +47,7 @@ module AbstractController def hidden_actions [] end - + def action_methods @action_methods ||= # All public instance methods of this class, including ancestors @@ -61,13 +60,13 @@ module AbstractController hidden_actions end end - + abstract! - + def initialize self.response_obj = {} end - + def process(action) @_action_name = action_name = action.to_s @@ -78,26 +77,27 @@ module AbstractController process_action(action_name) self end - + private - def action_methods self.class.action_methods end - + def action_method?(action) action_methods.include?(action) end - + # It is possible for respond_to?(action_name) to be false and # respond_to?(:action_missing) to be false if respond_to_action? # is overridden in a subclass. For instance, ActionController::Base # overrides it to include the case where a template matching the # action_name is found. def process_action(method_name) - send(method_name) + send_action(method_name) end + alias send_action send + def _handle_action_missing action_missing(@_action_name) end @@ -112,4 +112,4 @@ module AbstractController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/benchmarker.rb b/actionpack/lib/action_controller/abstract/benchmarker.rb index 9f5889c704..07294cede3 100644 --- a/actionpack/lib/action_controller/abstract/benchmarker.rb +++ b/actionpack/lib/action_controller/abstract/benchmarker.rb @@ -1,9 +1,9 @@ module AbstractController module Benchmarker - extend ActiveSupport::DependencyModule - - depends_on Logger - + extend ActiveSupport::Concern + + include Logger + module ClassMethods def benchmark(title, log_level = ::Logger::DEBUG, use_silence = true) if logger && logger.level >= log_level @@ -25,4 +25,4 @@ module AbstractController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/callbacks.rb b/actionpack/lib/action_controller/abstract/callbacks.rb index e4f9dd3112..c6d3413c30 100644 --- a/actionpack/lib/action_controller/abstract/callbacks.rb +++ b/actionpack/lib/action_controller/abstract/callbacks.rb @@ -1,8 +1,8 @@ module AbstractController module Callbacks - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on ActiveSupport::NewCallbacks + include ActiveSupport::NewCallbacks included do define_callbacks :process_action, "response_body" @@ -13,7 +13,7 @@ module AbstractController super end end - + module ClassMethods def _normalize_callback_options(options) if only = options[:only] @@ -21,11 +21,11 @@ module AbstractController options[:per_key] = {:if => only} end if except = options[:except] - except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") + except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") options[:per_key] = {:unless => except} end end - + [:before, :after, :around].each do |filter| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{filter}_filter(*names, &blk) @@ -37,6 +37,15 @@ module AbstractController end end + def prepend_#{filter}_filter(*names, &blk) + options = names.last.is_a?(Hash) ? names.pop : {} + _normalize_callback_options(options) + names.push(blk) if block_given? + names.each do |name| + process_action_callback(:#{filter}, name, options.merge(:prepend => true)) + end + end + def skip_#{filter}_filter(*names, &blk) options = names.last.is_a?(Hash) ? names.pop : {} _normalize_callback_options(options) @@ -51,4 +60,4 @@ module AbstractController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/exceptions.rb b/actionpack/lib/action_controller/abstract/exceptions.rb index ec4680629b..2f6c55f068 100644 --- a/actionpack/lib/action_controller/abstract/exceptions.rb +++ b/actionpack/lib/action_controller/abstract/exceptions.rb @@ -1,3 +1,3 @@ module AbstractController - class ActionNotFound < StandardError ; end -end
\ No newline at end of file + class ActionNotFound < StandardError; end +end diff --git a/actionpack/lib/action_controller/abstract/helpers.rb b/actionpack/lib/action_controller/abstract/helpers.rb index 41decfd0c7..0a2776de9c 100644 --- a/actionpack/lib/action_controller/abstract/helpers.rb +++ b/actionpack/lib/action_controller/abstract/helpers.rb @@ -1,8 +1,8 @@ module AbstractController module Helpers - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on Renderer + include Renderer included do extlib_inheritable_accessor :master_helper_module @@ -16,12 +16,12 @@ module AbstractController av end end - + module ClassMethods def inherited(klass) klass.master_helper_module = Module.new klass.master_helper_module.__send__ :include, master_helper_module - + super end @@ -57,7 +57,7 @@ module AbstractController ruby_eval end end - + def helper(*args, &block) args.flatten.each do |arg| case arg @@ -68,6 +68,5 @@ module AbstractController master_helper_module.module_eval(&block) if block_given? end end - end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/layouts.rb b/actionpack/lib/action_controller/abstract/layouts.rb index 3d6810bda9..273063f74b 100644 --- a/actionpack/lib/action_controller/abstract/layouts.rb +++ b/actionpack/lib/action_controller/abstract/layouts.rb @@ -1,8 +1,8 @@ module AbstractController module Layouts - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on Renderer + include Renderer included do extlib_inheritable_accessor :_layout_conditions @@ -17,18 +17,18 @@ module AbstractController conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } self._layout_conditions = conditions - + @_layout = layout || false # Converts nil to false _write_layout_method end - + def _implied_layout_name name.underscore end - + # Takes the specified layout and creates a _layout method to be called # by _default_layout - # + # # If the specified layout is a: # String:: return the string # Symbol:: call the method specified by the symbol @@ -57,11 +57,12 @@ module AbstractController end end end - + private - - def _layout(details) end # This will be overwritten - + # This will be overwritten + def _layout(details) + end + # :api: plugin # ==== # Override this to mutate the inbound layout name @@ -69,7 +70,7 @@ module AbstractController unless [String, FalseClass, NilClass].include?(name.class) raise ArgumentError, "String, false, or nil expected; you passed #{name.inspect}" end - + name && view_paths.find_by_parts(name, details, _layout_prefix(name)) end @@ -77,7 +78,7 @@ module AbstractController def _layout_prefix(name) "layouts" end - + def _default_layout(require_layout = false, details = {:formats => formats}) if require_layout && _action_has_layout? && !_layout(details) raise ArgumentError, @@ -87,7 +88,7 @@ module AbstractController begin _layout_for_name(_layout(details), details) if _action_has_layout? rescue NameError => e - raise NoMethodError, + raise NoMethodError, "You specified #{@_layout.inspect} as the layout, but no such method was found" end end @@ -103,4 +104,4 @@ module AbstractController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/logger.rb b/actionpack/lib/action_controller/abstract/logger.rb index 980ede0bed..d6fa843485 100644 --- a/actionpack/lib/action_controller/abstract/logger.rb +++ b/actionpack/lib/action_controller/abstract/logger.rb @@ -3,13 +3,13 @@ require 'active_support/core_ext/logger' module AbstractController module Logger - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern class DelayedLog def initialize(&blk) @blk = blk end - + def to_s @blk.call end @@ -19,10 +19,10 @@ module AbstractController included do cattr_accessor :logger end - + def process(action) ret = super - + if logger log = DelayedLog.new do "\n\nProcessing #{self.class.name}\##{action_name} " \ @@ -32,14 +32,14 @@ module AbstractController logger.info(log) end - + ret end - + def request_origin # this *needs* to be cached! # otherwise you'd get different results if calling it more than once @request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}" - end + end end end diff --git a/actionpack/lib/action_controller/abstract/renderer.rb b/actionpack/lib/action_controller/abstract/renderer.rb index d7c68549e1..dd58c7cb64 100644 --- a/actionpack/lib/action_controller/abstract/renderer.rb +++ b/actionpack/lib/action_controller/abstract/renderer.rb @@ -2,9 +2,9 @@ require "action_controller/abstract/logger" module AbstractController module Renderer - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on AbstractController::Logger + include AbstractController::Logger included do attr_internal :formats @@ -15,22 +15,22 @@ module AbstractController end def _action_view - @_action_view ||= ActionView::Base.new(self.class.view_paths, {}, self) + @_action_view ||= ActionView::Base.new(self.class.view_paths, {}, self) end - + def render(*args) if response_body raise AbstractController::DoubleRenderError, "OMG" end - + self.response_body = render_to_body(*args) end - + # Raw rendering of a template to a Rack-compatible body. # ==== # @option _prefix<String> The template's path prefix # @option _layout<String> The relative path to the layout template to use - # + # # :api: plugin def render_to_body(options = {}) # TODO: Refactor so we can just use the normal template logic for this @@ -46,7 +46,7 @@ module AbstractController # ==== # @option _prefix<String> The template's path prefix # @option _layout<String> The relative path to the layout template to use - # + # # :api: plugin def render_to_string(options = {}) AbstractController::Renderer.body_to_s(render_to_body(options)) @@ -55,8 +55,10 @@ module AbstractController def _render_template(options) _action_view._render_template_from_controller(options[:_template], options[:_layout], options, options[:_partial]) end - - def view_paths() _view_paths end + + def view_paths() + _view_paths + end # Return a string representation of a Rack-compatible response body. def self.body_to_s(body) @@ -71,7 +73,6 @@ module AbstractController end private - def _determine_template(options) name = (options[:_template_name] || action_name).to_s @@ -81,15 +82,18 @@ module AbstractController end module ClassMethods - def append_view_path(path) self.view_paths << path end - + + def prepend_view_path(path) + self.view_paths.unshift(path) + end + def view_paths self._view_paths end - + def view_paths=(paths) self._view_paths = paths.is_a?(ActionView::PathSet) ? paths : ActionView::Base.process_view_paths(paths) diff --git a/actionpack/lib/action_controller/base/chained/filters.rb b/actionpack/lib/action_controller/base/chained/filters.rb index e121c0129d..f528dd0686 100644 --- a/actionpack/lib/action_controller/base/chained/filters.rb +++ b/actionpack/lib/action_controller/base/chained/filters.rb @@ -1,11 +1,6 @@ module ActionController #:nodoc: module Filters #:nodoc: - def self.included(base) - base.class_eval do - extend ClassMethods - include ActionController::Filters::InstanceMethods - end - end + extend ActiveSupport::Concern class FilterChain < ActiveSupport::Callbacks::CallbackChain #:nodoc: def append_filter_to_chain(filters, filter_type, &block) diff --git a/actionpack/lib/action_controller/base/chained/flash.rb b/actionpack/lib/action_controller/base/chained/flash.rb index 6bd482d85a..42c6e430ca 100644 --- a/actionpack/lib/action_controller/base/chained/flash.rb +++ b/actionpack/lib/action_controller/base/chained/flash.rb @@ -26,11 +26,11 @@ module ActionController #:nodoc: # # See docs on the FlashHash class for more details about the flash. module Flash - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # TODO : Remove the defined? check when new base is the main base - depends_on Session if defined?(ActionController::Http) - + include Session if defined?(ActionController::Http) + included do # TODO : Remove the defined? check when new base is the main base if defined?(ActionController::Http) @@ -129,65 +129,68 @@ module ActionController #:nodoc: (@used.keys - keys).each{ |k| @used.delete(k) } end + def store(session, key = "flash") + return if self.empty? + session[key] = self + end + private # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods # use() # marks the entire flash as used # use('msg') # marks the "msg" entry as used # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) - def use(k=nil, v=true) - unless k.nil? - @used[k] = v - else - keys.each{ |key| use(key, v) } - end + # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself + # if no key is passed. + def use(key = nil, used = true) + Array(key || keys).each { |k| @used[k] = used } + return key ? self[key] : self end end module InstanceMethodsForBase #:nodoc: protected + def perform_action_with_flash + perform_action_without_flash + if defined? @_flash + @_flash.store(session) + remove_instance_variable(:@_flash) + end + end - def perform_action_with_flash - perform_action_without_flash - remove_instance_variable(:@_flash) if defined?(@_flash) - end - - def reset_session_with_flash - reset_session_without_flash - remove_instance_variable(:@_flash) if defined?(@_flash) - end + def reset_session_with_flash + reset_session_without_flash + remove_instance_variable(:@_flash) if defined?(@_flash) + end end module InstanceMethodsForNewBase #:nodoc: protected + def process_action(method_name) + super + if defined? @_flash + @_flash.store(session) + remove_instance_variable(:@_flash) + end + end - def reset_session - super - remove_flash_instance_variable - end - - def process_action(method_name) - super - remove_flash_instance_variable - end - - def remove_flash_instance_variable - remove_instance_variable(:@_flash) if defined?(@_flash) - end + def reset_session + super + remove_instance_variable(:@_flash) if defined?(@_flash) + end end protected + # Access the contents of the flash. Use <tt>flash["notice"]</tt> to + # read a notice you put there or <tt>flash["notice"] = "hello"</tt> + # to put a new one. + def flash #:doc: + if !defined?(@_flash) + @_flash = session["flash"] || FlashHash.new + @_flash.sweep + end - # Access the contents of the flash. Use <tt>flash["notice"]</tt> to - # read a notice you put there or <tt>flash["notice"] = "hello"</tt> - # to put a new one. - def flash #:doc: - unless defined?(@_flash) - @_flash = session["flash"] ||= FlashHash.new - @_flash.sweep + @_flash end - - @_flash - end end end diff --git a/actionpack/lib/action_controller/base/cookies.rb b/actionpack/lib/action_controller/base/cookies.rb index ca380e98d0..d4806623c3 100644 --- a/actionpack/lib/action_controller/base/cookies.rb +++ b/actionpack/lib/action_controller/base/cookies.rb @@ -51,7 +51,7 @@ module ActionController #:nodoc: protected # Returns the cookie container, which operates as described above. def cookies - CookieJar.new(self) + @cookies ||= CookieJar.new(self) end end diff --git a/actionpack/lib/action_controller/base/filter_parameter_logging.rb b/actionpack/lib/action_controller/base/filter_parameter_logging.rb index f5a678ca03..8370ba6fc0 100644 --- a/actionpack/lib/action_controller/base/filter_parameter_logging.rb +++ b/actionpack/lib/action_controller/base/filter_parameter_logging.rb @@ -1,16 +1,14 @@ module ActionController module FilterParameterLogging - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # TODO : Remove the defined? check when new base is the main base if defined?(ActionController::Http) - depends_on AbstractController::Logger + include AbstractController::Logger end included do - if defined?(ActionController::Http) - include InstanceMethodsForNewBase - end + include InstanceMethodsForNewBase end module ClassMethods diff --git a/actionpack/lib/action_controller/base/helpers.rb b/actionpack/lib/action_controller/base/helpers.rb index 96fa7896a9..f74158bc13 100644 --- a/actionpack/lib/action_controller/base/helpers.rb +++ b/actionpack/lib/action_controller/base/helpers.rb @@ -3,7 +3,7 @@ require 'active_support/dependencies' # FIXME: helper { ... } is broken on Ruby 1.9 module ActionController #:nodoc: module Helpers #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do # Initialize the base module to aggregate its helpers. diff --git a/actionpack/lib/action_controller/base/request_forgery_protection.rb b/actionpack/lib/action_controller/base/request_forgery_protection.rb index f30a4d41a3..c580280a0c 100644 --- a/actionpack/lib/action_controller/base/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/base/request_forgery_protection.rb @@ -3,11 +3,11 @@ module ActionController #:nodoc: end module RequestForgeryProtection - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # TODO : Remove the defined? check when new base is the main base if defined?(ActionController::Http) - depends_on AbstractController::Helpers, Session + include AbstractController::Helpers, Session end included do diff --git a/actionpack/lib/action_controller/base/streaming.rb b/actionpack/lib/action_controller/base/streaming.rb index 5872ba99a2..73d4bde6c1 100644 --- a/actionpack/lib/action_controller/base/streaming.rb +++ b/actionpack/lib/action_controller/base/streaming.rb @@ -2,11 +2,11 @@ module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, # instead of rendering. module Streaming - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # TODO : Remove the defined? check when new base is the main base if defined?(ActionController::Http) - depends_on ActionController::Renderer + include ActionController::Renderer end DEFAULT_SEND_FILE_OPTIONS = { diff --git a/actionpack/lib/action_controller/base/verification.rb b/actionpack/lib/action_controller/base/verification.rb index 3fa5a105b1..d87b348ed4 100644 --- a/actionpack/lib/action_controller/base/verification.rb +++ b/actionpack/lib/action_controller/base/verification.rb @@ -1,10 +1,10 @@ module ActionController #:nodoc: module Verification #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # TODO : Remove the defined? check when new base is the main base if defined?(ActionController::Http) - depends_on AbstractController::Callbacks, Session, Flash, Renderer + include AbstractController::Callbacks, Session, Flash, Renderer end # This module provides a class-level method for specifying that certain diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 2f2eec10f6..38cf1da6a8 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -24,7 +24,7 @@ module ActionController #:nodoc: # ActionController::Base.cache_store = :mem_cache_store, "localhost" # ActionController::Base.cache_store = MyOwnStore.new("parameter") module Caching - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern autoload :Actions, 'action_controller/caching/actions' autoload :Fragments, 'action_controller/caching/fragments' diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 430db3932f..54148b55d8 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -63,7 +63,7 @@ module ActionController #:nodoc: cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options) # TODO: Remove this once new base is swapped in. - if defined?(Http) + if defined?(ActionController::Http) around_filter cache_filter, filter_options else around_filter(filter_options) do |controller, action| @@ -92,16 +92,18 @@ module ActionController #:nodoc: end # TODO: Remove once New Base is merged - def filter(controller, action) - should_continue = before(controller) - action.call if should_continue - after(controller) - end - - def around_process_action(controller) - should_continue = before(controller) - yield if should_continue - after(controller) + if defined?(ActionController::Http) + def filter(controller) + should_continue = before(controller) + yield if should_continue + after(controller) + end + else + def filter(controller, action) + should_continue = before(controller) + action.call if should_continue + after(controller) + end end def before(controller) diff --git a/actionpack/lib/action_controller/new_base.rb b/actionpack/lib/action_controller/new_base.rb index 276be50614..df256985ac 100644 --- a/actionpack/lib/action_controller/new_base.rb +++ b/actionpack/lib/action_controller/new_base.rb @@ -4,6 +4,7 @@ module ActionController autoload :HideActions, "action_controller/new_base/hide_actions" autoload :Http, "action_controller/new_base/http" autoload :Layouts, "action_controller/new_base/layouts" + autoload :RackConvenience, "action_controller/new_base/rack_convenience" autoload :Rails2Compatibility, "action_controller/new_base/compatibility" autoload :Redirector, "action_controller/new_base/redirector" autoload :Renderer, "action_controller/new_base/renderer" @@ -40,5 +41,7 @@ module ActionController require 'action_controller/routing' end +autoload :HTML, 'action_controller/vendor/html-scanner' + require 'action_dispatch' require 'action_view' diff --git a/actionpack/lib/action_controller/new_base/base.rb b/actionpack/lib/action_controller/new_base/base.rb index ffe608ade4..d7b65d37fa 100644 --- a/actionpack/lib/action_controller/new_base/base.rb +++ b/actionpack/lib/action_controller/new_base/base.rb @@ -1,7 +1,7 @@ module ActionController class Base < Http abstract! - + include AbstractController::Benchmarker include AbstractController::Callbacks include AbstractController::Logger @@ -14,6 +14,7 @@ module ActionController include ActionController::Renderers::All include ActionController::Layouts include ActionController::ConditionalGet + include ActionController::RackConvenience # Legacy modules include SessionManagement @@ -38,9 +39,9 @@ module ActionController # TODO: Extract into its own module # This should be moved together with other normalizing behavior module ImplicitRender - def process_action(method_name) + def send_action(method_name) ret = super - default_render if response_body.nil? + default_render unless performed? ret end @@ -65,31 +66,31 @@ module ActionController ::ActionController::Base.subclasses << klass.to_s super end - + def self.subclasses @subclasses ||= [] end - + def self.app_loaded! @subclasses.each do |subclass| subclass.constantize._write_layout_method end end - + def _normalize_options(action = nil, options = {}, &blk) if action.is_a?(Hash) - options, action = action, nil + options, action = action, nil elsif action.is_a?(String) || action.is_a?(Symbol) key = case action = action.to_s when %r{^/} then :file when %r{/} then :template else :action - end + end options.merge! key => action elsif action options.merge! :partial => action end - + if options.key?(:action) && options[:action].to_s.index("/") options[:template] = options.delete(:action) end @@ -139,7 +140,7 @@ module ActionController # # When using <tt>redirect_to :back</tt>, if there is no referrer, # RedirectBackError will be raised. You may specify some fallback - # behavior for this case by rescuing RedirectBackError. + # behavior for this case by rescuing RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? @@ -165,7 +166,7 @@ module ActionController else url_for(options) end - + super(url, status) end end diff --git a/actionpack/lib/action_controller/new_base/compatibility.rb b/actionpack/lib/action_controller/new_base/compatibility.rb index b3190486e8..f278c2da14 100644 --- a/actionpack/lib/action_controller/new_base/compatibility.rb +++ b/actionpack/lib/action_controller/new_base/compatibility.rb @@ -1,6 +1,6 @@ module ActionController module Rails2Compatibility - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern class ::ActionController::ActionControllerError < StandardError #:nodoc: end @@ -9,43 +9,43 @@ module ActionController included do ::ActionController::UnknownAction = ::AbstractController::ActionNotFound ::ActionController::DoubleRenderError = ::AbstractController::DoubleRenderError - + cattr_accessor :session_options self.session_options = {} - + cattr_accessor :allow_concurrency self.allow_concurrency = false - + cattr_accessor :param_parsers self.param_parsers = { Mime::MULTIPART_FORM => :multipart_form, Mime::URL_ENCODED_FORM => :url_encoded_form, Mime::XML => :xml_simple, Mime::JSON => :json } - + cattr_accessor :relative_url_root self.relative_url_root = ENV['RAILS_RELATIVE_URL_ROOT'] - + cattr_accessor :default_charset self.default_charset = "utf-8" - + # cattr_reader :protected_instance_variables cattr_accessor :protected_instance_variables self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller @action_name @before_filter_chain_aborted @action_cache_path @_headers @_params @_flash @_response) - + # Indicates whether or not optimise the generated named # route helper methods cattr_accessor :optimise_named_routes self.optimise_named_routes = true - + cattr_accessor :resources_path_names self.resources_path_names = { :new => 'new', :edit => 'edit' } - + # Controls the resource action separator cattr_accessor :resource_action_separator self.resource_action_separator = "/" - + cattr_accessor :use_accept_header self.use_accept_header = true @@ -61,19 +61,29 @@ module ActionController # and images to a dedicated asset server away from the main web server. Example: # ActionController::Base.asset_host = "http://assets.example.com" cattr_accessor :asset_host + + cattr_accessor :ip_spoofing_check + self.ip_spoofing_check = true end - + # For old tests def initialize_template_class(*) end def assign_shortcuts(*) end # TODO: Remove this after we flip def template - _action_view + @template ||= _action_view + end + + def process_action(*) + template + super end module ClassMethods - def consider_all_requests_local() end + def consider_all_requests_local + end + def rescue_action(env) raise env["action_dispatch.rescue.exception"] end @@ -83,17 +93,12 @@ module ActionController @@cache_store = ActiveSupport::Cache.lookup_store(store_option) end end - - def initialize(*) - super - @template = _action_view - end - + def render_to_body(options) if options.is_a?(Hash) && options.key?(:template) options[:template].sub!(/^\//, '') end - + options[:text] = nil if options[:nothing] == true body = super @@ -107,8 +112,8 @@ module ActionController def method_for_action(action_name) super || (respond_to?(:method_missing) && "_handle_method_missing") - end - + end + def _layout_prefix(name) super unless name =~ /\blayouts/ end @@ -116,5 +121,18 @@ module ActionController def performed? response_body end + + # ==== Request only view path switching ==== + def append_view_path(path) + view_paths.push(*path) + end + + def prepend_view_path(path) + view_paths.unshift(*path) + end + + def view_paths + _action_view.view_paths + end end end diff --git a/actionpack/lib/action_controller/new_base/conditional_get.rb b/actionpack/lib/action_controller/new_base/conditional_get.rb index 116ce34494..d287ec4994 100644 --- a/actionpack/lib/action_controller/new_base/conditional_get.rb +++ b/actionpack/lib/action_controller/new_base/conditional_get.rb @@ -1,12 +1,15 @@ module ActionController module ConditionalGet - + extend ActiveSupport::Concern + + include RackConvenience + # Sets the etag, last_modified, or both on the response and renders a # "304 Not Modified" response if the request is already fresh. # # Parameters: # * <tt>:etag</tt> - # * <tt>:last_modified</tt> + # * <tt>:last_modified</tt> # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches). # # Example: @@ -18,14 +21,14 @@ module ActionController # # This will render the show template if the request isn't sending a matching etag or # If-Modified-Since header and just a "304 Not Modified" response if there's a match. - # + # def fresh_when(options) options.assert_valid_keys(:etag, :last_modified, :public) response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] - - if options[:public] + + if options[:public] cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip } cache_control.delete("private") cache_control.delete("no-cache") @@ -36,8 +39,8 @@ module ActionController if request.fresh?(response) head :not_modified end - end - + end + # Return a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of @@ -64,8 +67,8 @@ module ActionController end render :nothing => true, :status => status - end - + end + # Sets the etag and/or last_modified on the response and checks it against # the client request. If the request doesn't match the options provided, the # request is considered stale and should be generated from scratch. Otherwise, @@ -73,7 +76,7 @@ module ActionController # # Parameters: # * <tt>:etag</tt> - # * <tt>:last_modified</tt> + # * <tt>:last_modified</tt> # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches). # # Example: @@ -92,7 +95,7 @@ module ActionController fresh_when(options) !request.fresh?(response) end - + # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that # intermediate caches shouldn't cache the response. # @@ -114,10 +117,10 @@ module ActionController else cache_control << "private" end - + # This allows for additional headers to be passed through like 'max-stale' => 5.hours cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} - + response.headers["Cache-Control"] = cache_control.join(', ') end @@ -126,6 +129,5 @@ module ActionController def expires_now #:doc: response.headers["Cache-Control"] = "no-cache" end - end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/new_base/helpers.rb b/actionpack/lib/action_controller/new_base/helpers.rb index e00c3c338b..e8000be87b 100644 --- a/actionpack/lib/action_controller/new_base/helpers.rb +++ b/actionpack/lib/action_controller/new_base/helpers.rb @@ -4,9 +4,9 @@ require 'active_support/dependencies' module ActionController module Helpers - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on AbstractController::Helpers + include AbstractController::Helpers included do # Set the default directory for helpers @@ -24,10 +24,10 @@ module ActionController # # * <tt>*args</tt>: One or more modules, strings or symbols, or the special symbol <tt>:all</tt>. # * <tt>&block</tt>: A block defining helper methods. - # + # # ==== Examples - # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file - # and include the module in the template class. The second form illustrates how to include custom helpers + # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file + # and include the module in the template class. The second form illustrates how to include custom helpers # when working with namespaced controllers, or other cases where the file containing the helper definition is not # in one of Rails' standard load paths: # helper :foo # => requires 'foo_helper' and includes FooHelper @@ -40,17 +40,17 @@ module ActionController # <tt>ActionController::Base.helpers_dir</tt> (defaults to <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT). # helper :all # - # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available + # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available # to the template. # # One line # helper { def hello() "Hello, world!" end } # # Multi-line # helper do - # def foo(bar) - # "#{bar} is the very best" + # def foo(bar) + # "#{bar} is the very best" # end # end - # + # # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of # +symbols+, +strings+, +modules+ and blocks. # helper(:three, BlindHelper) { def mice() 'mice' end } @@ -106,25 +106,24 @@ module ActionController end private - - def default_helper_module! - unless name.blank? - module_name = name.sub(/Controller$|$/, 'Helper') - module_path = module_name.split('::').map { |m| m.underscore }.join('/') - require_dependency module_path - helper module_name.constantize + def default_helper_module! + unless name.blank? + module_name = name.sub(/Controller$|$/, 'Helper') + module_path = module_name.split('::').map { |m| m.underscore }.join('/') + require_dependency module_path + helper module_name.constantize + end + rescue MissingSourceFile => e + raise unless e.is_missing? module_path + rescue NameError => e + raise unless e.missing_name? module_name end - rescue MissingSourceFile => e - raise unless e.is_missing? module_path - rescue NameError => e - raise unless e.missing_name? module_name - end - # Extract helper names from files in app/helpers/**/*.rb - def all_application_helpers - extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/ - Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' } - end - end # ClassMethods + # Extract helper names from files in app/helpers/**/*.rb + def all_application_helpers + extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/ + Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' } + end + end end end diff --git a/actionpack/lib/action_controller/new_base/hide_actions.rb b/actionpack/lib/action_controller/new_base/hide_actions.rb index a29b09a893..b45e520bee 100644 --- a/actionpack/lib/action_controller/new_base/hide_actions.rb +++ b/actionpack/lib/action_controller/new_base/hide_actions.rb @@ -1,33 +1,39 @@ module ActionController module HideActions - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do extlib_inheritable_accessor :hidden_actions self.hidden_actions ||= Set.new end - def action_methods() self.class.action_names end - def action_names() action_methods end - - private - - def action_method?(action_name) - !hidden_actions.include?(action_name) && super + def action_methods + self.class.action_names end - - module ClassMethods - def hide_action(*args) - args.each do |arg| - self.hidden_actions << arg.to_s - end - end - - def action_methods - @action_names ||= Set.new(super.reject {|name| self.hidden_actions.include?(name.to_s)}) - end - def self.action_names() action_methods end + def action_names + action_methods end + + private + def action_method?(action_name) + !hidden_actions.include?(action_name) && super + end + + module ClassMethods + def hide_action(*args) + args.each do |arg| + self.hidden_actions << arg.to_s + end + end + + def action_methods + @action_names ||= Set.new(super.reject {|name| self.hidden_actions.include?(name.to_s)}) + end + + def self.action_names + action_methods + end + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/new_base/http.rb b/actionpack/lib/action_controller/new_base/http.rb index 2525e221a6..c96aaaa865 100644 --- a/actionpack/lib/action_controller/new_base/http.rb +++ b/actionpack/lib/action_controller/new_base/http.rb @@ -4,9 +4,9 @@ require 'active_support/core_ext/module/delegation' module ActionController class Http < AbstractController::Base abstract! - + # :api: public - attr_internal :request, :response, :params + attr_internal :params, :env # :api: public def self.controller_name @@ -14,54 +14,78 @@ module ActionController end # :api: public - def controller_name() self.class.controller_name end + def controller_name + self.class.controller_name + end - # :api: public + # :api: public def self.controller_path @controller_path ||= self.name.sub(/Controller$/, '').underscore end - - # :api: public - def controller_path() self.class.controller_path end - - # :api: private - def self.action_names() action_methods end - + + # :api: public + def controller_path + self.class.controller_path + end + + # :api: private + def self.action_names + action_methods + end + # :api: private - def action_names() action_methods end - + def action_names + action_methods + end + # :api: plugin def self.call(env) controller = new controller.call(env).to_rack end - - delegate :headers, :to => "@_response" - def params - @_params ||= @_request.parameters + # The details below can be overridden to support a specific + # Request and Response object. The default ActionController::Base + # implementation includes RackConvenience, which makes a request + # and response object available. You might wish to control the + # environment and response manually for performance reasons. + + attr_internal :status, :headers, :content_type + + def initialize(*) + @_headers = {} + super + end + + # Basic implements for content_type=, location=, and headers are + # provided to reduce the dependency on the RackConvenience module + # in Renderer and Redirector. + + def content_type=(type) + headers["Content-Type"] = type.to_s end - + + def location=(url) + headers["Location"] = url + end + # :api: private def call(name, env) - @_request = ActionDispatch::Request.new(env) - @_response = ActionDispatch::Response.new - @_response.request = request + @_env = env process(name) to_rack end - + + # :api: private + def to_rack + [status, headers, response_body] + end + def self.action(name) @actions ||= {} @actions[name.to_s] ||= proc do |env| new.call(name, env) end end - - # :api: private - def to_rack - @_response.prepare! - @_response.to_a - end end end diff --git a/actionpack/lib/action_controller/new_base/layouts.rb b/actionpack/lib/action_controller/new_base/layouts.rb index 35068db770..0ff71587d6 100644 --- a/actionpack/lib/action_controller/new_base/layouts.rb +++ b/actionpack/lib/action_controller/new_base/layouts.rb @@ -1,36 +1,34 @@ module ActionController module Layouts - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern + + include ActionController::Renderer + include AbstractController::Layouts - depends_on ActionController::Renderer - depends_on AbstractController::Layouts - module ClassMethods def _implied_layout_name controller_path end end - - private - def _determine_template(options) - super - if (!options.key?(:text) && !options.key?(:inline) && !options.key?(:partial)) || options.key?(:layout) - options[:_layout] = _layout_for_option(options.key?(:layout) ? options[:layout] : :none, options[:_template].details) + private + def _determine_template(options) + super + if (!options.key?(:text) && !options.key?(:inline) && !options.key?(:partial)) || options.key?(:layout) + options[:_layout] = _layout_for_option(options.key?(:layout) ? options[:layout] : :none, options[:_template].details) + end end - end - - def _layout_for_option(name, details) - case name - when String then _layout_for_name(name, details) - when true then _default_layout(true, details) - when :none then _default_layout(false, details) - when false, nil then nil - else - raise ArgumentError, - "String, true, or false, expected for `layout'; you passed #{name.inspect}" + + def _layout_for_option(name, details) + case name + when String then _layout_for_name(name, details) + when true then _default_layout(true, details) + when :none then _default_layout(false, details) + when false, nil then nil + else + raise ArgumentError, + "String, true, or false, expected for `layout'; you passed #{name.inspect}" + end end - end - end end diff --git a/actionpack/lib/action_controller/new_base/rack_convenience.rb b/actionpack/lib/action_controller/new_base/rack_convenience.rb new file mode 100644 index 0000000000..5dfa7d12f3 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/rack_convenience.rb @@ -0,0 +1,33 @@ +module ActionController + module RackConvenience + extend ActiveSupport::Concern + + included do + delegate :headers, :status=, :location=, + :status, :location, :content_type, :to => "@_response" + attr_internal :request, :response + end + + def call(name, env) + @_request = ActionDispatch::Request.new(env) + @_response = ActionDispatch::Response.new + @_response.request = request + super + end + + def params + @_params ||= @_request.parameters + end + + # :api: private + def to_rack + @_response.prepare! + @_response.to_a + end + + def response_body=(body) + response.body = body if response + super + end + end +end diff --git a/actionpack/lib/action_controller/new_base/redirector.rb b/actionpack/lib/action_controller/new_base/redirector.rb index ff7b74341c..20060b001f 100644 --- a/actionpack/lib/action_controller/new_base/redirector.rb +++ b/actionpack/lib/action_controller/new_base/redirector.rb @@ -6,14 +6,14 @@ module ActionController super(message || DEFAULT_MESSAGE) end end - + module Redirector def redirect_to(url, status) #:doc: raise AbstractController::DoubleRenderError if response_body logger.info("Redirected to #{url}") if logger && logger.info? - response.status = status - response.location = url.gsub(/[\r\n]/, '') + self.status = status + self.location = url.gsub(/[\r\n]/, '') self.response_body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>" end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/new_base/render_options.rb b/actionpack/lib/action_controller/new_base/render_options.rb index 581a92cb7b..fc9a02626f 100644 --- a/actionpack/lib/action_controller/new_base/render_options.rb +++ b/actionpack/lib/action_controller/new_base/render_options.rb @@ -1,12 +1,12 @@ module ActionController module RenderOptions - extend ActiveSupport::DependencyModule - + extend ActiveSupport::Concern + included do extlib_inheritable_accessor :_renderers self._renderers = [] end - + module ClassMethods def _write_render_options renderers = _renderers.map do |r| @@ -17,33 +17,31 @@ module ActionController end RUBY_EVAL end - + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def _handle_render_options(options) #{renderers.join} end RUBY_EVAL end - + def _add_render_option(name) _renderers << name _write_render_options end end - + def render_to_body(options) _handle_render_options(options) || super end end - - module RenderOption - extend ActiveSupport::DependencyModule - included do - extend ActiveSupport::DependencyModule - depends_on ::ActionController::RenderOptions + module RenderOption #:nodoc: + def self.extended(base) + base.extend ActiveSupport::Concern + base.send :include, ::ActionController::RenderOptions - def self.register_renderer(name) + def base.register_renderer(name) included { _add_render_option(name) } end end @@ -51,57 +49,55 @@ module ActionController module Renderers module Json - include RenderOption + extend RenderOption register_renderer :json - + def _render_json(json, options) json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? - response.content_type ||= Mime::JSON + self.content_type ||= Mime::JSON self.response_body = json - end + end end module Js - include RenderOption + extend RenderOption register_renderer :js def _render_js(js, options) - response.content_type ||= Mime::JS + self.content_type ||= Mime::JS self.response_body = js end end module Xml - include RenderOption + extend RenderOption register_renderer :xml def _render_xml(xml, options) - response.content_type ||= Mime::XML + self.content_type ||= Mime::XML self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml : xml end end - module Rjs - include RenderOption + module RJS + extend RenderOption register_renderer :update def _render_update(proc, options) generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(_action_view, &proc) - response.content_type = Mime::JS + self.content_type = Mime::JS self.response_body = generator.to_s end end module All - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - included do - include ::ActionController::Renderers::Json - include ::ActionController::Renderers::Js - include ::ActionController::Renderers::Xml - include ::ActionController::Renderers::Rjs - end + include ActionController::Renderers::Json + include ActionController::Renderers::Js + include ActionController::Renderers::Xml + include ActionController::Renderers::RJS end end end diff --git a/actionpack/lib/action_controller/new_base/renderer.rb b/actionpack/lib/action_controller/new_base/renderer.rb index 987751a601..2fab501302 100644 --- a/actionpack/lib/action_controller/new_base/renderer.rb +++ b/actionpack/lib/action_controller/new_base/renderer.rb @@ -1,23 +1,18 @@ module ActionController module Renderer - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern + + include AbstractController::Renderer - depends_on AbstractController::Renderer - def process_action(*) self.formats = request.formats.map {|x| x.to_sym} super end - - def response_body=(body) - response.body = body if response - super - end def render(options) super options[:_template] ||= _action_view._partial - response.content_type ||= begin + self.content_type ||= begin mime = options[:_template].mime_type formats.include?(mime && mime.to_sym) || formats.include?(:all) ? mime : Mime::Type.lookup_by_extension(formats.first) end @@ -26,7 +21,7 @@ module ActionController def render_to_body(options) _process_options(options) - + if options.key?(:partial) _render_partial(options[:partial], options) end @@ -34,51 +29,50 @@ module ActionController super end - private + private + def _prefix + controller_path + end - def _prefix - controller_path - end + def _determine_template(options) + if options.key?(:text) + options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) + elsif options.key?(:inline) + handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") + template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) + options[:_template] = template + elsif options.key?(:template) + options[:_template_name] = options[:template] + elsif options.key?(:file) + options[:_template_name] = options[:file] + elsif !options.key?(:partial) + options[:_template_name] = (options[:action] || action_name).to_s + options[:_prefix] = _prefix + end - def _determine_template(options) - if options.key?(:text) - options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) - elsif options.key?(:inline) - handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") - template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) - options[:_template] = template - elsif options.key?(:template) - options[:_template_name] = options[:template] - elsif options.key?(:file) - options[:_template_name] = options[:file] - elsif !options.key?(:partial) - options[:_template_name] = (options[:action] || action_name).to_s - options[:_prefix] = _prefix + super end - - super - end - def _render_partial(partial, options) - case partial - when true - options[:_prefix] = _prefix - when String - options[:_prefix] = _prefix unless partial.index('/') - options[:_template_name] = partial - else - options[:_partial_object] = true - return + def _render_partial(partial, options) + case partial + when true + options[:_prefix] = _prefix + when String + options[:_prefix] = _prefix unless partial.index('/') + options[:_template_name] = partial + else + options[:_partial_object] = true + return + end + + options[:_partial] = options[:object] || true + end + + def _process_options(options) + status, content_type, location = options.values_at(:status, :content_type, :location) + self.status = status if status + self.content_type = content_type if content_type + self.headers["Location"] = url_for(location) if location end - - options[:_partial] = options[:object] || true - end - - def _process_options(options) - status, content_type, location = options.values_at(:status, :content_type, :location) - response.status = status if status - response.content_type = content_type if content_type - response.headers["Location"] = url_for(location) if location - end end end diff --git a/actionpack/lib/action_controller/new_base/rescuable.rb b/actionpack/lib/action_controller/new_base/rescuable.rb index 29ffe19d5f..029e643d93 100644 --- a/actionpack/lib/action_controller/new_base/rescuable.rb +++ b/actionpack/lib/action_controller/new_base/rescuable.rb @@ -15,7 +15,7 @@ module ActionController #:nodoc: # behavior is achieved by overriding the <tt>rescue_action_in_public</tt> # and <tt>rescue_action_locally</tt> methods. module Rescue - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do include ActiveSupport::Rescuable @@ -32,22 +32,21 @@ module ActionController #:nodoc: attr_internal :rescued_exception - private - - def method_for_action(action_name) - return action_name if self.rescued_exception = request.env.delete("action_dispatch.rescue.exception") - super - end + private + def method_for_action(action_name) + return action_name if self.rescued_exception = request.env.delete("action_dispatch.rescue.exception") + super + end - def _rescue_action - rescue_with_handler(rescued_exception) || raise(rescued_exception) - end + def _rescue_action + rescue_with_handler(rescued_exception) || raise(rescued_exception) + end - def process_action(*) - super - rescue Exception => exception - self.rescued_exception = exception - _rescue_action - end + def process_action(*) + super + rescue Exception => exception + self.rescued_exception = exception + _rescue_action + end end end diff --git a/actionpack/lib/action_controller/new_base/session.rb b/actionpack/lib/action_controller/new_base/session.rb index a8715555fb..bcedd6e1c7 100644 --- a/actionpack/lib/action_controller/new_base/session.rb +++ b/actionpack/lib/action_controller/new_base/session.rb @@ -1,5 +1,9 @@ module ActionController module Session + extend ActiveSupport::Concern + + include RackConvenience + def session @_request.session end diff --git a/actionpack/lib/action_controller/new_base/testing.rb b/actionpack/lib/action_controller/new_base/testing.rb index d8c3421587..a4a1116d9e 100644 --- a/actionpack/lib/action_controller/new_base/testing.rb +++ b/actionpack/lib/action_controller/new_base/testing.rb @@ -1,5 +1,8 @@ module ActionController module Testing + extend ActiveSupport::Concern + + include RackConvenience # OMG MEGA HAX def process_with_new_base_test(request, response) @@ -12,7 +15,7 @@ module ActionController set_test_assigns ret end - + def set_test_assigns @assigns = {} (instance_variable_names - self.class.protected_instance_variables).each do |var| @@ -26,5 +29,11 @@ module ActionController @_response ||= ActionDispatch::Response.new @_response.headers.replace(new_headers) end + + module ClassMethods + def before_filters + _process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name} + end + end end end diff --git a/actionpack/lib/action_controller/new_base/url_for.rb b/actionpack/lib/action_controller/new_base/url_for.rb index 94de9fab50..7119c14cd3 100644 --- a/actionpack/lib/action_controller/new_base/url_for.rb +++ b/actionpack/lib/action_controller/new_base/url_for.rb @@ -1,5 +1,9 @@ module ActionController module UrlFor + extend ActiveSupport::Concern + + include RackConvenience + def process_action(*) initialize_current_url super @@ -21,7 +25,7 @@ module ActionController # by this method. def default_url_options(options = nil) end - + def rewrite_options(options) #:nodoc: if defaults = default_url_options(options) defaults.merge(options) @@ -29,7 +33,7 @@ module ActionController options end end - + def url_for(options = {}) options ||= {} case options @@ -42,4 +46,4 @@ module ActionController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/routing/resources.rb b/actionpack/lib/action_controller/routing/resources.rb index 86abb7b2f4..05c782d226 100644 --- a/actionpack/lib/action_controller/routing/resources.rb +++ b/actionpack/lib/action_controller/routing/resources.rb @@ -1,3 +1,6 @@ +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/try' + module ActionController # == Overview # diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 4f936d51d2..2603855476 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -1,7 +1,7 @@ module ActionController module Routing class Segment #:nodoc: - RESERVED_PCHAR = ':@&=+$,;' + RESERVED_PCHAR = ':@&=+$,;%' SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" if RUBY_VERSION >= '1.9' UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze diff --git a/actionpack/lib/action_controller/testing/integration.rb b/actionpack/lib/action_controller/testing/integration.rb index cc157816e2..af4ccb7837 100644 --- a/actionpack/lib/action_controller/testing/integration.rb +++ b/actionpack/lib/action_controller/testing/integration.rb @@ -301,7 +301,7 @@ module ActionController # A module used to extend ActionController::Base, so that integration tests # can capture the controller used to satisfy a request. module ControllerCapture #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do alias_method_chain :initialize, :capture diff --git a/actionpack/lib/action_controller/testing/process2.rb b/actionpack/lib/action_controller/testing/process2.rb index 677dd41781..1c6fd2d80a 100644 --- a/actionpack/lib/action_controller/testing/process2.rb +++ b/actionpack/lib/action_controller/testing/process2.rb @@ -51,9 +51,10 @@ module ActionController @request.session = ActionController::TestSession.new(session) unless session.nil? @request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash - build_request_uri(action, parameters) + @controller.request = @request @controller.params.merge!(parameters) + build_request_uri(action, parameters) # Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest @controller.process_with_new_base_test(@request, @response) @response diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index a992f7d912..51e0868995 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,3 +1,6 @@ +require 'set' +require 'active_support/core_ext/class/inheritable_attributes' + module HTML class Sanitizer def sanitize(text, options = {}) diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index edb522ff49..b9db7a4508 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -162,8 +162,15 @@ module ActionDispatch # :nodoc: end def assign_default_content_type_and_charset! - self.content_type ||= Mime::HTML - self.charset ||= default_charset unless sending_file? + if type = headers['Content-Type'] || headers['type'] + unless type =~ /charset=/ || sending_file? + headers['Content-Type'] = "#{type}; charset=#{default_charset}" + end + else + type = Mime::HTML.to_s + type += "; charset=#{default_charset}" unless sending_file? + headers['Content-Type'] = type + end end def prepare! diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 4d598669c7..bfff307669 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -10,7 +10,8 @@ module ActionDispatch @@rescue_responses = Hash.new(:internal_server_error) @@rescue_responses.update({ 'ActionController::RoutingError' => :not_found, - 'ActionController::UnknownAction' => :not_found, + # TODO: Clean this up after the switch + ActionController::UnknownAction.name => :not_found, 'ActiveRecord::RecordNotFound' => :not_found, 'ActiveRecord::StaleObjectError' => :conflict, 'ActiveRecord::RecordInvalid' => :unprocessable_entity, @@ -23,14 +24,17 @@ module ActionDispatch cattr_accessor :rescue_templates @@rescue_templates = Hash.new('diagnostics') @@rescue_templates.update({ - 'ActionView::MissingTemplate' => 'missing_template', - 'ActionController::RoutingError' => 'routing_error', - 'ActionController::UnknownAction' => 'unknown_action', - 'ActionView::TemplateError' => 'template_error' + 'ActionView::MissingTemplate' => 'missing_template', + 'ActionController::RoutingError' => 'routing_error', + ActionController::UnknownAction.name => 'unknown_action', + 'ActionView::TemplateError' => 'template_error' }) FAILSAFE_RESPONSE = [500, {'Content-Type' => 'text/html'}, - ['<html><body><h1>500 Internal Server Error</h1></body></html>']] + ["<html><body><h1>500 Internal Server Error</h1>" << + "If you are the administrator of this website, then please read this web " << + "application's log file and/or the web server's log file to find out what " << + "went wrong.</body></html>"]] def initialize(app, consider_all_requests_local = false) @app = app diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 89d1a49403..e6d6b5a3ef 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/diff' + module ActionDispatch module Assertions # Suite of assertions to test routes generated by Rails and the handling of requests made to them. diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 9e39536653..b4197479a0 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -124,7 +124,11 @@ module ActionView # Use an alternate output buffer for the duration of the block. # Defaults to a new empty string. - def with_output_buffer(buf = '') #:nodoc: + def with_output_buffer(buf = nil) #:nodoc: + unless buf + buf = '' + buf.force_encoding(output_buffer.encoding) if buf.respond_to?(:force_encoding) + end self.output_buffer, old_buffer = buf, output_buffer yield output_buffer @@ -134,9 +138,12 @@ module ActionView # Add the output buffer to the response body and start a new one. def flush_output_buffer #:nodoc: - if output_buffer && output_buffer != '' + if output_buffer && !output_buffer.empty? response.body_parts << output_buffer - self.output_buffer = '' + new = '' + new.force_encoding(output_buffer.encoding) if new.respond_to?(:force_encoding) + self.output_buffer = new + nil end end end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index daf38fe3da..c96b1fc8d2 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -353,7 +353,8 @@ module ActionView disable_with << ";#{options.delete('onclick')}" if options['onclick'] options["onclick"] = "if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }" - options["onclick"] << "else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }" + options["onclick"] << "else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';" + options["onclick"] << "hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }" options["onclick"] << "this.setAttribute('originalValue', this.value);this.disabled = true;#{disable_with};" options["onclick"] << "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());" options["onclick"] << "if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" diff --git a/actionpack/lib/action_view/render/partials.rb b/actionpack/lib/action_view/render/partials.rb index f2429e8cef..eacf117bea 100644 --- a/actionpack/lib/action_view/render/partials.rb +++ b/actionpack/lib/action_view/render/partials.rb @@ -171,7 +171,7 @@ module ActionView # <% end %> module Partials extend ActiveSupport::Memoizable - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do attr_accessor :_partial diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 95f11d6490..d773df7d29 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -16,11 +16,7 @@ module ActionView self.default_format = Mime::HTML def compile(template) - src = ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src - - # Ruby 1.9 prepends an encoding to the source. However this is - # useless because you can only set an encoding on the first line - RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src + ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src end end end diff --git a/actionpack/lib/action_view/template/template.rb b/actionpack/lib/action_view/template/template.rb index f61dd591a5..a9897258d2 100644 --- a/actionpack/lib/action_view/template/template.rb +++ b/actionpack/lib/action_view/template/template.rb @@ -52,16 +52,26 @@ module ActionView locals_code = locals.keys.map! { |key| "#{key} = local_assigns[:#{key}];" }.join + code = @handler.call(self) + encoding_comment = $1 if code.sub!(/\A(#.*coding.*)\n/, '') + source = <<-end_src def #{method_name}(local_assigns) - old_output_buffer = output_buffer;#{locals_code};#{@handler.call(self)} + old_output_buffer = output_buffer;#{locals_code};#{code} ensure self.output_buffer = old_output_buffer end end_src + if encoding_comment + source = "#{encoding_comment}\n#{source}" + line = -1 + else + line = 0 + end + begin - ActionView::Base::CompiledTemplates.module_eval(source, identifier, 0) + ActionView::Base::CompiledTemplates.module_eval(source, identifier, line) method_name rescue Exception => e # errors from template code if logger = (view && view.logger) @@ -79,4 +89,4 @@ module ActionView "_render_template_#{@identifier.hash}_#{__id__}_#{locals.keys.hash}".gsub('-', "_") end end -end
\ No newline at end of file +end diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb index 39d0017dd8..7199da3441 100644 --- a/actionpack/test/controller/cookie_test.rb +++ b/actionpack/test/controller/cookie_test.rb @@ -123,6 +123,12 @@ class CookieTest < ActionController::TestCase assert_cookie_header "user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT" end + def test_cookies_persist_throughout_request + get :authenticate + cookies = @controller.send(:cookies) + assert_equal 'david', cookies['user_name'] + end + private def assert_cookie_header(expected) header = @response.headers["Set-Cookie"] diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 22aa50adb2..5e28ef6007 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -1,15 +1,36 @@ require 'abstract_unit' +require 'active_support/core_ext/symbol' -class << ActionController::Base - %w(append_around_filter prepend_after_filter prepend_around_filter prepend_before_filter skip_after_filter skip_before_filter skip_filter).each do |pending| - define_method(pending) do |*args| - $stderr.puts "#{pending} unimplemented: #{args.inspect}" - end unless method_defined?(pending) +class ActionController::Base + class << self + %w(append_around_filter prepend_after_filter prepend_around_filter prepend_before_filter skip_after_filter skip_before_filter skip_filter).each do |pending| + define_method(pending) do |*args| + $stderr.puts "#{pending} unimplemented: #{args.inspect}" + end unless method_defined?(pending) + end + + if defined?(ActionController::Http) + def before_filters + filters = _process_action_callbacks.select { |c| c.kind == :before } + filters.map! { |c| c.instance_variable_get(:@raw_filter) } + end + end + end + + if defined?(ActionController::Http) + def assigns(key = nil) + assigns = {} + instance_variable_names.each do |ivar| + next if ActionController::Base.protected_instance_variables.include?(ivar) + assigns[ivar[1..-1]] = instance_variable_get(ivar) + end + + key.nil? ? assigns : assigns[key.to_s] + end end end -class FilterTest < Test::Unit::TestCase - include ActionController::TestProcess +class FilterTest < ActionController::TestCase class TestController < ActionController::Base before_filter :ensure_login @@ -468,108 +489,108 @@ class FilterTest < Test::Unit::TestCase def test_running_filters test_process(PrependingController) - assert_equal %w( wonderful_life ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"] end def test_running_filters_with_proc test_process(ProcController) - assert @controller.template.assigns["ran_proc_filter"] + assert assigns["ran_proc_filter"] end def test_running_filters_with_implicit_proc test_process(ImplicitProcController) - assert @controller.template.assigns["ran_proc_filter"] + assert assigns["ran_proc_filter"] end def test_running_filters_with_class test_process(AuditController) - assert @controller.template.assigns["was_audited"] + assert assigns["was_audited"] end def test_running_anomolous_yet_valid_condition_filters test_process(AnomolousYetValidConditionController) - assert_equal %w( ensure_login ), @controller.template.assigns["ran_filter"] - assert @controller.template.assigns["ran_class_filter"] - assert @controller.template.assigns["ran_proc_filter1"] - assert @controller.template.assigns["ran_proc_filter2"] + assert_equal %w( ensure_login ), assigns["ran_filter"] + assert assigns["ran_class_filter"] + assert assigns["ran_proc_filter1"] + assert assigns["ran_proc_filter2"] test_process(AnomolousYetValidConditionController, "show_without_filter") - assert_equal nil, @controller.template.assigns["ran_filter"] - assert !@controller.template.assigns["ran_class_filter"] - assert !@controller.template.assigns["ran_proc_filter1"] - assert !@controller.template.assigns["ran_proc_filter2"] + assert_equal nil, assigns["ran_filter"] + assert !assigns["ran_class_filter"] + assert !assigns["ran_proc_filter1"] + assert !assigns["ran_proc_filter2"] end def test_running_conditional_options test_process(ConditionalOptionsFilter) - assert_equal %w( ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login ), assigns["ran_filter"] end def test_running_collection_condition_filters test_process(ConditionalCollectionFilterController) - assert_equal %w( ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login ), assigns["ran_filter"] test_process(ConditionalCollectionFilterController, "show_without_filter") - assert_equal nil, @controller.template.assigns["ran_filter"] + assert_equal nil, assigns["ran_filter"] test_process(ConditionalCollectionFilterController, "another_action") - assert_equal nil, @controller.template.assigns["ran_filter"] + assert_equal nil, assigns["ran_filter"] end def test_running_only_condition_filters test_process(OnlyConditionSymController) - assert_equal %w( ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login ), assigns["ran_filter"] test_process(OnlyConditionSymController, "show_without_filter") - assert_equal nil, @controller.template.assigns["ran_filter"] + assert_equal nil, assigns["ran_filter"] test_process(OnlyConditionProcController) - assert @controller.template.assigns["ran_proc_filter"] + assert assigns["ran_proc_filter"] test_process(OnlyConditionProcController, "show_without_filter") - assert !@controller.template.assigns["ran_proc_filter"] + assert !assigns["ran_proc_filter"] test_process(OnlyConditionClassController) - assert @controller.template.assigns["ran_class_filter"] + assert assigns["ran_class_filter"] test_process(OnlyConditionClassController, "show_without_filter") - assert !@controller.template.assigns["ran_class_filter"] + assert !assigns["ran_class_filter"] end def test_running_except_condition_filters test_process(ExceptConditionSymController) - assert_equal %w( ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login ), assigns["ran_filter"] test_process(ExceptConditionSymController, "show_without_filter") - assert_equal nil, @controller.template.assigns["ran_filter"] + assert_equal nil, assigns["ran_filter"] test_process(ExceptConditionProcController) - assert @controller.template.assigns["ran_proc_filter"] + assert assigns["ran_proc_filter"] test_process(ExceptConditionProcController, "show_without_filter") - assert !@controller.template.assigns["ran_proc_filter"] + assert !assigns["ran_proc_filter"] test_process(ExceptConditionClassController) - assert @controller.template.assigns["ran_class_filter"] + assert assigns["ran_class_filter"] test_process(ExceptConditionClassController, "show_without_filter") - assert !@controller.template.assigns["ran_class_filter"] + assert !assigns["ran_class_filter"] end def test_running_before_and_after_condition_filters test_process(BeforeAndAfterConditionController) - assert_equal %w( ensure_login clean_up_tmp), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"] test_process(BeforeAndAfterConditionController, "show_without_filter") - assert_equal nil, @controller.template.assigns["ran_filter"] + assert_equal nil, assigns["ran_filter"] end def test_around_filter test_process(AroundFilterController) - assert @controller.template.assigns["before_ran"] - assert @controller.template.assigns["after_ran"] + assert assigns["before_ran"] + assert assigns["after_ran"] end def test_before_after_class_filter test_process(BeforeAfterClassFilterController) - assert @controller.template.assigns["before_ran"] - assert @controller.template.assigns["after_ran"] + assert assigns["before_ran"] + assert assigns["after_ran"] end def test_having_properties_in_around_filter test_process(AroundFilterController) - assert_equal "before and after", @controller.template.assigns["execution_log"] + assert_equal "before and after", assigns["execution_log"] end def test_prepending_and_appending_around_filter @@ -582,7 +603,7 @@ class FilterTest < Test::Unit::TestCase def test_rendering_breaks_filtering_chain response = test_process(RenderingController) assert_equal "something else", response.body - assert !@controller.template.assigns["ran_action"] + assert !assigns["ran_action"] end def test_filters_with_mixed_specialization_run_in_order @@ -601,33 +622,33 @@ class FilterTest < Test::Unit::TestCase %w(foo bar baz).each do |action| request = ActionController::TestRequest.new request.query_parameters[:choose] = action - response = DynamicDispatchController.action.call(request.env).last + response = DynamicDispatchController.action(action).call(request.env).last assert_equal action, response.body end end def test_running_prepended_before_and_after_filter test_process(PrependingBeforeAndAfterController) - assert_equal %w( before_all between_before_all_and_after_all after_all ), @controller.template.assigns["ran_filter"] + assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"] end def test_skipping_and_limiting_controller test_process(SkippingAndLimitedController, "index") - assert_equal %w( ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login ), assigns["ran_filter"] test_process(SkippingAndLimitedController, "public") - assert_nil @controller.template.assigns["ran_filter"] + assert_nil assigns["ran_filter"] end def test_skipping_and_reordering_controller test_process(SkippingAndReorderingController, "index") - assert_equal %w( find_record ensure_login ), @controller.template.assigns["ran_filter"] + assert_equal %w( find_record ensure_login ), assigns["ran_filter"] end def test_conditional_skipping_of_filters test_process(ConditionalSkippingController, "login") - assert_nil @controller.template.assigns["ran_filter"] + assert_nil assigns["ran_filter"] test_process(ConditionalSkippingController, "change_password") - assert_equal %w( ensure_login find_user ), @controller.template.assigns["ran_filter"] + assert_equal %w( ensure_login find_user ), assigns["ran_filter"] test_process(ConditionalSkippingController, "login") assert_nil @controller.template.controller.instance_variable_get("@ran_after_filter") @@ -637,23 +658,23 @@ class FilterTest < Test::Unit::TestCase def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent conditional_in_parent ), @controller.template.assigns['ran_filter'] + assert_equal %w( conditional_in_parent conditional_in_parent ), assigns['ran_filter'] test_process(ChildOfConditionalParentController, 'another_action') - assert_nil @controller.template.assigns['ran_filter'] + assert_nil assigns['ran_filter'] end def test_condition_skipping_of_filters_when_siblings_also_have_conditions test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent conditional_in_parent ), @controller.template.assigns['ran_filter'], "1" + assert_equal %w( conditional_in_parent conditional_in_parent ), assigns['ran_filter'], "1" test_process(AnotherChildOfConditionalParentController) - assert_equal nil, @controller.template.assigns['ran_filter'] + assert_equal nil, assigns['ran_filter'] test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent conditional_in_parent ), @controller.template.assigns['ran_filter'] + assert_equal %w( conditional_in_parent conditional_in_parent ), assigns['ran_filter'] end def test_changing_the_requirements test_process(ChangingTheRequirementsController, "go_wild") - assert_equal nil, @controller.template.assigns['ran_filter'] + assert_equal nil, assigns['ran_filter'] end def test_a_rescuing_around_filter @@ -806,9 +827,8 @@ class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters skip_filter :after end -class YieldingAroundFiltersTest < Test::Unit::TestCase +class YieldingAroundFiltersTest < ActionController::TestCase include PostsController::AroundExceptions - include ActionController::TestProcess def test_base controller = PostsController @@ -846,8 +866,8 @@ class YieldingAroundFiltersTest < Test::Unit::TestCase def test_with_proc test_process(ControllerWithProcFilter,'no_raise') - assert @controller.template.assigns['before'] - assert @controller.template.assigns['after'] + assert assigns['before'] + assert assigns['after'] end def test_nested_filters @@ -868,12 +888,12 @@ class YieldingAroundFiltersTest < Test::Unit::TestCase def test_filter_order_with_all_filter_types test_process(ControllerWithAllTypesOfFilters,'no_raise') - assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) around (after yield) after', @controller.template.assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) around (after yield) after', assigns['ran_filter'].join(' ') end def test_filter_order_with_skip_filter_method test_process(ControllerWithTwoLessFilters,'no_raise') - assert_equal 'before around (before yield) around (after yield)', @controller.template.assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') end def test_first_filter_in_multiple_before_filter_chain_halts @@ -903,9 +923,6 @@ class YieldingAroundFiltersTest < Test::Unit::TestCase protected def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - process(action) end end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 84e27d7779..c448f36cb3 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -122,7 +122,7 @@ class FlashTest < ActionController::TestCase assert_nil assigns["flash_copy"]["that"], "On second flash" assert_equal "hello again", assigns["flash_copy"]["this"], "On second flash" end - + def test_flash_after_reset_session get :use_flash_after_reset_session assert_equal "hello", assigns["flashy_that"] @@ -130,6 +130,11 @@ class FlashTest < ActionController::TestCase assert_nil assigns["flashy_that_reset"] end + def test_does_not_set_the_session_if_the_flash_is_empty + get :std_action + assert_nil session["flash"] + end + def test_sweep_after_halted_filter_chain get :std_action assert_nil assigns["flash_copy"]["foo"] @@ -140,4 +145,19 @@ class FlashTest < ActionController::TestCase get :std_action assert_nil assigns["flash_copy"]["foo"] end + + def test_keep_and_discard_return_values + flash = ActionController::Flash::FlashHash.new + flash.update(:foo => :foo_indeed, :bar => :bar_indeed) + + assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed + assert_nil flash.discard(:unknown) # non existant key passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard()) # nothing passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil)) # nothing passed + + assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed + assert_nil flash.keep(:unknown) # non existant key passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep()) # nothing passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil)) # nothing passed + end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 11bffdb42e..6b08a04b10 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -90,6 +90,11 @@ class StaticSegmentTest < Test::Unit::TestCase assert_equal 'Hello World', s.interpolation_chunk end + def test_value_should_not_be_double_unescaped + s = ROUTING::StaticSegment.new('%D0%9A%D0%B0%D1%80%D1%82%D0%B0') # Карта + assert_equal '%D0%9A%D0%B0%D1%80%D1%82%D0%B0', s.interpolation_chunk + end + def test_regexp_chunk_should_escape_specials s = ROUTING::StaticSegment.new('Hello*World') assert_equal 'Hello\*World', s.regexp_chunk @@ -644,9 +649,8 @@ class RoutingTest < Test::Unit::TestCase ActionController::Routing.use_controllers! nil - silence_warnings do - Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + '/controller_fixtures') - end + Object.send(:remove_const, :RAILS_ROOT) if defined?(::RAILS_ROOT) + Object.const_set(:RAILS_ROOT, File.dirname(__FILE__) + '/controller_fixtures') ActionController::Routing.controller_paths = [ RAILS_ROOT, RAILS_ROOT + '/app/controllers', RAILS_ROOT + '/vendor/plugins/bad_plugin/lib' @@ -2482,7 +2486,8 @@ end class RouteLoadingTest < Test::Unit::TestCase def setup routes.instance_variable_set '@routes_last_modified', nil - silence_warnings { Object.const_set :RAILS_ROOT, '.' } + Object.remove_const(:RAILS_ROOT) if defined?(::RAILS_ROOT) + Object.const_set :RAILS_ROOT, '.' routes.add_configuration_file(File.join(RAILS_ROOT, 'config', 'routes.rb')) @stat = stub_everything diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb index 0ac10634b2..c732d1c910 100644 --- a/actionpack/test/controller/view_paths_test.rb +++ b/actionpack/test/controller/view_paths_test.rb @@ -22,7 +22,7 @@ class ViewLoadPathsTest < ActionController::TestCase end def setup - TestController.view_paths = nil + # TestController.view_paths = nil @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @@ -48,7 +48,7 @@ class ViewLoadPathsTest < ActionController::TestCase def assert_paths(*paths) controller = paths.first.is_a?(Class) ? paths.shift : @controller - assert_equal expand(paths), controller.view_paths.map(&:to_s) + assert_equal expand(paths), controller.view_paths.map { |p| p.to_s } end def test_template_load_path_was_set_correctly diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 3090a70244..2db76818ac 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -5,6 +5,9 @@ class CookieStoreTest < ActionController::IntegrationTest SessionKey = '_myapp_session' SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' + # Make sure Session middleware doesnt get included in the middleware stack + ActionController::Base.session_store = nil + DispatcherApp = ActionController::Dispatcher.new CookieStoreApp = ActionDispatch::Session::CookieStore.new(DispatcherApp, :key => SessionKey, :secret => SessionSecret) diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 0c0c087340..ce1973853e 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -61,7 +61,7 @@ class ShowExceptionsTest < ActionController::IntegrationTest get "/not_found" assert_response 404 - assert_match /ActionController::UnknownAction/, body + assert_match /#{ActionController::UnknownAction.name}/, body get "/method_not_allowed" assert_response 405 @@ -99,7 +99,7 @@ class ShowExceptionsTest < ActionController::IntegrationTest get "/not_found" assert_response 404 - assert_match /ActionController::UnknownAction/, body + assert_match /#{ActionController::UnknownAction.name}/, body get "/method_not_allowed" assert_response 405 diff --git a/actionpack/test/fixtures/test/utf8.html.erb b/actionpack/test/fixtures/test/utf8.html.erb index 0b4d19aa0e..58cd03b439 100644 --- a/actionpack/test/fixtures/test/utf8.html.erb +++ b/actionpack/test/fixtures/test/utf8.html.erb @@ -1,2 +1,5 @@ +<%# encoding: utf-8 -%> Русский текст -日本語のテキスト
\ No newline at end of file +<%= "日".encoding %> +<%= @output_buffer.encoding %> +<%= __ENCODING__ %> diff --git a/actionpack/test/template/body_parts_test.rb b/actionpack/test/template/body_parts_test.rb index e17092a452..4e7aa63f96 100644 --- a/actionpack/test/template/body_parts_test.rb +++ b/actionpack/test/template/body_parts_test.rb @@ -4,6 +4,9 @@ class BodyPartsTest < ActionController::TestCase RENDERINGS = [Object.new, Object.new, Object.new] class TestController < ActionController::Base + def performed? + defined?(ActionController::Http) ? true : super + end def index RENDERINGS.each do |rendering| @template.punctuate_body! rendering @@ -16,7 +19,7 @@ class BodyPartsTest < ActionController::TestCase def test_body_parts get :index - pending do + pending(:old_base) do # TestProcess buffers body_parts into body # TODO: Rewrite test w/o going through process assert_equal RENDERINGS, @response.body_parts diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index ea0be4a27a..5ca4d4d6ea 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -253,14 +253,14 @@ class FormTagHelperTest < ActionView::TestCase def test_submit_tag assert_dom_equal( - %(<input name='commit' type='submit' value='Save' onclick="if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';alert('hello!');result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" />), + %(<input name='commit' onclick="if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';alert('hello!');result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" type="submit" value="Save" />), submit_tag("Save", :disable_with => "Saving...", :onclick => "alert('hello!')") ) end def test_submit_tag_with_no_onclick_options assert_dom_equal( - %(<input name='commit' type='submit' value='Save' onclick="if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" />), + %(<input name='commit' onclick="if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" type="submit" value="Save" />), submit_tag("Save", :disable_with => "Saving...") ) end @@ -274,7 +274,7 @@ class FormTagHelperTest < ActionView::TestCase def test_submit_tag_with_confirmation_and_with_disable_with assert_dom_equal( - %(<input name="commit" type="submit" value="Save" onclick="if (!confirm('Are you sure?')) return false; if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" />), + %(<input name="commit" onclick="if (!confirm('Are you sure?')) return false; if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" type="submit" value="Save" />), submit_tag("Save", :disable_with => "Saving...", :confirm => "Are you sure?") ) end diff --git a/actionpack/test/template/output_buffer_test.rb b/actionpack/test/template/output_buffer_test.rb index 171cfb63e1..36bbaf9099 100644 --- a/actionpack/test/template/output_buffer_test.rb +++ b/actionpack/test/template/output_buffer_test.rb @@ -9,33 +9,52 @@ class OutputBufferTest < ActionController::TestCase tests TestController - def test_flush_output_buffer - pending - # TODO: This tests needs to be rewritten due - # The @response is not the same response object assigned - # to the @controller.template - - # Start with the default body parts - # --- - # get :index - # assert_equal ['foo'], @response.body_parts - # assert_nil @controller.template.output_buffer - # - # # Nil output buffer is skipped - # @controller.template.flush_output_buffer - # assert_nil @controller.template.output_buffer - # assert_equal ['foo'], @response.body_parts - # - # # Empty output buffer is skipped - # @controller.template.output_buffer = '' - # @controller.template.flush_output_buffer - # assert_equal '', @controller.template.output_buffer - # assert_equal ['foo'], @response.body_parts - # - # # Flushing appends the output buffer to the body parts - # @controller.template.output_buffer = 'bar' - # @controller.template.flush_output_buffer - # assert_equal '', @controller.template.output_buffer - # assert_equal ['foo', 'bar'], @response.body_parts + def setup + get :index + assert_equal ['foo'], body_parts end + + test 'output buffer is nil after rendering' do + assert_nil output_buffer + end + + test 'flushing ignores nil output buffer' do + @controller.template.flush_output_buffer + assert_nil output_buffer + assert_equal ['foo'], body_parts + end + + test 'flushing ignores empty output buffer' do + @controller.template.output_buffer = '' + @controller.template.flush_output_buffer + assert_equal '', output_buffer + assert_equal ['foo'], body_parts + end + + test 'flushing appends the output buffer to the body parts' do + @controller.template.output_buffer = 'bar' + @controller.template.flush_output_buffer + assert_equal '', output_buffer + assert_equal ['foo', 'bar'], body_parts + end + + if '1.9'.respond_to?(:force_encoding) + test 'flushing preserves output buffer encoding' do + original_buffer = ' '.force_encoding(Encoding::EUC_JP) + @controller.template.output_buffer = original_buffer + @controller.template.flush_output_buffer + assert_equal ['foo', original_buffer], body_parts + assert_not_equal original_buffer, output_buffer + assert_equal Encoding::EUC_JP, output_buffer.encoding + end + end + + protected + def output_buffer + @controller.template.output_buffer + end + + def body_parts + @controller.template.response.body_parts + end end diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 71291f009c..a56d7aee75 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -249,7 +249,7 @@ module RenderTestCases if '1.9'.respond_to?(:force_encoding) def test_render_utf8_template result = @view.render(:file => "test/utf8.html.erb", :layouts => "layouts/yield") - assert_equal "Русский текст\n日本語のテキスト", result + assert_equal "Русский текст\nUTF-8\nUTF-8\nUTF-8\n", result assert_equal Encoding::UTF_8, result.encoding end end diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 544f49310a..ae2fbdb002 100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -7,9 +7,8 @@ task :default => :test Rake::TestTask.new do |t| t.libs << "test" - t.pattern = 'test/**/*_test.rb' + t.test_files = Dir.glob("test/cases/**/*_test.rb").sort t.verbose = true - t.warning = true end task :isolated_test do ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 4ed7b0889d..803f5b0157 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,41 @@ -require 'active_model/observing' -# disabled until they're tested -# require 'active_model/callbacks' -# require 'active_model/validations' -require 'active_model/base'
\ No newline at end of file +#-- +# Copyright (c) 2004-2009 David Heinemeier Hansson +# +# 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: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# 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. +#++ + +activesupport_path = "#{File.dirname(__FILE__)}/../../../activesupport/lib" +$:.unshift(activesupport_path) if File.directory?(activesupport_path) +require 'active_support' + +require 'active_support/inflector' + +module ActiveModel + autoload :Base, 'active_model/base' + autoload :Observing, 'active_model/observing' + autoload :Validations, 'active_model/validations' + autoload :Errors, 'active_model/errors' + autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods' + autoload :TestCase, 'active_model/test_case' + autoload :StateMachine, 'active_model/state_machine' + autoload :ValidationsRepairHelper, 'active_model/validations_repair_helper' +end + +I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb deleted file mode 100644 index c94f76109f..0000000000 --- a/activemodel/lib/active_model/callbacks.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'active_model/core' - -module ActiveModel - module Callbacks - - end -end
\ No newline at end of file diff --git a/activemodel/lib/active_model/core.rb b/activemodel/lib/active_model/core.rb deleted file mode 100644 index 4201bcf158..0000000000 --- a/activemodel/lib/active_model/core.rb +++ /dev/null @@ -1,3 +0,0 @@ -activesupport_path = "#{File.dirname(__FILE__)}/../../../activesupport/lib" -$:.unshift(activesupport_path) if File.directory?(activesupport_path) -require 'active_support/inflector' diff --git a/activemodel/lib/active_model/deprecated_error_methods.rb b/activemodel/lib/active_model/deprecated_error_methods.rb index e0cbd9ba29..433de8931a 100644 --- a/activemodel/lib/active_model/deprecated_error_methods.rb +++ b/activemodel/lib/active_model/deprecated_error_methods.rb @@ -1,18 +1,18 @@ module ActiveModel module DeprecatedErrorMethods def on(attribute) - ActiveSupport::Deprecation.warn "Errors#on have been deprecated, use Errors#[] instead" - self[attribute] + message = "Errors#on have been deprecated, use Errors#[] instead.\n" + message << "Also note that the behaviour of Errors#[] has changed. Errors#[] now always returns an Array. An empty Array is " + message << "returned when there are no errors on the specified attribute." + ActiveSupport::Deprecation.warn(message) + + errors = self[attribute] + errors.size < 2 ? errors.first : errors end def on_base ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead" - on(:base) - end - - def add(attribute, msg = Errors.default_error_messages[:invalid]) - ActiveSupport::Deprecation.warn "Errors#add(attribute, msg) has been deprecated, use Errors#[attribute] << msg instead" - self[attribute] << msg + ActiveSupport::Deprecation.silence { on(:base) } end def add_to_base(msg) @@ -25,11 +25,6 @@ module ActiveModel self[attribute].any? end - def full_messages - ActiveSupport::Deprecation.warn "Errors#full_messages has been deprecated, use Errors#to_a instead" - to_a - end - def each_full ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead" to_a.each { |error| yield error } diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index bcf0810290..4be91d0505 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,40 +1,18 @@ module ActiveModel class Errors < Hash include DeprecatedErrorMethods - - @@default_error_messages = { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is %d characters)", - :too_short => "is too short (minimum is %d characters)", - :wrong_length => "is the wrong length (should be %d characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than %d", - :greater_than_or_equal_to => "must be greater than or equal to %d", - :equal_to => "must be equal to %d", - :less_than => "must be less than %d", - :less_than_or_equal_to => "must be less than or equal to %d", - :odd => "must be odd", - :even => "must be even" - } - - ## - # :singleton-method: - # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. - cattr_accessor :default_error_messages + + def initialize(base) + @base = base + super() + end alias_method :get, :[] alias_method :set, :[]= def [](attribute) if errors = get(attribute.to_sym) - errors.size == 1 ? errors.first : errors + errors else set(attribute.to_sym, []) end @@ -55,28 +33,126 @@ module ActiveModel end def to_a - inject([]) do |errors_with_attributes, (attribute, errors)| - if error.blank? - errors_with_attributes - else - if attr == :base - errors_with_attributes << error - else - errors_with_attributes << (attribute.to_s.humanize + " " + error) - end - end - end + full_messages + end + + def count + to_a.size end def to_xml(options={}) + require 'builder' unless defined? ::Builder options[:root] ||= "errors" options[:indent] ||= 2 - options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) + options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) options[:builder].instruct! unless options.delete(:skip_instruct) options[:builder].errors do |e| to_a.each { |error| e.error(error) } end end + + # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> + # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one + # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. + # If no +messsage+ is supplied, :invalid is assumed. + # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). + def add(attribute, message = nil, options = {}) + message ||= :invalid + message = generate_message(attribute, message, options) if message.is_a?(Symbol) + self[attribute] << message + end + + # Will add an error message to each of the attributes in +attributes+ that is empty. + def add_on_empty(attributes, custom_message = nil) + [attributes].flatten.each do |attribute| + value = @base.get_attribute_value(attribute) + is_empty = value.respond_to?(:empty?) ? value.empty? : false + add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty + end + end + + # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). + def add_on_blank(attributes, custom_message = nil) + [attributes].flatten.each do |attribute| + value = @base.get_attribute_value(attribute) + add(attribute, :blank, :default => custom_message) if value.blank? + end + end + + # Returns all the full error messages in an array. + # + # class Company + # validates_presence_of :name, :address, :email + # validates_length_of :name, :in => 5..30 + # end + # + # company = Company.create(:address => '123 First St.') + # company.errors.full_messages # => + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + def full_messages(options = {}) + full_messages = [] + + each do |attribute, messages| + next if messages.empty? + + if attribute == :base + messages.each {|m| full_messages << m } + else + attr_name = attribute.to_s.humanize + prefix = attr_name + I18n.t('activemodel.errors.format.separator', :default => ' ') + messages.each do |m| + full_messages << "#{prefix}#{m}" + end + end + end + + full_messages + end + + # Translates an error message in it's default scope (<tt>activemodel.errrors.messages</tt>). + # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, + # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the + # default message (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name, + # translated attribute name and the value are available for interpolation. + # + # When using inheritence in your models, it will check all the inherited models too, but only if the model itself + # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt> + # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations: + # + # <ol> + # <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li> + # <li><tt>activemodel.errors.models.admin.blank</tt></li> + # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li> + # <li><tt>activemodel.errors.models.user.blank</tt></li> + # <li><tt>activemodel.errors.messages.blank</tt></li> + # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li> + # </ol> + def generate_message(attribute, message = :invalid, options = {}) + message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) + + klass_ancestors = [@base.class] + klass_ancestors += @base.class.ancestors.reject {|x| x.is_a?(Module)} + + defaults = klass_ancestors.map do |klass| + [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", + :"models.#{klass.name.underscore}.#{message}" ] + end + + defaults << options.delete(:default) + defaults = defaults.compact.flatten << :"messages.#{message}" + + key = defaults.shift + value = @base.get_attribute_value(attribute) + + options = { :default => defaults, + :model => @base.class.name.humanize, + :attribute => attribute.to_s.humanize, + :value => value, + :scope => [:activemodel, :errors] + }.merge(options) + + I18n.translate(key, options) + end end end
\ No newline at end of file diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml new file mode 100644 index 0000000000..0c2cf9ea33 --- /dev/null +++ b/activemodel/lib/active_model/locale/en.yml @@ -0,0 +1,24 @@ +en: + activemodel: + errors: + # The values :model, :attribute and :value are always available for interpolation + # The value :count is available when applicable. Can be used for pluralization. + messages: + inclusion: "is not included in the list" + exclusion: "is reserved" + invalid: "is invalid" + confirmation: "doesn't match confirmation" + accepted: "must be accepted" + empty: "can't be empty" + blank: "can't be blank" + too_long: "is too long (maximum is {{count}} characters)" + too_short: "is too short (minimum is {{count}} characters)" + wrong_length: "is the wrong length (should be {{count}} characters)" + not_a_number: "is not a number" + greater_than: "must be greater than {{count}}" + greater_than_or_equal_to: "must be greater than or equal to {{count}}" + equal_to: "must be equal to {{count}}" + less_than: "must be less than {{count}}" + less_than_or_equal_to: "must be less than or equal to {{count}}" + odd: "must be odd" + even: "must be even" diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 9e99d7472c..d3c6d8e482 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -1,6 +1,5 @@ require 'observer' require 'singleton' -require 'active_model/core' module ActiveModel module Observing diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb index bce90fd743..1172d31ea3 100644 --- a/activemodel/lib/active_model/state_machine.rb +++ b/activemodel/lib/active_model/state_machine.rb @@ -1,5 +1,10 @@ module ActiveModel module StateMachine + autoload :Event, 'active_model/state_machine/event' + autoload :Machine, 'active_model/state_machine/machine' + autoload :State, 'active_model/state_machine/state' + autoload :StateTransition, 'active_model/state_machine/state_transition' + class InvalidTransition < Exception end diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb new file mode 100644 index 0000000000..4cb5c9cbc0 --- /dev/null +++ b/activemodel/lib/active_model/test_case.rb @@ -0,0 +1,18 @@ +require "active_support/test_case" + +module ActiveModel #:nodoc: + class TestCase < ActiveSupport::TestCase #:nodoc: + def with_kcode(kcode) + if RUBY_VERSION < '1.9' + orig_kcode, $KCODE = $KCODE, kcode + begin + yield + ensure + $KCODE = orig_kcode + end + else + yield + end + end + end +end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 460d2d82e5..336c2757fc 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,16 +1,13 @@ -require 'active_model/core' - module ActiveModel module Validations - def self.included(base) # :nodoc: - base.extend(ClassMethods) - base.__send__(:include, ActiveSupport::Callbacks) - base.define_callbacks :validate, :validate_on_create, :validate_on_update + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :validate end module ClassMethods - DEFAULT_VALIDATION_OPTIONS = { :on => :save, :allow_nil => false, :allow_blank => false, :message => nil }.freeze - # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldly and # you're looking for more descriptive declaration of your validations. @@ -38,7 +35,7 @@ module ActiveModel # end # # This usage applies to +validate_on_create+ and +validate_on_update as well+. - # + # Validates each attribute against a block. # # class Person < ActiveRecord::Base @@ -48,7 +45,7 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should @@ -62,9 +59,9 @@ module ActiveModel attrs = attrs.flatten # Declare the validation. - send(validation_method(options[:on] || :save), options) do |record| + send(validation_method(options[:on]), options) do |record| attrs.each do |attr| - value = record.send(attr) + value = record.get_attribute_value(attr) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) yield record, attr, value end @@ -72,50 +69,31 @@ module ActiveModel end private - def validation_method(on) - case on - when :save then :validate - when :create then :validate_on_create - when :update then :validate_on_update - end - end + + def validation_method(on) + :validate + end end # Returns the Errors object that holds all information about attribute error messages. def errors - @errors ||= Errors.new + @errors ||= Errors.new(self) end # Runs all the specified validations and returns true if no errors were added otherwise false. def valid? errors.clear - run_callbacks(:validate) - - if responds_to?(:validate) - ActiveSupport::Deprecations.warn "Base#validate has been deprecated, please use Base.validate :method instead" - validate - end - - if new_record? - run_callbacks(:validate_on_create) - - if responds_to?(:validate_on_create) - ActiveSupport::Deprecations.warn( - "Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead") - validate_on_create - end - else - run_callbacks(:validate_on_update) + errors.empty? + end - if responds_to?(:validate_on_update) - ActiveSupport::Deprecations.warn( - "Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead") - validate_on_update - end - end + # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise. + def invalid? + !valid? + end - errors.empty? + def get_attribute_value(attribute) + respond_to?(attribute.to_sym) ? send(attribute.to_sym) : instance_variable_get(:"@#{attribute}") end end end @@ -123,4 +101,4 @@ end Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| filename = File.basename(path) require "active_model/validations/#{filename}" -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 9be7d51ffb..0c9ef51726 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -8,16 +8,16 @@ module ActiveModel # validates_acceptance_of :eula, :message => "must be abided" # end # - # If the database column does not exist, the <tt>:terms_of_service</tt> attribute is entirely virtual. This check is - # performed only if <tt>:terms_of_service</tt> is not +nil+ and by default on save. + # If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is + # performed only if +terms_of_service+ is not +nil+ and by default on save. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "must be accepted") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. (default is +true+) + # * <tt>:message</tt> - A custom error message (default is: "must be accepted"). + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true). # * <tt>:accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which # makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database - # column, since the attribute is typecasted from "1" to +true+ before validation. + # column, since the attribute is typecast from "1" to +true+ before validation. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -25,19 +25,22 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } + configuration = { :allow_nil => true, :accept => "1" } configuration.update(attr_names.extract_options!) db_cols = begin column_names - rescue ActiveRecord::StatementInvalid + rescue Exception # To ignore both statement and connection errors [] end + names = attr_names.reject { |name| db_cols.include?(name.to_s) } attr_accessor(*names) validates_each(attr_names,configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] + unless value == configuration[:accept] + record.errors.add(attr_name, :accepted, :default => configuration[:message]) + end end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index ba4a18adb7..b9823172f7 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -21,8 +21,8 @@ module ActiveModel # validates_presence_of :password_confirmation, :if => :password_changed? # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation"). + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -30,13 +30,14 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } - configuration.update(attr_names.extract_options!) + configuration = attr_names.extract_options! attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + record.errors.add(attr_name, :confirmation, :default => configuration[:message]) + end end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 1592000fc7..0aa9848ee1 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -6,14 +6,14 @@ module ActiveModel # class Person < ActiveRecord::Base # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" - # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed" + # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed" # end # # Configuration options: - # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of - # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved") - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. + # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -21,15 +21,16 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } - configuration.update(attr_names.extract_options!) + configuration = attr_names.extract_options! enum = configuration[:in] || configuration[:within] raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) + if enum.include?(value) + record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 1320ef646a..8efce8ba2b 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -13,11 +13,11 @@ module ActiveModel # A regular expression must be provided or else an exception will be raised. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) - # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!) - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:message</tt> - A custom error message (default is: "is invalid"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!). + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -25,13 +25,15 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } + configuration = { :with => nil } configuration.update(attr_names.extract_options!) raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] + unless value.to_s =~ configuration[:with] + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index f288810d90..a4bc8fe035 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -6,14 +6,14 @@ module ActiveModel # class Person < ActiveRecord::Base # validates_inclusion_of :gender, :in => %w( m f ) # validates_inclusion_of :age, :in => 0..99 - # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list" + # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list" # end # # Configuration options: - # * <tt>:in</tt> - An enumerable object of available items - # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list") - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is null (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * <tt>:in</tt> - An enumerable object of available items. + # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -21,15 +21,16 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } - configuration.update(attr_names.extract_options!) + configuration = attr_names.extract_options! enum = configuration[:in] || configuration[:within] raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) + unless enum.include?(value) + record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 673ad33974..bb9a269a02 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -2,90 +2,88 @@ module ActiveModel module Validations module ClassMethods ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze - + # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # # class Person < ActiveRecord::Base - # validates_length_of :first_name, :maximum => 30 - # validates_length_of :last_name, :maximum => 30, :message => "less than %d if you don't mind" + # validates_length_of :first_name, :maximum=>30 + # validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind" # validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :phone, :in => 7..32, :allow_blank => true # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" - # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character" - # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me." + # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character" + # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me." + # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) } # end # # Configuration options: - # * <tt>:minimum</tt> - The minimum size of the attribute - # * <tt>:maximum</tt> - The maximum size of the attribute - # * <tt>:is</tt> - The exact size of the attribute - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute - # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt> + # * <tt>:minimum</tt> - The minimum size of the attribute. + # * <tt>:maximum</tt> - The maximum size of the attribute. + # * <tt>:is</tt> - The exact size of the attribute. + # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. + # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)") - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)") - # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)") - # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>:too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)"). + # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)"). + # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)"). + # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. + # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to + # count words as in above example.) + # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. def validates_length_of(*attrs) # Merge given options with defaults. - options = { - :too_long => ActiveRecord::Errors.default_error_messages[:too_long], - :too_short => ActiveRecord::Errors.default_error_messages[:too_short], - :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] - }.merge(DEFAULT_VALIDATION_OPTIONS) + options = { :tokenizer => lambda {|value| value.split(//)} } options.update(attrs.extract_options!.symbolize_keys) # Ensure that one and only one range option is specified. range_options = ALL_RANGE_OPTIONS & options.keys case range_options.size - when 0 - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' - when 1 - # Valid number of options; do nothing. - else - raise ArgumentError, 'Too many range options specified. Choose only one.' + when 0 + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + when 1 + # Valid number of options; do nothing. + else + raise ArgumentError, 'Too many range options specified. Choose only one.' end # Get range option and value. option = range_options.first option_value = options[range_options.first] + key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option] + custom_message = options[:message] || options[key] case option - when :within, :in - raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - - too_short = options[:too_short] % option_value.begin - too_long = options[:too_long] % option_value.end + when :within, :in + raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - validates_each(attrs, options) do |record, attr, value| - value = value.split(//) if value.kind_of?(String) - if value.nil? or value.size < option_value.begin - record.errors.add(attr, too_short) - elsif value.size > option_value.end - record.errors.add(attr, too_long) - end + validates_each(attrs, options) do |record, attr, value| + value = options[:tokenizer].call(value) if value.kind_of?(String) + if value.nil? or value.size < option_value.begin + record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin) + elsif value.size > option_value.end + record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end) end - when :is, :minimum, :maximum - raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 - - # Declare different validations per option. - validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } + end + when :is, :minimum, :maximum + raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 - message = (options[:message] || options[message_options[option]]) % option_value + # Declare different validations per option. + validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - validates_each(attrs, options) do |record, attr, value| - value = value.split(//) if value.kind_of?(String) - record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + validates_each(attrs, options) do |record, attr, value| + value = options[:tokenizer].call(value) if value.kind_of?(String) + unless !value.nil? and value.size.method(validity_checks[option])[option_value] + record.errors.add(attr, key, :default => custom_message, :count => option_value) end + end end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 92ca5f4082..79fca2f1ea 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -5,10 +5,9 @@ module ActiveModel :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', :odd => 'odd?', :even => 'even?' }.freeze - # Validates whether the value of the specified attribute is numeric by trying to convert it to - # a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>integer</tt> is true). + # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression + # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). # # class Person < ActiveRecord::Base # validates_numericality_of :value, :on => :create @@ -33,10 +32,9 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_numericality_of(*attr_names) - configuration = { :on => :save, :only_integer => false, :allow_nil => false } + configuration = { :only_integer => false, :allow_nil => false } configuration.update(attr_names.extract_options!) - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys (numericality_options - [ :odd, :even ]).each do |option| @@ -44,33 +42,41 @@ module ActiveModel end validates_each(attr_names,configuration) do |record, attr_name, value| - raw_value = record.send("#{attr_name}_before_type_cast") || value + before_type_cast = "#{attr_name}_before_type_cast" + + if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send("#{attr_name}_before_type_cast") || value + else + raw_value = value + end next if configuration[:allow_nil] and raw_value.nil? if configuration[:only_integer] unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) next end raw_value = raw_value.to_i else - begin - raw_value = Kernel.Float(raw_value.to_s) + begin + raw_value = Kernel.Float(raw_value) rescue ArgumentError, TypeError - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) next end end numericality_options.each do |option| case option - when :odd, :even - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] - else - message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option] - message = message % configuration[option] if configuration[option] - record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] + when :odd, :even + unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) + end + else + unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] + record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) + end end end end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 62e466901b..518bc8a952 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -7,29 +7,26 @@ module ActiveModel # validates_presence_of :first_name # end # - # The +first_name+ attribute must be in the object and it cannot be blank. + # The first_name attribute must be in the object and it cannot be blank. # - # If you want to validate the presence of a boolean field (where the real values are +true+ and +false+), - # you will want to use + # If you want to validate the presence of a boolean field (where the real values are true and false), + # you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. # - # validates_inclusion_of :field_name, :in => [true, false] - # - # This is due to the way Object#blank? handles boolean values: - # - # false.blank? # => true + # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "can't be blank") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # * <tt>message</tt> - A custom error message (default is: "can't be blank"). + # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, + # <tt>:update</tt>). + # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). + # The method, proc or string should return or evaluate to a true or false value. + # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). + # The method, proc or string should return or evaluate to a true or false value. + # def validates_presence_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } - configuration.update(attr_names.extract_options!) + configuration = attr_names.extract_options! # can't use validates_each here, because it cannot cope with nonexistent attributes, # while errors.add_on_empty can diff --git a/activemodel/lib/active_model/validations/uniqueness.rb b/activemodel/lib/active_model/validations/uniqueness.rb deleted file mode 100644 index 593d2cfd54..0000000000 --- a/activemodel/lib/active_model/validations/uniqueness.rb +++ /dev/null @@ -1,104 +0,0 @@ -module ActiveModel - module Validations - module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user - # can be named "davidhh". - # - # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id - # end - # - # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, - # making sure that a teacher can only be on the schedule once per semester for a particular class. - # - # class TeacherSchedule < ActiveRecord::Base - # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] - # end - # - # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified - # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. - # - # Because this check is performed outside the database there is still a chance that duplicate values - # will be inserted in two parallel transactions. To guarantee against this you should create a - # unique index on the field. See +add_index+ for more information. - # - # Configuration options: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken") - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_uniqueness_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] } - configuration.update(attr_names.extract_options!) - - validates_each(attr_names,configuration) do |record, attr_name, value| - # The check for an existing value should be run from a class that - # isn't abstract. This means working down from the current class - # (self), to the first non-abstract class. Since classes don't know - # their subclasses, we have to build the hierarchy between self and - # the record's class. - class_hierarchy = [record.class] - while class_hierarchy.first != self - class_hierarchy.insert(0, class_hierarchy.first.superclass) - end - - # Now we can work our way down the tree to the first non-abstract - # class (which has a database table to query from). - finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - - if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?) - condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" - condition_params = [value] - else - # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't. - # Hence, this is needed only for sqlite. - condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" - condition_params = [value.downcase] - end - - if scope = configuration[:scope] - Array(scope).map do |scope_item| - scope_value = record.send(scope_item) - condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}" - condition_params << scope_value - end - end - - unless record.new_record? - condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" - condition_params << record.send(:id) - end - - results = finder_class.with_exclusive_scope do - connection.select_all( - construct_finder_sql( - :select => attr_name, - :from => finder_class.quoted_table_name, - :conditions => [condition_sql, *condition_params] - ) - ) - end - - unless results.length.zero? - found = true - - # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate - # column in ruby when case sensitive option - if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? - found = results.any? { |a| a[attr_name.to_s] == value } - end - - record.errors.add(attr_name, configuration[:message]) if found - end - end - end - end - end -end diff --git a/activemodel/lib/active_model/validations_repair_helper.rb b/activemodel/lib/active_model/validations_repair_helper.rb new file mode 100644 index 0000000000..432e411308 --- /dev/null +++ b/activemodel/lib/active_model/validations_repair_helper.rb @@ -0,0 +1,44 @@ +module ActiveModel + module ValidationsRepairHelper + extend ActiveSupport::Concern + + module Toolbox + def self.record_validations(*model_classes) + model_classes.inject({}) do |repair, klass| + repair[klass] ||= {} + [:validate, :validate_on_create, :validate_on_update].each do |callback| + the_callback = klass.instance_variable_get("@#{callback.to_s}_callbacks") + repair[klass][callback] = (the_callback.nil? ? nil : the_callback.dup) + end + repair + end + end + + def self.reset_validations(recorded) + recorded.each do |klass, repairs| + [:validate, :validate_on_create, :validate_on_update].each do |callback| + klass.instance_variable_set("@#{callback.to_s}_callbacks", repairs[callback]) + end + end + end + end + + module ClassMethods + def repair_validations(*model_classes) + setup do + @validation_repairs = Toolbox.record_validations(*model_classes) + end + teardown do + Toolbox.reset_validations(@validation_repairs) + end + end + end + + def repair_validations(*model_classes, &block) + validation_repairs = Toolbox.record_validations(*model_classes) + return block.call + ensure + Toolbox.reset_validations(validation_repairs) + end + end +end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb new file mode 100644 index 0000000000..f023109a60 --- /dev/null +++ b/activemodel/test/cases/helper.rb @@ -0,0 +1,20 @@ +$:.unshift(File.dirname(__FILE__) + '/../../lib') +$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib') + +require 'config' + +require 'active_model' + +# Show backtraces for deprecated behavior for quicker cleanup. +ActiveSupport::Deprecation.debug = true + +require 'rubygems' +require 'test/unit' +gem 'mocha', '>= 0.9.5' +require 'mocha' + +begin + require 'ruby-debug' + Debugger.start +rescue LoadError +end diff --git a/activemodel/test/observing_test.rb b/activemodel/test/cases/observing_test.rb index dc41c9f881..421ac4b4f8 100644 --- a/activemodel/test/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class ObservedModel < ActiveModel::Base class Observer diff --git a/activemodel/test/state_machine/event_test.rb b/activemodel/test/cases/state_machine/event_test.rb index 05e7c78e8a..2a0ef53a3f 100644 --- a/activemodel/test/state_machine/event_test.rb +++ b/activemodel/test/cases/state_machine/event_test.rb @@ -1,5 +1,4 @@ -require 'test_helper' -require 'active_model/state_machine/event' +require 'cases/helper' class EventTest < ActiveModel::TestCase def setup diff --git a/activemodel/test/state_machine/machine_test.rb b/activemodel/test/cases/state_machine/machine_test.rb index d23c223160..2aa954d80c 100644 --- a/activemodel/test/state_machine/machine_test.rb +++ b/activemodel/test/cases/state_machine/machine_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class MachineTestSubject include ActiveModel::StateMachine diff --git a/activemodel/test/state_machine/state_test.rb b/activemodel/test/cases/state_machine/state_test.rb index fbf9ce7b0a..527bfd4c04 100644 --- a/activemodel/test/state_machine/state_test.rb +++ b/activemodel/test/cases/state_machine/state_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class StateTestSubject include ActiveModel::StateMachine diff --git a/activemodel/test/state_machine/state_transition_test.rb b/activemodel/test/cases/state_machine/state_transition_test.rb index 966396fab1..17f9d88be7 100644 --- a/activemodel/test/state_machine/state_transition_test.rb +++ b/activemodel/test/cases/state_machine/state_transition_test.rb @@ -1,5 +1,4 @@ -require 'test_helper' -require 'active_model/state_machine/state_transition' +require 'cases/helper' class StateTransitionTest < ActiveModel::TestCase test 'should set from, to, and opts attr readers' do diff --git a/activemodel/test/state_machine_test.rb b/activemodel/test/cases/state_machine_test.rb index 312d8728ba..f66299741e 100644 --- a/activemodel/test/state_machine_test.rb +++ b/activemodel/test/cases/state_machine_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class StateMachineSubject include ActiveModel::StateMachine diff --git a/activemodel/test/cases/tests_database.rb b/activemodel/test/cases/tests_database.rb new file mode 100644 index 0000000000..0f4475fa2d --- /dev/null +++ b/activemodel/test/cases/tests_database.rb @@ -0,0 +1,41 @@ +require 'logger' + +$:.unshift(File.dirname(__FILE__) + '/../../../activerecord/lib') +require 'active_record' +require 'active_record/fixtures' + +module ActiveModel + module TestsDatabase + mattr_accessor :connected + + def self.included(base) + unless self.connected + setup_connection + setup_schema + end + + base.send :include, ActiveRecord::TestFixtures + end + + def self.setup_schema + original, $stdout = $stdout, StringIO.new + load(SCHEMA_FILE) + ensure + $stdout = original + self.connected = true + end + + def self.setup_connection + defaults = { :database => ':memory:' } + begin + adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' + options = defaults.merge :adapter => adapter, :timeout => 500 + ActiveRecord::Base.establish_connection(options) + rescue Exception + $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.' + options = defaults.merge :adapter => 'sqlite' + ActiveRecord::Base.establish_connection(options) + end + end + end +end diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb new file mode 100644 index 0000000000..88e5fdb358 --- /dev/null +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -0,0 +1,80 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/reply' +require 'models/developer' +require 'models/person' + +class AcceptanceValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_terms_of_service_agreement_no_acceptance + Topic.validates_acceptance_of(:terms_of_service, :on => :create) + + t = Topic.create("title" => "We should not be confirmed") + assert t.save + end + + def test_terms_of_service_agreement + Topic.validates_acceptance_of(:terms_of_service, :on => :create) + + t = Topic.create("title" => "We should be confirmed","terms_of_service" => "") + assert !t.save + assert_equal ["must be accepted"], t.errors[:terms_of_service] + + t.terms_of_service = "1" + assert t.save + end + + def test_eula + Topic.validates_acceptance_of(:eula, :message => "must be abided", :on => :create) + + t = Topic.create("title" => "We should be confirmed","eula" => "") + assert !t.save + assert_equal ["must be abided"], t.errors[:eula] + + t.eula = "1" + assert t.save + end + + def test_terms_of_service_agreement_with_accept_value + Topic.validates_acceptance_of(:terms_of_service, :on => :create, :accept => "I agree.") + + t = Topic.create("title" => "We should be confirmed", "terms_of_service" => "") + assert !t.save + assert_equal ["must be accepted"], t.errors[:terms_of_service] + + t.terms_of_service = "I agree." + assert t.save + end + + def test_validates_acceptance_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.salary = "0" + assert !d.valid? + assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last + end + end + + def test_validates_acceptance_of_for_ruby_class + repair_validations(Person) do + Person.validates_acceptance_of :karma + + p = Person.new + p.karma = "" + + assert p.invalid? + assert_equal ["must be accepted"], p.errors[:karma] + + p.karma = "1" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb new file mode 100644 index 0000000000..4c716d5d48 --- /dev/null +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -0,0 +1,140 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' + +class ConditionalValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_if_validation_using_method_true + # When the method returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_unless_validation_using_method_true + # When the method returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert !t.errors[:title].any? + end + + def test_if_validation_using_method_false + # When the method returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true_but_its_not ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_unless_validation_using_method_false + # When the method returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true_but_its_not ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_if_validation_using_string_true + # When the evaluated string returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "a = 1; a == 1" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_unless_validation_using_string_true + # When the evaluated string returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "a = 1; a == 1" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_if_validation_using_string_false + # When the evaluated string returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "false") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_unless_validation_using_string_false + # When the evaluated string returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "false") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_if_validation_using_block_true + # When the block returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :if => Proc.new { |r| r.content.size > 4 } ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_unless_validation_using_block_true + # When the block returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :unless => Proc.new { |r| r.content.size > 4 } ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_if_validation_using_block_false + # When the block returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :if => Proc.new { |r| r.title != "uhohuhoh"} ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_unless_validation_using_block_false + # When the block returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :unless => Proc.new { |r| r.title != "uhohuhoh"} ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + # previous implementation of validates_presence_of eval'd the + # string with the wrong binding, this regression test is to + # ensure that it works correctly + def test_validation_with_if_as_string + Topic.validates_presence_of(:title) + Topic.validates_presence_of(:author_name, :if => "title.to_s.match('important')") + + t = Topic.new + assert t.invalid?, "A topic without a title should not be valid" + assert t.errors[:author_name].empty?, "A topic without an 'important' title should not require an author" + + t.title = "Just a title" + assert t.valid?, "A topic with a basic title should be valid" + + t.title = "A very important title" + assert !t.valid?, "A topic with an important title, but without an author, should not be valid" + assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author" + + t.author_name = "Hubert J. Farnsworth" + assert t.valid?, "A topic with an important title and author should be valid" + end +end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb new file mode 100644 index 0000000000..1d6f2a6ec5 --- /dev/null +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -0,0 +1,68 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class ConfirmationValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_no_title_confirmation + Topic.validates_confirmation_of(:title) + + t = Topic.new(:author_name => "Plutarch") + assert t.valid? + + t.title_confirmation = "Parallel Lives" + assert !t.valid? + + t.title_confirmation = nil + t.title = "Parallel Lives" + assert t.valid? + + t.title_confirmation = "Parallel Lives" + assert t.valid? + end + + def test_title_confirmation + Topic.validates_confirmation_of(:title) + + t = Topic.create("title" => "We should be confirmed","title_confirmation" => "") + assert !t.save + + t.title_confirmation = "We should be confirmed" + assert t.save + end + + def test_validates_confirmation_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes" + d = Developer.new + d.name = "John" + d.name_confirmation = "Johnny" + assert !d.valid? + assert_equal ["confirm 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_confirmation_of_for_ruby_class + repair_validations(Person) do + Person.validates_confirmation_of :karma + + p = Person.new + p.karma_confirmation = "None" + assert p.invalid? + + assert_equal ["doesn't match confirmation"], p.errors[:karma] + + p.karma = "None" + assert p.valid? + end + end + +end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb new file mode 100644 index 0000000000..584f009e84 --- /dev/null +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -0,0 +1,46 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/person' + +class ExclusionValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validates_exclusion_of + Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) ) + + assert Topic.create("title" => "something", "content" => "abc").valid? + assert !Topic.create("title" => "monkey", "content" => "abc").valid? + end + + def test_validates_exclusion_of_with_formatted_message + Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option {{value}} is restricted" ) + + assert Topic.create("title" => "something", "content" => "abc") + + t = Topic.create("title" => "monkey") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["option monkey is restricted"], t.errors[:title] + end + + def test_validates_exclusion_of_for_ruby_class + repair_validations(Person) do + Person.validates_exclusion_of :karma, :in => %w( abe monkey ) + + p = Person.new + p.karma = "abe" + assert p.invalid? + + assert_equal ["is reserved"], p.errors[:karma] + + p.karma = "Lifo" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb new file mode 100644 index 0000000000..2c06a9dd02 --- /dev/null +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -0,0 +1,98 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class PresenceValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validate_format + Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") + + t = Topic.create("title" => "i'm incorrect", "content" => "Validation macros rule!") + assert !t.valid?, "Shouldn't be valid" + assert !t.save, "Shouldn't save because it's invalid" + assert_equal ["is bad data"], t.errors[:title] + assert t.errors[:content].empty? + + t.title = "Validation macros rule!" + + assert t.save + assert t.errors[:title].empty? + + assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } + end + + def test_validate_format_with_allow_blank + Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank=>true) + assert !Topic.create("title" => "Shouldn't be valid").valid? + assert Topic.create("title" => "").valid? + assert Topic.create("title" => nil).valid? + assert Topic.create("title" => "Validation macros rule!").valid? + end + + # testing ticket #3142 + def test_validate_format_numeric + Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") + + t = Topic.create("title" => "72x", "content" => "6789") + assert !t.valid?, "Shouldn't be valid" + assert !t.save, "Shouldn't save because it's invalid" + assert_equal ["is bad data"], t.errors[:title] + assert t.errors[:content].empty? + + t.title = "-11" + assert !t.valid?, "Shouldn't be valid" + + t.title = "03" + assert !t.valid?, "Shouldn't be valid" + + t.title = "z44" + assert !t.valid?, "Shouldn't be valid" + + t.title = "5v7" + assert !t.valid?, "Shouldn't be valid" + + t.title = "1" + + assert t.save + assert t.errors[:title].empty? + end + + def test_validate_format_with_formatted_message + Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be {{value}}") + t = Topic.create(:title => 'Invalid title') + assert_equal ["can't be Invalid title"], t.errors[:title] + end + + def test_validates_format_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes" + d = Developer.new + d.name = d.name_confirmation = "John 32" + assert !d.valid? + assert_equal ["format 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_format_of_for_ruby_class + repair_validations(Person) do + Person.validates_format_of :karma, :with => /\A\d+\Z/ + + p = Person.new + p.karma = "Pixies" + assert p.invalid? + + assert_equal ["is invalid"], p.errors[:karma] + + p.karma = "1234" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb new file mode 100644 index 0000000000..37bba5e95e --- /dev/null +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -0,0 +1,182 @@ +require "cases/helper" +require 'cases/tests_database' + +require 'models/person' + +class I18nGenerateMessageValidationTest < Test::Unit::TestCase + def setup + reset_callbacks Person + @person = Person.new + + @old_load_path, @old_backend = I18n.load_path, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + + I18n.backend.store_translations :'en', { + :activemodel => { + :errors => { + :messages => { + :inclusion => "is not included in the list", + :exclusion => "is reserved", + :invalid => "is invalid", + :confirmation => "doesn't match confirmation", + :accepted => "must be accepted", + :empty => "can't be empty", + :blank => "can't be blank", + :too_long => "is too long (maximum is {{count}} characters)", + :too_short => "is too short (minimum is {{count}} characters)", + :wrong_length => "is the wrong length (should be {{count}} characters)", + :not_a_number => "is not a number", + :greater_than => "must be greater than {{count}}", + :greater_than_or_equal_to => "must be greater than or equal to {{count}}", + :equal_to => "must be equal to {{count}}", + :less_than => "must be less than {{count}}", + :less_than_or_equal_to => "must be less than or equal to {{count}}", + :odd => "must be odd", + :even => "must be even" + } + } + } + } + end + + def teardown + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + def test_generate_message_inclusion_with_default_message + assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') + end + + def test_generate_message_inclusion_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + def test_generate_message_exclusion_with_default_message + assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') + end + + def test_generate_message_exclusion_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + def test_generate_message_invalid_with_default_message + assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') + end + + def test_generate_message_invalid_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) + def test_generate_message_confirmation_with_default_message + assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation, :default => nil) + end + + def test_generate_message_confirmation_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, :default => 'custom message') + end + + # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) + def test_generate_message_accepted_with_default_message + assert_equal "must be accepted", @person.errors.generate_message(:title, :accepted, :default => nil) + end + + def test_generate_message_accepted_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, :default => 'custom message') + end + + # add_on_empty: generate_message(attr, :empty, :default => custom_message) + def test_generate_message_empty_with_default_message + assert_equal "can't be empty", @person.errors.generate_message(:title, :empty, :default => nil) + end + + def test_generate_message_empty_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :empty, :default => 'custom message') + end + + # add_on_blank: generate_message(attr, :blank, :default => custom_message) + def test_generate_message_blank_with_default_message + assert_equal "can't be blank", @person.errors.generate_message(:title, :blank, :default => nil) + end + + def test_generate_message_blank_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :blank, :default => 'custom message') + end + + # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + def test_generate_message_too_long_with_default_message + assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, :default => nil, :count => 10) + end + + def test_generate_message_too_long_with_custom_message + assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + def test_generate_message_too_short_with_default_message + assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, :default => nil, :count => 10) + end + + def test_generate_message_too_short_with_custom_message + assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) + def test_generate_message_wrong_length_with_default_message + assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) + end + + def test_generate_message_wrong_length_with_custom_message + assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) + end + + # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + def test_generate_message_not_a_number_with_default_message + assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') + end + + def test_generate_message_not_a_number_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + def test_generate_message_greater_than_with_default_message + assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_greater_than_or_equal_to_with_default_message + assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_equal_to_with_default_message + assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_with_default_message + assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_or_equal_to_with_default_message + assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_odd_with_default_message + assert_equal "must be odd", @person.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_even_with_default_message + assert_equal "must be even", @person.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) + end +end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb new file mode 100644 index 0000000000..cc68d847a2 --- /dev/null +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -0,0 +1,579 @@ +require "cases/helper" +require 'cases/tests_database' + +require 'models/person' + +class I18nValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + + def setup + reset_callbacks Person + + @person = Person.new + + @old_load_path, @old_backend = I18n.load_path, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations('en', :activemodel => {:errors => {:messages => {:custom => nil}}}) + end + + def teardown + reset_callbacks Person + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + def test_percent_s_interpolation_syntax_in_error_messages_was_deprecated + assert_not_deprecated do + default = "%s interpolation syntax was deprecated" + assert_equal default, I18n.t(:does_not_exist, :default => default, :value => 'this') + end + end + + def test_percent_d_interpolation_syntax_in_error_messages_was_deprecated + assert_not_deprecated do + default = "%d interpolation syntaxes are deprecated" + assert_equal default, I18n.t(:does_not_exist, :default => default, :count => 2) + end + end + + def test_errors_add_on_empty_generates_message + @person.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) + @person.errors.add_on_empty :title + end + + def test_errors_add_on_empty_generates_message_with_custom_default_message + @person.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) + @person.errors.add_on_empty :title, 'custom' + end + + def test_errors_add_on_blank_generates_message + @person.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @person.errors.add_on_blank :title + end + + def test_errors_add_on_blank_generates_message_with_custom_default_message + @person.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @person.errors.add_on_blank :title, 'custom' + end + + # ActiveRecord::Validations + # validates_confirmation_of w/ mocha + def test_validates_confirmation_of_generates_message + Person.validates_confirmation_of :title + @person.title_confirmation = 'foo' + @person.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) + @person.valid? + end + + def test_validates_confirmation_of_generates_message_with_custom_default_message + Person.validates_confirmation_of :title, :message => 'custom' + @person.title_confirmation = 'foo' + @person.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) + @person.valid? + end + + # validates_acceptance_of w/ mocha + + def test_validates_acceptance_of_generates_message + Person.validates_acceptance_of :title, :allow_nil => false + @person.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) + @person.valid? + end + + def test_validates_acceptance_of_generates_message_with_custom_default_message + Person.validates_acceptance_of :title, :message => 'custom', :allow_nil => false + @person.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) + @person.valid? + end + + # validates_presence_of w/ mocha + + def test_validates_presence_of_generates_message + Person.validates_presence_of :title + @person.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @person.valid? + end + + def test_validates_presence_of_generates_message_with_custom_default_message + Person.validates_presence_of :title, :message => 'custom' + @person.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short + Person.validates_length_of :title, :within => 3..5 + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long + Person.validates_length_of :title, :within => 3..5 + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @person.valid? + end + + # validates_length_of :within w/ mocha + + def test_validates_length_of_within_generates_message_with_title_too_short + Person.validates_length_of :title, :within => 3..5 + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long + Person.validates_length_of :title, :within => 3..5 + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @person.valid? + end + + # validates_length_of :is w/ mocha + + def test_validates_length_of_is_generates_message + Person.validates_length_of :title, :is => 5 + @person.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) + @person.valid? + end + + def test_validates_length_of_is_generates_message_with_custom_default_message + Person.validates_length_of :title, :is => 5, :message => 'custom' + @person.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) + @person.valid? + end + + # validates_format_of w/ mocha + + def test_validates_format_of_generates_message + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @person.title = '72x' + @person.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) + @person.valid? + end + + def test_validates_format_of_generates_message_with_custom_default_message + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' + @person.title = '72x' + @person.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) + @person.valid? + end + + # validates_inclusion_of w/ mocha + + def test_validates_inclusion_of_generates_message + Person.validates_inclusion_of :title, :in => %w(a b c) + @person.title = 'z' + @person.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) + @person.valid? + end + + def test_validates_inclusion_of_generates_message_with_custom_default_message + Person.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' + @person.title = 'z' + @person.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) + @person.valid? + end + + # validates_exclusion_of w/ mocha + + def test_validates_exclusion_of_generates_message + Person.validates_exclusion_of :title, :in => %w(a b c) + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) + @person.valid? + end + + def test_validates_exclusion_of_generates_message_with_custom_default_message + Person.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of without :only_integer w/ mocha + + def test_validates_numericality_of_generates_message + Person.validates_numericality_of :title + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :message => 'custom' + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of with :only_integer w/ mocha + + def test_validates_numericality_of_only_integer_generates_message + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :only_integer => true, :message => 'custom' + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of :odd w/ mocha + + def test_validates_numericality_of_odd_generates_message + Person.validates_numericality_of :title, :only_integer => true, :odd => true + @person.title = 0 + @person.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' + @person.title = 0 + @person.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of :less_than w/ mocha + + def test_validates_numericality_of_less_than_generates_message + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @person.title = 1 + @person.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' + @person.title = 1 + @person.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) + @person.valid? + end + + # validates_confirmation_of w/o mocha + + def test_validates_confirmation_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:confirmation => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}} + + Person.validates_confirmation_of :title + @person.title_confirmation = 'foo' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_confirmation_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}} + + Person.validates_confirmation_of :title + @person.title_confirmation = 'foo' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_acceptance_of w/o mocha + + def test_validates_acceptance_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:accepted => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}} + + Person.validates_acceptance_of :title, :allow_nil => false + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_acceptance_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}} + + Person.validates_acceptance_of :title, :allow_nil => false + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_presence_of w/o mocha + + def test_validates_presence_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:blank => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}} + + Person.validates_presence_of :title + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_presence_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}} + + Person.validates_presence_of :title + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_length_of :within w/o mocha + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:too_short => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}} + + Person.validates_length_of :title, :within => 3..5 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}} + + Person.validates_length_of :title, :within => 3..5 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_length_of :is w/o mocha + + def test_validates_length_of_is_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_length_of_is_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + def test_validates_length_of_is_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_length_of_is_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + + # validates_format_of w/o mocha + + def test_validates_format_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:invalid => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}} + + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_format_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}} + + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_inclusion_of w/o mocha + + def test_validates_inclusion_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:inclusion => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}} + + Person.validates_inclusion_of :title, :in => %w(a b c) + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_inclusion_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}} + + Person.validates_inclusion_of :title, :in => %w(a b c) + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_exclusion_of w/o mocha + + def test_validates_exclusion_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:exclusion => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}} + + Person.validates_exclusion_of :title, :in => %w(a b c) + @person.title = 'a' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_exclusion_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}} + + Person.validates_exclusion_of :title, :in => %w(a b c) + @person.title = 'a' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of without :only_integer w/o mocha + + def test_validates_numericality_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title + @person.title = 'a' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of with :only_integer w/o mocha + + def test_validates_numericality_of_only_integer_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_only_integer_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of :odd w/o mocha + + def test_validates_numericality_of_odd_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:odd => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :odd => true + @person.title = 0 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_odd_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :odd => true + @person.title = 0 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of :less_than w/o mocha + + def test_validates_numericality_of_less_than_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:less_than => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @person.title = 1 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_less_than_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @person.title = 1 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + def test_validations_with_message_symbol_must_translate + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:custom_error => "I am a custom error"}}} + Person.validates_presence_of :title, :message => :custom_error + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + + def test_validates_with_message_symbol_must_translate_per_attribute + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}} + Person.validates_presence_of :title, :message => :custom_error + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + + def test_validates_with_message_symbol_must_translate_per_model + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:custom_error => "I am a custom error"}}}} + Person.validates_presence_of :title, :message => :custom_error + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + + def test_validates_with_message_string + Person.validates_presence_of :title, :message => "I am a custom error" + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + +end
\ No newline at end of file diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb new file mode 100644 index 0000000000..bc1b0365d2 --- /dev/null +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -0,0 +1,80 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class InclusionValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validates_inclusion_of + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) ) + + assert !Topic.create("title" => "a!", "content" => "abc").valid? + assert !Topic.create("title" => "a b", "content" => "abc").valid? + assert !Topic.create("title" => nil, "content" => "def").valid? + + t = Topic.create("title" => "a", "content" => "I know you are but what am I?") + assert t.valid? + t.title = "uhoh" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is not included in the list"], t.errors[:title] + + assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) } + assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) } + + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) } + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) } + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) } + end + + def test_validates_inclusion_of_with_allow_nil + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true ) + + assert !Topic.create("title" => "a!", "content" => "abc").valid? + assert !Topic.create("title" => "", "content" => "abc").valid? + assert Topic.create("title" => nil, "content" => "abc").valid? + end + + def test_validates_inclusion_of_with_formatted_message + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option {{value}} is not in the list" ) + + assert Topic.create("title" => "a", "content" => "abc").valid? + + t = Topic.create("title" => "uhoh", "content" => "abc") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["option uhoh is not in the list"], t.errors[:title] + end + + def test_validates_inclusion_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.salary = "90,000" + assert !d.valid? + assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last + end + end + + def test_validates_inclusion_of_for_ruby_class + repair_validations(Person) do + Person.validates_inclusion_of :karma, :in => %w( abe monkey ) + + p = Person.new + p.karma = "Lifo" + assert p.invalid? + + assert_equal ["is not included in the list"], p.errors[:karma] + + p.karma = "monkey" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb new file mode 100644 index 0000000000..4a2f72feab --- /dev/null +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -0,0 +1,449 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class LengthValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validates_length_of_with_allow_nil + Topic.validates_length_of( :title, :is => 5, :allow_nil=>true ) + + assert !Topic.create("title" => "ab").valid? + assert !Topic.create("title" => "").valid? + assert Topic.create("title" => nil).valid? + assert Topic.create("title" => "abcde").valid? + end + + def test_validates_length_of_with_allow_blank + Topic.validates_length_of( :title, :is => 5, :allow_blank=>true ) + + assert !Topic.create("title" => "ab").valid? + assert Topic.create("title" => "").valid? + assert Topic.create("title" => nil).valid? + assert Topic.create("title" => "abcde").valid? + end + + def test_validates_length_of_using_minimum + Topic.validates_length_of :title, :minimum => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "not" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] + + t.title = "" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] + + t.title = nil + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] + end + + def test_optionally_validates_length_of_using_minimum + Topic.validates_length_of :title, :minimum => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_maximum + Topic.validates_length_of :title, :maximum => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "notvalid" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] + + t.title = "" + assert t.valid? + + t.title = nil + assert !t.valid? + end + + def test_optionally_validates_length_of_using_maximum + Topic.validates_length_of :title, :maximum => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_within + Topic.validates_length_of(:title, :content, :within => 3..5) + + t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") + assert !t.valid? + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] + assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] + + t.title = nil + t.content = nil + assert !t.valid? + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content] + + t.title = "abe" + t.content = "mad" + assert t.valid? + end + + def test_optionally_validates_length_of_using_within + Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true + + t = Topic.create('title' => 'abc', 'content' => 'abcd') + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_optionally_validates_length_of_using_within_on_create + Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: {{count}}" + + t = Topic.create("title" => "thisisnotvalid", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + assert_equal ["my string is too long: 10"], t.errors[:title] + + t.title = "butthisis" + assert t.save + + t.title = "few" + assert t.save + + t.content = "andthisislong" + assert t.save + + t.content = t.title = "iamfine" + assert t.save + end + + def test_optionally_validates_length_of_using_within_on_update + Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: {{count}}" + + t = Topic.create("title" => "vali", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + + t.title = "not" + assert !t.save + assert t.errors[:title].any? + assert_equal ["my string is too short: 5"], t.errors[:title] + + t.title = "valid" + t.content = "andthisistoolong" + assert !t.save + assert t.errors[:content].any? + + t.content = "iamfine" + assert t.save + end + + def test_validates_length_of_using_is + Topic.validates_length_of :title, :is => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "notvalid" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title] + + t.title = "" + assert !t.valid? + + t.title = nil + assert !t.valid? + end + + def test_optionally_validates_length_of_using_is + Topic.validates_length_of :title, :is => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_bignum + bigmin = 2 ** 30 + bigmax = 2 ** 32 + bigrange = bigmin...bigmax + assert_nothing_raised do + Topic.validates_length_of :title, :is => bigmin + 5 + Topic.validates_length_of :title, :within => bigrange + Topic.validates_length_of :title, :in => bigrange + Topic.validates_length_of :title, :minimum => bigmin + Topic.validates_length_of :title, :maximum => bigmax + end + end + + def test_validates_length_of_nasty_params + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") } + end + + def test_validates_length_of_custom_errors_for_minimum_with_message + Topic.validates_length_of( :title, :minimum=>5, :message=>"boo {{count}}" ) + t = Topic.create("title" => "uhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["boo 5"], t.errors[:title] + end + + def test_validates_length_of_custom_errors_for_minimum_with_too_short + Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo {{count}}" ) + t = Topic.create("title" => "uhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors[:title] + end + + def test_validates_length_of_custom_errors_for_maximum_with_message + Topic.validates_length_of( :title, :maximum=>5, :message=>"boo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["boo 5"], t.errors[:title] + end + + def test_validates_length_of_custom_errors_for_in + Topic.validates_length_of(:title, :in => 10..20, :message => "hoo {{count}}") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 10"], t.errors["title"] + + t = Topic.create("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 20"], t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_maximum_with_too_long + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_is_with_message + Topic.validates_length_of( :title, :is=>5, :message=>"boo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["boo 5"], t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_is_with_wrong_length + Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_validates_length_of_using_minimum_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :minimum => 5 + + t = Topic.create("title" => "一二三四五", "content" => "whatever") + assert t.valid? + + t.title = "一二三四" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] + end + end + + def test_validates_length_of_using_maximum_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :maximum => 5 + + t = Topic.create("title" => "一二三四五", "content" => "whatever") + assert t.valid? + + t.title = "一二34五六" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"] + end + end + + def test_validates_length_of_using_within_utf8 + with_kcode('UTF8') do + Topic.validates_length_of(:title, :content, :within => 3..5) + + t = Topic.new("title" => "一二", "content" => "12三四五六七") + assert !t.valid? + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] + assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] + t.title = "一二三" + t.content = "12三" + assert t.valid? + end + end + + def test_optionally_validates_length_of_using_within_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :within => 3..5, :allow_nil => true + + t = Topic.create(:title => "一二三四五") + assert t.valid?, t.errors.inspect + + t = Topic.create(:title => "一二三") + assert t.valid?, t.errors.inspect + + t.title = nil + assert t.valid?, t.errors.inspect + end + end + + def test_optionally_validates_length_of_using_within_on_create_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: {{count}}" + + t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + assert_equal "長すぎます: 10", t.errors[:title].first + + t.title = "一二三四五六七八九" + assert t.save + + t.title = "一二3" + assert t.save + + t.content = "一二三四五六七八九十" + assert t.save + + t.content = t.title = "一二三四五六" + assert t.save + end + end + + def test_optionally_validates_length_of_using_within_on_update_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: {{count}}" + + t = Topic.create("title" => "一二三4", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + + t.title = "1二三4" + assert !t.save + assert t.errors[:title].any? + assert_equal ["短すぎます: 5"], t.errors[:title] + + t.title = "一二三四五六七八九十A" + assert !t.save + assert t.errors[:title].any? + + t.title = "一二345" + assert t.save + end + end + + def test_validates_length_of_using_is_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :is => 5 + + t = Topic.create("title" => "一二345", "content" => "whatever") + assert t.valid? + + t.title = "一二345六" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"] + end + end + + def test_validates_length_of_with_block + Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least {{count}} words.", + :tokenizer => lambda {|str| str.scan(/\w+/) } + t = Topic.create!(:content => "this content should be long enough") + assert t.valid? + + t.content = "not long enough" + assert !t.valid? + assert t.errors[:content].any? + assert_equal ["Your essay must be at least 5 words."], t.errors[:content] + end + + def test_validates_length_of_with_custom_too_long_using_quotes + repair_validations(Developer) do + Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Jeffrey" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_length_of_with_custom_too_short_using_quotes + repair_validations(Developer) do + Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_length_of_with_custom_message_using_quotes + repair_validations(Developer) do + Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_length_of_for_ruby_class + repair_validations(Person) do + Person.validates_length_of :karma, :minimum => 5 + + p = Person.new + p.karma = "Pix" + assert p.invalid? + + assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma] + + p.karma = "The Smiths" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb new file mode 100644 index 0000000000..0af6eb69ce --- /dev/null +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -0,0 +1,179 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class NumericalityValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + NIL = [nil] + BLANK = ["", " ", " \t \r \n"] + BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits + FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5) + INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) + FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS + INTEGERS = [0, 10, -10] + INTEGER_STRINGS + BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } + JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"] + INFINITY = [1.0/0.0] + + def test_default_validates_numericality_of + Topic.validates_numericality_of :approved + invalid!(NIL + BLANK + JUNK) + valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + + def test_validates_numericality_of_with_nil_allowed + Topic.validates_numericality_of :approved, :allow_nil => true + + invalid!(JUNK) + valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + + def test_validates_numericality_of_with_integer_only + Topic.validates_numericality_of :approved, :only_integer => true + + invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY) + valid!(INTEGERS) + end + + def test_validates_numericality_of_with_integer_only_and_nil_allowed + Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true + + invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY) + valid!(NIL + BLANK + INTEGERS) + end + + def test_validates_numericality_with_greater_than + Topic.validates_numericality_of :approved, :greater_than => 10 + + invalid!([-10, 10], 'must be greater than 10') + valid!([11]) + end + + def test_validates_numericality_with_greater_than_or_equal + Topic.validates_numericality_of :approved, :greater_than_or_equal_to => 10 + + invalid!([-9, 9], 'must be greater than or equal to 10') + valid!([10]) + end + + def test_validates_numericality_with_equal_to + Topic.validates_numericality_of :approved, :equal_to => 10 + + invalid!([-10, 11] + INFINITY, 'must be equal to 10') + valid!([10]) + end + + def test_validates_numericality_with_less_than + Topic.validates_numericality_of :approved, :less_than => 10 + + invalid!([10], 'must be less than 10') + valid!([-9, 9]) + end + + def test_validates_numericality_with_less_than_or_equal_to + Topic.validates_numericality_of :approved, :less_than_or_equal_to => 10 + + invalid!([11], 'must be less than or equal to 10') + valid!([-10, 10]) + end + + def test_validates_numericality_with_odd + Topic.validates_numericality_of :approved, :odd => true + + invalid!([-2, 2], 'must be odd') + valid!([-1, 1]) + end + + def test_validates_numericality_with_even + Topic.validates_numericality_of :approved, :even => true + + invalid!([-1, 1], 'must be even') + valid!([-2, 2]) + end + + def test_validates_numericality_with_greater_than_less_than_and_even + Topic.validates_numericality_of :approved, :greater_than => 1, :less_than => 4, :even => true + + invalid!([1, 3, 4]) + valid!([2]) + end + + def test_validates_numericality_with_numeric_message + Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than {{count}}" + topic = Topic.new("title" => "numeric test", "approved" => 10) + + assert !topic.valid? + assert_equal ["smaller than 4"], topic.errors[:approved] + + Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than {{count}}" + topic = Topic.new("title" => "numeric test", "approved" => 1) + + assert !topic.valid? + assert_equal ["greater than 4"], topic.errors[:approved] + end + + def test_numericality_with_getter_method + repair_validations(Developer) do + Developer.validates_numericality_of( :salary ) + developer = Developer.new("name" => "michael", "salary" => nil) + developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") + assert developer.valid? + end + end + + def test_numericality_with_allow_nil_and_getter_method + repair_validations(Developer) do + Developer.validates_numericality_of( :salary, :allow_nil => true) + developer = Developer.new("name" => "michael", "salary" => nil) + developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") + assert developer.valid? + end + end + + def test_validates_numericality_of_for_ruby_class + repair_validations(Person) do + Person.validates_numericality_of :karma, :allow_nil => false + + p = Person.new + p.karma = "Pix" + assert p.invalid? + + assert_equal ["is not a number"], p.errors[:karma] + + p.karma = "1234" + assert p.valid? + end + end + + private + + def invalid!(values, error = nil) + with_each_topic_approved_value(values) do |topic, value| + assert !topic.valid?, "#{value.inspect} not rejected as a number" + assert topic.errors[:approved].any?, "FAILED for #{value.inspect}" + assert_equal error, topic.errors[:approved].first if error + end + end + + def valid!(values) + with_each_topic_approved_value(values) do |topic, value| + assert topic.valid?, "#{value.inspect} not accepted as a number" + end + end + + def with_each_topic_approved_value(values) + topic = Topic.new(:title => "numeric test", :content => "whatever") + values.each do |value| + topic.approved = value + yield topic, value + end + end +end diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb new file mode 100644 index 0000000000..f6bed4903a --- /dev/null +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -0,0 +1,57 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class PresenceValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validate_presences + Topic.validates_presence_of(:title, :content) + + t = Topic.create + assert !t.save + assert_equal ["can't be blank"], t.errors[:title] + assert_equal ["can't be blank"], t.errors[:content] + + t.title = "something" + t.content = " " + + assert !t.save + assert_equal ["can't be blank"], t.errors[:content] + + t.content = "like stuff" + + assert t.save + end + + def test_validates_presence_of_with_custom_message_using_quotes + repair_validations(Developer) do + Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:non_existent] + end + end + + def test_validates_presence_of_for_ruby_class + repair_validations(Person) do + Person.validates_presence_of :karma + + p = Person.new + assert p.invalid? + + assert_equal ["can't be blank"], p.errors[:karma] + + p.karma = "Cold" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb new file mode 100644 index 0000000000..8c89494247 --- /dev/null +++ b/activemodel/test/cases/validations_test.rb @@ -0,0 +1,178 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/reply' +require 'models/developer' + +class ValidationsTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + # Most of the tests mess with the validations of Topic, so lets repair it all the time. + # Other classes we mess with will be dealt with in the specific tests + repair_validations(Topic) + + def test_single_field_validation + r = Reply.new + r.title = "There's no content!" + assert !r.valid?, "A reply without content shouldn't be saveable" + + r.content = "Messa content!" + assert r.valid?, "A reply with content should be saveable" + end + + def test_single_attr_validation_and_error_msg + r = Reply.new + r.title = "There's no content!" + assert !r.valid? + assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid" + assert_equal ["Empty"], r.errors["content"], "A reply without content should contain an error" + assert_equal 1, r.errors.count + end + + def test_double_attr_validation_and_error_msg + r = Reply.new + assert !r.valid? + + assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid" + assert_equal ["Empty"], r.errors["title"], "A reply without title should contain an error" + + assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid" + assert_equal ["Empty"], r.errors["content"], "A reply without content should contain an error" + + assert_equal 2, r.errors.count + end + + def test_single_error_per_attr_iteration + r = Reply.new + r.save + + errors = [] + r.errors.each {|attr, messages| errors << [attr.to_s, messages] } + + assert errors.include?(["title", "Empty"]) + assert errors.include?(["content", "Empty"]) + end + + def test_multiple_errors_per_attr_iteration_with_full_error_composition + r = Reply.new + r.title = "Wrong Create" + r.content = "Mismatch" + r.save + + errors = r.errors.to_a + + assert_equal "Title is Wrong Create", errors[0] + assert_equal "Title is Content Mismatch", errors[1] + assert_equal 2, r.errors.count + end + + def test_errors_on_base + r = Reply.new + r.content = "Mismatch" + r.save + r.errors[:base] << "Reply is not dignifying" + + errors = [] + r.errors.to_a.each { |error| errors << error } + + assert_equal ["Reply is not dignifying"], r.errors[:base] + + assert errors.include?("Title Empty") + assert errors.include?("Reply is not dignifying") + assert_equal 2, r.errors.count + end + + def test_validates_each + hits = 0 + Topic.validates_each(:title, :content, [:title, :content]) do |record, attr| + record.errors.add attr, 'gotcha' + hits += 1 + end + t = Topic.new("title" => "valid", "content" => "whatever") + assert !t.save + assert_equal 4, hits + assert_equal %w(gotcha gotcha), t.errors[:title] + assert_equal %w(gotcha gotcha), t.errors[:content] + end + + def test_validate_block + Topic.validate { |topic| topic.errors.add("title", "will never be valid") } + t = Topic.create("title" => "Title", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["will never be valid"], t.errors["title"] + end + + def test_invalid_validator + Topic.validate 3 + assert_raise(ArgumentError) { t = Topic.create } + end + + def test_errors_to_xml + r = Reply.new :title => "Wrong Create" + assert !r.valid? + xml = r.errors.to_xml(:skip_instruct => true) + assert_equal "<errors>", xml.first(8) + assert xml.include?("<error>Title is Wrong Create</error>") + assert xml.include?("<error>Content Empty</error>") + end + + def test_validation_order + Topic.validates_presence_of :title + Topic.validates_length_of :title, :minimum => 2 + + t = Topic.new("title" => "") + assert !t.valid? + assert_equal "can't be blank", t.errors["title"].first + end + + def test_invalid_should_be_the_opposite_of_valid + Topic.validates_presence_of :title + + t = Topic.new + assert t.invalid? + assert t.errors[:title].any? + + t.title = 'Things are going to change' + assert !t.invalid? + end + + def test_deprecated_error_messages_on + Topic.validates_presence_of :title + + t = Topic.new + assert t.invalid? + + [:title, "title"].each do |attribute| + assert_deprecated { assert_equal "can't be blank", t.errors.on(attribute) } + end + + Topic.validates_each(:title) do |record, attribute| + record.errors[attribute] << "invalid" + end + + assert t.invalid? + + [:title, "title"].each do |attribute| + assert_deprecated do + assert t.errors.on(attribute).include?("invalid") + assert t.errors.on(attribute).include?("can't be blank") + end + end + end + + def test_deprecated_errors_on_base_and_each + t = Topic.new + assert t.valid? + + assert_deprecated { t.errors.add_to_base "invalid topic" } + assert_deprecated { assert_equal "invalid topic", t.errors.on_base } + assert_deprecated { assert t.errors.invalid?(:base) } + + all_errors = t.errors.to_a + assert_deprecated { assert_equal all_errors, t.errors.each_full{|err| err} } + end +end
\ No newline at end of file diff --git a/activemodel/test/config.rb b/activemodel/test/config.rb new file mode 100644 index 0000000000..0b577a9936 --- /dev/null +++ b/activemodel/test/config.rb @@ -0,0 +1,3 @@ +TEST_ROOT = File.expand_path(File.dirname(__FILE__)) +FIXTURES_ROOT = TEST_ROOT + "/fixtures" +SCHEMA_FILE = TEST_ROOT + "/schema.rb" diff --git a/activemodel/test/fixtures/topics.yml b/activemodel/test/fixtures/topics.yml new file mode 100644 index 0000000000..e4c61ce2d8 --- /dev/null +++ b/activemodel/test/fixtures/topics.yml @@ -0,0 +1,41 @@ +first: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: 2003-07-16t15:28:11.2233+01:00 + last_read: 2004-04-15 + bonus_time: 2005-01-30t15:28:00.00+01:00 + content: Have a nice day + approved: false + replies_count: 1 + +second: + id: 2 + title: The Second Topic of the day + author_name: Mary + written_on: 2004-07-15t15:28:00.0099+01:00 + content: Have a nice day + approved: true + replies_count: 0 + parent_id: 1 + type: Reply + +third: + id: 3 + title: The Third Topic of the day + author_name: Nick + written_on: 2005-07-15t15:28:00.0099+01:00 + content: I'm a troll + approved: true + replies_count: 1 + +fourth: + id: 4 + title: The Fourth Topic of the day + author_name: Carl + written_on: 2006-07-15t15:28:00.0099+01:00 + content: Why not? + approved: true + type: Reply + parent_id: 3 diff --git a/activemodel/test/models/developer.rb b/activemodel/test/models/developer.rb new file mode 100644 index 0000000000..5e6eefeed1 --- /dev/null +++ b/activemodel/test/models/developer.rb @@ -0,0 +1,6 @@ +class Developer < ActiveRecord::Base + validates_inclusion_of :salary, :in => 50000..200000 + validates_length_of :name, :within => 3..20 + + attr_accessor :name_confirmation +end diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb new file mode 100644 index 0000000000..d98420f900 --- /dev/null +++ b/activemodel/test/models/person.rb @@ -0,0 +1,5 @@ +class Person + include ActiveModel::Validations + + attr_accessor :title, :karma +end diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb new file mode 100644 index 0000000000..acfd801674 --- /dev/null +++ b/activemodel/test/models/reply.rb @@ -0,0 +1,34 @@ +require 'models/topic' + +class Reply < Topic + validate :errors_on_empty_content + validate_on_create :title_is_wrong_create + + validate :check_empty_title + validate_on_create :check_content_mismatch + validate_on_update :check_wrong_update + + attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read + + def check_empty_title + errors[:title] << "Empty" unless attribute_present?("title") + end + + def errors_on_empty_content + errors[:content] << "Empty" unless attribute_present?("content") + end + + def check_content_mismatch + if attribute_present?("title") && attribute_present?("content") && content == "Mismatch" + errors[:title] << "is Content Mismatch" + end + end + + def title_is_wrong_create + errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create" + end + + def check_wrong_update + errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update" + end +end diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb new file mode 100644 index 0000000000..1350aa17e7 --- /dev/null +++ b/activemodel/test/models/topic.rb @@ -0,0 +1,9 @@ +class Topic < ActiveRecord::Base + def condition_is_true + true + end + + def condition_is_true_but_its_not + false + end +end diff --git a/activemodel/test/schema.rb b/activemodel/test/schema.rb new file mode 100644 index 0000000000..56b824d445 --- /dev/null +++ b/activemodel/test/schema.rb @@ -0,0 +1,14 @@ +ActiveRecord::Schema.define do + create_table :topics, :force => true do |t| + t.string :title + t.string :author_name + t.text :content + t.boolean :approved, :default => true + t.string :type + end + + create_table :developers, :force => true do |t| + t.string :name + t.float :salary + end +end diff --git a/activemodel/test/test_helper.rb b/activemodel/test/test_helper.rb deleted file mode 100644 index 5b5678e42d..0000000000 --- a/activemodel/test/test_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rubygems' -require 'test/unit' - -gem 'mocha', '>= 0.9.3' -require 'mocha' - -require 'active_model' -require 'active_model/state_machine' - -$:.unshift File.dirname(__FILE__) + "/../../activesupport/lib" -require 'active_support' -require 'active_support/test_case' - -class ActiveModel::TestCase < ActiveSupport::TestCase -end - -begin - require 'ruby-debug' - Debugger.start -rescue LoadError -end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 4d501d319f..ee6f99edb5 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,6 +25,13 @@ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" $:.unshift(activesupport_path) if File.directory?(activesupport_path) require 'active_support' +begin + require 'active_model' +rescue LoadError + $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" + require 'active_model' +end + module ActiveRecord # TODO: Review explicit loads to see if they will automatically be handled by the initializer. def self.load_all! diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 359e70f5ed..9ecf231a66 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -1,6 +1,6 @@ module ActiveRecord module Aggregations # :nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern def clear_aggregation_cache #:nodoc: self.class.reflect_on_all_aggregations.to_a.each do |assoc| diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 41158a66bc..e41fda7a4b 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -1,7 +1,7 @@ module ActiveRecord # See ActiveRecord::AssociationPreload::ClassMethods for documentation. module AssociationPreload #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # Implements the details of eager loading of ActiveRecord associations. # Application developers should not use this module directly. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a5e2afa785..b72a3205a1 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -79,7 +79,7 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # These classes will be loaded when associations are created. # So there is no need to eager load them. diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index d5e215af9d..15358979c2 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/enumerable' module ActiveRecord module AttributeMethods #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern DEFAULT_SUFFIXES = %w(= ? _before_type_cast) ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4ab2818282..a540570f42 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -125,7 +125,7 @@ module ActiveRecord # post.author.name = '' # post.save(false) # => true module AutosaveAssociation - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } @@ -250,7 +250,7 @@ module ActiveRecord unless association.marked_for_destruction? association.errors.each do |attribute, message| attribute = "#{reflection.name}_#{attribute}" - errors.add(attribute, message) unless errors.on(attribute) + errors[attribute] << message if errors[attribute].empty? end end else diff --git a/activerecord/lib/active_record/batches.rb b/activerecord/lib/active_record/batches.rb index 4836601297..e41d38fb8f 100644 --- a/activerecord/lib/active_record/batches.rb +++ b/activerecord/lib/active_record/batches.rb @@ -1,6 +1,6 @@ module ActiveRecord module Batches # :nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # When processing large numbers of records, it's often a good idea to do # so in batches to prevent memory ballooning. diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 7afa7c49bd..727f4c1dc6 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -1,6 +1,6 @@ module ActiveRecord module Calculations #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from] diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a77fdb1c13..36f5f2ce47 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -211,7 +211,7 @@ module ActiveRecord # needs to be aware of it because an ordinary +save+ will raise such exception # instead of quietly returning +false+. module Callbacks - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern CALLBACKS = %w( after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index ac84f6b209..178767e0c3 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -34,7 +34,7 @@ module ActiveRecord # person.name << 'by' # person.name_change # => ['uncle bob', 'uncle bobby'] module Dirty - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was'] diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3eb547f713..6eeeddc9e1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -807,7 +807,7 @@ end module ActiveRecord module TestFixtures - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do setup :setup_fixtures diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index cf4f8864c6..cec5ca3324 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -42,7 +42,7 @@ module ActiveRecord # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method. # This method uses the same syntax as <tt>set_table_name</tt> module Optimistic - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do cattr_accessor :lock_optimistically, :instance_writer => false diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index e7151a3d47..1b22fa5e24 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/hash/except' module ActiveRecord module NamedScope - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # All subclasses of ActiveRecord::Base have one named scope: # * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt> diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 500b9b8ae0..bc4cca7855 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/object/try' module ActiveRecord module NestedAttributes #:nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 1ca76c7b2f..89ec0962bf 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -3,7 +3,7 @@ require 'set' module ActiveRecord module Observing # :nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern module ClassMethods # Activates the observers assigned. Examples: diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 3747ba449d..0baa9654b7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,6 +1,6 @@ module ActiveRecord module Reflection # :nodoc: - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations. # This information can, for example, be used in a form builder that took an Active Record object and created input diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index de530a3456..2d90ef35aa 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -99,8 +99,15 @@ HEADER next if column.name == pk spec = {} spec[:name] = column.name.inspect - spec[:type] = column.type.to_s - spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal + + # AR has an optimisation which handles zero-scale decimals as integers. This + # code ensures that the dumper still dumps the column as a decimal. + spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) } + 'decimal' + else + column.type.to_s + end + spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' spec[:precision] = column.precision.inspect if !column.precision.nil? spec[:scale] = column.scale.inspect if !column.scale.nil? spec[:null] = 'false' if !column.null diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb index d376fd5e1b..67e2b2abb3 100644 --- a/activerecord/lib/active_record/serializers/json_serializer.rb +++ b/activerecord/lib/active_record/serializers/json_serializer.rb @@ -2,7 +2,7 @@ require 'active_support/json' module ActiveRecord #:nodoc: module Serialization - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do cattr_accessor :include_root_in_json, :instance_writer => false diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 3734e170af..da075dabd3 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -8,7 +8,7 @@ module ActiveRecord # Timestamps are in the local timezone by default but you can use UTC by setting # <tt>ActiveRecord::Base.default_timezone = :utc</tt> module Timestamp - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do alias_method_chain :create, :timestamps diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 471a81dfb5..4f8ccdd40e 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -3,7 +3,7 @@ require 'thread' module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern class TransactionError < ActiveRecordError # :nodoc: end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 0532a06bf2..14c7699705 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -16,59 +16,46 @@ module ActiveRecord end end - # Active Record validation is reported to and from this object, which is used by Base#save to - # determine whether the object is in a valid state to be saved. See usage example in Validations. - class Errors - include Enumerable - + class Errors < ActiveModel::Errors class << self def default_error_messages - ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').") + message = "Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages')." + ActiveSupport::Deprecation.warn(message) + I18n.translate 'activerecord.errors.messages' end end - def initialize(base) # :nodoc: - @base, @errors = base, {} - end - - # Adds an error to the base object instead of any particular attribute. This is used - # to report errors that don't tie to any specific attribute, but rather to the object - # as a whole. These error messages don't get prepended with any field name when iterating - # with +each_full+, so they should be complete sentences. - def add_to_base(msg) - add(:base, msg) - end + # Returns all the full error messages in an array. + # + # class Company < ActiveRecord::Base + # validates_presence_of :name, :address, :email + # validates_length_of :name, :in => 5..30 + # end + # + # company = Company.create(:address => '123 First St.') + # company.errors.full_messages # => + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + def full_messages(options = {}) + full_messages = [] - # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> - # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one - # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. - # If no +messsage+ is supplied, :invalid is assumed. - # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). - def add(attribute, message = nil, options = {}) - message ||= :invalid - message = generate_message(attribute, message, options) if message.is_a?(Symbol) - @errors[attribute.to_s] ||= [] - @errors[attribute.to_s] << message - end + each do |attribute, messages| + next if messages.empty? - # Will add an error message to each of the attributes in +attributes+ that is empty. - def add_on_empty(attributes, custom_message = nil) - for attr in [attributes].flatten - value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - is_empty = value.respond_to?(:empty?) ? value.empty? : false - add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty + if attribute == :base + messages.each {|m| full_messages << m } + else + attr_name = @base.class.human_attribute_name(attribute.to_s) + prefix = attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + messages.each do |m| + full_messages << "#{prefix}#{m}" + end + end end - end - # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). - def add_on_blank(attributes, custom_message = nil) - for attr in [attributes].flatten - value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - add(attr, :blank, :default => custom_message) if value.blank? - end + full_messages end - + # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>). # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the @@ -88,7 +75,6 @@ module ActiveRecord # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li> # </ol> def generate_message(attribute, message = :invalid, options = {}) - message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) defaults = @base.class.self_and_descendants_from_active_record.map do |klass| @@ -111,876 +97,22 @@ module ActiveRecord I18n.translate(key, options) end - - # Returns true if the specified +attribute+ has errors associated with it. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.invalid?(:name) # => true - # company.errors.invalid?(:address) # => false - def invalid?(attribute) - !@errors[attribute.to_s].nil? - end - - # Returns +nil+, if no errors are associated with the specified +attribute+. - # Returns the error message, if one error is associated with the specified +attribute+. - # Returns an array of error messages, if more than one error is associated with the specified +attribute+. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"] - # company.errors.on(:email) # => "can't be blank" - # company.errors.on(:address) # => nil - def on(attribute) - errors = @errors[attribute.to_s] - return nil if errors.nil? - errors.size == 1 ? errors.first : errors - end - - alias :[] :on - - # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>. - def on_base - on(:base) - end - - # Yields each attribute and associated message per error added. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } - # # => name - is too short (minimum is 5 characters) - # # name - can't be blank - # # address - can't be blank - def each - @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } - end - - # Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned - # through iteration as "First name can't be empty". - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.each_full{|msg| puts msg } - # # => Name is too short (minimum is 5 characters) - # # Name can't be blank - # # Address can't be blank - def each_full - full_messages.each { |msg| yield msg } - end - - # Returns all the full error messages in an array. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] - def full_messages(options = {}) - full_messages = [] - - @errors.each_key do |attr| - @errors[attr].each do |message| - next unless message - - if attr == "base" - full_messages << message - else - attr_name = @base.class.human_attribute_name(attr) - full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message - end - end - end - full_messages - end - - # Returns true if no errors have been added. - def empty? - @errors.empty? - end - - # Removes all errors that have been added. - def clear - @errors = {} - end - - # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. - def size - @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } - end - - alias_method :count, :size - alias_method :length, :size - - # Returns an XML representation of this error object. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.to_xml - # # => <?xml version="1.0" encoding="UTF-8"?> - # # <errors> - # # <error>Name is too short (minimum is 5 characters)</error> - # # <error>Name can't be blank</error> - # # <error>Address can't be blank</error> - # # </errors> - def to_xml(options={}) - require 'builder' unless defined? ::Builder - options[:root] ||= "errors" - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) - - options[:builder].instruct! unless options.delete(:skip_instruct) - options[:builder].errors do |e| - full_messages.each { |msg| e.error(msg) } - end - end - end - - # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. - # - # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and - # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring - # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). - # - # Example: - # - # class Person < ActiveRecord::Base - # protected - # def validate - # errors.add_on_empty %w( first_name last_name ) - # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/ - # end - # - # def validate_on_create # is only run the first time a new object is saved - # unless valid_discount?(membership_discount) - # errors.add("membership_discount", "has expired") - # end - # end - # - # def validate_on_update - # errors.add_to_base("No changes have occurred") if unchanged_attributes? - # end - # end - # - # person = Person.new("first_name" => "David", "phone_number" => "what?") - # person.save # => false (and doesn't do the save) - # person.errors.empty? # => false - # person.errors.count # => 2 - # person.errors.on "last_name" # => "can't be empty" - # person.errors.on "phone_number" # => "has invalid format" - # person.errors.each_full { |msg| puts msg } - # # => "Last name can't be empty\n" + - # # "Phone number has invalid format" - # - # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" } - # person.save # => true (and person is now saved in the database) - # - # An Errors object is automatically created for every Active Record. module Validations - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - VALIDATIONS = %w( validate validate_on_create validate_on_update ) + include ActiveSupport::Callbacks + include ActiveModel::Validations included do alias_method_chain :save, :validation alias_method_chain :save!, :validation - include ActiveSupport::Callbacks - define_callbacks *VALIDATIONS + define_callbacks :validate_on_create, :validate_on_update end - # Active Record classes can implement validations in several ways. The highest level, easiest to read, - # and recommended approach is to use the declarative <tt>validates_..._of</tt> class methods (and - # +validates_associated+) documented below. These are sufficient for most model validations. - # - # Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative - # validation methods, but like all the lower-level approaches it requires manually adding to the errors collection - # when the record is invalid. - # - # At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+ - # to add validation methods or blocks. These are ActiveSupport::Callbacks and follow the same rules of inheritance - # and chaining. - # - # The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+ - # as documented in ActiveRecord::Validations. - # - # == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods - # - # Calls to these methods add a validation method or block to the class. Again, this approach is recommended - # only when the higher-level methods documented below (<tt>validates_..._of</tt> and +validates_associated+) are - # insufficient to handle the required validation. - # - # This can be done with a symbol pointing to a method: - # - # class Comment < ActiveRecord::Base - # validate :must_be_friends - # - # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) - # end - # end - # - # Or with a block which is passed the current record to be validated: - # - # class Comment < ActiveRecord::Base - # validate do |comment| - # comment.must_be_friends - # end - # - # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) - # end - # end - # - # This usage applies to +validate_on_create+ and +validate_on_update+ as well. module ClassMethods - DEFAULT_VALIDATION_OPTIONS = { - :on => :save, - :allow_nil => false, - :allow_blank => false, - :message => nil - }.freeze - - ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze - ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', - :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', - :odd => 'odd?', :even => 'even?' }.freeze - - # Validates each attribute against a block. - # - # class Person < ActiveRecord::Base - # validates_each :first_name, :last_name do |record, attr, value| - # record.errors.add attr, 'starts with z.' if value[0] == ?z - # end - # end - # - # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. - # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_each(*attrs) - options = attrs.extract_options!.symbolize_keys - attrs = attrs.flatten - - # Declare the validation. - send(validation_method(options[:on] || :save), options) do |record| - attrs.each do |attr| - value = record.send(attr) - next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) - yield record, attr, value - end - end - end - - # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: - # - # Model: - # class Person < ActiveRecord::Base - # validates_confirmation_of :user_name, :password - # validates_confirmation_of :email_address, :message => "should match confirmation" - # end - # - # View: - # <%= password_field "person", "password" %> - # <%= password_field "person", "password_confirmation" %> - # - # The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password. - # To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed - # only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence - # check for the confirmation attribute: - # - # validates_presence_of :password_confirmation, :if => :password_changed? - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_confirmation_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) - - validates_each(attr_names, configuration) do |record, attr_name, value| - unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") - record.errors.add(attr_name, :confirmation, :default => configuration[:message]) - end - end - end - - # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: - # - # class Person < ActiveRecord::Base - # validates_acceptance_of :terms_of_service - # validates_acceptance_of :eula, :message => "must be abided" - # end - # - # If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is - # performed only if +terms_of_service+ is not +nil+ and by default on save. - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "must be accepted"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true). - # * <tt>:accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which - # makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database - # column, since the attribute is typecast from "1" to +true+ before validation. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_acceptance_of(*attr_names) - configuration = { :on => :save, :allow_nil => true, :accept => "1" } - configuration.update(attr_names.extract_options!) - - db_cols = begin - column_names - rescue Exception # To ignore both statement and connection errors - [] - end - names = attr_names.reject { |name| db_cols.include?(name.to_s) } - attr_accessor(*names) - - validates_each(attr_names,configuration) do |record, attr_name, value| - unless value == configuration[:accept] - record.errors.add(attr_name, :accepted, :default => configuration[:message]) - end - end - end - - # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: - # - # class Person < ActiveRecord::Base - # validates_presence_of :first_name - # end - # - # The first_name attribute must be in the object and it cannot be blank. - # - # If you want to validate the presence of a boolean field (where the real values are true and false), - # you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. - # - # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. - # - # Configuration options: - # * <tt>message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, - # <tt>:update</tt>). - # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # - def validates_presence_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - # can't use validates_each here, because it cannot cope with nonexistent attributes, - # while errors.add_on_empty can - send(validation_method(configuration[:on]), configuration) do |record| - record.errors.add_on_blank(attr_names, configuration[:message]) - end - end - - # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: - # - # class Person < ActiveRecord::Base - # validates_length_of :first_name, :maximum=>30 - # validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind" - # validates_length_of :fax, :in => 7..32, :allow_nil => true - # validates_length_of :phone, :in => 7..32, :allow_blank => true - # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" - # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character" - # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me." - # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) } - # end - # - # Configuration options: - # * <tt>:minimum</tt> - The minimum size of the attribute. - # * <tt>:maximum</tt> - The maximum size of the attribute. - # * <tt>:is</tt> - The exact size of the attribute. - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. - # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. - # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. - # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)"). - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)"). - # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)"). - # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to - # count words as in above example.) - # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. - def validates_length_of(*attrs) - # Merge given options with defaults. - options = { - :tokenizer => lambda {|value| value.split(//)} - }.merge(DEFAULT_VALIDATION_OPTIONS) - options.update(attrs.extract_options!.symbolize_keys) - - # Ensure that one and only one range option is specified. - range_options = ALL_RANGE_OPTIONS & options.keys - case range_options.size - when 0 - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' - when 1 - # Valid number of options; do nothing. - else - raise ArgumentError, 'Too many range options specified. Choose only one.' - end - - # Get range option and value. - option = range_options.first - option_value = options[range_options.first] - key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option] - custom_message = options[:message] || options[key] - - case option - when :within, :in - raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - - validates_each(attrs, options) do |record, attr, value| - value = options[:tokenizer].call(value) if value.kind_of?(String) - if value.nil? or value.size < option_value.begin - record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin) - elsif value.size > option_value.end - record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end) - end - end - when :is, :minimum, :maximum - raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 - - # Declare different validations per option. - validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - - validates_each(attrs, options) do |record, attr, value| - value = options[:tokenizer].call(value) if value.kind_of?(String) - unless !value.nil? and value.size.method(validity_checks[option])[option_value] - record.errors.add(attr, key, :default => custom_message, :count => option_value) - end - end - end - end - - alias_method :validates_size_of, :validates_length_of - - - # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user - # can be named "davidhh". - # - # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id - # end - # - # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, - # making sure that a teacher can only be on the schedule once per semester for a particular class. - # - # class TeacherSchedule < ActiveRecord::Base - # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] - # end - # - # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified - # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. - # - # Configuration options: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # - # === Concurrency and integrity - # - # Using this validation method in conjunction with ActiveRecord::Base#save - # does not guarantee the absence of duplicate record insertions, because - # uniqueness checks on the application level are inherently prone to race - # conditions. For example, suppose that two users try to post a Comment at - # the same time, and a Comment's title must be unique. At the database-level, - # the actions performed by these users could be interleaved in the following manner: - # - # User 1 | User 2 - # ------------------------------------+-------------------------------------- - # # User 1 checks whether there's | - # # already a comment with the title | - # # 'My Post'. This is not the case. | - # SELECT * FROM comments | - # WHERE title = 'My Post' | - # | - # | # User 2 does the same thing and also - # | # infers that his title is unique. - # | SELECT * FROM comments - # | WHERE title = 'My Post' - # | - # # User 1 inserts his comment. | - # INSERT INTO comments | - # (title, content) VALUES | - # ('My Post', 'hi!') | - # | - # | # User 2 does the same thing. - # | INSERT INTO comments - # | (title, content) VALUES - # | ('My Post', 'hello!') - # | - # | # ^^^^^^ - # | # Boom! We now have a duplicate - # | # title! - # - # This could even happen if you use transactions with the 'serializable' - # isolation level. There are several ways to get around this problem: - # - By locking the database table before validating, and unlocking it after - # saving. However, table locking is very expensive, and thus not - # recommended. - # - By locking a lock file before validating, and unlocking it after saving. - # This does not work if you've scaled your Rails application across - # multiple web servers (because they cannot share lock files, or cannot - # do that efficiently), and thus not recommended. - # - Creating a unique index on the field, by using - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the - # rare case that a race condition occurs, the database will guarantee - # the field's uniqueness. - # - # When the database catches such a duplicate insertion, - # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid - # exception. You can either choose to let this error propagate (which - # will result in the default Rails exception page being shown), or you - # can catch it and restart the transaction (e.g. by telling the user - # that the title already exists, and asking him to re-enter the title). - # This technique is also known as optimistic concurrency control: - # http://en.wikipedia.org/wiki/Optimistic_concurrency_control - # - # Active Record currently provides no way to distinguish unique - # index constraint errors from other types of database errors, so you - # will have to parse the (database-specific) exception message to detect - # such a case. - def validates_uniqueness_of(*attr_names) - configuration = { :case_sensitive => true } - configuration.update(attr_names.extract_options!) - - validates_each(attr_names,configuration) do |record, attr_name, value| - # The check for an existing value should be run from a class that - # isn't abstract. This means working down from the current class - # (self), to the first non-abstract class. Since classes don't know - # their subclasses, we have to build the hierarchy between self and - # the record's class. - class_hierarchy = [record.class] - while class_hierarchy.first != self - class_hierarchy.insert(0, class_hierarchy.first.superclass) - end - - # Now we can work our way down the tree to the first non-abstract - # class (which has a database table to query from). - finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - - column = finder_class.columns_hash[attr_name.to_s] - - if value.nil? - comparison_operator = "IS ?" - elsif column.text? - comparison_operator = "#{connection.case_sensitive_equality_operator} ?" - value = column.limit ? value.to_s[0, column.limit] : value.to_s - else - comparison_operator = "= ?" - end - - sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" - - if value.nil? || (configuration[:case_sensitive] || !column.text?) - condition_sql = "#{sql_attribute} #{comparison_operator}" - condition_params = [value] - else - condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" - condition_params = [value.mb_chars.downcase] - end - - if scope = configuration[:scope] - Array(scope).map do |scope_item| - scope_value = record.send(scope_item) - condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) - condition_params << scope_value - end - end - - unless record.new_record? - condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" - condition_params << record.send(:id) - end - - finder_class.with_exclusive_scope do - if finder_class.exists?([condition_sql, *condition_params]) - record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value) - end - end - end - end - - - # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression - # provided. - # - # class Person < ActiveRecord::Base - # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create - # end - # - # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. - # - # A regular expression must be provided or else an exception will be raised. - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_format_of(*attr_names) - configuration = { :on => :save, :with => nil } - configuration.update(attr_names.extract_options!) - - raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) - - validates_each(attr_names, configuration) do |record, attr_name, value| - unless value.to_s =~ configuration[:with] - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) - end - end - end - - # Validates whether the value of the specified attribute is available in a particular enumerable object. - # - # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, :in => %w( m f ) - # validates_inclusion_of :age, :in => 0..99 - # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list" - # end - # - # Configuration options: - # * <tt>:in</tt> - An enumerable object of available items. - # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_inclusion_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - enum = configuration[:in] || configuration[:within] - - raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) - - validates_each(attr_names, configuration) do |record, attr_name, value| - unless enum.include?(value) - record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) - end - end - end - - # Validates that the value of the specified attribute is not in a particular enumerable object. - # - # class Person < ActiveRecord::Base - # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" - # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" - # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed" - # end - # - # Configuration options: - # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. - # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_exclusion_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - enum = configuration[:in] || configuration[:within] - - raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) - - validates_each(attr_names, configuration) do |record, attr_name, value| - if enum.include?(value) - record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) - end - end - end - - # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. - # - # class Book < ActiveRecord::Base - # has_many :pages - # belongs_to :library - # - # validates_associated :pages, :library - # end - # - # Warning: If, after the above definition, you then wrote: - # - # class Page < ActiveRecord::Base - # belongs_to :book - # - # validates_associated :book - # end - # - # this would specify a circular dependency and cause infinite recursion. - # - # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association - # is both present and guaranteed to be valid, you also need to use +validates_presence_of+. - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_associated(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - validates_each(attr_names, configuration) do |record, attr_name, value| - unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all? - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) - end - end - end - - # Validates whether the value of the specified attribute is numeric by trying to convert it to - # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). - # - # class Person < ActiveRecord::Base - # validates_numericality_of :value, :on => :create - # end - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is not a number"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float columns empty strings are converted to +nil+. - # * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value. - # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value. - # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value. - # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value. - # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value. - # * <tt>:odd</tt> - Specifies the value must be an odd number. - # * <tt>:even</tt> - Specifies the value must be an even number. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_numericality_of(*attr_names) - configuration = { :on => :save, :only_integer => false, :allow_nil => false } - configuration.update(attr_names.extract_options!) - - - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys - - (numericality_options - [ :odd, :even ]).each do |option| - raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) - end - - validates_each(attr_names,configuration) do |record, attr_name, value| - raw_value = record.send("#{attr_name}_before_type_cast") || value - - next if configuration[:allow_nil] and raw_value.nil? - - if configuration[:only_integer] - unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - next - end - raw_value = raw_value.to_i - else - begin - raw_value = Kernel.Float(raw_value) - rescue ArgumentError, TypeError - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - next - end - end - - numericality_options.each do |option| - case option - when :odd, :even - unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] - record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) - end - else - record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] - end - end - end - end - # Creates an object just like Base.create but calls save! instead of save # so an exception is raised if the record is invalid. def create!(attributes = nil, &block) @@ -994,75 +126,82 @@ module ActiveRecord end end - private - def validation_method(on) - case on - when :save then :validate - when :create then :validate_on_create - when :update then :validate_on_update - end + def validation_method(on) + case on + when :create + :validate_on_create + when :update + :validate_on_update + else + :validate end + end end - # The validation process on save can be skipped by passing false. The regular Base#save method is - # replaced with this when the validations module is mixed in, which it is by default. - def save_with_validation(perform_validation = true) - if perform_validation && valid? || !perform_validation - save_without_validation - else - false + module InstanceMethods + # The validation process on save can be skipped by passing false. The regular Base#save method is + # replaced with this when the validations module is mixed in, which it is by default. + def save_with_validation(perform_validation = true) + if perform_validation && valid? || !perform_validation + save_without_validation + else + false + end end - end - # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false - # if the record is not valid. - def save_with_validation! - if valid? - save_without_validation! - else - raise RecordInvalid.new(self) + # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false + # if the record is not valid. + def save_with_validation! + if valid? + save_without_validation! + else + raise RecordInvalid.new(self) + end end - end - # Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false. - def valid? - errors.clear + # Runs all the specified validations and returns true if no errors were added otherwise false. + def valid? + errors.clear - run_callbacks(:validate) - validate + run_callbacks(:validate) - if new_record? - run_callbacks(:validate_on_create) - validate_on_create - else - run_callbacks(:validate_on_update) - validate_on_update - end + if respond_to?(:validate) + ActiveSupport::Deprecation.warn("Base#validate has been deprecated, please use Base.validate :method instead") + validate + end - errors.empty? - end + if new_record? + run_callbacks(:validate_on_create) - # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise. - def invalid? - !valid? - end + if respond_to?(:validate_on_create) + ActiveSupport::Deprecation.warn("Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead") + validate_on_create + end + else + run_callbacks(:validate_on_update) - # Returns the Errors object that holds all information about attribute error messages. - def errors - @errors ||= Errors.new(self) - end + if respond_to?(:validate_on_update) + ActiveSupport::Deprecation.warn("Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead") + validate_on_update + end + end - protected - # Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes. - def validate + errors.empty? end - # Overwrite this method for validation checks used only on creation. - def validate_on_create + # Returns the Errors object that holds all information about attribute error messages. + def errors + @errors ||= Errors.new(self) end - # Overwrite this method for validation checks used only on updates. - def validate_on_update + def get_attribute_value(attribute) + respond_to?(attribute.to_sym) ? send(attribute.to_sym) : self[attribute.to_sym] end + end end end + +Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| + filename = File.basename(path) + require "active_record/validations/#{filename}" +end diff --git a/activemodel/lib/active_model/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b2d78af580..92f47d770f 100644 --- a/activemodel/lib/active_model/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,4 +1,4 @@ -module ActiveModel +module ActiveRecord module Validations module ClassMethods # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. @@ -18,14 +18,14 @@ module ActiveModel # validates_associated :book # end # - # ...this would specify a circular dependency and cause infinite recursion. + # this would specify a circular dependency and cause infinite recursion. # # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association # is both present and guaranteed to be valid, you also need to use +validates_presence_of+. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -33,12 +33,12 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } - configuration.update(attr_names.extract_options!) + configuration = attr_names.extract_options! validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless - (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all? + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + end end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb new file mode 100644 index 0000000000..edec4e9e43 --- /dev/null +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -0,0 +1,159 @@ +module ActiveRecord + module Validations + module ClassMethods + # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user + # can be named "davidhh". + # + # class Person < ActiveRecord::Base + # validates_uniqueness_of :user_name, :scope => :account_id + # end + # + # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, + # making sure that a teacher can only be on the schedule once per semester for a particular class. + # + # class TeacherSchedule < ActiveRecord::Base + # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] + # end + # + # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified + # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. + # + # Configuration options: + # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). + # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. + # + # === Concurrency and integrity + # + # Using this validation method in conjunction with ActiveRecord::Base#save + # does not guarantee the absence of duplicate record insertions, because + # uniqueness checks on the application level are inherently prone to race + # conditions. For example, suppose that two users try to post a Comment at + # the same time, and a Comment's title must be unique. At the database-level, + # the actions performed by these users could be interleaved in the following manner: + # + # User 1 | User 2 + # ------------------------------------+-------------------------------------- + # # User 1 checks whether there's | + # # already a comment with the title | + # # 'My Post'. This is not the case. | + # SELECT * FROM comments | + # WHERE title = 'My Post' | + # | + # | # User 2 does the same thing and also + # | # infers that his title is unique. + # | SELECT * FROM comments + # | WHERE title = 'My Post' + # | + # # User 1 inserts his comment. | + # INSERT INTO comments | + # (title, content) VALUES | + # ('My Post', 'hi!') | + # | + # | # User 2 does the same thing. + # | INSERT INTO comments + # | (title, content) VALUES + # | ('My Post', 'hello!') + # | + # | # ^^^^^^ + # | # Boom! We now have a duplicate + # | # title! + # + # This could even happen if you use transactions with the 'serializable' + # isolation level. There are several ways to get around this problem: + # - By locking the database table before validating, and unlocking it after + # saving. However, table locking is very expensive, and thus not + # recommended. + # - By locking a lock file before validating, and unlocking it after saving. + # This does not work if you've scaled your Rails application across + # multiple web servers (because they cannot share lock files, or cannot + # do that efficiently), and thus not recommended. + # - Creating a unique index on the field, by using + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the + # rare case that a race condition occurs, the database will guarantee + # the field's uniqueness. + # + # When the database catches such a duplicate insertion, + # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # exception. You can either choose to let this error propagate (which + # will result in the default Rails exception page being shown), or you + # can catch it and restart the transaction (e.g. by telling the user + # that the title already exists, and asking him to re-enter the title). + # This technique is also known as optimistic concurrency control: + # http://en.wikipedia.org/wiki/Optimistic_concurrency_control + # + # Active Record currently provides no way to distinguish unique + # index constraint errors from other types of database errors, so you + # will have to parse the (database-specific) exception message to detect + # such a case. + def validates_uniqueness_of(*attr_names) + configuration = { :case_sensitive => true } + configuration.update(attr_names.extract_options!) + + validates_each(attr_names,configuration) do |record, attr_name, value| + # The check for an existing value should be run from a class that + # isn't abstract. This means working down from the current class + # (self), to the first non-abstract class. Since classes don't know + # their subclasses, we have to build the hierarchy between self and + # the record's class. + class_hierarchy = [record.class] + while class_hierarchy.first != self + class_hierarchy.insert(0, class_hierarchy.first.superclass) + end + + # Now we can work our way down the tree to the first non-abstract + # class (which has a database table to query from). + finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } + + column = finder_class.columns_hash[attr_name.to_s] + + if value.nil? + comparison_operator = "IS ?" + elsif column.text? + comparison_operator = "#{connection.case_sensitive_equality_operator} ?" + value = column.limit ? value.to_s[0, column.limit] : value.to_s + else + comparison_operator = "= ?" + end + + sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" + + if value.nil? || (configuration[:case_sensitive] || !column.text?) + condition_sql = "#{sql_attribute} #{comparison_operator}" + condition_params = [value] + else + condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" + condition_params = [value.mb_chars.downcase] + end + + if scope = configuration[:scope] + Array(scope).map do |scope_item| + scope_value = record.send(scope_item) + condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) + condition_params << scope_value + end + end + + unless record.new_record? + condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" + condition_params << record.send(:id) + end + + finder_class.with_exclusive_scope do + if finder_class.exists?([condition_sql, *condition_params]) + record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value) + end + end + end + end + end + end +end diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index cb7fe9698b..f313a75233 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -7,7 +7,7 @@ require 'models/categorization' require 'active_support/core_ext/array/random_access' module Remembered - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern included do after_create :remember diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 1ddb3f49bf..7140de77ea 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -183,7 +183,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase account = firm.build_account assert !account.save - assert_equal "can't be empty", account.errors.on("credit_limit") + assert_equal ["can't be empty"], account.errors["credit_limit"] end def test_build_association_twice_without_saving_affects_nothing @@ -219,7 +219,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.account assert !account.save assert_equal account, firm.account - assert_equal "can't be empty", account.errors.on("credit_limit") + assert_equal ["can't be empty"], account.errors["credit_limit"] end def test_create diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index f95b2c0079..ddca5e962d 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -61,7 +61,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert !firm.account.valid? assert !firm.valid? assert !firm.save - assert_equal "is invalid", firm.errors.on("account") + assert_equal ["is invalid"], firm.errors["account"] end def test_save_succeeds_for_invalid_has_one_with_validate_false @@ -160,7 +160,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert !log.developer.valid? assert !log.valid? assert !log.save - assert_equal "is invalid", log.errors.on("developer") + assert_equal ["is invalid"], log.errors["developer"] end def test_save_succeeds_for_invalid_belongs_to_with_validate_false @@ -650,16 +650,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @pirate.ship.name = '' - assert !@pirate.valid? - assert !@pirate.errors.on(:ship_name).blank? + assert @pirate.invalid? + assert @pirate.errors[:ship_name].any? end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid @pirate.ship.name = nil @pirate.catchphrase = nil - assert !@pirate.valid? - assert !@pirate.errors.on(:ship_name).blank? - assert !@pirate.errors.on(:catchphrase).blank? + assert @pirate.invalid? + assert @pirate.errors[:ship_name].any? + assert @pirate.errors[:catchphrase].any? end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -740,16 +740,16 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @ship.pirate.catchphrase = '' - assert !@ship.valid? - assert !@ship.errors.on(:pirate_catchphrase).blank? + assert @ship.invalid? + assert @ship.errors[:pirate_catchphrase].any? end def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid @ship.name = nil @ship.pirate.catchphrase = nil - assert !@ship.valid? - assert !@ship.errors.on(:name).blank? - assert !@ship.errors.on(:pirate_catchphrase).blank? + assert @ship.invalid? + assert @ship.errors[:name].any? + assert @ship.errors[:pirate_catchphrase].any? end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -811,16 +811,16 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each { |child| child.name = '' } assert !@pirate.valid? - assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") - assert @pirate.errors.on(@association_name).blank? + assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"] + assert @pirate.errors[@association_name].empty? end def test_should_not_use_default_invalid_error_on_associated_models @pirate.send(@association_name).build(:name => '') assert !@pirate.valid? - assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") - assert @pirate.errors.on(@association_name).blank? + assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"] + assert @pirate.errors[@association_name].empty? end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid @@ -828,8 +828,8 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.catchphrase = nil assert !@pirate.valid? - assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") - assert !@pirate.errors.on(:catchphrase).blank? + assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"] + assert @pirate.errors[:catchphrase].any? end def test_should_allow_to_bypass_validations_on_the_associated_models_on_update diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f82784836e..c30186286d 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -63,7 +63,7 @@ end class ActiveSupport::TestCase include ActiveRecord::TestFixtures - include ActiveRecord::Testing::RepairHelper + include ActiveModel::ValidationsRepairHelper self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false diff --git a/activerecord/test/cases/repair_helper.rb b/activerecord/test/cases/repair_helper.rb index 686bfee46d..80d04010d6 100644 --- a/activerecord/test/cases/repair_helper.rb +++ b/activerecord/test/cases/repair_helper.rb @@ -1,7 +1,7 @@ module ActiveRecord module Testing module RepairHelper - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern module Toolbox def self.record_validations(*model_classes) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index f9ad7f3ba3..4f02be3c06 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -190,4 +190,9 @@ class SchemaDumperTest < ActiveRecord::TestCase output = stream.string assert_match %r{:precision => 3,[[:space:]]+:scale => 2,[[:space:]]+:default => 2.78}, output end + + def test_schema_dump_keeps_large_precision_integer_columns_as_decimal + output = standard_dump + assert_match %r{t.decimal\s+"atoms_in_universe",\s+:precision => 55,\s+:scale => 0}, output + end end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb new file mode 100644 index 0000000000..b1203c12ed --- /dev/null +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -0,0 +1,101 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/topic' +require 'models/reply' +require 'models/owner' + +class AssociationValidationTest < ActiveRecord::TestCase + fixtures :topics, :owners + + repair_validations(Topic) + + def test_validates_size_of_association + repair_validations(Owner) do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + pet = o.pets.build('name' => 'apet') + assert o.valid? + end + end + + def test_validates_size_of_association_using_within + repair_validations(Owner) do + assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + + pet = o.pets.build('name' => 'apet') + assert o.valid? + + 2.times { o.pets.build('name' => 'apet') } + assert !o.save + assert o.errors[:pets].any? + end + end + + def test_validates_associated_many + Topic.validates_associated( :replies ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")] + assert !t.valid? + assert t.errors[:replies].any? + assert_equal 1, r.errors.count # make sure all associated objects have been validated + assert_equal 0, r2.errors.count + assert_equal 1, r3.errors.count + assert_equal 0, r4.errors.count + r.content = r3.content = "non-empty" + assert t.valid? + end + + def test_validates_associated_one + repair_validations(Reply) do + Reply.validates_associated( :topic ) + Topic.validates_presence_of( :content ) + r = Reply.new("title" => "A reply", "content" => "with content!") + r.topic = Topic.create("title" => "uhohuhoh") + assert !r.valid? + assert r.errors[:topic].any? + r.topic.content = "non-empty" + assert r.valid? + end + end + + def test_validates_associated_with_custom_message_using_quotes + repair_validations(Reply) do + Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" + Topic.validates_presence_of :content + r = Reply.create("title" => "A reply", "content" => "with content!") + r.topic = Topic.create("title" => "uhohuhoh") + assert !r.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], r.errors[:topic] + end + end + + def test_validates_associated_missing + repair_validations(Reply) do + Reply.validates_presence_of(:topic) + r = Reply.create("title" => "A reply", "content" => "with content!") + assert !r.valid? + assert r.errors[:topic].any? + + r.topic = Topic.find :first + assert r.valid? + end + end + + def test_validates_size_of_association_utf8 + repair_validations(Owner) do + with_kcode('UTF8') do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'あいうえおかきくけこ') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'あいうえおかきくけこ') + assert o.valid? + end + end + end +end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb new file mode 100644 index 0000000000..29c10de4fe --- /dev/null +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -0,0 +1,166 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class I18nGenerateMessageValidationTest < Test::Unit::TestCase + def setup + reset_callbacks Topic + @topic = Topic.new + I18n.backend.store_translations :'en', { + :activerecord => { + :errors => { + :messages => { + :taken => "has already been taken", + } + } + } + } + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + def test_generate_message_inclusion_with_default_message + assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') + end + + def test_generate_message_inclusion_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + def test_generate_message_exclusion_with_default_message + assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') + end + + def test_generate_message_exclusion_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + def test_generate_message_invalid_with_default_message + assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') + end + + def test_generate_message_invalid_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) + def test_generate_message_confirmation_with_default_message + assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil) + end + + def test_generate_message_confirmation_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message') + end + + # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) + def test_generate_message_accepted_with_default_message + assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil) + end + + def test_generate_message_accepted_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message') + end + + # add_on_empty: generate_message(attr, :empty, :default => custom_message) + def test_generate_message_empty_with_default_message + assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil) + end + + def test_generate_message_empty_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message') + end + + # add_on_blank: generate_message(attr, :blank, :default => custom_message) + def test_generate_message_blank_with_default_message + assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil) + end + + def test_generate_message_blank_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message') + end + + # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + def test_generate_message_too_long_with_default_message + assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10) + end + + def test_generate_message_too_long_with_custom_message + assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + def test_generate_message_too_short_with_default_message + assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10) + end + + def test_generate_message_too_short_with_custom_message + assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) + def test_generate_message_wrong_length_with_default_message + assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) + end + + def test_generate_message_wrong_length_with_custom_message + assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) + end + + # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + def test_generate_message_not_a_number_with_default_message + assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') + end + + def test_generate_message_not_a_number_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + def test_generate_message_greater_than_with_default_message + assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_greater_than_or_equal_to_with_default_message + assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_equal_to_with_default_message + assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_with_default_message + assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_or_equal_to_with_default_message + assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_odd_with_default_message + assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_even_with_default_message + assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) + end + + # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) + def test_generate_message_taken_with_default_message + assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') + end + + def test_generate_message_taken_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title') + end + +end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 20d1369a64..0303147e50 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'models/topic' require 'models/reply' -class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase +class I18nValidationTest < ActiveRecord::TestCase def setup reset_callbacks Topic @topic = Topic.new @@ -54,7 +54,6 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase # ActiveRecord::Errors def test_errors_generate_message_translates_custom_model_attribute_key - I18n.expects(:translate).with( :topic, { :count => 1, @@ -122,6 +121,55 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase end + # validates_uniqueness_of w/ mocha + + def test_validates_uniqueness_of_generates_message + Topic.validates_uniqueness_of :title + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'}) + @topic.valid? + end + + def test_validates_uniqueness_of_generates_message_with_custom_default_message + Topic.validates_uniqueness_of :title, :message => 'custom' + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'}) + @topic.valid? + end + + # validates_associated w/ mocha + + def test_validates_associated_generates_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + def test_validates_associated_generates_message_with_custom_default_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + # validates_associated w/o mocha + + def test_validates_associated_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal ['custom message'], replied_topic.errors[:replies] + end + + def test_validates_associated_finds_global_default_translation + I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal ['global message'], replied_topic.errors[:replies] + end + def test_errors_add_on_empty_generates_message @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) @topic.errors.add_on_empty :title @@ -143,7 +191,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase end def test_errors_full_messages_translates_human_attribute_name_for_model_attributes - @topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] } + @topic.errors.add('title', 'empty') I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title') @topic.errors.full_messages :locale => 'en' end @@ -260,22 +308,6 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase @topic.valid? end - # validates_uniqueness_of w/ mocha - - def test_validates_uniqueness_of_generates_message - Topic.validates_uniqueness_of :title - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'}) - @topic.valid? - end - - def test_validates_uniqueness_of_generates_message_with_custom_default_message - Topic.validates_uniqueness_of :title, :message => 'custom' - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'}) - @topic.valid? - end - # validates_format_of w/ mocha def test_validates_format_of_generates_message @@ -388,20 +420,6 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase @topic.valid? end - # validates_associated w/ mocha - - def test_validates_associated_generates_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end - - def test_validates_associated_generates_message_with_custom_default_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end - # validates_confirmation_of w/o mocha def test_validates_confirmation_of_finds_custom_model_key_translation @@ -411,7 +429,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_confirmation_of_finds_global_default_translation @@ -420,7 +438,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_acceptance_of w/o mocha @@ -431,7 +449,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_acceptance_of_finds_global_default_translation @@ -439,7 +457,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_presence_of w/o mocha @@ -450,7 +468,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_presence_of_finds_global_default_translation @@ -458,7 +476,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_length_of :within w/o mocha @@ -469,7 +487,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :within => 3..5 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_length_of_within_finds_global_default_translation @@ -477,7 +495,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :within => 3..5 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_length_of :is w/o mocha @@ -488,7 +506,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_length_of_is_finds_global_default_translation @@ -496,18 +514,16 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end - # validates_uniqueness_of w/o mocha - def test_validates_length_of_is_finds_custom_model_key_translation I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}} Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_length_of_is_finds_global_default_translation @@ -515,7 +531,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end @@ -527,7 +543,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_format_of_finds_global_default_translation @@ -535,7 +551,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_inclusion_of w/o mocha @@ -546,7 +562,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_inclusion_of_finds_global_default_translation @@ -554,7 +570,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_exclusion_of w/o mocha @@ -566,7 +582,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_exclusion_of_finds_global_default_translation @@ -575,7 +591,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of without :only_integer w/o mocha @@ -587,7 +603,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title @topic.title = 'a' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_finds_global_default_translation @@ -596,7 +612,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of with :only_integer w/o mocha @@ -608,7 +624,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_only_integer_finds_global_default_translation @@ -617,7 +633,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of :odd w/o mocha @@ -629,7 +645,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_odd_finds_global_default_translation @@ -638,7 +654,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of :less_than w/o mocha @@ -650,7 +666,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_less_than_finds_global_default_translation @@ -659,27 +675,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) - end - - - # validates_associated w/o mocha - - def test_validates_associated_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} - - Topic.validates_associated :replies - replied_topic.valid? - assert_equal 'custom message', replied_topic.errors.on(:replies) - end - - def test_validates_associated_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} - - Topic.validates_associated :replies - replied_topic.valid? - assert_equal 'global message', replied_topic.errors.on(:replies) + assert_equal ['global message'], @topic.errors[:title] end def test_validations_with_message_symbol_must_translate @@ -687,7 +683,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title, :message => :custom_error @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) + assert_equal ["I am a custom error"], @topic.errors[:title] end def test_validates_with_message_symbol_must_translate_per_attribute @@ -695,7 +691,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title, :message => :custom_error @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) + assert_equal ["I am a custom error"], @topic.errors[:title] end def test_validates_with_message_symbol_must_translate_per_model @@ -703,195 +699,13 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title, :message => :custom_error @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) + assert_equal ["I am a custom error"], @topic.errors[:title] end def test_validates_with_message_string Topic.validates_presence_of :title, :message => "I am a custom error" @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) - end - -end - -class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase - def setup - reset_callbacks Topic - @topic = Topic.new - I18n.backend.store_translations :'en', { - :activerecord => { - :errors => { - :messages => { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is {{count}} characters)", - :too_short => "is too short (minimum is {{count}} characters)", - :wrong_length => "is the wrong length (should be {{count}} characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than {{count}}", - :greater_than_or_equal_to => "must be greater than or equal to {{count}}", - :equal_to => "must be equal to {{count}}", - :less_than => "must be less than {{count}}", - :less_than_or_equal_to => "must be less than or equal to {{count}}", - :odd => "must be odd", - :even => "must be even" - } - } - } - } - end - - def reset_callbacks(*models) - models.each do |model| - model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) - model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new) - model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new) - end - end - - # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) - def test_generate_message_inclusion_with_default_message - assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') - end - - def test_generate_message_inclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) - def test_generate_message_exclusion_with_default_message - assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') - end - - def test_generate_message_exclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - def test_generate_message_invalid_with_default_message - assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') - end - - def test_generate_message_invalid_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) - def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil) - end - - def test_generate_message_confirmation_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message') - end - - # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) - def test_generate_message_accepted_with_default_message - assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil) - end - - def test_generate_message_accepted_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message') - end - - # add_on_empty: generate_message(attr, :empty, :default => custom_message) - def test_generate_message_empty_with_default_message - assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil) - end - - def test_generate_message_empty_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message') - end - - # add_on_blank: generate_message(attr, :blank, :default => custom_message) - def test_generate_message_blank_with_default_message - assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil) - end - - def test_generate_message_blank_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message') - end - - # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) - def test_generate_message_too_long_with_default_message - assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10) - end - - def test_generate_message_too_long_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) - def test_generate_message_too_short_with_default_message - assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10) - end - - def test_generate_message_too_short_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) - def test_generate_message_wrong_length_with_default_message - assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) - end - - def test_generate_message_wrong_length_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) - end - - # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) - def test_generate_message_taken_with_default_message - assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') - end - - def test_generate_message_taken_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - def test_generate_message_not_a_number_with_default_message - assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') - end - - def test_generate_message_not_a_number_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) - def test_generate_message_greater_than_with_default_message - assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_greater_than_or_equal_to_with_default_message - assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + assert_equal ["I am a custom error"], @topic.errors[:title] end - - def test_generate_message_equal_to_with_default_message - assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_with_default_message - assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_or_equal_to_with_default_message - assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_odd_with_default_message - assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_even_with_default_message - assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) - end - end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb new file mode 100644 index 0000000000..961db51d1d --- /dev/null +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -0,0 +1,278 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/topic' +require 'models/reply' +require 'models/warehouse_thing' +require 'models/guid' +require 'models/event' + +# The following methods in Topic are used in test_conditional_validation_* +class Topic + has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" + has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" +end + +class UniqueReply < Reply + validates_uniqueness_of :content, :scope => 'parent_id' +end + +class SillyUniqueReply < UniqueReply +end + +class Wizard < ActiveRecord::Base + self.abstract_class = true + + validates_uniqueness_of :name +end + +class IneptWizard < Wizard + validates_uniqueness_of :city +end + +class Conjurer < IneptWizard +end + +class Thaumaturgist < IneptWizard +end + +class UniquenessValidationTest < ActiveRecord::TestCase + fixtures :topics, 'warehouse-things' + + repair_validations(Topic) + + def test_validate_uniqueness + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'm unique!") + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert_equal ["has already been taken"], t2.errors[:title] + + t2.title = "Now Im really also unique" + assert t2.save, "Should now save t2 as unique" + end + + def test_validates_uniquness_with_newline_chars + Topic.validates_uniqueness_of(:title, :case_sensitive => false) + + t = Topic.new("title" => "new\nline") + assert t.save, "Should save t as unique" + end + + def test_validate_uniqueness_with_scope + repair_validations(Reply) do + Reply.validates_uniqueness_of(:content, :scope => "parent_id") + + t = Topic.create("title" => "I'm unique!") + + r1 = t.replies.create "title" => "r1", "content" => "hello world" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "title" => "r2", "content" => "hello world" + assert !r2.valid?, "Saving r2 first time" + + r2.content = "something else" + assert r2.save, "Saving r2 second time" + + t2 = Topic.create("title" => "I'm unique too!") + r3 = t2.replies.create "title" => "r3", "content" => "hello world" + assert r3.valid?, "Saving r3" + end + end + + def test_validate_uniqueness_scoped_to_defining_class + t = Topic.create("title" => "What, me worry?") + + r1 = t.unique_replies.create "title" => "r1", "content" => "a barrel of fun" + assert r1.valid?, "Saving r1" + + r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun" + assert !r2.valid?, "Saving r2" + + # Should succeed as validates_uniqueness_of only applies to + # UniqueReply and its subclasses + r3 = t.replies.create "title" => "r2", "content" => "a barrel of fun" + assert r3.valid?, "Saving r3" + end + + def test_validate_uniqueness_with_scope_array + repair_validations(Reply) do + Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id]) + + t = Topic.create("title" => "The earth is actually flat!") + + r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..." + assert !r2.valid?, "Saving r2. Double reply by same author." + + r2.author_email_address = "jeremy_alt_email@rubyonrails.com" + assert r2.save, "Saving r2 the second time." + + r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic" + assert !r3.valid?, "Saving r3" + + r3.author_name = "jj" + assert r3.save, "Saving r3 the second time." + + r3.author_name = "jeremy" + assert !r3.save, "Saving r3 the third time." + end + end + + def test_validate_case_insensitive_uniqueness + Topic.validates_uniqueness_of(:title, :parent_id, :case_sensitive => false, :allow_nil => true) + + t = Topic.new("title" => "I'm unique!", :parent_id => 2) + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1) + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert t2.errors[:title].any? + assert t2.errors[:parent_id].any? + assert_equal ["has already been taken"], t2.errors[:title] + + t2.title = "I'm truly UNIQUE!" + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert t2.errors[:title].empty? + assert t2.errors[:parent_id].any? + + t2.parent_id = 4 + assert t2.save, "Should now save t2 as unique" + + t2.parent_id = nil + t2.title = nil + assert t2.valid?, "should validate with nil" + assert t2.save, "should save with nil" + + with_kcode('UTF8') do + t_utf8 = Topic.new("title" => "Я тоже уникальный!") + assert t_utf8.save, "Should save t_utf8 as unique" + + # If database hasn't UTF-8 character set, this test fails + if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" + t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") + assert !t2_utf8.valid?, "Shouldn't be valid" + assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" + end + end + end + + def test_validate_case_sensitive_uniqueness + Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'M UNIQUE!") + assert t2.valid?, "Should be valid" + assert t2.save, "Should save t2 as unique" + assert t2.errors[:title].empty? + assert t2.errors[:parent_id].empty? + assert_not_equal ["has already been taken"], t2.errors[:title] + + t3 = Topic.new("title" => "I'M uNiQUe!") + assert t3.valid?, "Should be valid" + assert t3.save, "Should save t2 as unique" + assert t3.errors[:title].empty? + assert t3.errors[:parent_id].empty? + assert_not_equal ["has already been taken"], t3.errors[:title] + end + + def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer + Topic.validates_uniqueness_of(:title, :case_sensitve => true) + t = Topic.create!('title' => 101) + + t2 = Topic.new('title' => 101) + assert !t2.valid? + assert t2.errors[:title] + end + + def test_validate_uniqueness_with_non_standard_table_names + i1 = WarehouseThing.create(:value => 1000) + assert !i1.valid?, "i1 should not be valid" + assert i1.errors[:value].any?, "Should not be empty" + end + + def test_validates_uniqueness_inside_with_scope + Topic.validates_uniqueness_of(:title) + + Topic.with_scope(:find => { :conditions => { :author_name => "David" } }) do + t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") + assert t1.save + t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") + assert !t2.valid? + end + end + + def test_validate_uniqueness_with_columns_which_are_sql_keywords + repair_validations(Guid) do + Guid.validates_uniqueness_of :key + g = Guid.new + g.key = "foo" + assert_nothing_raised { !g.valid? } + end + end + + def test_validate_uniqueness_with_limit + # Event.title is limited to 5 characters + e1 = Event.create(:title => "abcde") + assert e1.valid?, "Could not create an event with a unique, 5 character title" + e2 = Event.create(:title => "abcdefgh") + assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" + end + + def test_validate_straight_inheritance_uniqueness + w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork") + assert w1.valid?, "Saving w1" + + # Should use validation from base class (which is abstract) + w2 = IneptWizard.new(:name => "Rincewind", :city => "Quirm") + assert !w2.valid?, "w2 shouldn't be valid" + assert w2.errors[:name].any?, "Should have errors for name" + assert_equal ["has already been taken"], w2.errors[:name], "Should have uniqueness message for name" + + w3 = Conjurer.new(:name => "Rincewind", :city => "Quirm") + assert !w3.valid?, "w3 shouldn't be valid" + assert w3.errors[:name].any?, "Should have errors for name" + assert_equal ["has already been taken"], w3.errors[:name], "Should have uniqueness message for name" + + w4 = Conjurer.create(:name => "The Amazing Bonko", :city => "Quirm") + assert w4.valid?, "Saving w4" + + w5 = Thaumaturgist.new(:name => "The Amazing Bonko", :city => "Lancre") + assert !w5.valid?, "w5 shouldn't be valid" + assert w5.errors[:name].any?, "Should have errors for name" + assert_equal ["has already been taken"], w5.errors[:name], "Should have uniqueness message for name" + + w6 = Thaumaturgist.new(:name => "Mustrum Ridcully", :city => "Quirm") + assert !w6.valid?, "w6 shouldn't be valid" + assert w6.errors[:city].any?, "Should have errors for city" + assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city" + end + + def test_validates_uniqueness_of_with_custom_message_using_quotes + repair_validations(Developer) do + Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "David" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end +end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index c20f5ae63e..a4e874e5e6 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -10,94 +10,43 @@ require 'models/owner' require 'models/pet' require 'models/event' -# The following methods in Topic are used in test_conditional_validation_* -class Topic - has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" - has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" - - def condition_is_true - true - end - - def condition_is_true_but_its_not - false - end -end - class ProtectedPerson < ActiveRecord::Base set_table_name 'people' attr_accessor :addon attr_protected :first_name end -class UniqueReply < Reply - validates_uniqueness_of :content, :scope => 'parent_id' -end - -class SillyUniqueReply < UniqueReply -end - -class Wizard < ActiveRecord::Base - self.abstract_class = true +class DeprecatedPerson < ActiveRecord::Base + set_table_name 'people' - validates_uniqueness_of :name -end + protected -class IneptWizard < Wizard - validates_uniqueness_of :city -end + def validate + errors[:name] << "always invalid" + end -class Conjurer < IneptWizard -end + def validate_on_create + errors[:name] << "invalid on create" + end -class Thaumaturgist < IneptWizard + def validate_on_update + errors[:name] << "invalid on update" + end end - class ValidationsTest < ActiveRecord::TestCase - fixtures :topics, :developers, 'warehouse-things' + fixtures :topics, :developers # Most of the tests mess with the validations of Topic, so lets repair it all the time. # Other classes we mess with will be dealt with in the specific tests repair_validations(Topic) - def test_single_field_validation - r = Reply.new - r.title = "There's no content!" - assert !r.valid?, "A reply without content shouldn't be saveable" - - r.content = "Messa content!" - assert r.valid?, "A reply with content should be saveable" - end - - def test_single_attr_validation_and_error_msg - r = Reply.new - r.title = "There's no content!" - assert !r.valid? - assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid" - assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error" - assert_equal 1, r.errors.count - end - - def test_double_attr_validation_and_error_msg - r = Reply.new - assert !r.valid? - - assert r.errors.invalid?("title"), "A reply without title should mark that attribute as invalid" - assert_equal "Empty", r.errors.on("title"), "A reply without title should contain an error" - - assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid" - assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error" - - assert_equal 2, r.errors.count - end - def test_error_on_create r = Reply.new r.title = "Wrong Create" assert !r.valid? - assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid" - assert_equal "is Wrong Create", r.errors.on("title"), "A reply with a bad content should contain an error" + assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" + assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error" end def test_error_on_update @@ -109,8 +58,8 @@ class ValidationsTest < ActiveRecord::TestCase r.title = "Wrong Update" assert !r.save, "Second save should fail" - assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid" - assert_equal "is Wrong Update", r.errors.on("title"), "A reply with a bad content should contain an error" + assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" + assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error" end def test_invalid_record_exception @@ -172,47 +121,6 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_single_error_per_attr_iteration - r = Reply.new - r.save - - errors = [] - r.errors.each { |attr, msg| errors << [attr, msg] } - - assert errors.include?(["title", "Empty"]) - assert errors.include?(["content", "Empty"]) - end - - def test_multiple_errors_per_attr_iteration_with_full_error_composition - r = Reply.new - r.title = "Wrong Create" - r.content = "Mismatch" - r.save - - errors = [] - r.errors.each_full { |error| errors << error } - - assert_equal "Title is Wrong Create", errors[0] - assert_equal "Title is Content Mismatch", errors[1] - assert_equal 2, r.errors.count - end - - def test_errors_on_base - r = Reply.new - r.content = "Mismatch" - r.save - r.errors.add_to_base "Reply is not dignifying" - - errors = [] - r.errors.each_full { |error| errors << error } - - assert_equal "Reply is not dignifying", r.errors.on_base - - assert errors.include?("Title Empty") - assert errors.include?("Reply is not dignifying") - assert_equal 2, r.errors.count - end - def test_create_without_validation reply = Reply.new assert !reply.save @@ -225,96 +133,6 @@ class ValidationsTest < ActiveRecord::TestCase assert count+1, Reply.count end - def test_validates_each - hits = 0 - Topic.validates_each(:title, :content, [:title, :content]) do |record, attr| - record.errors.add attr, 'gotcha' - hits += 1 - end - t = Topic.new("title" => "valid", "content" => "whatever") - assert !t.save - assert_equal 4, hits - assert_equal %w(gotcha gotcha), t.errors.on(:title) - assert_equal %w(gotcha gotcha), t.errors.on(:content) - end - - def test_no_title_confirmation - Topic.validates_confirmation_of(:title) - - t = Topic.new(:author_name => "Plutarch") - assert t.valid? - - t.title_confirmation = "Parallel Lives" - assert !t.valid? - - t.title_confirmation = nil - t.title = "Parallel Lives" - assert t.valid? - - t.title_confirmation = "Parallel Lives" - assert t.valid? - end - - def test_title_confirmation - Topic.validates_confirmation_of(:title) - - t = Topic.create("title" => "We should be confirmed","title_confirmation" => "") - assert !t.save - - t.title_confirmation = "We should be confirmed" - assert t.save - end - - def test_terms_of_service_agreement_no_acceptance - Topic.validates_acceptance_of(:terms_of_service, :on => :create) - - t = Topic.create("title" => "We should not be confirmed") - assert t.save - end - - def test_terms_of_service_agreement - Topic.validates_acceptance_of(:terms_of_service, :on => :create) - - t = Topic.create("title" => "We should be confirmed","terms_of_service" => "") - assert !t.save - assert_equal "must be accepted", t.errors.on(:terms_of_service) - - t.terms_of_service = "1" - assert t.save - end - - - def test_eula - Topic.validates_acceptance_of(:eula, :message => "must be abided", :on => :create) - - t = Topic.create("title" => "We should be confirmed","eula" => "") - assert !t.save - assert_equal "must be abided", t.errors.on(:eula) - - t.eula = "1" - assert t.save - end - - def test_terms_of_service_agreement_with_accept_value - Topic.validates_acceptance_of(:terms_of_service, :on => :create, :accept => "I agree.") - - t = Topic.create("title" => "We should be confirmed", "terms_of_service" => "") - assert !t.save - assert_equal "must be accepted", t.errors.on(:terms_of_service) - - t.terms_of_service = "I agree." - assert t.save - end - - def test_validates_acceptance_of_as_database_column - repair_validations(Reply) do - Reply.validates_acceptance_of(:author_name) - - reply = Reply.create("author_name" => "Dan Brown") - assert_equal "Dan Brown", reply["author_name"] - end - end - def test_validates_acceptance_of_with_non_existant_table Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base) @@ -323,868 +141,6 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_validate_presences - Topic.validates_presence_of(:title, :content) - - t = Topic.create - assert !t.save - assert_equal "can't be blank", t.errors.on(:title) - assert_equal "can't be blank", t.errors.on(:content) - - t.title = "something" - t.content = " " - - assert !t.save - assert_equal "can't be blank", t.errors.on(:content) - - t.content = "like stuff" - - assert t.save - end - - def test_validate_uniqueness - Topic.validates_uniqueness_of(:title) - - t = Topic.new("title" => "I'm unique!") - assert t.save, "Should save t as unique" - - t.content = "Remaining unique" - assert t.save, "Should still save t as unique" - - t2 = Topic.new("title" => "I'm unique!") - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert_equal "has already been taken", t2.errors.on(:title) - - t2.title = "Now Im really also unique" - assert t2.save, "Should now save t2 as unique" - end - - def test_validates_uniquness_with_newline_chars - Topic.validates_uniqueness_of(:title, :case_sensitive => false) - - t = Topic.new("title" => "new\nline") - assert t.save, "Should save t as unique" - end - - def test_validate_uniqueness_with_scope - repair_validations(Reply) do - Reply.validates_uniqueness_of(:content, :scope => "parent_id") - - t = Topic.create("title" => "I'm unique!") - - r1 = t.replies.create "title" => "r1", "content" => "hello world" - assert r1.valid?, "Saving r1" - - r2 = t.replies.create "title" => "r2", "content" => "hello world" - assert !r2.valid?, "Saving r2 first time" - - r2.content = "something else" - assert r2.save, "Saving r2 second time" - - t2 = Topic.create("title" => "I'm unique too!") - r3 = t2.replies.create "title" => "r3", "content" => "hello world" - assert r3.valid?, "Saving r3" - end - end - - def test_validate_uniqueness_scoped_to_defining_class - t = Topic.create("title" => "What, me worry?") - - r1 = t.unique_replies.create "title" => "r1", "content" => "a barrel of fun" - assert r1.valid?, "Saving r1" - - r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun" - assert !r2.valid?, "Saving r2" - - # Should succeed as validates_uniqueness_of only applies to - # UniqueReply and its subclasses - r3 = t.replies.create "title" => "r2", "content" => "a barrel of fun" - assert r3.valid?, "Saving r3" - end - - def test_validate_uniqueness_with_scope_array - repair_validations(Reply) do - Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id]) - - t = Topic.create("title" => "The earth is actually flat!") - - r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply" - assert r1.valid?, "Saving r1" - - r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..." - assert !r2.valid?, "Saving r2. Double reply by same author." - - r2.author_email_address = "jeremy_alt_email@rubyonrails.com" - assert r2.save, "Saving r2 the second time." - - r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic" - assert !r3.valid?, "Saving r3" - - r3.author_name = "jj" - assert r3.save, "Saving r3 the second time." - - r3.author_name = "jeremy" - assert !r3.save, "Saving r3 the third time." - end - end - - def test_validate_case_insensitive_uniqueness - Topic.validates_uniqueness_of(:title, :parent_id, :case_sensitive => false, :allow_nil => true) - - t = Topic.new("title" => "I'm unique!", :parent_id => 2) - assert t.save, "Should save t as unique" - - t.content = "Remaining unique" - assert t.save, "Should still save t as unique" - - t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1) - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert t2.errors.on(:title) - assert t2.errors.on(:parent_id) - assert_equal "has already been taken", t2.errors.on(:title) - - t2.title = "I'm truly UNIQUE!" - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert_nil t2.errors.on(:title) - assert t2.errors.on(:parent_id) - - t2.parent_id = 4 - assert t2.save, "Should now save t2 as unique" - - t2.parent_id = nil - t2.title = nil - assert t2.valid?, "should validate with nil" - assert t2.save, "should save with nil" - - with_kcode('UTF8') do - t_utf8 = Topic.new("title" => "Я тоже уникальный!") - assert t_utf8.save, "Should save t_utf8 as unique" - - # If database hasn't UTF-8 character set, this test fails - if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" - t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") - assert !t2_utf8.valid?, "Shouldn't be valid" - assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" - end - end - end - - def test_validate_case_sensitive_uniqueness - Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true) - - t = Topic.new("title" => "I'm unique!") - assert t.save, "Should save t as unique" - - t.content = "Remaining unique" - assert t.save, "Should still save t as unique" - - t2 = Topic.new("title" => "I'M UNIQUE!") - assert t2.valid?, "Should be valid" - assert t2.save, "Should save t2 as unique" - assert !t2.errors.on(:title) - assert !t2.errors.on(:parent_id) - assert_not_equal "has already been taken", t2.errors.on(:title) - - t3 = Topic.new("title" => "I'M uNiQUe!") - assert t3.valid?, "Should be valid" - assert t3.save, "Should save t2 as unique" - assert !t3.errors.on(:title) - assert !t3.errors.on(:parent_id) - assert_not_equal "has already been taken", t3.errors.on(:title) - end - - def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer - Topic.validates_uniqueness_of(:title, :case_sensitve => true) - t = Topic.create!('title' => 101) - - t2 = Topic.new('title' => 101) - assert !t2.valid? - assert t2.errors.on(:title) - end - - def test_validate_uniqueness_with_non_standard_table_names - i1 = WarehouseThing.create(:value => 1000) - assert !i1.valid?, "i1 should not be valid" - assert i1.errors.on(:value), "Should not be empty" - end - - def test_validates_uniqueness_inside_with_scope - Topic.validates_uniqueness_of(:title) - - Topic.with_scope(:find => { :conditions => { :author_name => "David" } }) do - t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") - assert t1.save - t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") - assert !t2.valid? - end - end - - def test_validate_uniqueness_with_columns_which_are_sql_keywords - repair_validations(Guid) do - Guid.validates_uniqueness_of :key - g = Guid.new - g.key = "foo" - assert_nothing_raised { !g.valid? } - end - end - - def test_validate_uniqueness_with_limit - # Event.title is limited to 5 characters - e1 = Event.create(:title => "abcde") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "abcdefgh") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" - end - - def test_validate_straight_inheritance_uniqueness - w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork") - assert w1.valid?, "Saving w1" - - # Should use validation from base class (which is abstract) - w2 = IneptWizard.new(:name => "Rincewind", :city => "Quirm") - assert !w2.valid?, "w2 shouldn't be valid" - assert w2.errors.on(:name), "Should have errors for name" - assert_equal "has already been taken", w2.errors.on(:name), "Should have uniqueness message for name" - - w3 = Conjurer.new(:name => "Rincewind", :city => "Quirm") - assert !w3.valid?, "w3 shouldn't be valid" - assert w3.errors.on(:name), "Should have errors for name" - assert_equal "has already been taken", w3.errors.on(:name), "Should have uniqueness message for name" - - w4 = Conjurer.create(:name => "The Amazing Bonko", :city => "Quirm") - assert w4.valid?, "Saving w4" - - w5 = Thaumaturgist.new(:name => "The Amazing Bonko", :city => "Lancre") - assert !w5.valid?, "w5 shouldn't be valid" - assert w5.errors.on(:name), "Should have errors for name" - assert_equal "has already been taken", w5.errors.on(:name), "Should have uniqueness message for name" - - w6 = Thaumaturgist.new(:name => "Mustrum Ridcully", :city => "Quirm") - assert !w6.valid?, "w6 shouldn't be valid" - assert w6.errors.on(:city), "Should have errors for city" - assert_equal "has already been taken", w6.errors.on(:city), "Should have uniqueness message for city" - end - - def test_validate_format - Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") - - t = Topic.create("title" => "i'm incorrect", "content" => "Validation macros rule!") - assert !t.valid?, "Shouldn't be valid" - assert !t.save, "Shouldn't save because it's invalid" - assert_equal "is bad data", t.errors.on(:title) - assert_nil t.errors.on(:content) - - t.title = "Validation macros rule!" - - assert t.save - assert_nil t.errors.on(:title) - - assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } - end - - def test_validate_format_with_allow_blank - Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank=>true) - assert !Topic.create("title" => "Shouldn't be valid").valid? - assert Topic.create("title" => "").valid? - assert Topic.create("title" => nil).valid? - assert Topic.create("title" => "Validation macros rule!").valid? - end - - # testing ticket #3142 - def test_validate_format_numeric - Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") - - t = Topic.create("title" => "72x", "content" => "6789") - assert !t.valid?, "Shouldn't be valid" - assert !t.save, "Shouldn't save because it's invalid" - assert_equal "is bad data", t.errors.on(:title) - assert_nil t.errors.on(:content) - - t.title = "-11" - assert !t.valid?, "Shouldn't be valid" - - t.title = "03" - assert !t.valid?, "Shouldn't be valid" - - t.title = "z44" - assert !t.valid?, "Shouldn't be valid" - - t.title = "5v7" - assert !t.valid?, "Shouldn't be valid" - - t.title = "1" - - assert t.save - assert_nil t.errors.on(:title) - end - - def test_validate_format_with_formatted_message - Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be {{value}}") - t = Topic.create(:title => 'Invalid title') - assert_equal "can't be Invalid title", t.errors.on(:title) - end - - def test_validates_inclusion_of - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) ) - - assert !Topic.create("title" => "a!", "content" => "abc").valid? - assert !Topic.create("title" => "a b", "content" => "abc").valid? - assert !Topic.create("title" => nil, "content" => "def").valid? - - t = Topic.create("title" => "a", "content" => "I know you are but what am I?") - assert t.valid? - t.title = "uhoh" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is not included in the list", t.errors["title"] - - assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) } - assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) } - - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) } - end - - def test_validates_inclusion_of_with_allow_nil - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true ) - - assert !Topic.create("title" => "a!", "content" => "abc").valid? - assert !Topic.create("title" => "", "content" => "abc").valid? - assert Topic.create("title" => nil, "content" => "abc").valid? - end - - def test_numericality_with_getter_method - repair_validations(Developer) do - Developer.validates_numericality_of( :salary ) - developer = Developer.new("name" => "michael", "salary" => nil) - developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") - assert developer.valid? - end - end - - def test_validates_length_of_with_allow_nil - Topic.validates_length_of( :title, :is => 5, :allow_nil=>true ) - - assert !Topic.create("title" => "ab").valid? - assert !Topic.create("title" => "").valid? - assert Topic.create("title" => nil).valid? - assert Topic.create("title" => "abcde").valid? - end - - def test_validates_length_of_with_allow_blank - Topic.validates_length_of( :title, :is => 5, :allow_blank=>true ) - - assert !Topic.create("title" => "ab").valid? - assert Topic.create("title" => "").valid? - assert Topic.create("title" => nil).valid? - assert Topic.create("title" => "abcde").valid? - end - - def test_validates_inclusion_of_with_formatted_message - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option {{value}} is not in the list" ) - - assert Topic.create("title" => "a", "content" => "abc").valid? - - t = Topic.create("title" => "uhoh", "content" => "abc") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "option uhoh is not in the list", t.errors["title"] - end - - def test_numericality_with_allow_nil_and_getter_method - repair_validations(Developer) do - Developer.validates_numericality_of( :salary, :allow_nil => true) - developer = Developer.new("name" => "michael", "salary" => nil) - developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") - assert developer.valid? - end - end - - def test_validates_exclusion_of - Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) ) - - assert Topic.create("title" => "something", "content" => "abc").valid? - assert !Topic.create("title" => "monkey", "content" => "abc").valid? - end - - def test_validates_exclusion_of_with_formatted_message - Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option {{value}} is restricted" ) - - assert Topic.create("title" => "something", "content" => "abc") - - t = Topic.create("title" => "monkey") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "option monkey is restricted", t.errors["title"] - end - - def test_validates_length_of_using_minimum - Topic.validates_length_of :title, :minimum => 5 - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = "not" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - - t.title = "" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - - t.title = nil - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - end - - def test_optionally_validates_length_of_using_minimum - Topic.validates_length_of :title, :minimum => 5, :allow_nil => true - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_validates_length_of_using_maximum - Topic.validates_length_of :title, :maximum => 5 - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = "notvalid" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors["title"] - - t.title = "" - assert t.valid? - - t.title = nil - assert !t.valid? - end - - def test_optionally_validates_length_of_using_maximum - Topic.validates_length_of :title, :maximum => 5, :allow_nil => true - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_validates_length_of_using_within - Topic.validates_length_of(:title, :content, :within => 3..5) - - t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") - assert !t.valid? - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content) - - t.title = nil - t.content = nil - assert !t.valid? - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:content) - - t.title = "abe" - t.content = "mad" - assert t.valid? - end - - def test_optionally_validates_length_of_using_within - Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true - - t = Topic.create('title' => 'abc', 'content' => 'abcd') - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_optionally_validates_length_of_using_within_on_create - Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: {{count}}" - - t = Topic.create("title" => "thisisnotvalid", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - assert_equal "my string is too long: 10", t.errors[:title] - - t.title = "butthisis" - assert t.save - - t.title = "few" - assert t.save - - t.content = "andthisislong" - assert t.save - - t.content = t.title = "iamfine" - assert t.save - end - - def test_optionally_validates_length_of_using_within_on_update - Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: {{count}}" - - t = Topic.create("title" => "vali", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - - t.title = "not" - assert !t.save - assert t.errors.on(:title) - assert_equal "my string is too short: 5", t.errors[:title] - - t.title = "valid" - t.content = "andthisistoolong" - assert !t.save - assert t.errors.on(:content) - - t.content = "iamfine" - assert t.save - end - - def test_validates_length_of_using_is - Topic.validates_length_of :title, :is => 5 - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = "notvalid" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is the wrong length (should be 5 characters)", t.errors["title"] - - t.title = "" - assert !t.valid? - - t.title = nil - assert !t.valid? - end - - def test_optionally_validates_length_of_using_is - Topic.validates_length_of :title, :is => 5, :allow_nil => true - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_validates_length_of_using_bignum - bigmin = 2 ** 30 - bigmax = 2 ** 32 - bigrange = bigmin...bigmax - assert_nothing_raised do - Topic.validates_length_of :title, :is => bigmin + 5 - Topic.validates_length_of :title, :within => bigrange - Topic.validates_length_of :title, :in => bigrange - Topic.validates_length_of :title, :minimum => bigmin - Topic.validates_length_of :title, :maximum => bigmax - end - end - - def test_validates_length_with_globally_modified_error_message - ActiveSupport::Deprecation.silence do - ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}' - end - Topic.validates_length_of :title, :minimum => 10 - t = Topic.create(:title => 'too short') - assert !t.valid? - - assert_equal 'tu est trops petit hombre 10', t.errors['title'] - end - - def test_validates_size_of_association - repair_validations(Owner) do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors.on(:pets) - pet = o.pets.build('name' => 'apet') - assert o.valid? - end - end - - def test_validates_size_of_association_using_within - repair_validations(Owner) do - assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors.on(:pets) - - pet = o.pets.build('name' => 'apet') - assert o.valid? - - 2.times { o.pets.build('name' => 'apet') } - assert !o.save - assert o.errors.on(:pets) - end - end - - def test_validates_length_of_nasty_params - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") } - end - - def test_validates_length_of_custom_errors_for_minimum_with_message - Topic.validates_length_of( :title, :minimum=>5, :message=>"boo {{count}}" ) - t = Topic.create("title" => "uhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "boo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_minimum_with_too_short - Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo {{count}}" ) - t = Topic.create("title" => "uhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_maximum_with_message - Topic.validates_length_of( :title, :maximum=>5, :message=>"boo {{count}}" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "boo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_in - Topic.validates_length_of(:title, :in => 10..20, :message => "hoo {{count}}") - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 10", t.errors["title"] - - t = Topic.create("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 20", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_maximum_with_too_long - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_is_with_message - Topic.validates_length_of( :title, :is=>5, :message=>"boo {{count}}" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "boo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_is_with_wrong_length - Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo {{count}}" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_validates_length_of_using_minimum_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :minimum => 5 - - t = Topic.create("title" => "一二三四五", "content" => "whatever") - assert t.valid? - - t.title = "一二三四" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - end - end - - def test_validates_length_of_using_maximum_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :maximum => 5 - - t = Topic.create("title" => "一二三四五", "content" => "whatever") - assert t.valid? - - t.title = "一二34五六" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors["title"] - end - end - - def test_validates_length_of_using_within_utf8 - with_kcode('UTF8') do - Topic.validates_length_of(:title, :content, :within => 3..5) - - t = Topic.new("title" => "一二", "content" => "12三四五六七") - assert !t.valid? - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content) - t.title = "一二三" - t.content = "12三" - assert t.valid? - end - end - - def test_optionally_validates_length_of_using_within_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 3..5, :allow_nil => true - - t = Topic.create(:title => "一二三四五") - assert t.valid?, t.errors.inspect - - t = Topic.create(:title => "一二三") - assert t.valid?, t.errors.inspect - - t.title = nil - assert t.valid?, t.errors.inspect - end - end - - def test_optionally_validates_length_of_using_within_on_create_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: {{count}}" - - t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - assert_equal "長すぎます: 10", t.errors[:title] - - t.title = "一二三四五六七八九" - assert t.save - - t.title = "一二3" - assert t.save - - t.content = "一二三四五六七八九十" - assert t.save - - t.content = t.title = "一二三四五六" - assert t.save - end - end - - def test_optionally_validates_length_of_using_within_on_update_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: {{count}}" - - t = Topic.create("title" => "一二三4", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - - t.title = "1二三4" - assert !t.save - assert t.errors.on(:title) - assert_equal "短すぎます: 5", t.errors[:title] - - t.title = "一二三四五六七八九十A" - assert !t.save - assert t.errors.on(:title) - - t.title = "一二345" - assert t.save - end - end - - def test_validates_length_of_using_is_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :is => 5 - - t = Topic.create("title" => "一二345", "content" => "whatever") - assert t.valid? - - t.title = "一二345六" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is the wrong length (should be 5 characters)", t.errors["title"] - end - end - - def test_validates_length_of_with_block - Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least {{count}} words.", - :tokenizer => lambda {|str| str.scan(/\w+/) } - t = Topic.create!(:content => "this content should be long enough") - assert t.valid? - - t.content = "not long enough" - assert !t.valid? - assert t.errors.on(:content) - assert_equal "Your essay must be at least 5 words.", t.errors[:content] - end - - def test_validates_size_of_association_utf8 - repair_validations(Owner) do - with_kcode('UTF8') do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'あいうえおかきくけこ') - assert !o.save - assert o.errors.on(:pets) - o.pets.build('name' => 'あいうえおかきくけこ') - assert o.valid? - end - end - end - - def test_validates_associated_many - Topic.validates_associated( :replies ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")] - assert !t.valid? - assert t.errors.on(:replies) - assert_equal 1, r.errors.count # make sure all associated objects have been validated - assert_equal 0, r2.errors.count - assert_equal 1, r3.errors.count - assert_equal 0, r4.errors.count - r.content = r3.content = "non-empty" - assert t.valid? - end - - def test_validates_associated_one - repair_validations(Reply) do - Reply.validates_associated( :topic ) - Topic.validates_presence_of( :content ) - r = Reply.new("title" => "A reply", "content" => "with content!") - r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? - assert r.errors.on(:topic) - r.topic.content = "non-empty" - assert r.valid? - end - end - - def test_validate_block - Topic.validate { |topic| topic.errors.add("title", "will never be valid") } - t = Topic.create("title" => "Title", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "will never be valid", t.errors["title"] - end - - def test_invalid_validator - Topic.validate 3 - assert_raise(ArgumentError) { t = Topic.create } - end - def test_throw_away_typing d = Developer.new("name" => "David", "salary" => "100,000") assert !d.valid? @@ -1192,411 +148,40 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal "100,000", d.salary_before_type_cast end - def test_validates_acceptance_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.salary = "0" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last - end - end - - def test_validates_confirmation_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes" - d = Developer.new - d.name = "John" - d.name_confirmation = "Johnny" - assert !d.valid? - assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_format_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes" - d = Developer.new - d.name = d.name_confirmation = "John 32" - assert !d.valid? - assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_inclusion_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.salary = "90,000" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last - end - end - - def test_validates_length_of_with_custom_too_long_using_quotes - repair_validations(Developer) do - Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Jeffrey" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_length_of_with_custom_too_short_using_quotes - repair_validations(Developer) do - Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Joe" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_length_of_with_custom_message_using_quotes - repair_validations(Developer) do - Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Joe" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_presence_of_with_custom_message_using_quotes - repair_validations(Developer) do - Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Joe" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent) - end - end - - def test_validates_uniqueness_of_with_custom_message_using_quotes - repair_validations(Developer) do - Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "David" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_associated_with_custom_message_using_quotes - repair_validations(Reply) do - Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" - Topic.validates_presence_of :content - r = Reply.create("title" => "A reply", "content" => "with content!") - r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? - assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic) + def test_validates_length_with_globally_modified_error_message + ActiveSupport::Deprecation.silence do + ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}' end - end - def test_if_validation_using_method_true - # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_unless_validation_using_method_true - # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_if_validation_using_method_false - # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true_but_its_not ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_unless_validation_using_method_false - # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true_but_its_not ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_if_validation_using_string_true - # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "a = 1; a == 1" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_unless_validation_using_string_true - # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "a = 1; a == 1" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_if_validation_using_string_false - # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "false") - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_unless_validation_using_string_false - # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "false") - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_if_validation_using_block_true - # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :if => Proc.new { |r| r.content.size > 4 } ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + Topic.validates_length_of :title, :minimum => 10 + t = Topic.create(:title => 'too short') assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_unless_validation_using_block_true - # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :unless => Proc.new { |r| r.content.size > 4 } ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - def test_if_validation_using_block_false - # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :if => Proc.new { |r| r.title != "uhohuhoh"} ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) + assert_equal ['tu est trops petit hombre 10'], t.errors[:title] end - def test_unless_validation_using_block_false - # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :unless => Proc.new { |r| r.title != "uhohuhoh"} ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_validates_associated_missing + def test_validates_acceptance_of_as_database_column repair_validations(Reply) do - Reply.validates_presence_of(:topic) - r = Reply.create("title" => "A reply", "content" => "with content!") - assert !r.valid? - assert r.errors.on(:topic) + Reply.validates_acceptance_of(:author_name) - r.topic = Topic.find :first - assert r.valid? + reply = Reply.create("author_name" => "Dan Brown") + assert_equal "Dan Brown", reply["author_name"] end end - def test_errors_to_xml - r = Reply.new :title => "Wrong Create" - assert !r.valid? - xml = r.errors.to_xml(:skip_instruct => true) - assert_equal "<errors>", xml.first(8) - assert xml.include?("<error>Title is Wrong Create</error>") - assert xml.include?("<error>Content Empty</error>") - end - - def test_validation_order - Topic.validates_presence_of :title - Topic.validates_length_of :title, :minimum => 2 - - t = Topic.new("title" => "") - assert !t.valid? - assert_equal "can't be blank", t.errors.on("title").first - end - - def test_invalid_should_be_the_opposite_of_valid - Topic.validates_presence_of :title - - t = Topic.new - assert t.invalid? - assert t.errors.invalid?(:title) - - t.title = 'Things are going to change' - assert !t.invalid? - end - - # previous implementation of validates_presence_of eval'd the - # string with the wrong binding, this regression test is to - # ensure that it works correctly - def test_validation_with_if_as_string - Topic.validates_presence_of(:title) - Topic.validates_presence_of(:author_name, :if => "title.to_s.match('important')") - - t = Topic.new - assert !t.valid?, "A topic without a title should not be valid" - assert !t.errors.invalid?("author_name"), "A topic without an 'important' title should not require an author" - - t.title = "Just a title" - assert t.valid?, "A topic with a basic title should be valid" - - t.title = "A very important title" - assert !t.valid?, "A topic with an important title, but without an author, should not be valid" - assert t.errors.invalid?("author_name"), "A topic with an 'important' title should require an author" - - t.author_name = "Hubert J. Farnsworth" - assert t.valid?, "A topic with an important title and author should be valid" - end -end - - -class ValidatesNumericalityTest < ActiveRecord::TestCase - NIL = [nil] - BLANK = ["", " ", " \t \r \n"] - BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits - FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5) - INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) - FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS - INTEGERS = [0, 10, -10] + INTEGER_STRINGS - BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } - JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"] - INFINITY = [1.0/0.0] + def test_deprecated_validation_instance_methods + tom = DeprecatedPerson.new - repair_validations(Topic) - - def test_default_validates_numericality_of - Topic.validates_numericality_of :approved - - invalid!(NIL + BLANK + JUNK) - valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) - end - - def test_validates_numericality_of_with_nil_allowed - Topic.validates_numericality_of :approved, :allow_nil => true - - invalid!(JUNK) - valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY) - end - - def test_validates_numericality_of_with_integer_only - Topic.validates_numericality_of :approved, :only_integer => true - - invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY) - valid!(INTEGERS) - end - - def test_validates_numericality_of_with_integer_only_and_nil_allowed - Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true - - invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY) - valid!(NIL + BLANK + INTEGERS) - end - - def test_validates_numericality_with_greater_than - Topic.validates_numericality_of :approved, :greater_than => 10 - - invalid!([-10, 10], 'must be greater than 10') - valid!([11]) - end - - def test_validates_numericality_with_greater_than_or_equal - Topic.validates_numericality_of :approved, :greater_than_or_equal_to => 10 - - invalid!([-9, 9], 'must be greater than or equal to 10') - valid!([10]) - end - - def test_validates_numericality_with_equal_to - Topic.validates_numericality_of :approved, :equal_to => 10 - - invalid!([-10, 11] + INFINITY, 'must be equal to 10') - valid!([10]) - end - - def test_validates_numericality_with_less_than - Topic.validates_numericality_of :approved, :less_than => 10 - - invalid!([10], 'must be less than 10') - valid!([-9, 9]) - end - - def test_validates_numericality_with_less_than_or_equal_to - Topic.validates_numericality_of :approved, :less_than_or_equal_to => 10 - - invalid!([11], 'must be less than or equal to 10') - valid!([-10, 10]) - end - - def test_validates_numericality_with_odd - Topic.validates_numericality_of :approved, :odd => true - - invalid!([-2, 2], 'must be odd') - valid!([-1, 1]) - end - - def test_validates_numericality_with_even - Topic.validates_numericality_of :approved, :even => true - - invalid!([-1, 1], 'must be even') - valid!([-2, 2]) - end - - def test_validates_numericality_with_greater_than_less_than_and_even - Topic.validates_numericality_of :approved, :greater_than => 1, :less_than => 4, :even => true - - invalid!([1, 3, 4]) - valid!([2]) - end - - def test_validates_numericality_with_numeric_message - Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than {{count}}" - topic = Topic.new("title" => "numeric test", "approved" => 10) - - assert !topic.valid? - assert_equal "smaller than 4", topic.errors.on(:approved) - - Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than {{count}}" - topic = Topic.new("title" => "numeric test", "approved" => 1) - - assert !topic.valid? - assert_equal "greater than 4", topic.errors.on(:approved) - end - - private - def invalid!(values, error=nil) - with_each_topic_approved_value(values) do |topic, value| - assert !topic.valid?, "#{value.inspect} not rejected as a number" - assert topic.errors.on(:approved) - assert_equal error, topic.errors.on(:approved) if error - end + assert_deprecated do + assert tom.invalid? + assert_equal ["always invalid", "invalid on create"], tom.errors[:name] end - def valid!(values) - with_each_topic_approved_value(values) do |topic, value| - assert topic.valid?, "#{value.inspect} not accepted as a number" - end - end + tom.save(false) - def with_each_topic_approved_value(values) - topic = Topic.new("title" => "numeric test", "content" => "whatever") - values.each do |value| - topic.approved = value - yield topic, value - end + assert_deprecated do + assert tom.invalid? + assert_equal ["always invalid", "invalid on update"], tom.errors[:name] end + end end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index eb68153bbe..4c2514b0b3 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -145,10 +145,13 @@ class Account < ActiveRecord::Base true end + validate :check_empty_credit_limit + protected - def validate - errors.add_on_empty "credit_limit" - end + + def check_empty_credit_limit + errors.add_on_empty "credit_limit" + end private diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 3c34efbe16..8b84c2fb5e 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -54,10 +54,13 @@ module MyApplication i.belongs_to :nested_unqualified_billing_firm, :class_name => 'Nested::Firm' end + validate :check_empty_credit_limit + protected - def validate - errors.add_on_empty "credit_limit" - end + + def check_empty_credit_limit + errors.add_on_empty "credit_limit" + end end end end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 1c990acab6..616c07687c 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -11,26 +11,30 @@ class Reply < Topic attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read - def validate - errors.add("title", "Empty") unless attribute_present? "title" + validate :check_empty_title + validate_on_create :check_content_mismatch + validate_on_update :check_wrong_update + + def check_empty_title + errors[:title] << "Empty" unless attribute_present?("title") end def errors_on_empty_content - errors.add("content", "Empty") unless attribute_present? "content" + errors[:content] << "Empty" unless attribute_present?("content") end - def validate_on_create + def check_content_mismatch if attribute_present?("title") && attribute_present?("content") && content == "Mismatch" - errors.add("title", "is Content Mismatch") + errors[:title] << "is Content Mismatch" end end def title_is_wrong_create - errors.add("title", "is Wrong Create") if attribute_present?("title") && title == "Wrong Create" + errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create" end - def validate_on_update - errors.add("title", "is Wrong Update") if attribute_present?("title") && title == "Wrong Update" + def check_wrong_update + errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update" end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 6e8813d8ab..b2aaccb352 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -273,6 +273,7 @@ ActiveRecord::Schema.define do t.decimal :my_house_population, :precision => 2, :scale => 0 t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78 t.float :temperature + t.decimal :atoms_in_universe, :precision => 55, :scale => 0 end create_table :orders, :force => true do |t| diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb index 720abee72e..b46801affc 100644 --- a/activeresource/lib/active_resource.rb +++ b/activeresource/lib/active_resource.rb @@ -25,6 +25,18 @@ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" $:.unshift(activesupport_path) if File.directory?(activesupport_path) require 'active_support' +begin + require 'active_model' +rescue LoadError + $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" + require 'active_model' +end + +require 'active_resource/formats' +require 'active_resource/base' +require 'active_resource/validations' +require 'active_resource/custom_methods' + module ActiveResource autoload :Base, 'active_resource/base' end diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index dc24e713ff..11a7bbba3e 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -1,12 +1,14 @@ require 'active_support' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/module/attr_accessor_with_default' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/misc' require 'set' +require 'uri' module ActiveResource autoload :Formats, 'active_resource/formats' @@ -346,9 +348,9 @@ module ActiveResource # Do not include any modules in the default element name. This makes it easier to seclude ARes objects # in a separate namespace without having to set element_name repeatedly. - attr_accessor_with_default(:element_name) { to_s.split("::").last.underscore } #:nodoc: + attr_accessor_with_default(:element_name) { ActiveSupport::Inflector.underscore(to_s.split("::").last) } #:nodoc: - attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc: + attr_accessor_with_default(:collection_name) { ActiveSupport::Inflector.pluralize(element_name) } #:nodoc: attr_accessor_with_default(:primary_key, 'id') #:nodoc: # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>) @@ -384,7 +386,7 @@ module ActiveResource end_code silence_warnings { instance_eval code, __FILE__, __LINE__ } rescue - logger.error "Couldn't set prefix: #{$!}\n #{code}" + logger.error "Couldn't set prefix: #{$!}\n #{code}" if logger raise end diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index da6084fe9f..95c5467647 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -6,203 +6,8 @@ module ActiveResource end # Active Resource validation is reported to and from this object, which is used by Base#save - # to determine whether the object is in a valid state to be saved. See usage example in Validations. - class Errors - include Enumerable - attr_reader :errors - - delegate :empty?, :to => :errors - - def initialize(base) # :nodoc: - @base, @errors = base, {} - end - - # Adds an error to the base object instead of any particular attribute. This is used - # to report errors that don't tie to any specific attribute, but rather to the object - # as a whole. These error messages don't get prepended with any field name when iterating - # with +each_full+, so they should be complete sentences. - # - # ==== Examples - # my_folder = Folder.find(1) - # my_folder.errors.add_to_base("You can't edit an existing folder") - # my_folder.errors.on_base - # # => "You can't edit an existing folder" - # - # my_folder.errors.add_to_base("This folder has been tagged as frozen") - # my_folder.valid? - # # => false - # my_folder.errors.on_base - # # => ["You can't edit an existing folder", "This folder has been tagged as frozen"] - # - def add_to_base(msg) - add(:base, msg) - end - - # Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter) - # with the error message in +msg+. - # - # ==== Examples - # my_resource = Node.find(1) - # my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base' - # my_resource.errors.on('name') - # # => 'can not be "base"!' - # - # my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == '' - # my_resource.valid? - # # => false - # my_resource.errors.on('desc') - # # => 'can not be blank!' - # - def add(attribute, msg) - @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? - @errors[attribute.to_s] << msg - end - - # Returns true if the specified +attribute+ has errors associated with it. - # - # ==== Examples - # my_resource = Disk.find(1) - # my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main' - # my_resource.errors.on('location') - # # => 'must be Main!' - # - # my_resource.errors.invalid?('location') - # # => true - # my_resource.errors.invalid?('name') - # # => false - def invalid?(attribute) - !@errors[attribute.to_s].nil? - end - - # Returns +nil+ if no errors are associated with the specified +attribute+. - # Returns the error message if one error is associated with the specified +attribute+. - # Returns an array of error messages if more than one error is associated with the specified +attribute+. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # my_person.errors.on('login') - # # => nil - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.on('login') - # # => 'can not be empty' - # - # my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10 - # my_person.errors.on('login') - # # => ['can not be empty', 'can not be longer than 10 characters'] - def on(attribute) - errors = @errors[attribute.to_s] - return nil if errors.nil? - errors.size == 1 ? errors.first : errors - end - - alias :[] :on - - # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>. - # - # ==== Examples - # my_account = Account.find(1) - # my_account.errors.on_base - # # => nil - # - # my_account.errors.add_to_base("This account is frozen") - # my_account.errors.on_base - # # => "This account is frozen" - # - # my_account.errors.add_to_base("This account has been closed") - # my_account.errors.on_base - # # => ["This account is frozen", "This account has been closed"] - # - def on_base - on(:base) - end - - # Yields each attribute and associated message per error added. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # messages = '' - # my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"} - # messages - # # => "Login can not be empty<br />Password can not be empty<br />" - # - def each - @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } - end - - # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned - # through iteration as "First name can't be empty". - # - # ==== Examples - # my_person = Person.new(params[:person]) - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # messages = '' - # my_person.errors.each_full {|msg| messages += msg + "<br/>"} - # messages - # # => "Login can not be empty<br />Password can not be empty<br />" - # - def each_full - full_messages.each { |msg| yield msg } - end - - # Returns all the full error messages in an array. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # messages = '' - # my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"} - # messages - # # => "Login can not be empty<br />Password can not be empty<br />" - # - def full_messages - full_messages = [] - - @errors.each_key do |attr| - @errors[attr].each do |msg| - next if msg.nil? - - if attr == "base" - full_messages << msg - else - full_messages << [attr.humanize, msg].join(' ') - end - end - end - full_messages - end - - def clear - @errors = {} - end - - # Returns the total number of errors added. Two errors added to the same attribute will be counted as such - # with this as well. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # my_person.errors.size - # # => 0 - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # my_person.error.size - # # => 2 - # - def size - @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } - end - - alias_method :count, :size - alias_method :length, :size - + # to determine whether the object in a valid state to be saved. See usage example in Validations. + class Errors < ActiveModel::Errors # Grabs errors from the XML response. def from_xml(xml) clear @@ -215,7 +20,7 @@ module ActiveResource end end - add_to_base message if attr_message.nil? + self[:base] << message if attr_message.nil? end end end @@ -236,7 +41,7 @@ module ActiveResource # person.errors.empty? # => false # person.errors.count # => 1 # person.errors.full_messages # => ["Last name can't be empty"] - # person.errors.on(:last_name) # => "can't be empty" + # person.errors[:last_name] # => ["can't be empty"] # person.last_name = "Halpert" # person.save # => true (and person is now saved to the remote service) # diff --git a/activeresource/test/base_errors_test.rb b/activeresource/test/base_errors_test.rb index 7ae92c7d98..28813821df 100644 --- a/activeresource/test/base_errors_test.rb +++ b/activeresource/test/base_errors_test.rb @@ -20,21 +20,21 @@ class BaseErrorsTest < Test::Unit::TestCase end def test_should_parse_errors_to_individual_attributes - assert @person.errors.invalid?(:name) - assert_equal "can't be blank", @person.errors.on(:age) + assert @person.errors[:name].any? + assert_equal ["can't be blank"], @person.errors[:age] assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name] - assert_equal "Person quota full for today.", @person.errors.on_base + assert_equal ["Person quota full for today."], @person.errors[:base] end def test_should_iterate_over_errors errors = [] - @person.errors.each { |attribute, message| errors << [attribute, message] } + @person.errors.each { |attribute, message| errors << [attribute.to_s, message] } assert errors.include?(["name", "can't be blank"]) end def test_should_iterate_over_full_errors errors = [] - @person.errors.each_full { |message| errors << message } + @person.errors.to_a.each { |message| errors << message } assert errors.include?("Name can't be blank") end diff --git a/activesupport/lib/active_support/autoload.rb b/activesupport/lib/active_support/autoload.rb index ed229d1c5f..75706855d6 100644 --- a/activesupport/lib/active_support/autoload.rb +++ b/activesupport/lib/active_support/autoload.rb @@ -5,7 +5,7 @@ module ActiveSupport autoload :BufferedLogger, 'active_support/buffered_logger' autoload :Cache, 'active_support/cache' autoload :Callbacks, 'active_support/callbacks' - autoload :NewCallbacks, 'active_support/new_callbacks' + autoload :Concern, 'active_support/concern' autoload :ConcurrentHash, 'active_support/concurrent_hash' autoload :DependencyModule, 'active_support/dependency_module' autoload :Deprecation, 'active_support/deprecation' @@ -15,6 +15,7 @@ module ActiveSupport autoload :MessageEncryptor, 'active_support/message_encryptor' autoload :MessageVerifier, 'active_support/message_verifier' autoload :Multibyte, 'active_support/multibyte' + autoload :NewCallbacks, 'active_support/new_callbacks' autoload :OptionMerger, 'active_support/option_merger' autoload :OrderedHash, 'active_support/ordered_hash' autoload :OrderedOptions, 'active_support/ordered_options' diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 4bbcd8e4c4..3b5fccc737 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -45,7 +45,7 @@ module ActiveSupport elsif value.nil? value = super local_cache.write(key, value || NULL) if local_cache - value + value.duplicable? ? value.dup : value else # forcing the value to be immutable value.duplicable? ? value.dup : value diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb new file mode 100644 index 0000000000..dcf1e8152f --- /dev/null +++ b/activesupport/lib/active_support/concern.rb @@ -0,0 +1,25 @@ +require 'active_support/dependency_module' + +module ActiveSupport + module Concern + include DependencyModule + + def append_features(base) + if super + base.extend const_get("ClassMethods") if const_defined?("ClassMethods") + base.send :include, const_get("InstanceMethods") if const_defined?("InstanceMethods") + base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") + end + end + + def included(base = nil, &block) + if base.nil? + @_included_block = block + else + super + end + end + + alias_method :include, :depends_on + end +end diff --git a/activesupport/lib/active_support/dependency_module.rb b/activesupport/lib/active_support/dependency_module.rb index 9872b9654b..6847c0f86a 100644 --- a/activesupport/lib/active_support/dependency_module.rb +++ b/activesupport/lib/active_support/dependency_module.rb @@ -1,19 +1,9 @@ module ActiveSupport module DependencyModule def append_features(base) - return if base < self + return false if base < self (@_dependencies ||= []).each { |dep| base.send(:include, dep) } super - base.extend const_get("ClassMethods") if const_defined?("ClassMethods") - base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") - end - - def included(base = nil, &block) - if base.nil? - @_included_block = block - else - super - end end def depends_on(*mods) diff --git a/activesupport/lib/active_support/new_callbacks.rb b/activesupport/lib/active_support/new_callbacks.rb index f8108780f1..8512c659b0 100644 --- a/activesupport/lib/active_support/new_callbacks.rb +++ b/activesupport/lib/active_support/new_callbacks.rb @@ -287,6 +287,14 @@ module ActiveSupport when Proc @klass.send(:define_method, method_name, &filter) method_name << (filter.arity == 1 ? "(self)" : "") + when Method + @klass.send(:define_method, "#{method_name}_method") { filter } + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{method_name}(&blk) + #{method_name}_method.call(self, &blk) + end + RUBY_EVAL + method_name when String @klass.class_eval <<-RUBY_EVAL def #{method_name} @@ -298,11 +306,34 @@ module ActiveSupport kind, name = @kind, @name @klass.send(:define_method, "#{method_name}_object") { filter } - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{method_name}(&blk) - #{method_name}_object.send("#{kind}_#{name}", self, &blk) - end - RUBY_EVAL + if kind == :around + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{method_name}(&blk) + if :#{kind} == :around && #{method_name}_object.respond_to?(:filter) + #{method_name}_object.send("filter", self, &blk) + # TODO: Deprecate this + elsif #{method_name}_object.respond_to?(:before) && #{method_name}_object.respond_to?(:after) + should_continue = #{method_name}_object.before(self) + yield if should_continue + #{method_name}_object.after(self) + else + #{method_name}_object.send("#{kind}_#{name}", self, &blk) + end + end + RUBY_EVAL + else + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{method_name}(&blk) + if #{method_name}_object.respond_to?(:#{kind}) + #{method_name}_object.#{kind}(self, &blk) + elsif #{method_name}_object.respond_to?(:filter) + #{method_name}_object.send("filter", self, &blk) + else + #{method_name}_object.send("#{kind}_#{name}", self, &blk) + end + end + RUBY_EVAL + end method_name end end @@ -444,7 +475,9 @@ module ActiveSupport self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)} Callback.new(filter, type, options.dup, self, :#{symbol}) end - self._#{symbol}_callbacks.push(*filters) + options[:prepend] ? + self._#{symbol}_callbacks.unshift(*filters) : + self._#{symbol}_callbacks.push(*filters) _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator), options) diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index bd237a5c8e..51d04d9388 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -180,6 +180,15 @@ uses_memcached 'memcached backed store' do end end + def test_stored_objects_should_not_be_frozen + @cache.with_local_cache do + @cache.write('foo', 'bar') + end + @cache.with_local_cache do + assert !@cache.read('foo').frozen? + end + end + def test_write_should_return_true_on_success @cache.with_local_cache do result = @cache.write('foo', 'bar') diff --git a/activesupport/test/dependency_module_test.rb b/activesupport/test/concern_test.rb index be7db0fa7b..4cbe56a2d2 100644 --- a/activesupport/test/dependency_module_test.rb +++ b/activesupport/test/concern_test.rb @@ -1,9 +1,9 @@ require 'abstract_unit' -require 'active_support/dependency_module' +require 'active_support/concern' -class DependencyModuleTest < Test::Unit::TestCase +class ConcernTest < Test::Unit::TestCase module Baz - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern module ClassMethods def baz @@ -19,6 +19,9 @@ class DependencyModuleTest < Test::Unit::TestCase end end + module InstanceMethods + end + included do self.included_ran = true end @@ -29,9 +32,9 @@ class DependencyModuleTest < Test::Unit::TestCase end module Bar - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on Baz + include Baz def bar "bar" @@ -43,9 +46,9 @@ class DependencyModuleTest < Test::Unit::TestCase end module Foo - extend ActiveSupport::DependencyModule + extend ActiveSupport::Concern - depends_on Bar, Baz + include Bar, Baz end def setup @@ -55,17 +58,23 @@ class DependencyModuleTest < Test::Unit::TestCase def test_module_is_included_normally @klass.send(:include, Baz) assert_equal "baz", @klass.new.baz - assert_equal DependencyModuleTest::Baz, @klass.included_modules[0] + assert @klass.included_modules.include?(ConcernTest::Baz) @klass.send(:include, Baz) assert_equal "baz", @klass.new.baz - assert_equal DependencyModuleTest::Baz, @klass.included_modules[0] + assert @klass.included_modules.include?(ConcernTest::Baz) end def test_class_methods_are_extended @klass.send(:include, Baz) assert_equal "baz", @klass.baz - assert_equal DependencyModuleTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] + assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] + end + + def test_instance_methods_are_included + @klass.send(:include, Baz) + assert_equal "baz", @klass.new.baz + assert @klass.included_modules.include?(ConcernTest::Baz::InstanceMethods) end def test_included_block_is_ran @@ -78,11 +87,11 @@ class DependencyModuleTest < Test::Unit::TestCase assert_equal "bar", @klass.new.bar assert_equal "bar+baz", @klass.new.baz assert_equal "baz", @klass.baz - assert_equal [DependencyModuleTest::Bar, DependencyModuleTest::Baz], @klass.included_modules[0..1] + assert @klass.included_modules.include?(ConcernTest::Bar) end - def test_depends_on_with_multiple_modules + def test_dependencies_with_multiple_modules @klass.send(:include, Foo) - assert_equal [DependencyModuleTest::Foo, DependencyModuleTest::Bar, DependencyModuleTest::Baz], @klass.included_modules[0..2] + assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz::InstanceMethods, ConcernTest::Baz], @klass.included_modules[0..3] end end diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 8e7dfb38cc..782afd5aa4 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Ruby 1.9: use UTF-8 for default internal and external encodings. [Jeremy Kemper] + * Added db/seeds.rb as a default file for storing seed data for the database. Can be loaded with rake db:seed (or created alongside the db with db:setup). (This is also known as the "Stop Putting Gawd Damn Seed Data In Your Migrations" feature) [DHH] diff --git a/railties/lib/commands/server.rb b/railties/lib/commands/server.rb index 91ac7752ef..01dd33fa8c 100644 --- a/railties/lib/commands/server.rb +++ b/railties/lib/commands/server.rb @@ -22,17 +22,17 @@ options = { ARGV.clone.options do |opts| opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } + "Runs Rails on the specified port.", "Default: #{options[:Port]}") { |v| options[:Port] = v } opts.on("-b", "--binding=ip", String, - "Binds Rails to the specified ip.", "Default: 0.0.0.0") { |v| options[:Host] = v } + "Binds Rails to the specified ip.", "Default: #{options[:Host]}") { |v| options[:Host] = v } opts.on("-c", "--config=file", String, "Use custom rackup configuration file") { |v| options[:config] = v } opts.on("-d", "--daemon", "Make server run as a Daemon.") { options[:detach] = true } opts.on("-u", "--debugger", "Enable ruby-debugging for the server.") { options[:debugger] = true } opts.on("-e", "--environment=name", String, "Specifies the environment to run this server under (test/development/production).", - "Default: development") { |v| options[:environment] = v } - opts.on("-P", "--path=/path", String, "Runs Rails app mounted at a specific path.", "Default: /") { |v| options[:path] = v } + "Default: #{options[:environment]}") { |v| options[:environment] = v } + opts.on("-P", "--path=/path", String, "Runs Rails app mounted at a specific path.", "Default: #{options[:path]}") { |v| options[:path] = v } opts.separator "" diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 3c0d5940ea..7273cea0c5 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -426,10 +426,14 @@ Run `rake gems:install` to install the missing gems. # should override this behaviour and set the relevant +default_charset+ # on ActionController::Base. # - # For Ruby 1.9, this does nothing. Specify the default encoding in the Ruby - # shebang line if you don't want UTF-8. + # For Ruby 1.9, UTF-8 is the default internal and external encoding. def initialize_encoding - $KCODE='u' if RUBY_VERSION < '1.9' + if RUBY_VERSION < '1.9' + $KCODE='u' + else + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 + end end # This initialization routine does nothing unless <tt>:active_record</tt> @@ -445,7 +449,8 @@ Run `rake gems:install` to install the missing gems. def initialize_database_middleware if configuration.frameworks.include?(:active_record) - if ActionController::Base.session_store == ActiveRecord::SessionStore + if configuration.frameworks.include?(:action_controller) && + ActionController::Base.session_store.name == 'ActiveRecord::SessionStore' configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::QueryCache else @@ -886,7 +891,7 @@ Run `rake gems:install` to install the missing gems. # Enable threaded mode. Allows concurrent requests to controller actions and # multiple database connections. Also disables automatic dependency loading - # after boot, and disables reloading code on every request, as these are + # after boot, and disables reloading code on every request, as these are # fundamentally incompatible with thread safety. def threadsafe! self.preload_frameworks = true @@ -1129,3 +1134,4 @@ class Rails::OrderedOptions < Array #:nodoc: return false end end + diff --git a/railties/lib/rails_generator/generators/components/model_subclass/USAGE b/railties/lib/rails_generator/generators/components/model_subclass/USAGE new file mode 100644 index 0000000000..a4b558a401 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model_subclass/USAGE @@ -0,0 +1,13 @@ +Description: + Create a model subclass of parent, used for Single Table Inheritance. + + Both subclass and parent name can be either CamelCased or under_scored. + + This generates a model class in app/models and a unit test in test/unit. + +Examples: + `./script/generate model_subclass admin user` + + creates an Admin model, which will inheritate from User model, test: + Model: app/models/admin.rb + Test: test/unit/admin_test.rb diff --git a/railties/lib/rails_generator/generators/components/model_subclass/model_subclass_generator.rb b/railties/lib/rails_generator/generators/components/model_subclass/model_subclass_generator.rb new file mode 100644 index 0000000000..e8ac3da2cd --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model_subclass/model_subclass_generator.rb @@ -0,0 +1,32 @@ +class ModelSubclassGenerator < Rails::Generator::NamedBase + default_options :skip_unit_test => false + + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_name, "#{class_name}Test" + + # Model and test directories. + m.directory File.join('app/models', class_path) + m.directory File.join('test/unit', class_path) + + # Model class and unit test + m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb"), :assigns => assigns + m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb"), :assigns => assigns + + end + end + + protected + def banner + "Usage: #{$0} #{spec.name} Subclass Parent" + end + + def assigns + {:parent_class_name => parent_class_name} + end + + def parent_class_name + @args.first.try(:camelize) || usage + end +end diff --git a/railties/lib/rails_generator/generators/components/model_subclass/templates/model.rb b/railties/lib/rails_generator/generators/components/model_subclass/templates/model.rb new file mode 100644 index 0000000000..d0037b322b --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model_subclass/templates/model.rb @@ -0,0 +1,3 @@ +class <%= class_name %> < <%= parent_class_name %> + +end
\ No newline at end of file diff --git a/railties/lib/rails_generator/generators/components/model_subclass/templates/unit_test.rb b/railties/lib/rails_generator/generators/components/model_subclass/templates/unit_test.rb new file mode 100644 index 0000000000..3e0bc29d3a --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model_subclass/templates/unit_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class <%= class_name %>Test < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/railties/lib/tasks/databases.rake b/railties/lib/tasks/databases.rake index cdab5d8bb0..0e256737f9 100644 --- a/railties/lib/tasks/databases.rake +++ b/railties/lib/tasks/databases.rake @@ -56,12 +56,28 @@ namespace :db do when 'mysql' @charset = ENV['CHARSET'] || 'utf8' @collation = ENV['COLLATION'] || 'utf8_general_ci' + creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} begin ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], :charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)) + ActiveRecord::Base.connection.create_database(config['database'], creation_options) ActiveRecord::Base.establish_connection(config) - rescue - $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation} (if you set the charset manually, make sure you have a matching collation)" + rescue Mysql::Error => sqlerr + if sqlerr.errno == Mysql::Error::ER_ACCESS_DENIED_ERROR + print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>" + root_password = $stdin.gets.strip + grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \ + "TO '#{config['username']}'@'localhost' " \ + "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" + ActiveRecord::Base.establish_connection(config.merge( + 'database' => nil, 'username' => 'root', 'password' => root_password)) + ActiveRecord::Base.connection.create_database(config['database'], creation_options) + ActiveRecord::Base.connection.execute grant_statement + ActiveRecord::Base.establish_connection(config) + else + $stderr.puts sqlerr.error + $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}" + $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] + end end when 'postgresql' @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8' @@ -282,7 +298,9 @@ namespace :db do ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"] ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"] search_path = abcs[RAILS_ENV]["schema_search_path"] - search_path = "--schema=#{search_path}" if search_path + unless search_path.blank? + search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ") + end `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}` raise "Error dumping database" if $?.exitstatus == 1 when "sqlite", "sqlite3" diff --git a/railties/test/generators/rails_model_subclass_generator_test.rb b/railties/test/generators/rails_model_subclass_generator_test.rb new file mode 100644 index 0000000000..30066b5a3c --- /dev/null +++ b/railties/test/generators/rails_model_subclass_generator_test.rb @@ -0,0 +1,15 @@ +require 'generators/generator_test_helper' + +class RailsModelSubclassGeneratorTest < GeneratorTestCase + + def test_model_subclass_generates_resources + run_generator('model_subclass', %w(Car Product)) + + assert_generated_model_for :car, "Product" + assert_generated_unit_test_for :car + end + + def test_model_subclass_must_have_a_parent_class_name + assert_raise(Rails::Generator::UsageError) { run_generator('model_subclass', %w(Car)) } + end +end
\ No newline at end of file diff --git a/railties/test/initializer_test.rb b/railties/test/initializer_test.rb index d77a045e56..bdda319916 100644 --- a/railties/test/initializer_test.rb +++ b/railties/test/initializer_test.rb @@ -309,7 +309,7 @@ class InitializerSetupI18nTests < Test::Unit::TestCase config.i18n.load_path << "my/other/locale.yml" Rails::Initializer.run(:initialize_i18n, config) - assert_equal [ + assert_equal [ File.expand_path(File.dirname(__FILE__) + "/../../activesupport/lib/active_support/locale/en.yml"), File.expand_path(File.dirname(__FILE__) + "/../../actionpack/lib/action_view/locale/en.yml"), File.expand_path(File.dirname(__FILE__) + "/../../activerecord/lib/active_record/locale/en.yml"), @@ -363,17 +363,31 @@ class InitializerDatabaseMiddlewareTest < Test::Unit::TestCase ensure ActionController::Base.session_store = store end + + def test_ensure_database_middleware_doesnt_use_action_controller_on_initializing + @config.frameworks -= [:action_controller] + store = ActionController::Base.session_store + ActionController::Base.session_store = ActiveRecord::SessionStore + + @config.middleware.expects(:use).with(ActiveRecord::ConnectionAdapters::ConnectionManagement) + @config.middleware.expects(:use).with(ActiveRecord::QueryCache) + + Rails::Initializer.run(:initialize_database_middleware, @config) + ensure + ActionController::Base.session_store = store + @config.frameworks += [:action_controller] + end end class InitializerViewPathsTest < Test::Unit::TestCase def setup @config = Rails::Configuration.new @config.frameworks = [:action_view, :action_controller, :action_mailer] - + ActionController::Base.stubs(:view_paths).returns(stub) ActionMailer::Base.stubs(:view_paths).returns(stub) end - + def test_load_view_paths_doesnt_perform_anything_when_action_view_not_in_frameworks @config.frameworks -= [:action_view] ActionController::Base.view_paths.expects(:load!).never @@ -390,4 +404,5 @@ class RailsRootTest < Test::Unit::TestCase def test_rails_dot_root_should_be_a_pathname assert_equal File.join(RAILS_ROOT, 'app', 'controllers'), Rails.root.join('app', 'controllers').to_s end -end
\ No newline at end of file +end + diff --git a/tools/profile_requires b/tools/profile index 927467bc4e..927467bc4e 100755 --- a/tools/profile_requires +++ b/tools/profile |