diff options
195 files changed, 2652 insertions, 1995 deletions
diff --git a/.gitmodules b/.gitmodules index d0be7ff194..fd4fd34d3e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "arel"] path = arel url = git://github.com/rails/arel.git -[submodule "rack"] - path = rack - url = git://github.com/rails/rack.git @@ -8,7 +8,7 @@ gem "rails", "3.0.pre", :path => "railties" end # AS -gem "i18n", ">= 0.3.0" +gem "i18n", "0.3.3" # AR gem "arel", "0.2.pre", :git => "git://github.com/rails/arel.git" @@ -20,7 +20,6 @@ only :test do end # AP -gem "rack", "1.1.0", :git => "git://github.com/rack/rack.git" gem "rack-test", "0.5.3" gem "RedCloth", ">= 4.2.2" diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 8adea46d35..0c1106b605 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| s.homepage = "http://www.rubyonrails.org" s.add_dependency('actionpack', '= 3.0.pre') - s.add_dependency('mail', '~> 1.5.0') + s.add_dependency('mail', '~> 1.5.2') s.files = Dir['CHANGELOG', 'README', 'MIT-LICENSE', 'lib/**/*'] s.has_rdoc = true diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 0248e29cb7..e644c47e9c 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -259,10 +259,9 @@ module ActionMailer #:nodoc: include AbstractController::LocalizedCache include AbstractController::Layouts include AbstractController::Helpers + include AbstractController::UrlFor helper ActionMailer::MailHelper - - include ActionController::UrlWriter include ActionMailer::DeprecatedBody private_class_method :new #:nodoc: @@ -500,7 +499,7 @@ module ActionMailer #:nodoc: logger.debug "\n#{mail.encoded}" end - ActiveSupport::Notifications.instrument(:deliver_mail, :mail => mail) do + ActiveSupport::Notifications.instrument("action_mailer.deliver", :mail => mail) do begin self.delivery_method.perform_delivery(mail) if perform_deliveries rescue Exception => e # Net::SMTP errors or sendmail pipe errors diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index e8632d4559..318a1e46d1 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -1,6 +1,3 @@ -require 'active_support/test_case' -require 'action_mailer/base' - module ActionMailer class NonInferrableMailerError < ::StandardError def initialize(name) diff --git a/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb b/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb index 3a0e04bd77..81cc7906d8 100755 --- a/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb +++ b/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb @@ -36,7 +36,7 @@ # 2003.03.28 Austin Ziegler # Added the ability for a hyphenator to receive the formatter # object. Fixed a bug for strings matching /\A\s*\Z/ failing -# entirely. Fixed a test case failing under 1.6.8. +# entirely. Fixed a test case failing under 1.6.8. # 2003.04.04 Austin Ziegler # Handle the case of hyphenators returning nil for first/rest. # 2003.09.17 Austin Ziegler @@ -78,7 +78,7 @@ module Text #:nodoc: # 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. + # SOFTWARE. class Format VERSION = '0.63' diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index af6f1bc92e..50b8a53006 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -10,7 +10,6 @@ require 'rubygems' require 'test/unit' require 'action_mailer' -require 'action_mailer/test_case' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 782b4229fb..7673b9fe56 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Added that ActionController::Base now does helper :all instead of relying on the default ApplicationController in Rails to do it [DHH] + * Added ActionDispatch::Request#authorization to access the http authentication header regardless of its proxy hiding [DHH] * Added :alert, :notice, and :flash as options to ActionController::Base#redirect_to that'll automatically set the proper flash before the redirection [DHH]. Examples: diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 237ab577ba..efc35a7e56 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -15,4 +15,5 @@ module AbstractController autoload :LocalizedCache autoload :Logger autoload :Rendering + autoload :UrlFor end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index a6889d5d01..48725ad82a 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -86,6 +86,11 @@ module AbstractController abstract! + # Initialize controller with nil formats. + def initialize #:nodoc: + @_formats = nil + end + # Calls the action going through the entire action dispatch stack. # # The actual method that is called is determined by calling diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 332d86b089..d57136dbb8 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -17,12 +17,6 @@ module AbstractController self._view_paths ||= ActionView::PathSet.new end - # Initialize controller with nil formats. - def initialize(*) #:nodoc: - @_formats = nil - super - end - # An instance of a view class. The default view class is ActionView::Base # # The view class must have the following methods: diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb new file mode 100644 index 0000000000..6b7d2b1f34 --- /dev/null +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -0,0 +1,156 @@ +module AbstractController + # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse + # is also possible: an URL can be generated from one of your routing definitions. + # URL generation functionality is centralized in this module. + # + # See AbstractController::Routing and AbstractController::Resources for general + # information about routing and routes.rb. + # + # <b>Tip:</b> If you need to generate URLs from your models or some other place, + # then AbstractController::UrlFor is what you're looking for. Read on for + # an introduction. + # + # == URL generation from parameters + # + # As you may know, some functions - such as AbstractController::Base#url_for + # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set + # of parameters. For example, you've probably had the chance to write code + # like this in one of your views: + # + # <%= link_to('Click here', :controller => 'users', + # :action => 'new', :message => 'Welcome!') %> + # + # #=> Generates a link to: /users/new?message=Welcome%21 + # + # link_to, and all other functions that require URL generation functionality, + # actually use AbstractController::UrlFor under the hood. And in particular, + # they use the AbstractController::UrlFor#url_for method. One can generate + # the same path as the above example by using the following code: + # + # include UrlFor + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :only_path => true) + # # => "/users/new?message=Welcome%21" + # + # Notice the <tt>:only_path => true</tt> part. This is because UrlFor has no + # information about the website hostname that your Rails app is serving. So if you + # want to include the hostname as well, then you must also pass the <tt>:host</tt> + # argument: + # + # include UrlFor + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :host => 'www.example.com') # Changed this. + # # => "http://www.example.com/users/new?message=Welcome%21" + # + # By default, all controllers and views have access to a special version of url_for, + # that already knows what the current hostname is. So if you use url_for in your + # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt> + # argument. + # + # For convenience reasons, mailers provide a shortcut for AbstractController::UrlFor#url_for. + # So within mailers, you only have to type 'url_for' instead of 'AbstractController::UrlFor#url_for' + # in full. However, mailers don't have hostname information, and what's why you'll still + # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # + # + # == URL generation for named routes + # + # UrlFor also allows one to access methods that have been auto-generated from + # named routes. For example, suppose that you have a 'users' resource in your + # <b>routes.rb</b>: + # + # map.resources :users + # + # This generates, among other things, the method <tt>users_path</tt>. By default, + # this method is accessible from your controllers, views and mailers. If you need + # to access this auto-generated method from other places (such as a model), then + # you can do that by including AbstractController::UrlFor in your class: + # + # class User < ActiveRecord::Base + # include AbstractController::UrlFor + # + # def base_uri + # user_path(self) + # end + # end + # + # User.find(1).base_uri # => "/users/1" + # + module UrlFor + extend ActiveSupport::Concern + + included do + ActionController::Routing::Routes.install_helpers(self) + extlib_inheritable_accessor :default_url_options, + :instance_writer => false, :instance_reader => false + self.default_url_options ||= {} + end + + # Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in + # the form of a hash, just like the one you would use for url_for directly. Example: + # + # def default_url_options(options) + # { :project => @project.active? ? @project.url_name : "unknown" } + # end + # + # As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the + # urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set + # by this method. + def default_url_options(options = nil) + self.class.default_url_options + end + + def rewrite_options(options) #:nodoc: + if options.delete(:use_defaults) != false && (defaults = default_url_options(options)) + defaults.merge(options) + else + options + end + end + + # Generate a url based on the options provided, default_url_options and the + # routes defined in routes.rb. The following options are supported: + # + # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. + # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. + # * <tt>:host</tt> - Specifies the host the link should be targeted at. + # If <tt>:only_path</tt> is false, this option must be + # provided either explicitly, or via +default_url_options+. + # * <tt>:port</tt> - Optionally specify the port to connect to. + # * <tt>:anchor</tt> - An anchor name to be appended to the path. + # * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the + # +relative_url_root+ set in AbstractController::Base.relative_url_root. + # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" + # + # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to + # +url_for+ is forwarded to the Routes module. + # + # Examples: + # + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + def url_for(options = {}) + options ||= {} + case options + when String + options + when Hash + _url_rewriter.rewrite(rewrite_options(options)) + else + polymorphic_url(options) + end + end + + protected + + def _url_rewriter + ActionController::UrlRewriter + end + end +end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index d66fc3fcc9..8271f6f3e7 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -27,27 +27,28 @@ module ActionController autoload :MimeResponds autoload :RackDelegation autoload :Redirecting - autoload :Rendering autoload :Renderers + autoload :Rendering autoload :RequestForgeryProtection autoload :Rescue autoload :Responder autoload :SessionManagement autoload :Streaming + autoload :Testing autoload :UrlFor autoload :Verification end autoload :Dispatcher, 'action_controller/dispatch/dispatcher' - autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :Routing, 'action_controller/deprecated' autoload :Integration, 'action_controller/deprecated/integration_test' autoload :IntegrationTest, 'action_controller/deprecated/integration_test' + autoload :PerformanceTest, 'action_controller/deprecated/performance_test' + autoload :Routing, 'action_controller/deprecated' + autoload :TestCase, 'action_controller/test_case' eager_autoload do autoload :RecordIdentifier autoload :UrlRewriter - autoload :UrlWriter, 'action_controller/url_rewriter' # TODO: Don't autoload exceptions, setup explicit # requires for files that need them diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index b23be66910..746ab3e6db 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -6,6 +6,8 @@ module ActionController include AbstractController::Layouts include ActionController::Helpers + helper :all # By default, all helpers should be included + include ActionController::HideActions include ActionController::UrlFor include ActionController::Redirecting diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 69ed84da95..5c0d754ad6 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -62,9 +62,9 @@ module ActionController #:nodoc: end def log_event(name, before, after, instrumenter_id, payload) - if name.to_s =~ /(read|write|cache|expire|exist)_(fragment|page)\??/ + if name.to_s =~ /action_controller\.((read|write|expire|exist)_(fragment|page)\??)/ key_or_path = payload[:key] || payload[:path] - human_name = name.to_s.humanize + human_name = $1.humanize duration = (after - before) * 1000 logger.info("#{human_name} #{key_or_path.inspect} (%.1fms)" % duration) else diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index f569d0dd8b..a0c5ed797e 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -53,7 +53,7 @@ module ActionController #:nodoc: return content unless cache_configured? key = fragment_cache_key(key) - ActiveSupport::Notifications.instrument(:write_fragment, :key => key) do + instrument_fragment_cache :write_fragment, key do cache_store.write(key, content, options) end content @@ -64,7 +64,7 @@ module ActionController #:nodoc: return unless cache_configured? key = fragment_cache_key(key) - ActiveSupport::Notifications.instrument(:read_fragment, :key => key) do + instrument_fragment_cache :read_fragment, key do cache_store.read(key, options) end end @@ -74,7 +74,7 @@ module ActionController #:nodoc: return unless cache_configured? key = fragment_cache_key(key) - ActiveSupport::Notifications.instrument(:exist_fragment?, :key => key) do + instrument_fragment_cache :exist_fragment?, key do cache_store.exist?(key, options) end end @@ -101,16 +101,18 @@ module ActionController #:nodoc: key = fragment_cache_key(key) unless key.is_a?(Regexp) message = nil - ActiveSupport::Notifications.instrument(:expire_fragment, :key => key) do + instrument_fragment_cache :expire_fragment, key do if key.is_a?(Regexp) - message = "Expired fragments matching: #{key.source}" cache_store.delete_matched(key, options) else - message = "Expired fragment: #{key}" cache_store.delete(key, options) end end end + + def instrument_fragment_cache(name, key) + ActiveSupport::Notifications.instrument("action_controller.#{name}", :key => key){ yield } + end end end end diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index d46f528c7e..5797eeebd6 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -64,7 +64,7 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - ActiveSupport::Notifications.instrument(:expire_page, :path => path) do + instrument_page_cache :expire_page, path do File.delete(path) if File.exist?(path) end end @@ -75,7 +75,7 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - ActiveSupport::Notifications.instrument(:cache_page, :path => path) do + instrument_page_cache :write_page, path do FileUtils.makedirs(File.dirname(path)) File.open(path, "wb+") { |f| f.write(content) } end @@ -107,6 +107,10 @@ module ActionController #:nodoc: def page_cache_path(path) page_cache_directory + page_cache_file(path) end + + def instrument_page_cache(name, path) + ActiveSupport::Notifications.instrument("action_controller.#{name}", :path => path){ yield } + end end # Expires the page that was cached with the +options+ as a key. Example: diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index c82d9cf369..37be8b3999 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,6 +1,7 @@ module ActionController module Head - include UrlFor + extend ActiveSupport::Concern + include ActionController::UrlFor # Return a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. diff --git a/actionpack/lib/action_controller/metal/logger.rb b/actionpack/lib/action_controller/metal/logger.rb index 4f4370e5f0..bf5f3b774f 100644 --- a/actionpack/lib/action_controller/metal/logger.rb +++ b/actionpack/lib/action_controller/metal/logger.rb @@ -15,7 +15,8 @@ module ActionController attr_internal :view_runtime def process_action(action) - ActiveSupport::Notifications.instrument(:process_action, :controller => self, :action => action) do + ActiveSupport::Notifications.instrument("action_controller.process_action", + :controller => self, :action => action) do super end end @@ -50,7 +51,7 @@ module ActionController # This is the hook invoked by ActiveSupport::Notifications.subscribe. # If you need to log any event, overwrite the method and do it here. def log_event(name, before, after, instrumenter_id, payload) #:nodoc: - if name == :process_action + if name == "action_controller.process_action" duration = [(after - before) * 1000, 0.01].max controller = payload[:controller] request = controller.request @@ -66,7 +67,7 @@ module ActionController message << " [#{request.request_uri rescue "unknown"}]" logger.info(message) - elsif name == :render_template + elsif name == "action_view.render_template" # TODO Make render_template logging work if you are using just ActionView duration = (after - before) * 1000 message = "Rendered #{payload[:identifier]}" diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 7a2f9a6fc5..7b277c0ae0 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -9,7 +9,9 @@ module ActionController module Redirecting extend ActiveSupport::Concern + include AbstractController::Logger + include ActionController::UrlFor # Redirects the browser to the target specified in +options+. This parameter can take one of three forms: # diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 8c3810ebcb..73feacb872 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -2,40 +2,14 @@ module ActionController module UrlFor extend ActiveSupport::Concern - include RackDelegation + include AbstractController::UrlFor + include ActionController::RackDelegation - # Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in - # the form of a hash, just like the one you would use for url_for directly. Example: - # - # def default_url_options(options) - # { :project => @project.active? ? @project.url_name : "unknown" } - # end - # - # As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the - # urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set - # by this method. - def default_url_options(options = nil) - end - - def rewrite_options(options) #:nodoc: - if defaults = default_url_options(options) - defaults.merge(options) - else - options - end - end + protected - def url_for(options = {}) - options ||= {} - case options - when String - options - when Hash - @url ||= UrlRewriter.new(request, params) - @url.rewrite(rewrite_options(options)) - else - polymorphic_url(options) - end + def _url_rewriter + return ActionController::UrlRewriter unless request + @_url_rewriter ||= ActionController::UrlRewriter.new(request, params) end end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index f861d12905..6b94c32b2a 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -23,26 +23,6 @@ module ActionController app.reload_routes! end - # Include middleware to serve up static assets - initializer "action_controller.initialize_static_server" do |app| - if app.config.serve_static_assets - app.config.middleware.use(ActionDispatch::Static, Rails.public_path) - end - end - - initializer "action_controller.initialize_middleware_stack" do |app| - middleware = app.config.middleware - middleware.use(::Rack::Lock, :if => lambda { ActionController::Base.allow_concurrency }) - middleware.use(::Rack::Runtime) - middleware.use(ActionDispatch::ShowExceptions, lambda { ActionController::Base.consider_all_requests_local }) - middleware.use(ActionDispatch::Callbacks, lambda { ActionController::Dispatcher.prepare_each_request }) - middleware.use(lambda { ActionController::Base.session_store }, lambda { ActionController::Base.session_options }) - middleware.use(ActionDispatch::ParamsParser) - middleware.use(::Rack::MethodOverride) - middleware.use(::Rack::Head) - middleware.use(ActionDispatch::StringCoercion) - end - initializer "action_controller.initialize_framework_caches" do ActionController::Base.cache_store ||= RAILS_CACHE end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 398ea52495..7e6da0e61d 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,6 +1,5 @@ -require 'active_support/test_case' require 'rack/session/abstract/id' -require 'action_controller/metal/testing' +require 'action_view/test_case' module ActionController class TestRequest < ActionDispatch::TestRequest #:nodoc: diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index 52b66c9303..933a1fa8f9 100644 --- a/actionpack/lib/action_controller/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -1,153 +1,21 @@ -module ActionController - # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse - # is also possible: an URL can be generated from one of your routing definitions. - # URL generation functionality is centralized in this module. - # - # See ActionController::Routing and ActionController::Resources for general - # information about routing and routes.rb. - # - # <b>Tip:</b> If you need to generate URLs from your models or some other place, - # then ActionController::UrlWriter is what you're looking for. Read on for - # an introduction. - # - # == URL generation from parameters - # - # As you may know, some functions - such as ActionController::Base#url_for - # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set - # of parameters. For example, you've probably had the chance to write code - # like this in one of your views: - # - # <%= link_to('Click here', :controller => 'users', - # :action => 'new', :message => 'Welcome!') %> - # - # #=> Generates a link to: /users/new?message=Welcome%21 - # - # link_to, and all other functions that require URL generation functionality, - # actually use ActionController::UrlWriter under the hood. And in particular, - # they use the ActionController::UrlWriter#url_for method. One can generate - # the same path as the above example by using the following code: - # - # include UrlWriter - # url_for(:controller => 'users', - # :action => 'new', - # :message => 'Welcome!', - # :only_path => true) - # # => "/users/new?message=Welcome%21" - # - # Notice the <tt>:only_path => true</tt> part. This is because UrlWriter has no - # information about the website hostname that your Rails app is serving. So if you - # want to include the hostname as well, then you must also pass the <tt>:host</tt> - # argument: - # - # include UrlWriter - # url_for(:controller => 'users', - # :action => 'new', - # :message => 'Welcome!', - # :host => 'www.example.com') # Changed this. - # # => "http://www.example.com/users/new?message=Welcome%21" - # - # By default, all controllers and views have access to a special version of url_for, - # that already knows what the current hostname is. So if you use url_for in your - # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt> - # argument. - # - # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for. - # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for' - # in full. However, mailers don't have hostname information, and what's why you'll still - # have to specify the <tt>:host</tt> argument when generating URLs in mailers. - # - # - # == URL generation for named routes - # - # UrlWriter also allows one to access methods that have been auto-generated from - # named routes. For example, suppose that you have a 'users' resource in your - # <b>routes.rb</b>: - # - # map.resources :users - # - # This generates, among other things, the method <tt>users_path</tt>. By default, - # this method is accessible from your controllers, views and mailers. If you need - # to access this auto-generated method from other places (such as a model), then - # you can do that by including ActionController::UrlWriter in your class: - # - # class User < ActiveRecord::Base - # include ActionController::UrlWriter - # - # def base_uri - # user_path(self) - # end - # end - # - # User.find(1).base_uri # => "/users/1" - module UrlWriter - def self.included(base) #:nodoc: - ActionController::Routing::Routes.install_helpers(base) - base.mattr_accessor :default_url_options - - # The default options for urls written by this writer. Typically a <tt>:host</tt> pair is provided. - base.default_url_options ||= {} - end - - # Generate a url based on the options provided, default_url_options and the - # routes defined in routes.rb. The following options are supported: - # - # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. - # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. - # * <tt>:host</tt> - Specifies the host the link should be targeted at. - # If <tt>:only_path</tt> is false, this option must be - # provided either explicitly, or via +default_url_options+. - # * <tt>:port</tt> - Optionally specify the port to connect to. - # * <tt>:anchor</tt> - An anchor name to be appended to the path. - # * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the - # +relative_url_root+ set in ActionController::Base.relative_url_root. - # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" - # - # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to - # +url_for+ is forwarded to the Routes module. - # - # Examples: - # - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' - # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' - def url_for(options) - options = self.class.default_url_options.merge(options) - - url = '' - - unless options.delete(:only_path) - url << (options.delete(:protocol) || 'http') - url << '://' unless url.match("://") - - raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] - - url << options.delete(:host) - url << ":#{options.delete(:port)}" if options.key?(:port) - else - # Delete the unused options to prevent their appearance in the query string. - [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } - end - trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) - url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] - anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] - generated = Routing::Routes.generate(options, {}) - url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) - url << anchor if anchor - - url - end - end +require 'active_support/core_ext/hash/except' +module ActionController # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. class UrlRewriter #:nodoc: RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root] + def initialize(request, parameters) @request, @parameters = request, parameters end def rewrite(options = {}) - rewrite_url(options) + options[:host] ||= @request.host_with_port + options[:protocol] ||= @request.protocol + + self.class.rewrite(options, @request.symbolized_path_parameters) do |options| + process_path_options(options) + end end def to_str @@ -156,49 +24,53 @@ module ActionController alias_method :to_s, :to_str - private - # Given a path and options, returns a rewritten URL string - def rewrite_url(options) - rewritten_url = "" - - unless options[:only_path] - rewritten_url << (options[:protocol] || @request.protocol) - rewritten_url << "://" unless rewritten_url.match("://") - rewritten_url << rewrite_authentication(options) - rewritten_url << (options[:host] || @request.host_with_port) - rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) - end - - path = rewrite_path(options) - rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] - rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor] - - rewritten_url + def self.rewrite(options, path_segments=nil) + rewritten_url = "" + + unless options[:only_path] + rewritten_url << (options[:protocol] || "http") + rewritten_url << "://" unless rewritten_url.match("://") + rewritten_url << rewrite_authentication(options) + + raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] + + rewritten_url << options[:host] + rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) end - # Given a Hash of options, generates a route - def rewrite_path(options) - options = options.symbolize_keys - options.update(options[:params].symbolize_keys) if options[:params] + path_options = options.except(*RESERVED_OPTIONS) + path_options = yield(path_options) if block_given? + path = Routing::Routes.generate(path_options, path_segments || {}) + + rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] + rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) + rewritten_url << "##{Rack::Utils.escape(options[:anchor].to_param.to_s)}" if options[:anchor] - if (overwrite = options.delete(:overwrite_params)) - options.update(@parameters.symbolize_keys) - options.update(overwrite.symbolize_keys) - end + rewritten_url + end - RESERVED_OPTIONS.each { |k| options.delete(k) } + protected - # Generates the query string, too - Routing::Routes.generate(options, @request.symbolized_path_parameters) + def self.rewrite_authentication(options) + if options[:user] && options[:password] + "#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@" + else + "" end + end + + # Given a Hash of options, generates a route + def process_path_options(options) + options = options.symbolize_keys + options.update(options[:params].symbolize_keys) if options[:params] - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@" - else - "" - end + if (overwrite = options.delete(:overwrite_params)) + options.update(@parameters.symbolize_keys) + options.update(overwrite.symbolize_keys) end + + options + end + end end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 1e87a016f9..ad6d54b6a2 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -47,7 +47,6 @@ module ActionDispatch autoload :Rescue autoload :ShowExceptions autoload :Static - autoload :StringCoercion end autoload :MiddlewareStack, 'action_dispatch/middleware/stack' diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index f27f22c7e7..04a101dbb2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -179,7 +179,7 @@ module ActionDispatch 'cookie containing the session data. Use ' + 'config.action_controller.session = { :key => ' + '"_myapp_session", :secret => "some secret phrase" } in ' + - 'config/environment.rb' + 'config/application.rb' end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 4ebc8a2ab9..10f04dcdf6 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,7 +1,24 @@ require 'active_support/core_ext/exception' +require 'active_support/notifications' require 'action_dispatch/http/request' module ActionDispatch + # This middleware rescues any exception returned by the application and renders + # nice exception pages if it's being rescued locally. + # + # Every time an exception is caught, a notification is published, becoming a good API + # to deal with exceptions. So, if you want send an e-mail through ActionMailer + # everytime this notification is published, you just need to do the following: + # + # ActiveSupport::Notifications.subscribe "action_dispatch.show_exception" do |name, start, end, instrumentation_id, payload| + # ExceptionNotifier.deliver_exception(start, payload) + # end + # + # The payload is a hash which has two pairs: + # + # * :env - Contains the rack env for the given request; + # * :exception - The exception raised; + # class ShowExceptions LOCALHOST = '127.0.0.1'.freeze @@ -44,8 +61,11 @@ module ActionDispatch def call(env) @app.call(env) rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + ActiveSupport::Notifications.instrument 'action_dispatch.show_exception', + :env => env, :exception => exception do + raise exception if env['action_dispatch.show_exceptions'] == false + render_exception(env, exception) + end end private diff --git a/actionpack/lib/action_dispatch/middleware/string_coercion.rb b/actionpack/lib/action_dispatch/middleware/string_coercion.rb deleted file mode 100644 index 232e947835..0000000000 --- a/actionpack/lib/action_dispatch/middleware/string_coercion.rb +++ /dev/null @@ -1,29 +0,0 @@ -module ActionDispatch - class StringCoercion - class UglyBody < ActiveSupport::BasicObject - def initialize(body) - @body = body - end - - def each - @body.each do |part| - yield part.to_s - end - end - - private - def method_missing(*args, &block) - @body.__send__(*args, &block) - end - end - - def initialize(app) - @app = app - end - - def call(env) - status, headers, body = @app.call(env) - [status, headers, UglyBody.new(body)] - end - end -end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8f33346a4f..3e577c7f99 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -200,13 +200,22 @@ module ActionDispatch path = args.shift || block path_proc = path.is_a?(Proc) ? path : proc { |params| path % params } status = options[:status] || 301 + body = 'Moved Permanently' lambda do |env| - req = Rack::Request.new(env) - params = path_proc.call(env["action_dispatch.request.path_parameters"]) - url = req.scheme + '://' + req.host + params + req = Request.new(env) - [ status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently'] ] + uri = URI.parse(path_proc.call(req.params.symbolize_keys)) + uri.scheme ||= req.scheme + uri.host ||= req.host + uri.port ||= req.port unless req.port == 80 + + headers = { + 'Location' => uri.to_s, + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + [ status, headers, [body] ] end end @@ -283,7 +292,7 @@ module ActionDispatch end def namespace(path) - scope("/#{path}") { yield } + scope("/#{path}", :name_prefix => path.to_s) { yield } end def constraints(constraints = {}) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index bd397432ce..90893aa0e6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -74,9 +74,8 @@ module ActionDispatch @routes = {} @helpers = [] - @module ||= Module.new - @module.instance_methods.each do |selector| - @module.class_eval { remove_method selector } + @module ||= Module.new do + instance_methods.each { |selector| remove_method(selector) } end end @@ -138,67 +137,87 @@ module ActionDispatch end end - def named_helper_module_eval(code, *args) - @module.module_eval(code, *args) - end - def define_hash_access(route, name, kind, options) selector = hash_access_name(name, kind) - named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks + + # We use module_eval to avoid leaks + @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 def #{selector}(options = nil) # def hash_for_users_url(options = nil) options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} end # end protected :#{selector} # protected :hash_for_users_url - end_eval + END_EVAL helpers << selector end + # Create a url helper allowing ordered parameters to be associated + # with corresponding dynamic segments, so you can do: + # + # foo_url(bar, baz, bang) + # + # Instead of: + # + # foo_url(:bar => bar, :baz => baz, :bang => bang) + # + # Also allow options hash, so you can do: + # + # foo_url(bar, baz, bang, :sort_by => 'baz') + # def define_url_helper(route, name, kind, options) selector = url_helper_name(name, kind) - # The segment keys used for positional parameters - hash_access_method = hash_access_name(name, kind) - # allow ordered parameters to be associated with corresponding - # dynamic segments, so you can do - # - # foo_url(bar, baz, bang) - # - # instead of + # We use module_eval to avoid leaks. # - # foo_url(:bar => bar, :baz => baz, :bang => bang) + # def users_url(*args) + # if args.empty? || Hash === args.first + # options = hash_for_users_url(args.first || {}) + # else + # options = hash_for_users_url(args.extract_options!) + # default = default_url_options(options) if self.respond_to?(:default_url_options, true) + # options = (default ||= {}).merge(options) # - # Also allow options hash, so you can do + # keys = [] + # keys -= options.keys if args.size < keys.size - 1 # - # foo_url(bar, baz, bang, :sort_by => 'baz') + # args = args.zip(keys).inject({}) do |h, (v, k)| + # h[k] = v + # h + # end # - named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks - def #{selector}(*args) # def users_url(*args) - # - opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first - args.first || {} # args.first || {} - else # else - options = args.extract_options! # options = args.extract_options! - args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)| - h[k] = v # h[k] = v - h # h - end # end - options.merge(args) # options.merge(args) - end # end - # - url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts)) - # - end # end - #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL. - def formatted_#{selector}(*args) # def formatted_users_url(*args) - ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn( - "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " + - "Please pass format to the standard " + # "Please pass format to the standard " + - "#{selector} method instead.", caller) # "users_url method instead.", caller) - #{selector}(*args) # users_url(*args) - end # end - protected :#{selector} # protected :users_url - end_eval + # # Tell url_for to skip default_url_options + # options[:use_defaults] = false + # options.merge!(args) + # end + # + # url_for(options) + # end + @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(*args) + if args.empty? || Hash === args.first + options = #{hash_access_method}(args.first || {}) + else + options = #{hash_access_method}(args.extract_options!) + default = default_url_options(options) if self.respond_to?(:default_url_options, true) + options = (default ||= {}).merge(options) + + keys = #{route.segment_keys.inspect} + keys -= options.keys if args.size < keys.size - 1 # take format into account + + args = args.zip(keys).inject({}) do |h, (v, k)| + h[k] = v + h + end + + # Tell url_for to skip default_url_options + options[:use_defaults] = false + options.merge!(args) + end + + url_for(options) + end + protected :#{selector} + END_EVAL helpers << selector end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 5686bbdbde..c2486d3730 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -2,6 +2,15 @@ module ActionDispatch module Assertions # A small suite of assertions that test responses from Rails applications. module ResponseAssertions + extend ActiveSupport::Concern + + included do + # TODO: Need to pull in AV::Template monkey patches that track which + # templates are rendered. assert_template should probably be part + # of AV instead of AD. + require 'action_view/test_case' + end + # Asserts that the response is one of the following types: # # * <tt>:success</tt> - Status code was 200 diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index c2dc591ff7..a6b1126e2b 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -524,7 +524,7 @@ module ActionDispatch fix_content = lambda do |node| # Gets around a bug in the Rails 1.1 HTML parser. - node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) } + node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) } end selected = elements.map do |element| diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2a5f5dcd5c..4ec47d146c 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -1,6 +1,5 @@ require 'stringio' require 'uri' -require 'active_support/test_case' require 'active_support/core_ext/object/metaclass' require 'rack/test' diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 8ce6e82524..93aa69c060 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -52,6 +52,8 @@ module ActionView autoload :TemplateHandler, 'action_view/template' autoload :TemplateHandlers, 'action_view/template' end + + autoload :TestCase, 'action_view/test_case' end require 'action_view/erb/util' diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index c70f29f098..87b7adf6c4 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -216,7 +216,7 @@ module ActionView end options[:object_name] ||= params.first - I18n.with_options :locale => options[:locale], :scope => [:activemodel, :errors, :template] do |locale| + I18n.with_options :locale => options[:locale], :scope => [:errors, :template] do |locale| header_message = if options.include?(:header_message) options[:header_message] else diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 81c9c88820..c1f342bcdd 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -505,7 +505,7 @@ module ActionView # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation - # is found in the current I18n locale (through views.labels.<modelname>.<attribute>) or you specify it explicitly. + # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to # target labels for radio_button tags (where the value is used in the ID of the input tag). @@ -517,8 +517,8 @@ module ActionView # You can localize your labels based on model and attribute names. # For example you can define the following in your locale (e.g. en.yml) # - # views: - # labels: + # helpers: + # label: # post: # body: "Write your entire text here" # @@ -777,7 +777,7 @@ module ActionView options["for"] ||= name_and_id["id"] content = if text.blank? - I18n.t("views.labels.#{object_name}.#{method_name}", :default => "").presence + I18n.t("helpers.label.#{object_name}.#{method_name}", :default => "").presence else text.to_s end diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 935ab5f3e8..02ad637509 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -571,7 +571,7 @@ module ActionView option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags end if value.blank? && options[:prompt] - prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('support.select.prompt', :default => 'Please select') + prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select') "<option value=\"\">#{prompt}</option>\n" + option_tags else option_tags diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 397871b85e..64b71663c3 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -92,7 +92,7 @@ module ActionView :precision => precision, :delimiter => delimiter, :separator => separator) - ).gsub(/%u/, unit) + ).gsub(/%u/, unit).html_safe! rescue number end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 1d92bcb763..be15e227b9 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -567,7 +567,7 @@ module ActionView end link_text = block_given?? yield(href) : href - href = 'http://' + href unless href.index('http') == 0 + href = 'http://' + href unless href =~ %r{^[a-z]+://}i content_tag(:a, h(link_text), link_attributes.merge('href' => href)) + punctuation.reverse.join('') end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 5b136d4f54..14628c5404 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -11,6 +11,11 @@ module ActionView module UrlHelper include JavaScriptHelper + # Need to map default url options to controller one. + def default_url_options(*args) #:nodoc: + @controller.send(:default_url_options, *args) + end + # Returns the URL for the set of +options+ provided. This takes the # same options as +url_for+ in Action Controller (see the # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default @@ -461,10 +466,10 @@ module ActionView string = '' extras = '' - extras << "cc=#{CGI.escape(cc).gsub("+", "%20")}&" unless cc.nil? - extras << "bcc=#{CGI.escape(bcc).gsub("+", "%20")}&" unless bcc.nil? - extras << "body=#{CGI.escape(body).gsub("+", "%20")}&" unless body.nil? - extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil? + extras << "cc=#{Rack::Utils.escape(cc).gsub("+", "%20")}&" unless cc.nil? + extras << "bcc=#{Rack::Utils.escape(bcc).gsub("+", "%20")}&" unless bcc.nil? + extras << "body=#{Rack::Utils.escape(body).gsub("+", "%20")}&" unless body.nil? + extras << "subject=#{Rack::Utils.escape(subject).gsub("+", "%20")}&" unless subject.nil? extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty? email_address = email_address.to_s diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 5e2a92b89a..9918034020 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -102,16 +102,15 @@ minute: "Minute" second: "Seconds" - activemodel: - errors: - template: - header: - one: "1 error prohibited this {{model}} from being saved" - other: "{{count}} errors prohibited this {{model}} from being saved" - # The variable :count is also available - body: "There were problems with the following fields:" + errors: + template: + header: + one: "1 error prohibited this {{model}} from being saved" + other: "{{count}} errors prohibited this {{model}} from being saved" + # The variable :count is also available + body: "There were problems with the following fields:" - support: + helpers: select: # default value for :prompt => true in FormOptionsHelper prompt: "Please select" diff --git a/actionpack/lib/action_view/render/partials.rb b/actionpack/lib/action_view/render/partials.rb index 5158415c20..67a8ee4472 100644 --- a/actionpack/lib/action_view/render/partials.rb +++ b/actionpack/lib/action_view/render/partials.rb @@ -215,12 +215,13 @@ module ActionView options = @options if @collection - ActiveSupport::Notifications.instrument(:render_collection, :path => @path, - :count => @collection.size) do + ActiveSupport::Notifications.instrument("action_view.render_collection", + :path => @path, :count => @collection.size) do render_collection end else - content = ActiveSupport::Notifications.instrument(:render_partial, :path => @path) do + content = ActiveSupport::Notifications.instrument("action_view.render_partial", + :path => @path) do render_partial end diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index 48316cac53..2fdfad694d 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -93,7 +93,7 @@ module ActionView def _render_template(template, layout = nil, options = {}) locals = options[:locals] || {} - content = ActiveSupport::Notifications.instrument(:render_template, + content = ActiveSupport::Notifications.instrument("action_view.render_template", :identifier => template.identifier, :layout => (layout ? layout.identifier : nil)) do template.render(self, locals) end @@ -109,7 +109,8 @@ module ActionView end def _render_layout(layout, locals, &block) - ActiveSupport::Notifications.instrument(:render_layout, :identifier => layout.identifier) do + ActiveSupport::Notifications.instrument("action_view.render_layout", + :identifier => layout.identifier) do layout.render(self, locals){ |*name| _layout_for(*name, &block) } end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index be9a2ed50d..16d66b6eca 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -1,6 +1,3 @@ -require 'active_support/test_case' -require 'action_controller/test_case' - module ActionView class Base alias_method :initialize_without_template_tracking, :initialize diff --git a/actionpack/test/abstract/url_for_test.rb b/actionpack/test/abstract/url_for_test.rb new file mode 100644 index 0000000000..e5570349b8 --- /dev/null +++ b/actionpack/test/abstract/url_for_test.rb @@ -0,0 +1,272 @@ +require 'abstract_unit' + +module AbstractController + module Testing + + class UrlForTests < ActionController::TestCase + class W + include AbstractController::UrlFor + end + + def teardown + W.default_url_options.clear + end + + def add_host! + W.default_url_options[:host] = 'www.basecamphq.com' + end + + def test_exception_is_thrown_without_host + assert_raise RuntimeError do + W.new.url_for :controller => 'c', :action => 'a', :id => 'i' + end + end + + def test_anchor + assert_equal('/c/a#anchor', + W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => 'anchor') + ) + end + + def test_anchor_should_call_to_param + assert_equal('/c/a#anchor', + W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anchor')) + ) + end + + def test_anchor_should_be_cgi_escaped + assert_equal('/c/a#anc%2Fhor', + W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anc/hor')) + ) + end + + def test_default_host + add_host! + assert_equal('http://www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i') + ) + end + + def test_host_may_be_overridden + add_host! + assert_equal('http://37signals.basecamphq.com/c/a/i', + W.new.url_for(:host => '37signals.basecamphq.com', :controller => 'c', :action => 'a', :id => 'i') + ) + end + + def test_port + add_host! + assert_equal('http://www.basecamphq.com:3000/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :port => 3000) + ) + end + + def test_protocol + add_host! + assert_equal('https://www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') + ) + end + + def test_protocol_with_and_without_separator + add_host! + assert_equal('https://www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') + ) + assert_equal('https://www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https://') + ) + end + + def test_trailing_slash + add_host! + options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'} + assert_equal('http://www.basecamphq.com/foo/bar/33/', W.new.url_for(options) ) + end + + def test_trailing_slash_with_protocol + add_host! + options = { :trailing_slash => true,:protocol => 'https', :controller => 'foo', :action => 'bar', :id => '33'} + assert_equal('https://www.basecamphq.com/foo/bar/33/', W.new.url_for(options) ) + assert_equal 'https://www.basecamphq.com/foo/bar/33/?query=string', W.new.url_for(options.merge({:query => 'string'})) + end + + def test_trailing_slash_with_only_path + options = {:controller => 'foo', :trailing_slash => true} + assert_equal '/foo/', W.new.url_for(options.merge({:only_path => true})) + options.update({:action => 'bar', :id => '33'}) + assert_equal '/foo/bar/33/', W.new.url_for(options.merge({:only_path => true})) + assert_equal '/foo/bar/33/?query=string', W.new.url_for(options.merge({:query => 'string',:only_path => true})) + end + + def test_trailing_slash_with_anchor + options = {:trailing_slash => true, :controller => 'foo', :action => 'bar', :id => '33', :only_path => true, :anchor=> 'chapter7'} + assert_equal '/foo/bar/33/#chapter7', W.new.url_for(options) + assert_equal '/foo/bar/33/?query=string#chapter7', W.new.url_for(options.merge({:query => 'string'})) + end + + def test_trailing_slash_with_params + url = W.new.url_for(:trailing_slash => true, :only_path => true, :controller => 'cont', :action => 'act', :p1 => 'cafe', :p2 => 'link') + params = extract_params(url) + assert_equal params[0], { :p1 => 'cafe' }.to_query + assert_equal params[1], { :p2 => 'link' }.to_query + end + + def test_relative_url_root_is_respected + orig_relative_url_root = ActionController::Base.relative_url_root + ActionController::Base.relative_url_root = '/subdir' + + add_host! + assert_equal('https://www.basecamphq.com/subdir/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') + ) + ensure + ActionController::Base.relative_url_root = orig_relative_url_root + end + + def test_named_routes + with_routing do |set| + set.draw do |map| + match 'this/is/verbose', :to => 'home#index', :as => :no_args + match 'home/sweet/home/:user', :to => 'home#index', :as => :home + end + + # We need to create a new class in order to install the new named route. + kls = Class.new { include AbstractController::UrlFor } + controller = kls.new + assert controller.respond_to?(:home_url) + assert_equal 'http://www.basecamphq.com/home/sweet/home/again', + controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again') + + assert_equal("/home/sweet/home/alabama", controller.send(:home_path, :user => 'alabama', :host => 'unused')) + assert_equal("http://www.basecamphq.com/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'www.basecamphq.com')) + assert_equal("http://www.basecamphq.com/this/is/verbose", controller.send(:no_args_url, :host=>'www.basecamphq.com')) + end + end + + def test_relative_url_root_is_respected_for_named_routes + orig_relative_url_root = ActionController::Base.relative_url_root + ActionController::Base.relative_url_root = '/subdir' + + with_routing do |set| + set.draw do |map| + match '/home/sweet/home/:user', :to => 'home#index', :as => :home + end + + kls = Class.new { include AbstractController::UrlFor } + controller = kls.new + + assert_equal 'http://www.basecamphq.com/subdir/home/sweet/home/again', + controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again') + end + ensure + ActionController::Base.relative_url_root = orig_relative_url_root + end + + def test_only_path + with_routing do |set| + set.draw do |map| + match 'home/sweet/home/:user', :to => 'home#index', :as => :home + match ':controller/:action/:id' + end + + # We need to create a new class in order to install the new named route. + kls = Class.new { include AbstractController::UrlFor } + controller = kls.new + assert controller.respond_to?(:home_url) + assert_equal '/brave/new/world', + controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true) + + assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true)) + assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama')) + end + end + + def test_one_parameter + assert_equal('/c/a?param=val', + W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :param => 'val') + ) + end + + def test_two_parameters + url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :p1 => 'X1', :p2 => 'Y2') + params = extract_params(url) + assert_equal params[0], { :p1 => 'X1' }.to_query + assert_equal params[1], { :p2 => 'Y2' }.to_query + end + + def test_hash_parameter + url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:name => 'Bob', :category => 'prof'}) + params = extract_params(url) + assert_equal params[0], { 'query[category]' => 'prof' }.to_query + assert_equal params[1], { 'query[name]' => 'Bob' }.to_query + end + + def test_array_parameter + url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => ['Bob', 'prof']) + params = extract_params(url) + assert_equal params[0], { 'query[]' => 'Bob' }.to_query + assert_equal params[1], { 'query[]' => 'prof' }.to_query + end + + def test_hash_recursive_parameters + url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:person => {:name => 'Bob', :position => 'prof'}, :hobby => 'piercing'}) + params = extract_params(url) + assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query + assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query + assert_equal params[2], { 'query[person][position]' => 'prof' }.to_query + end + + def test_hash_recursive_and_array_parameters + url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => 101, :query => {:person => {:name => 'Bob', :position => ['prof', 'art director']}, :hobby => 'piercing'}) + assert_match %r(^/c/a/101), url + params = extract_params(url) + assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query + assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query + assert_equal params[2], { 'query[person][position][]' => 'art director' }.to_query + assert_equal params[3], { 'query[person][position][]' => 'prof' }.to_query + end + + def test_path_generation_for_symbol_parameter_keys + assert_generates("/image", :controller=> :image) + end + + def test_named_routes_with_nil_keys + with_routing do |set| + set.draw do |map| + match 'posts.:format', :to => 'posts#index', :as => :posts + match '/', :to => 'posts#index', :as => :main + end + + # We need to create a new class in order to install the new named route. + kls = Class.new { include AbstractController::UrlFor } + kls.default_url_options[:host] = 'www.basecamphq.com' + + controller = kls.new + params = {:action => :index, :controller => :posts, :format => :xml} + assert_equal("http://www.basecamphq.com/posts.xml", controller.send(:url_for, params)) + params[:format] = nil + assert_equal("http://www.basecamphq.com/", controller.send(:url_for, params)) + end + end + + def test_multiple_includes_maintain_distinct_options + first_class = Class.new { include AbstractController::UrlFor } + second_class = Class.new { include AbstractController::UrlFor } + + first_host, second_host = 'firsthost.com', 'secondhost.com' + + first_class.default_url_options[:host] = first_host + second_class.default_url_options[:host] = second_host + + assert_equal first_class.default_url_options[:host], first_host + assert_equal second_class.default_url_options[:host], second_host + end + + private + def extract_params(url) + url.split('?', 2).last.split('&').sort + end + end + end +end
\ No newline at end of file diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 8c65087898..3f7a5c89b9 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -19,8 +19,6 @@ require 'action_view' require 'action_view/base' require 'action_dispatch' require 'fixture_template' -require 'active_support/test_case' -require 'action_view/test_case' require 'active_support/dependencies' activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__) @@ -96,7 +94,6 @@ class ActiveSupport::TestCase end class MockLogger - attr_reader :logged attr_accessor :level def initialize @@ -108,12 +105,15 @@ class MockLogger @logged << args.first @logged << blk.call if block_given? end + + def logged + @logged.compact.map { |l| l.to_s.strip } + end end class ActionController::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) ActionDispatch::MiddlewareStack.new { |middleware| - middleware.use "ActionDispatch::StringCoercion" middleware.use "ActionDispatch::ShowExceptions" middleware.use "ActionDispatch::Callbacks" middleware.use "ActionDispatch::ParamsParser" diff --git a/actionpack/test/active_record_unit.rb b/actionpack/test/active_record_unit.rb index 9a094cf66b..4f2b052720 100644 --- a/actionpack/test/active_record_unit.rb +++ b/actionpack/test/active_record_unit.rb @@ -17,7 +17,6 @@ unless defined?(ActiveRecord) && defined?(Fixtures) raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR) $LOAD_PATH.unshift PATH_TO_AR require 'active_record' - require 'active_record/fixtures' rescue LoadError => e $stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}" ActiveRecordTestConnector.able_to_connect = false diff --git a/actionpack/test/activerecord/controller_runtime_test.rb b/actionpack/test/activerecord/controller_runtime_test.rb index 0f534da14b..9525dd8307 100644 --- a/actionpack/test/activerecord/controller_runtime_test.rb +++ b/actionpack/test/activerecord/controller_runtime_test.rb @@ -23,9 +23,11 @@ class ARLoggingTest < ActionController::TestCase end def test_log_with_active_record + # Wait pending notifications to be published + wait get :show wait - assert_match /ActiveRecord runtime/, logs[3] + assert_match /ActiveRecord runtime/, @controller.logger.logged[3] end private @@ -33,7 +35,4 @@ class ARLoggingTest < ActionController::TestCase @controller.logger = MockLogger.new end - def logs - @logs ||= @controller.logger.logged.compact.map {|l| l.to_s.strip} - end end diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index ad744421db..ea82758cf5 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -26,7 +26,7 @@ class Series < ActiveRecord::Base end class PolymorphicRoutesTest < ActionController::TestCase - include ActionController::UrlWriter + include ActionController::UrlFor self.default_url_options[:host] = 'example.com' def setup diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 65118f9bc9..1510a6a7e0 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -6,6 +6,7 @@ require 'pp' # require 'pp' early to prevent hidden_methods from not picking up module Submodule class ContainedEmptyController < ActionController::Base end + class ContainedNonEmptyController < ActionController::Base def public_action render :nothing => true @@ -20,12 +21,15 @@ module Submodule end hide_action :another_hidden_action end + class SubclassedController < ContainedNonEmptyController hide_action :public_action # Hiding it here should not affect the superclass. end end + class EmptyController < ActionController::Base end + class NonEmptyController < ActionController::Base def public_action render :nothing => true @@ -37,7 +41,6 @@ class NonEmptyController < ActionController::Base end class MethodMissingController < ActionController::Base - hide_action :shouldnt_be_called def shouldnt_be_called raise "NO WAY!" @@ -48,16 +51,15 @@ protected def method_missing(selector) render :text => selector.to_s end - end class DefaultUrlOptionsController < ActionController::Base - def default_url_options_action - render :nothing => true + def from_view + render :inline => "<%= #{params[:route]} %>" end def default_url_options(options = nil) - { :host => 'www.override.com', :action => 'new', :bacon => 'chunky' } + { :host => 'www.override.com', :action => 'new', :locale => 'en' } end end @@ -68,6 +70,7 @@ class ControllerClassTests < Test::Unit::TestCase assert_equal 'submodule/contained_empty', Submodule::ContainedEmptyController.controller_path assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path end + def test_controller_name assert_equal 'empty', EmptyController.controller_name assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name @@ -86,41 +89,16 @@ class ControllerInstanceTests < Test::Unit::TestCase def test_action_methods @empty_controllers.each do |c| - hide_mocha_methods_from_controller(c) assert_equal Set.new, c.class.__send__(:action_methods), "#{c.controller_path} should be empty!" end + @non_empty_controllers.each do |c| - hide_mocha_methods_from_controller(c) assert_equal Set.new(%w(public_action)), c.class.__send__(:action_methods), "#{c.controller_path} should not be empty!" end end - - protected - # Mocha adds some public instance methods to Object that would be - # considered actions, so explicitly hide_action them. - def hide_mocha_methods_from_controller(controller) - mocha_methods = [ - :expects, :mocha, :mocha_inspect, :reset_mocha, :stubba_object, - :stubba_method, :stubs, :verify, :__metaclass__, :__is_a__, :to_matcher, - ] - controller.class.__send__(:hide_action, *mocha_methods) - end end - class PerformActionTest < ActionController::TestCase - class MockLogger - attr_reader :logged - - def initialize - @logged = [] - end - - def method_missing(method, *args) - @logged << args.first.to_s - end - end - def use_controller(controller_class) @controller = controller_class.new @@ -128,9 +106,8 @@ class PerformActionTest < ActionController::TestCase # a more accurate simulation of what happens in "real life". @controller.logger = Logger.new(nil) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" rescue_action_in_public! @@ -145,8 +122,7 @@ class PerformActionTest < ActionController::TestCase def test_method_missing_is_not_an_action_name use_controller MethodMissingController - - assert ! @controller.__send__(:action_method?, 'method_missing') + assert !@controller.__send__(:action_method?, 'method_missing') get :method_missing assert_response :success @@ -172,16 +148,43 @@ class DefaultUrlOptionsTest < ActionController::TestCase def test_default_url_options_are_used_if_set with_routing do |set| set.draw do |map| - match 'default_url_options', :to => 'default_url_options#default_url_options_action', :as => :default_url_options + match 'from_view', :to => 'default_url_options#from_view', :as => :from_view match ':controller/:action' end - get :default_url_options_action # Make a dummy request so that the controller is initialized properly. + get :from_view, :route => "from_view_url" - assert_equal 'http://www.override.com/default_url_options/new?bacon=chunky', @controller.url_for(:controller => 'default_url_options') - assert_equal 'http://www.override.com/default_url_options?bacon=chunky', @controller.send(:default_url_options_url) + assert_equal 'http://www.override.com/from_view?locale=en', @response.body + assert_equal 'http://www.override.com/from_view?locale=en', @controller.send(:from_view_url) + assert_equal 'http://www.override.com/default_url_options/new?locale=en', @controller.url_for(:controller => 'default_url_options') end end + + def test_default_url_options_are_used_in_non_positional_parameters + with_routing do |set| + set.draw do |map| + scope("/:locale") do + resources :descriptions + end + match ':controller/:action' + end + + get :from_view, :route => "description_path(1)" + + assert_equal '/en/descriptions/1', @response.body + assert_equal '/en/descriptions', @controller.send(:descriptions_path) + assert_equal '/pl/descriptions', @controller.send(:descriptions_path, "pl") + assert_equal '/pl/descriptions', @controller.send(:descriptions_path, :locale => "pl") + assert_equal '/pl/descriptions.xml', @controller.send(:descriptions_path, "pl", "xml") + assert_equal '/en/descriptions.xml', @controller.send(:descriptions_path, :format => "xml") + assert_equal '/en/descriptions/1', @controller.send(:description_path, 1) + assert_equal '/pl/descriptions/1', @controller.send(:description_path, "pl", 1) + assert_equal '/pl/descriptions/1', @controller.send(:description_path, 1, :locale => "pl") + assert_equal '/pl/descriptions/1.xml', @controller.send(:description_path, "pl", 1, "xml") + assert_equal '/en/descriptions/1.xml', @controller.send(:description_path, 1, :format => "xml") + end + end + end class EmptyUrlOptionsTest < ActionController::TestCase @@ -197,15 +200,12 @@ class EmptyUrlOptionsTest < ActionController::TestCase get :public_action assert_equal "http://www.example.com/non_empty/public_action", @controller.url_for end -end -class EnsureNamedRoutesWorksTicket22BugTest < ActionController::TestCase - def test_named_routes_still_work + def test_named_routes_with_path_without_doing_a_request_first with_routing do |set| set.draw do |map| resources :things end - EmptyController.send :include, ActionController::UrlWriter assert_equal '/things', EmptyController.new.send(:things_path) end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 679eaf7b38..5a8dc0c358 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -630,17 +630,17 @@ class FragmentCachingTest < ActionController::TestCase end def test_fragment_for_logging - fragment_computed = false - events = [] - ActiveSupport::Notifications.subscribe { |*args| events << args } + # Wait pending notifications to be published + ActiveSupport::Notifications.notifier.wait + @controller.logger = MockLogger.new - buffer = 'generated till now -> ' - @controller.fragment_for(buffer, 'expensive') { fragment_computed = true } + fragment_computed = false + @controller.fragment_for('buffer', 'expensive') { fragment_computed = true } + ActiveSupport::Notifications.notifier.wait assert fragment_computed - assert_equal 'generated till now -> ', buffer - ActiveSupport::Notifications.notifier.wait - assert_equal [:exist_fragment?, :write_fragment], events.map(&:first) + assert_match /Exist fragment\? "views\/expensive"/, @controller.logger.logged[0] + assert_match /Write fragment "views\/expensive"/, @controller.logger.logged[1] end end diff --git a/actionpack/test/controller/logging_test.rb b/actionpack/test/controller/logging_test.rb index 4206dffa7e..594cf17312 100644 --- a/actionpack/test/controller/logging_test.rb +++ b/actionpack/test/controller/logging_test.rb @@ -19,6 +19,7 @@ class LoggingTest < ActionController::TestCase def setup super + wait # Wait pending notifications to be published set_logger end @@ -75,6 +76,6 @@ class LoggingTest < ActionController::TestCase end def logs - @logs ||= @controller.logger.logged.compact.map {|l| l.to_s.strip} + @logs ||= @controller.logger.logged end end diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index 428f40b9f8..c2b8cd85d8 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -100,286 +100,3 @@ class UrlRewriterTests < ActionController::TestCase end end -class UrlWriterTests < ActionController::TestCase - class W - include ActionController::UrlWriter - end - - def teardown - W.default_url_options.clear - end - - def add_host! - W.default_url_options[:host] = 'www.basecamphq.com' - end - - def test_exception_is_thrown_without_host - assert_raise RuntimeError do - W.new.url_for :controller => 'c', :action => 'a', :id => 'i' - end - end - - def test_anchor - assert_equal('/c/a#anchor', - W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => 'anchor') - ) - end - - def test_anchor_should_call_to_param - assert_equal('/c/a#anchor', - W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anchor')) - ) - end - - def test_anchor_should_be_cgi_escaped - assert_equal('/c/a#anc%2Fhor', - W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anc/hor')) - ) - end - - def test_default_host - add_host! - assert_equal('http://www.basecamphq.com/c/a/i', - W.new.url_for(:controller => 'c', :action => 'a', :id => 'i') - ) - end - - def test_host_may_be_overridden - add_host! - assert_equal('http://37signals.basecamphq.com/c/a/i', - W.new.url_for(:host => '37signals.basecamphq.com', :controller => 'c', :action => 'a', :id => 'i') - ) - end - - def test_port - add_host! - assert_equal('http://www.basecamphq.com:3000/c/a/i', - W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :port => 3000) - ) - end - - def test_protocol - add_host! - assert_equal('https://www.basecamphq.com/c/a/i', - W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') - ) - end - - def test_protocol_with_and_without_separator - add_host! - assert_equal('https://www.basecamphq.com/c/a/i', - W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') - ) - assert_equal('https://www.basecamphq.com/c/a/i', - W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https://') - ) - end - - def test_trailing_slash - add_host! - options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'} - assert_equal('http://www.basecamphq.com/foo/bar/33/', W.new.url_for(options) ) - end - - def test_trailing_slash_with_protocol - add_host! - options = { :trailing_slash => true,:protocol => 'https', :controller => 'foo', :action => 'bar', :id => '33'} - assert_equal('https://www.basecamphq.com/foo/bar/33/', W.new.url_for(options) ) - assert_equal 'https://www.basecamphq.com/foo/bar/33/?query=string', W.new.url_for(options.merge({:query => 'string'})) - end - - def test_trailing_slash_with_only_path - options = {:controller => 'foo', :trailing_slash => true} - assert_equal '/foo/', W.new.url_for(options.merge({:only_path => true})) - options.update({:action => 'bar', :id => '33'}) - assert_equal '/foo/bar/33/', W.new.url_for(options.merge({:only_path => true})) - assert_equal '/foo/bar/33/?query=string', W.new.url_for(options.merge({:query => 'string',:only_path => true})) - end - - def test_trailing_slash_with_anchor - options = {:trailing_slash => true, :controller => 'foo', :action => 'bar', :id => '33', :only_path => true, :anchor=> 'chapter7'} - assert_equal '/foo/bar/33/#chapter7', W.new.url_for(options) - assert_equal '/foo/bar/33/?query=string#chapter7', W.new.url_for(options.merge({:query => 'string'})) - end - - def test_trailing_slash_with_params - url = W.new.url_for(:trailing_slash => true, :only_path => true, :controller => 'cont', :action => 'act', :p1 => 'cafe', :p2 => 'link') - params = extract_params(url) - assert_equal params[0], { :p1 => 'cafe' }.to_query - assert_equal params[1], { :p2 => 'link' }.to_query - end - - def test_relative_url_root_is_respected - orig_relative_url_root = ActionController::Base.relative_url_root - ActionController::Base.relative_url_root = '/subdir' - - add_host! - assert_equal('https://www.basecamphq.com/subdir/c/a/i', - W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') - ) - ensure - ActionController::Base.relative_url_root = orig_relative_url_root - end - - def test_named_routes - with_routing do |set| - set.draw do |map| - match 'this/is/verbose', :to => 'home#index', :as => :no_args - match 'home/sweet/home/:user', :to => 'home#index', :as => :home - end - - # We need to create a new class in order to install the new named route. - kls = Class.new { include ActionController::UrlWriter } - controller = kls.new - assert controller.respond_to?(:home_url) - assert_equal 'http://www.basecamphq.com/home/sweet/home/again', - controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again') - - assert_equal("/home/sweet/home/alabama", controller.send(:home_path, :user => 'alabama', :host => 'unused')) - assert_equal("http://www.basecamphq.com/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'www.basecamphq.com')) - assert_equal("http://www.basecamphq.com/this/is/verbose", controller.send(:no_args_url, :host=>'www.basecamphq.com')) - end - end - - def test_relative_url_root_is_respected_for_named_routes - orig_relative_url_root = ActionController::Base.relative_url_root - ActionController::Base.relative_url_root = '/subdir' - - with_routing do |set| - set.draw do |map| - match '/home/sweet/home/:user', :to => 'home#index', :as => :home - end - - kls = Class.new { include ActionController::UrlWriter } - controller = kls.new - - assert_equal 'http://www.basecamphq.com/subdir/home/sweet/home/again', - controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again') - end - ensure - ActionController::Base.relative_url_root = orig_relative_url_root - end - - def test_only_path - with_routing do |set| - set.draw do |map| - match 'home/sweet/home/:user', :to => 'home#index', :as => :home - match ':controller/:action/:id' - end - - # We need to create a new class in order to install the new named route. - kls = Class.new { include ActionController::UrlWriter } - controller = kls.new - assert controller.respond_to?(:home_url) - assert_equal '/brave/new/world', - controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true) - - assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true)) - assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama')) - end - end - - def test_one_parameter - assert_equal('/c/a?param=val', - W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :param => 'val') - ) - end - - def test_two_parameters - url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :p1 => 'X1', :p2 => 'Y2') - params = extract_params(url) - assert_equal params[0], { :p1 => 'X1' }.to_query - assert_equal params[1], { :p2 => 'Y2' }.to_query - end - - def test_hash_parameter - url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:name => 'Bob', :category => 'prof'}) - params = extract_params(url) - assert_equal params[0], { 'query[category]' => 'prof' }.to_query - assert_equal params[1], { 'query[name]' => 'Bob' }.to_query - end - - def test_array_parameter - url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => ['Bob', 'prof']) - params = extract_params(url) - assert_equal params[0], { 'query[]' => 'Bob' }.to_query - assert_equal params[1], { 'query[]' => 'prof' }.to_query - end - - def test_hash_recursive_parameters - url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:person => {:name => 'Bob', :position => 'prof'}, :hobby => 'piercing'}) - params = extract_params(url) - assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query - assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query - assert_equal params[2], { 'query[person][position]' => 'prof' }.to_query - end - - def test_hash_recursive_and_array_parameters - url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => 101, :query => {:person => {:name => 'Bob', :position => ['prof', 'art director']}, :hobby => 'piercing'}) - assert_match %r(^/c/a/101), url - params = extract_params(url) - assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query - assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query - assert_equal params[2], { 'query[person][position][]' => 'art director' }.to_query - assert_equal params[3], { 'query[person][position][]' => 'prof' }.to_query - end - - def test_path_generation_for_symbol_parameter_keys - assert_generates("/image", :controller=> :image) - end - - def test_named_routes_with_nil_keys - with_routing do |set| - set.draw do |map| - match 'posts.:format', :to => 'posts#index', :as => :posts - match '/', :to => 'posts#index', :as => :main - end - - # We need to create a new class in order to install the new named route. - kls = Class.new { include ActionController::UrlWriter } - kls.default_url_options[:host] = 'www.basecamphq.com' - - controller = kls.new - params = {:action => :index, :controller => :posts, :format => :xml} - assert_equal("http://www.basecamphq.com/posts.xml", controller.send(:url_for, params)) - params[:format] = nil - assert_equal("http://www.basecamphq.com/", controller.send(:url_for, params)) - end - end - - def test_formatted_url_methods_are_deprecated - with_routing do |set| - set.draw do |map| - resources :posts - end - # We need to create a new class in order to install the new named route. - kls = Class.new { include ActionController::UrlWriter } - controller = kls.new - params = {:id => 1, :format => :xml} - assert_deprecated do - assert_equal("/posts/1.xml", controller.send(:formatted_post_path, params)) - end - assert_deprecated do - assert_equal("/posts/1.xml", controller.send(:formatted_post_path, 1, :xml)) - end - end - end - - def test_multiple_includes_maintain_distinct_options - first_class = Class.new { include ActionController::UrlWriter } - second_class = Class.new { include ActionController::UrlWriter } - - first_host, second_host = 'firsthost.com', 'secondhost.com' - - first_class.default_url_options[:host] = first_host - second_class.default_url_options[:host] = second_host - - assert_equal first_class.default_url_options[:host], first_host - assert_equal second_class.default_url_options[:host], second_host - end - - private - def extract_params(url) - url.split('?', 2).last.split('&').sort - end -end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index c4b0b9cdbf..32e2717789 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -29,6 +29,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match 'account/modulo/:name', :to => redirect("/%{name}s") match 'account/proc/:name', :to => redirect {|params| "/#{params[:name].pluralize}" } + match 'account/google' => redirect('http://www.google.com/') match 'openid/login', :via => [:get, :post], :to => "openid#login" @@ -65,7 +66,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :people do nested do - namespace ":access_token" do + scope "/:access_token" do resource :avatar end end @@ -432,12 +433,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest with_test_routes do get '/account/subscription' assert_equal 'subscriptions#show', @response.body + assert_equal '/account/subscription', account_subscription_path get '/account/credit' assert_equal 'credits#show', @response.body + assert_equal '/account/credit', account_credit_path get '/account/credit_card' assert_equal 'credit_cards#show', @response.body + assert_equal '/account/credit_card', account_credit_card_path end end @@ -472,7 +476,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'projects#index', @response.body end end - + def test_index with_test_routes do assert_equal '/info', info_path @@ -488,7 +492,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'projects#info', @response.body end end - + def test_convention_match_with_no_scope with_test_routes do assert_equal '/account/overview', account_overview_path @@ -497,6 +501,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_redirect_with_complete_url + with_test_routes do + get '/account/google' + assert_equal 301, @response.status + assert_equal 'http://www.google.com/', @response.headers['Location'] + assert_equal 'Moved Permanently', @response.body + end + end + + def test_redirect_with_port + previous_host, self.host = self.host, 'www.example.com:3000' + with_test_routes do + get '/account/login' + assert_equal 301, @response.status + assert_equal 'http://www.example.com:3000/login', @response.headers['Location'] + assert_equal 'Moved Permanently', @response.body + end + ensure + self.host = previous_host + end + private def with_test_routes real_routes, temp_routes = ActionController::Routing::Routes, Routes diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 9f6a93756c..951fb4a22e 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -104,4 +104,27 @@ class ShowExceptionsTest < ActionController::IntegrationTest assert_response 405 assert_match /ActionController::MethodNotAllowed/, body end + + test "publishes notifications" do + # Wait pending notifications to be published + ActiveSupport::Notifications.notifier.wait + + @app, event = ProductionApp, nil + self.remote_addr = '127.0.0.1' + + ActiveSupport::Notifications.subscribe('action_dispatch.show_exception') do |*args| + event = args + end + + get "/" + assert_response 500 + assert_match /puke/, body + + ActiveSupport::Notifications.notifier.wait + + assert_equal 'action_dispatch.show_exception', event.first + assert_kind_of Hash, event.last[:env] + assert_equal 'GET', event.last[:env]["REQUEST_METHOD"] + assert_kind_of RuntimeError, event.last[:exception] + end end diff --git a/actionpack/test/dispatch/string_coercion_test.rb b/actionpack/test/dispatch/string_coercion_test.rb deleted file mode 100644 index d79b17b932..0000000000 --- a/actionpack/test/dispatch/string_coercion_test.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'abstract_unit' - -class StringCoercionTest < ActiveSupport::TestCase - test "body responds to each" do - original_body = [] - body = ActionDispatch::StringCoercion::UglyBody.new(original_body) - - assert original_body.respond_to?(:each) - assert body.respond_to?(:each) - end - - test "body responds to to_path" do - original_body = [] - def original_body.to_path; end - body = ActionDispatch::StringCoercion::UglyBody.new(original_body) - - assert original_body.respond_to?(:to_path) - assert body.respond_to?(:to_path) - end - - test "body does not responds to to_path" do - original_body = [] - body = ActionDispatch::StringCoercion::UglyBody.new(original_body) - - assert !original_body.respond_to?(:to_path) - assert !body.respond_to?(:to_path) - end - - test "calls to_s on body parts" do - app = lambda { |env| - [200, {'Content-Type' => 'html'}, [1, 2, 3]] - } - app = ActionDispatch::StringCoercion.new(app) - parts = [] - status, headers, body = app.call({}) - body.each { |part| parts << part } - - assert_equal %w( 1 2 3 ), parts - end -end diff --git a/actionpack/test/template/active_model_helper_i18n_test.rb b/actionpack/test/template/active_model_helper_i18n_test.rb index 2465444fc5..4eb2f262bd 100644 --- a/actionpack/test/template/active_model_helper_i18n_test.rb +++ b/actionpack/test/template/active_model_helper_i18n_test.rb @@ -16,27 +16,27 @@ class ActiveModelHelperI18nTest < Test::Unit::TestCase stubs(:content_tag).returns 'content_tag' - I18n.stubs(:t).with(:'header', :locale => 'en', :scope => [:activemodel, :errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved" - I18n.stubs(:t).with(:'body', :locale => 'en', :scope => [:activemodel, :errors, :template]).returns 'There were problems with the following fields:' + I18n.stubs(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved" + I18n.stubs(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).returns 'There were problems with the following fields:' end def test_error_messages_for_given_a_header_option_it_does_not_translate_header_message - I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:activemodel, :errors, :template], :count => 1, :model => '').never + I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').never error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en') end def test_error_messages_for_given_no_header_option_it_translates_header_message - I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:activemodel, :errors, :template], :count => 1, :model => '').returns 'header message' + I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').returns 'header message' error_messages_for(:object => @object, :locale => 'en') end def test_error_messages_for_given_a_message_option_it_does_not_translate_message - I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:activemodel, :errors, :template]).never + I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).never error_messages_for(:object => @object, :message => 'message', :locale => 'en') end def test_error_messages_for_given_no_message_option_it_translates_message - I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:activemodel, :errors, :template]).returns 'There were problems with the following fields:' + I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).returns 'There were problems with the following fields:' error_messages_for(:object => @object, :locale => 'en') end end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index b1e9fe99a2..acadbd0cd0 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -16,8 +16,8 @@ class FormHelperTest < ActionView::TestCase } } }, - :views => { - :labels => { + :helpers => { + :label => { :post => { :body => "Write entire text here" } diff --git a/actionpack/test/template/form_options_helper_i18n_test.rb b/actionpack/test/template/form_options_helper_i18n_test.rb index 91e370efa7..4972ea6511 100644 --- a/actionpack/test/template/form_options_helper_i18n_test.rb +++ b/actionpack/test/template/form_options_helper_i18n_test.rb @@ -6,7 +6,7 @@ class FormOptionsHelperI18nTests < ActionView::TestCase def setup @prompt_message = 'Select!' I18n.backend.send(:init_translations) - I18n.backend.store_translations :en, :support => { :select => { :prompt => @prompt_message } } + I18n.backend.store_translations :en, :helpers => { :select => { :prompt => @prompt_message } } end def teardown @@ -14,7 +14,7 @@ class FormOptionsHelperI18nTests < ActionView::TestCase end def test_select_with_prompt_true_translates_prompt_message - I18n.expects(:translate).with('support.select.prompt', { :default => 'Please select' }) + I18n.expects(:translate).with('helpers.select.prompt', { :default => 'Please select' }) select('post', 'category', [], :prompt => true) end @@ -24,4 +24,4 @@ class FormOptionsHelperI18nTests < ActionView::TestCase select('post', 'category', [], :prompt => true) ) end -end
\ No newline at end of file +end diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index 08143ba680..088c07b8bb 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -360,6 +360,20 @@ class TextHelperTest < ActionView::TestCase assert_equal %(<p>#{link10_result} Link</p>), auto_link("<p>#{link10_raw} Link</p>") end + def test_auto_link_other_protocols + silence_warnings do + begin + old_re_value = ActionView::Helpers::TextHelper::AUTO_LINK_RE + ActionView::Helpers::TextHelper.const_set :AUTO_LINK_RE, %r{(ftp://)[^\s<]+} + link_raw = 'ftp://example.com/file.txt' + link_result = generate_result(link_raw) + assert_equal %(Download #{link_result}), auto_link("Download #{link_raw}") + ensure + ActionView::Helpers::TextHelper.const_set :AUTO_LINK_RE, old_re_value + end + end + end + def test_auto_link_already_linked linked1 = generate_result('Ruby On Rails', 'http://www.rubyonrails.com') linked2 = generate_result('www.rubyonrails.com', 'http://www.rubyonrails.com') diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 26500568ee..7489c0daa5 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Change the ActiveModel::Base.include_root_in_json default to true for Rails 3 [DHH] + * Add validates_format_of :without => /regexp/ option. #430 [Elliot Winkler, Peer Allan] Example : diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index f14016027c..6eab00c177 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -30,10 +30,12 @@ module ActiveModel extend ActiveSupport::Autoload autoload :AttributeMethods + autoload :BlockValidator, 'active_model/validator' autoload :Callbacks autoload :Conversion autoload :DeprecatedErrorMethods autoload :Dirty + autoload :EachValidator, 'active_model/validator' autoload :Errors autoload :Lint autoload :Name, 'active_model/naming' @@ -42,12 +44,11 @@ module ActiveModel autoload :Observing autoload :Serialization autoload :StateMachine + autoload :TestCase autoload :Translation + autoload :VERSION autoload :Validations autoload :Validator - autoload :EachValidator, 'active_model/validator' - autoload :BlockValidator, 'active_model/validator' - autoload :VERSION module Serializers extend ActiveSupport::Autoload diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index abc084a74b..0b6c75c46e 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -105,8 +105,7 @@ module ActiveModel else attr_name = attribute.to_s.gsub('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - options = { :default => "{{attribute}} {{message}}", :attribute => attr_name, - :scope => @base.class.i18n_scope } + options = { :default => "{{attribute}} {{message}}", :attribute => attr_name } messages.each do |m| full_messages << I18n.t(:"errors.format", options.merge(:message => m)) @@ -153,7 +152,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value, - :scope => [@base.class.i18n_scope, :errors] + :scope => [:errors] }.merge(options) I18n.translate(key, options) diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index 1cdb897f13..ea58021767 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -1,27 +1,26 @@ en: - activemodel: - errors: - # model.errors.full_messages format. - format: "{{attribute}} {{message}}" + errors: + # The default format use in full error messages. + format: "{{attribute}} {{message}}" - # 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" + # 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/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index ee6d48bfc6..794de7dc55 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,19 +10,17 @@ module ActiveModel included do extend ActiveModel::Naming - cattr_accessor :include_root_in_json, :instance_writer => false + cattr_accessor :include_root_in_json, :instance_writer => true end # Returns a JSON string representing the model. Some configuration is # available through +options+. # - # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the - # top-level behavior of to_json. In a new Rails application, it is set to - # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>, + # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the + # top-level behavior of to_json. It is true by default. When it is <tt>true</tt>, # to_json will emit a single root node named after the object's type. For example: # # konata = User.find(1) - # ActiveRecord::Base.include_root_in_json = true # konata.to_json # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} } diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb index 4cb5c9cbc0..6328807ad7 100644 --- a/activemodel/lib/active_model/test_case.rb +++ b/activemodel/lib/active_model/test_case.rb @@ -1,5 +1,3 @@ -require "active_support/test_case" - module ActiveModel #:nodoc: class TestCase < ActiveSupport::TestCase #:nodoc: def with_kcode(kcode) diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index e5ef1e6114..2d2df269d0 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -25,13 +25,14 @@ module ActiveModel # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) defaults = lookup_ancestors.map do |klass| - :"#{klass.model_name.underscore}.#{attribute}" + :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}" end + defaults << :"attributes.#{attribute}" defaults << options.delete(:default) if options[:default] defaults << attribute.to_s.humanize - options.reverse_merge! :scope => [self.i18n_scope, :attributes], :count => 1, :default => defaults + options.reverse_merge! :count => 1, :default => defaults I18n.translate(defaults.shift, options) end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d5460a58bd..276472ea46 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -15,21 +15,26 @@ module ActiveModel module ClassMethods # Validates each attribute against a block. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Validations + # # 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>: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 + # 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 + # 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(*attr_names, &block) options = attr_names.extract_options!.symbolize_keys @@ -42,7 +47,9 @@ module ActiveModel # # This can be done with a symbol pointing to a method: # - # class Comment < ActiveRecord::Base + # class Comment + # include ActiveModel::Validations + # # validate :must_be_friends # # def must_be_friends @@ -52,7 +59,9 @@ module ActiveModel # # Or with a block which is passed the current record to be validated: # - # class Comment < ActiveRecord::Base + # class Comment + # include ActiveModel::Validations + # # validate do |comment| # comment.must_be_friends # end @@ -71,6 +80,13 @@ module ActiveModel end set_callback(:validate, *args, &block) end + + private + + def _merge_attributes(attr_names) + options = attr_names.extract_options! + options.merge(:attributes => attr_names) + end end # Returns the Errors object that holds all information about attribute error messages. @@ -90,27 +106,22 @@ module ActiveModel !valid? end - protected - # Hook method defining how an attribute value should be retieved. By default this is assumed - # to be an instance named after the attribute. Override this method in subclasses should you - # need to retrieve the value for a given attribute differently e.g. - # class MyClass - # include ActiveModel::Validations - # - # def initialize(data = {}) - # @data = data - # end - # - # private - # - # def read_attribute_for_validation(key) - # @data[key] - # end - # end - # - def read_attribute_for_validation(key) - send(key) - end + # Hook method defining how an attribute value should be retieved. By default this is assumed + # to be an instance named after the attribute. Override this method in subclasses should you + # need to retrieve the value for a given attribute differently e.g. + # class MyClass + # include ActiveModel::Validations + # + # def initialize(data = {}) + # @data = data + # end + # + # def read_attribute_for_validation(key) + # @data[key] + # end + # end + # + alias :read_attribute_for_validation :send end end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index bd9463ed27..0423fcd17f 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -10,6 +10,13 @@ module ActiveModel record.errors.add(attribute, :accepted, :default => options[:message]) end end + + def setup(klass) + # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility + # as instance_methods returns symbols unlike 1.8 which returns strings. + new_attributes = attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") } + klass.send(:attr_accessor, *new_attributes) + end end module ClassMethods @@ -37,18 +44,7 @@ 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) - options = 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_with AcceptanceValidator, options.merge(:attributes => attr_names) + validates_with AcceptanceValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index b06effdceb..8041d4b61f 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -6,6 +6,10 @@ module ActiveModel return if confirmed.nil? || value == confirmed record.errors.add(attribute, :confirmation, :default => options[:message]) end + + def setup(klass) + klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" }) + end end module ClassMethods @@ -38,9 +42,7 @@ 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) - options = attr_names.extract_options! - attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" })) - validates_with ConfirmationValidator, options.merge(:attributes => attr_names) + validates_with ConfirmationValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index f8759f253b..0a44c6d54f 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -2,6 +2,7 @@ module ActiveModel module Validations class ExclusionValidator < EachValidator def check_validity! + options[:in] ||= options.delete(:within) raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) end @@ -33,9 +34,7 @@ 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) - options = attr_names.extract_options! - options[:in] ||= options.delete(:within) - validates_with ExclusionValidator, options.merge(:attributes => attr_names) + validates_with ExclusionValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index d5427c2b03..9a9e7eca4d 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -8,6 +8,20 @@ module ActiveModel record.errors.add(attribute, :invalid, :default => options[:message], :value => value) end end + + def check_validity! + unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or" + raise ArgumentError, "Either :with or :without must be supplied (but not both)" + end + + if options[:with] && !options[:with].is_a?(Regexp) + raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" + end + + if options[:without] && !options[:without].is_a?(Regexp) + raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" + end + end end module ClassMethods @@ -43,21 +57,7 @@ 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) - options = attr_names.extract_options! - - unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or" - raise ArgumentError, "Either :with or :without must be supplied (but not both)" - end - - if options[:with] && !options[:with].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" - end - - if options[:without] && !options[:without].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" - end - - validates_with FormatValidator, options.merge(:attributes => attr_names) + validates_with FormatValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index a122e9e737..25b8c7866d 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -2,6 +2,7 @@ module ActiveModel module Validations class InclusionValidator < EachValidator def check_validity! + options[:in] ||= options.delete(:within) raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) end @@ -33,9 +34,7 @@ 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) - options = attr_names.extract_options! - options[:in] ||= options.delete(:within) - validates_with InclusionValidator, options.merge(:attributes => attr_names) + validates_with InclusionValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 6e90a75c17..f41ce34328 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -107,8 +107,7 @@ module ActiveModel # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. def validates_length_of(*attr_names) - options = attr_names.extract_options! - validates_with LengthValidator, options.merge(:attributes => attr_names) + validates_with LengthValidator, _merge_attributes(attr_names) end alias_method :validates_size_of, :validates_length_of diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index f2aab8c5b8..9dfc5125cd 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -103,8 +103,7 @@ module ActiveModel # end # def validates_numericality_of(*attr_names) - options = attr_names.extract_options! - validates_with NumericalityValidator, options.merge(:attributes => attr_names) + validates_with NumericalityValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index a4c6f866a7..4a71cf79b5 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -34,8 +34,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - options = attr_names.extract_options! - validates_with PresenceValidator, options.merge(:attributes => attr_names) + validates_with PresenceValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb new file mode 100644 index 0000000000..61caf32c06 --- /dev/null +++ b/activemodel/lib/active_model/validations/validates.rb @@ -0,0 +1,86 @@ +module ActiveModel + module Validations + module ClassMethods + # This method is a shortcut to all default validators and any custom + # validator classes ending in 'Validator'. Note that Rails default + # validators can be overridden inside specific classes by creating + # custom validator classes in their place such as PresenceValidator. + # + # Examples of using the default rails validators: + # validates :terms, :acceptance => true + # validates :password, :confirmation => true + # validates :username, :exclusion => { :in => %w(admin superuser) } + # validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create } + # validates :age, :inclusion => { :in => 0..9 } + # validates :first_name, :length => { :maximum => 30 } + # validates :age, :numericality => true + # validates :username, :presence => true + # validates :username, :uniqueness => true + # + # The power of the +validates+ method comes when using cusom validators + # and default validators in one call for a given attribute e.g. + # class EmailValidator < ActiveModel::EachValidator + # def validate_each(record, attribute, value) + # record.errors[attribute] << (options[:message] || "is not an email") unless + # value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + # end + # end + # + # class Person + # include ActiveModel::Validations + # attr_accessor :name, :email + # + # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } + # validates :email, :presence => true, :format => { :with => /@/ } + # end + # + # Validator classes my also exist within the class being validated + # allowing custom modules of validators to be included as needed e.g. + # + # module MyValidators + # class TitleValidator < ActiveModel::EachValidator + # def validate_each(record, attribute, value) + # record.errors[attribute] << "must start with 'the'" unless =~ /^the/i + # end + # end + # end + # + # class Film + # include ActiveModel::Validations + # include MyValidators + # + # validates :name, :title => true + # end + # + # The options :if, :unless, :on, :allow_blank and :allow_nil can be given to one specific + # validator: + # + # validates :password, :presence => { :if => :password_required? }, :confirmation => true + # + # Or to all at the same time: + # + # validates :password, :presence => true, :confirmation => true, :if => :password_required? + # + def validates(*attributes) + defaults = attributes.extract_options! + validations = defaults.slice!(:if, :unless, :on, :allow_blank, :allow_nil) + + raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? + raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) } + raise ArgumentError, "You need to supply at least one validation" if validations.empty? + + defaults.merge!(:attributes => attributes) + + validations.each do |key, options| + begin + validator = const_get("#{key.to_s.camelize}Validator") + rescue NameError + raise ArgumentError, "Unknown validator: '#{key}'" + end + + validates_with(validator, defaults.merge(options == true ? {} : options)) + end + end + end + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 8d521173c6..db563876af 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -2,14 +2,16 @@ module ActiveModel module Validations module ClassMethods - # Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions. + # Passes the record off to the class or classes specified and allows them + # to add errors based on more complex conditions. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # - # class MyValidator < ActiveRecord::Validator - # def validate + # class MyValidator < ActiveModel::Validator + # def validate(record) # if some_complex_logic # record.errors[:base] << "This record is invalid" # end @@ -23,37 +25,46 @@ module ActiveModel # # You may also pass it multiple classes, like so: # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator, MyOtherValidator, :on => :create # end # # Configuration options: - # * <tt>on</tt> - Specifies when this validation is active (<tt>:create</tt> or <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>). + # * <tt>on</tt> - Specifies when this validation is active + # (<tt>:create</tt> or <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>). + # * <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. # - # If you pass any additional configuration options, they will be passed to the class and available as <tt>options</tt>: + # If you pass any additional configuration options, they will be passed + # to the class and available as <tt>options</tt>: # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator, :my_custom_key => "my custom value" # end # - # class MyValidator < ActiveRecord::Validator - # def validate + # class MyValidator < ActiveModel::Validator + # def validate(record) # options[:my_custom_key] # => "my custom value" # end # end # def validates_with(*args, &block) options = args.extract_options! - args.each { |klass| validate(klass.new(options, &block), options) } + args.each do |klass| + validator = klass.new(options, &block) + validator.setup(self) if validator.respond_to?(:setup) + validate(validator, options) + end end end end -end - - +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 8c9f9c7fb3..382a4cc98d 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,12 +1,13 @@ module ActiveModel #:nodoc: - # A simple base class that can be used along with ActiveModel::Base.validates_with + # A simple base class that can be used along with ActiveModel::Validations::ClassMethods.validates_with # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # if some_complex_logic # record.errors[:base] = "This record is invalid" # end @@ -18,10 +19,11 @@ module ActiveModel #:nodoc: # end # end # - # Any class that inherits from ActiveModel::Validator will have access to <tt>record</tt>, - # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>. + # Any class that inherits from ActiveModel::Validator must implement a method + # called <tt>validate</tt> which accepts a <tt>record</tt>. # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # @@ -36,7 +38,7 @@ module ActiveModel #:nodoc: # from within the validators message # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # record.errors[:base] << "This is some custom error message" # record.errors[:first_name] << "This is some complex validation" # # etc... @@ -51,13 +53,47 @@ module ActiveModel #:nodoc: # @my_custom_field = options[:field_name] || :first_name # end # end + # + # The easiest way to add custom validators for validating individual attributes + # is with the convenient ActiveModel::EachValidator for example: + # + # class TitleValidator < ActiveModel::EachValidator + # def validate_each(record, attribute, value) + # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value) + # end + # end + # + # This can now be used in combination with the +validates+ method + # (see ActiveModel::Validations::ClassMethods.validates for more on this) + # + # class Person + # include ActiveModel::Validations + # attr_accessor :title + # + # validates :title, :presence => true, :title => true + # end + # + # Validator may also define a +setup+ instance method which will get called + # with the class that using that validator as it's argument. This can be + # useful when there are prerequisites such as an attr_accessor being present + # for example: + # + # class MyValidator < ActiveModel::Validator + # def setup(klass) + # klass.send :attr_accessor, :custom_attribute + # end + # end + # class Validator attr_reader :options + # Accepts options that will be made availible through the +options+ reader. def initialize(options) @options = options end + # Override this method in subclasses with validation logic, adding errors + # to the records +errors+ array where necessary. def validate(record) raise NotImplementedError end @@ -70,25 +106,37 @@ module ActiveModel #:nodoc: # All ActiveModel validations are built on top of this Validator. class EachValidator < Validator attr_reader :attributes - + + # Returns a new validator instance. All options will be available via the + # +options+ reader, however the <tt>:attributes</tt> option will be removed + # and instead be made available through the +attributes+ reader. def initialize(options) - @attributes = options.delete(:attributes) + @attributes = Array(options.delete(:attributes)) + raise ":attributes cannot be blank" if @attributes.empty? super check_validity! end + # Performs validation on the supplied record. By default this will call + # +validates_each+ to determine validity therefore subclasses should + # override +validates_each+ with validation logic. def validate(record) attributes.each do |attribute| - value = record.send(:read_attribute_for_validation, attribute) + value = record.read_attribute_for_validation(attribute) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) validate_each(record, attribute, value) end end + #Â Override this method in subclasses with the validation logic, adding + # errors to the records +errors+ array where necessary. def validate_each(record, attribute, value) raise NotImplementedError end + # Hook method that gets called by the initializer allowing verification + # that the arguments supplied are valid. You could for example raise an + # ArgumentError when invalid options are supplied. def check_validity! end end @@ -102,6 +150,8 @@ module ActiveModel #:nodoc: super end + private + def validate_each(record, attribute, value) @block.call(record, attribute, value) end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 30193956ea..917bb720d0 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -8,7 +8,6 @@ $:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) require 'config' require 'active_model' -require 'active_model/test_case' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true diff --git a/activemodel/test/cases/tests_database.rb b/activemodel/test/cases/tests_database.rb index 79668dd941..8ca54d2678 100644 --- a/activemodel/test/cases/tests_database.rb +++ b/activemodel/test/cases/tests_database.rb @@ -2,8 +2,6 @@ require 'logger' $:.unshift(File.dirname(__FILE__) + '/../../../activerecord/lib') require 'active_record' -require 'active_record/test_case' -require 'active_record/fixtures' module ActiveModel module TestsDatabase diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index bfc1ca12e6..e25d308ca1 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -11,6 +11,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } } assert_equal 'person name attribute', Person.human_attribute_name('name') end + + def test_translated_model_attributes_with_default + I18n.backend.store_translations 'en', :attributes => { :name => 'name default attribute' } + assert_equal 'name default attribute', Person.human_attribute_name('name') + end def test_translated_model_attributes_with_symbols I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } } diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 54b2405c92..6116ef71d4 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -7,42 +7,6 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase def setup Person.reset_callbacks(:validate) @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 # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index a7656fe219..7d33fcea98 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -9,10 +9,10 @@ class I18nValidationTest < ActiveModel::TestCase Person.reset_callbacks(:validate) @person = Person.new - @old_load_path, @old_backend = I18n.load_path, I18n.backend + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations('en', :activemodel => {:errors => {:messages => {:custom => nil}}}) + I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}}) end def teardown @@ -42,13 +42,13 @@ class I18nValidationTest < ActiveModel::TestCase end def test_errors_full_messages_translates_human_attribute_name_for_model_attributes - @person.errors.add('name', 'empty') - I18n.expects(:translate).with(:"person.name", :default => ['Name', 'Name'], :scope => [:activemodel, :attributes], :count => 1).returns('Name') - @person.errors.full_messages + @person.errors.add(:name, 'not found') + Person.expects(:human_attribute_name).with(:name, :default => 'Name').returns("Person's name") + assert_equal ["Person's name not found"], @person.errors.full_messages end def test_errors_full_messages_uses_format - I18n.backend.store_translations('en', :activemodel => {:errors => {:format => "Field {{attribute}} {{message}}"}}) + I18n.backend.store_translations('en', :errors => {:format => "Field {{attribute}} {{message}}"}) @person.errors.add('name', 'empty') assert_equal ["Field Name empty"], @person.errors.full_messages end @@ -254,8 +254,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:confirmation => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:confirmation => 'global message'}} Person.validates_confirmation_of :title @person.title_confirmation = 'foo' @@ -264,7 +264,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_confirmation_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:confirmation => 'global message'}} Person.validates_confirmation_of :title @person.title_confirmation = 'foo' @@ -275,8 +275,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:accepted => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:accepted => 'global message'}} Person.validates_acceptance_of :title, :allow_nil => false @person.valid? @@ -284,7 +284,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_acceptance_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:accepted => 'global message'}} Person.validates_acceptance_of :title, :allow_nil => false @person.valid? @@ -294,8 +294,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:blank => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:blank => 'global message'}} Person.validates_presence_of :title @person.valid? @@ -303,7 +303,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_presence_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:blank => 'global message'}} Person.validates_presence_of :title @person.valid? @@ -313,8 +313,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:too_short => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:too_short => 'global message'}} Person.validates_length_of :title, :within => 3..5 @person.valid? @@ -322,7 +322,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_length_of_within_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:too_short => 'global message'}} Person.validates_length_of :title, :within => 3..5 @person.valid? @@ -332,8 +332,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:wrong_length => 'global message'}} Person.validates_length_of :title, :is => 5 @person.valid? @@ -341,7 +341,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_length_of_is_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:wrong_length => 'global message'}} Person.validates_length_of :title, :is => 5 @person.valid? @@ -351,8 +351,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:invalid => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}} Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ @person.valid? @@ -360,7 +360,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_format_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}} Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ @person.valid? @@ -370,8 +370,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:inclusion => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:inclusion => 'global message'}} Person.validates_inclusion_of :title, :in => %w(a b c) @person.valid? @@ -379,7 +379,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_inclusion_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:inclusion => 'global message'}} Person.validates_inclusion_of :title, :in => %w(a b c) @person.valid? @@ -389,8 +389,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:exclusion => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:exclusion => 'global message'}} Person.validates_exclusion_of :title, :in => %w(a b c) @person.title = 'a' @@ -399,7 +399,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_exclusion_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:exclusion => 'global message'}} Person.validates_exclusion_of :title, :in => %w(a b c) @person.title = 'a' @@ -410,8 +410,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} Person.validates_numericality_of :title @person.title = 'a' @@ -420,7 +420,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_numericality_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} Person.validates_numericality_of :title, :only_integer => true @person.title = 'a' @@ -431,8 +431,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} Person.validates_numericality_of :title, :only_integer => true @person.title = 'a' @@ -441,7 +441,7 @@ class I18nValidationTest < ActiveModel::TestCase 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'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} Person.validates_numericality_of :title, :only_integer => true @person.title = 'a' @@ -452,8 +452,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:odd => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:odd => 'global message'}} Person.validates_numericality_of :title, :only_integer => true, :odd => true @person.title = 0 @@ -462,7 +462,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_numericality_of_odd_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:odd => 'global message'}} Person.validates_numericality_of :title, :only_integer => true, :odd => true @person.title = 0 @@ -473,8 +473,8 @@ class I18nValidationTest < ActiveModel::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:less_than => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:less_than => 'global message'}} Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 @person.title = 1 @@ -483,7 +483,7 @@ class I18nValidationTest < ActiveModel::TestCase end def test_validates_numericality_of_less_than_finds_global_default_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:less_than => 'global message'}} Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 @person.title = 1 @@ -494,7 +494,7 @@ class I18nValidationTest < ActiveModel::TestCase # test with validates_with def test_validations_with_message_symbol_must_translate - I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:custom_error => "I am a custom error"}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:custom_error => "I am a custom error"}} Person.validates_presence_of :title, :message => :custom_error @person.title = nil @person.valid? @@ -502,7 +502,7 @@ class I18nValidationTest < ActiveModel::TestCase 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"}}}}}} + I18n.backend.store_translations 'en', :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? @@ -510,7 +510,7 @@ class I18nValidationTest < ActiveModel::TestCase 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"}}}} + I18n.backend.store_translations 'en', :errors => {:models => {:person => {:custom_error => "I am a custom error"}}} Person.validates_presence_of :title, :message => :custom_error @person.title = nil @person.valid? diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb new file mode 100644 index 0000000000..181ff38b64 --- /dev/null +++ b/activemodel/test/cases/validations/validates_test.rb @@ -0,0 +1,88 @@ +# encoding: utf-8 +require 'cases/helper' +require 'models/person' +require 'models/person_with_validator' +require 'validators/email_validator' + +class ValidatesTest < ActiveModel::TestCase + setup :reset_callbacks + teardown :reset_callbacks + + def reset_callbacks + Person.reset_callbacks(:validate) + end + + def test_validates_with_built_in_validation + Person.validates :title, :numericality => true + person = Person.new + person.valid? + assert person.errors[:title].include?('is not a number') + end + + def test_validates_with_built_in_validation_and_options + Person.validates :title, :numericality => { :message => 'my custom message' } + person = Person.new + person.valid? + assert person.errors[:title].include?('my custom message') + end + + def test_validates_with_validator_class + Person.validates :karma, :email => true + person = Person.new + person.valid? + assert person.errors[:karma].include?('is not an email') + end + + def test_validates_with_if_as_local_conditions + Person.validates :karma, :presence => true, :email => { :unless => :condition_is_true } + person = Person.new + person.valid? + assert !person.errors[:karma].include?('is not an email') + assert person.errors[:karma].include?('can\'t be blank') + end + + def test_validates_with_if_as_shared_conditions + Person.validates :karma, :presence => true, :email => true, :if => :condition_is_true + person = Person.new + person.valid? + assert person.errors[:karma].include?('is not an email') + assert person.errors[:karma].include?('can\'t be blank') + end + + def test_validates_with_unless_shared_conditions + Person.validates :karma, :presence => true, :email => true, :unless => :condition_is_true + person = Person.new + assert person.valid? + end + + def test_validates_with_allow_nil_shared_conditions + Person.validates :karma, :length => { :minimum => 20 }, :email => true, :allow_nil => true + person = Person.new + assert person.valid? + end + + def test_validates_with_validator_class_and_options + Person.validates :karma, :email => { :message => 'my custom message' } + person = Person.new + person.valid? + assert person.errors[:karma].include?('my custom message') + end + + def test_validates_with_unknown_validator + assert_raise(ArgumentError) { Person.validates :karma, :unknown => true } + end + + def test_validates_with_included_validator + PersonWithValidator.validates :title, :presence => true + person = PersonWithValidator.new + person.valid? + assert person.errors[:title].include?('Local validator') + end + + def test_validates_with_included_validator_and_options + PersonWithValidator.validates :title, :presence => { :custom => ' please' } + person = PersonWithValidator.new + person.valid? + assert person.errors[:title].include?('Local validator please') + end +end
\ No newline at end of file diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 9e9b925df2..66b072ea38 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -39,6 +39,18 @@ class ValidatesWithTest < ActiveRecord::TestCase end end + class ValidatorPerEachAttribute < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << "Value is #{value}" + end + end + + class ValidatorCheckValidity < ActiveModel::EachValidator + def check_validity! + raise "boom!" + end + end + test "vaidation with class that adds errors" do Topic.validates_with(ValidatorThatAddsErrors) topic = Topic.new @@ -108,6 +120,28 @@ class ValidatesWithTest < ActiveRecord::TestCase Topic.validates_with(validator, :if => "1 == 1", :foo => :bar) assert topic.valid? end + + test "calls setup method of validator passing in self when validator has setup method" do + topic = Topic.new + validator = stub_everything + validator.stubs(:new).returns(validator) + validator.stubs(:validate) + validator.stubs(:respond_to?).with(:setup).returns(true) + validator.expects(:setup).with(Topic).once + Topic.validates_with(validator) + assert topic.valid? + end + + test "doesn't call setup method of validator when validator has no setup method" do + topic = Topic.new + validator = stub_everything + validator.stubs(:new).returns(validator) + validator.stubs(:validate) + validator.stubs(:respond_to?).with(:setup).returns(false) + validator.expects(:setup).with(Topic).never + Topic.validates_with(validator) + assert topic.valid? + end test "validates_with with options" do Topic.validates_with(ValidatorThatValidatesOptions, :field => :first_name) @@ -116,4 +150,39 @@ class ValidatesWithTest < ActiveRecord::TestCase assert topic.errors[:base].include?(ERROR_MESSAGE) end + test "validates_with each validator" do + Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content]) + topic = Topic.new :title => "Title", :content => "Content" + assert !topic.valid? + assert_equal ["Value is Title"], topic.errors[:title] + assert_equal ["Value is Content"], topic.errors[:content] + end + + test "each validator checks validity" do + assert_raise RuntimeError do + Topic.validates_with(ValidatorCheckValidity, :attributes => [:title]) + end + end + + test "each validator expects attributes to be given" do + assert_raise RuntimeError do + Topic.validates_with(ValidatorPerEachAttribute) + end + end + + test "each validator skip nil values if :allow_nil is set to true" do + Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content], :allow_nil => true) + topic = Topic.new :content => "" + assert !topic.valid? + assert topic.errors[:title].empty? + assert_equal ["Value is "], topic.errors[:content] + end + + test "each validator skip blank values if :allow_blank is set to true" do + Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content], :allow_blank => true) + topic = Topic.new :content => "" + assert topic.valid? + assert topic.errors[:title].empty? + assert topic.errors[:content].empty? + end end diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb index 7ac70e6167..14a8be9ebc 100644 --- a/activemodel/test/models/custom_reader.rb +++ b/activemodel/test/models/custom_reader.rb @@ -8,8 +8,6 @@ class CustomReader def []=(key, value) @data[key] = value end - - private def read_attribute_for_validation(key) @data[key] diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb index c83d768379..53ac68055f 100644 --- a/activemodel/test/models/person.rb +++ b/activemodel/test/models/person.rb @@ -3,6 +3,10 @@ class Person extend ActiveModel::Translation attr_accessor :title, :karma, :salary + + def condition_is_true + true + end end class Child < Person diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb new file mode 100644 index 0000000000..f9763ea853 --- /dev/null +++ b/activemodel/test/models/person_with_validator.rb @@ -0,0 +1,11 @@ +class PersonWithValidator + include ActiveModel::Validations + + class PresenceValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank? + end + end + + attr_accessor :title, :karma +end diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb new file mode 100644 index 0000000000..cff47ac230 --- /dev/null +++ b/activemodel/test/validators/email_validator.rb @@ -0,0 +1,6 @@ +class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << (options[:message] || "is not an email") unless + value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + end +end
\ No newline at end of file diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 4846774deb..38bcf0c787 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,12 @@ *Edge* +* Changed ActiveRecord::Base.store_full_sti_class to be true by default reflecting the previously announced Rails 3 default [DHH] + +* Add Relation#except. [Pratik Naik] + + one_red_item = Item.where(:colour => 'red').limit(1) + all_items = one_red_item.except(:where, :limit) + * Add Relation#delete_all. [Pratik Naik] Item.where(:colour => 'red').delete_all diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index cf439b0dc0..d5b6d40514 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -55,6 +55,7 @@ module ActiveRecord autoload :FinderMethods autoload :CalculationMethods autoload :PredicateBuilder + autoload :SpawnMethods end autoload :Base @@ -133,6 +134,9 @@ module ActiveRecord autoload :AbstractAdapter end end + + autoload :TestCase + autoload :TestFixtures, 'active_record/fixtures' end Arel::Table.engine = Arel::Sql::Engine.new(ActiveRecord::Base) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d74b21b690..1320f5b624 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1465,7 +1465,7 @@ module ActiveRecord after_destroy(method_name) end - def find_with_associations(options = {}, join_dependency) + def find_with_associations(options, join_dependency) rows = select_all_rows(options, join_dependency) join_dependency.instantiate(rows) rescue ThrowResult @@ -1770,7 +1770,7 @@ module ActiveRecord end def using_limitable_reflections?(reflections) - reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero? + reflections.collect(&:collection_association?).length.zero? end def column_aliases(join_dependency) @@ -1843,7 +1843,7 @@ module ActiveRecord case associations when Symbol, String reflection = base.reflections[associations] - if reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro) + if reflection && reflection.collection_association? records.each { |record| record.send(reflection.name).target.uniq! } end when Array @@ -1853,12 +1853,11 @@ module ActiveRecord when Hash associations.keys.each do |name| reflection = base.reflections[name] - is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro) parent_records = [] records.each do |record| if descendant = record.send(reflection.name) - if is_collection + if reflection.collection_association? parent_records.concat descendant.target.uniq else parent_records << descendant diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 98ab64537e..f01d0903cd 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -158,47 +158,39 @@ module ActiveRecord # # For performance reasons, we don't check whether to validate at runtime, # but instead only define the method and callback when needed. However, - # this can change, for instance, when using nested attributes. Since we - # don't want the callbacks to get defined multiple times, there are - # guards that check if the save or validation methods have already been - # defined before actually defining them. + # this can change, for instance, when using nested attributes, which is + # called _after_ the association has been defined. Since we don't want + # the callbacks to get defined multiple times, there are guards that + # check if the save or validation methods have already been defined + # before actually defining them. def add_autosave_association_callbacks(reflection) - save_method = "autosave_associated_records_for_#{reflection.name}" - validation_method = "validate_associated_records_for_#{reflection.name}" - force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true) + save_method = :"autosave_associated_records_for_#{reflection.name}" + validation_method = :"validate_associated_records_for_#{reflection.name}" + collection = reflection.collection_association? - case reflection.macro - when :has_many, :has_and_belongs_to_many - unless method_defined?(save_method) + unless method_defined?(save_method) + if collection before_save :before_save_collection_association define_method(save_method) { save_collection_association(reflection) } # Doesn't use after_save as that would save associations added in after_create/after_update twice after_create save_method after_update save_method - end - - if !method_defined?(validation_method) && - (force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)) - define_method(validation_method) { validate_collection_association(reflection) } - validate validation_method - end - else - unless method_defined?(save_method) - case reflection.macro - when :has_one + else + if reflection.macro == :has_one define_method(save_method) { save_has_one_association(reflection) } after_save save_method - when :belongs_to + else define_method(save_method) { save_belongs_to_association(reflection) } before_save save_method end end + end - if !method_defined?(validation_method) && force_validation - define_method(validation_method) { validate_single_association(reflection) } - validate validation_method - end + if reflection.validate? && !method_defined?(validation_method) + method = (collection ? :validate_collection_association : :validate_single_association) + define_method(validation_method) { send(method, reflection) } + validate validation_method end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 70776c7aa2..026bf55aaa 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -550,7 +550,7 @@ module ActiveRecord #:nodoc: # Determine whether to store the full constant name including namespace when using STI superclass_delegating_accessor :store_full_sti_class - self.store_full_sti_class = false + self.store_full_sti_class = true # Stores the default scope for the class class_inheritable_accessor :default_scoping, :instance_writer => false @@ -1510,11 +1510,17 @@ module ActiveRecord #:nodoc: end def active_relation_table(table_name_alias = nil) - Arel::Table.new(table_name, :as => table_name_alias) + Arel::Table.new(table_name, :as => table_name_alias, :engine => active_relation_engine) end def active_relation_engine - @active_relation_engine ||= Arel::Sql::Engine.new(self) + @active_relation_engine ||= begin + if self == ActiveRecord::Base + Arel::Table.engine + else + connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.active_relation_engine + end + end end private diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index e2a8f03c8f..aecde5848c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -9,7 +9,6 @@ module ActiveRecord # * (-) <tt>valid</tt> # * (1) <tt>before_validation</tt> # * (-) <tt>validate</tt> - # * (-) <tt>validate_on_create</tt> # * (2) <tt>after_validation</tt> # * (3) <tt>before_save</tt> # * (4) <tt>before_create</tt> @@ -223,9 +222,10 @@ module ActiveRecord extend ActiveModel::Callbacks + define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] + define_model_callbacks :initialize, :find, :only => :after define_model_callbacks :save, :create, :update, :destroy - define_model_callbacks :validation, :only => [:before, :after] end module ClassMethods @@ -236,6 +236,24 @@ module ActiveRecord send(meth.to_sym, meth.to_sym) end end + + def before_validation(*args, &block) + options = args.last + if options.is_a?(Hash) && options[:on] + options[:if] = Array(options[:if]) + options[:if] << "@_on_validate == :#{options[:on]}" + end + set_callback(:validation, :before, *args, &block) + end + + def after_validation(*args, &block) + options = args.extract_options! + options[:prepend] = true + options[:if] = Array(options[:if]) + options[:if] << "!halted && value != false" + options[:if] << "@_on_validate == :#{options[:on]}" if options[:on] + set_callback(:validation, :after, *(args << options), &block) + end end def create_or_update_with_callbacks #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d09aa3c4d2..5eedf448a4 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -201,7 +201,7 @@ module ActiveRecord protected def log(sql, name) result = nil - ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name) do + ActiveSupport::Notifications.instrument("active_record.sql", :sql => sql, :name => name) do @runtime += Benchmark.ms { result = yield } end result diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 99b812b5fc..b528c14bcc 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -3,7 +3,6 @@ require 'yaml' require 'csv' require 'zlib' require 'active_support/dependencies' -require 'active_support/test_case' require 'active_support/core_ext/logger' if RUBY_VERSION < '1.9' @@ -823,8 +822,8 @@ module ActiveRecord superclass_delegating_accessor :pre_loaded_fixtures self.fixture_table_names = [] - self.use_transactional_fixtures = false - self.use_instantiated_fixtures = true + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.fixture_class_names = {} diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index e33d389f8c..4115cc8e17 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -1,33 +1,9 @@ en: - activerecord: - errors: - # model.errors.full_messages format. - format: "{{attribute}} {{message}}" - - # 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)" - 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" - record_invalid: "Validation failed: {{errors}}" - # Append your own errors here or at the model/attributes scope. + errors: + messages: + taken: "has already been taken" + record_invalid: "Validation failed: {{errors}}" + # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. # The values :model, :attribute and :value are always available for interpolation. @@ -42,7 +18,14 @@ en: # Will define custom blank validation message for User model and # custom blank validation message for login attribute of User model. #models: - + + # Attributes names common to most models + #attributes: + #created_at: "Created at" + #updated_at: "Updated at" + + # ActiveRecord models configuration + #activerecord: # Translate model names. Used in Model.human_name(). #models: # For example, @@ -55,4 +38,3 @@ en: # user: # login: "Handle" # will translate User attribute "login" as "Handle" - diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 09a558a636..0022e8cc37 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -339,6 +339,10 @@ module ActiveRecord self.verbose = save end + def connection + ActiveRecord::Base.connection + end + def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' @@ -346,7 +350,7 @@ module ActiveRecord unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) end - Base.connection.send(method, *arguments, &block) + connection.send(method, *arguments, &block) end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index ff3a51d5c0..9038888d22 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -188,6 +188,8 @@ module ActiveRecord # the parent model is saved. This happens inside the transaction initiated # by the parents save method. See ActiveRecord::AutosaveAssociation. module ClassMethods + REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } } + # Defines an attributes writer for the specified association(s). If you # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you # will need to add the attribute writer to the allowed list. @@ -229,23 +231,14 @@ module ActiveRecord options = { :allow_destroy => false, :update_only => false } options.update(attr_names.extract_options!) options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only) + options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank attr_names.each do |association_name| if reflection = reflect_on_association(association_name) - type = case reflection.macro - when :has_one, :belongs_to - :one_to_one - when :has_many, :has_and_belongs_to_many - :collection - end - reflection.options[:autosave] = true add_autosave_association_callbacks(reflection) - self.nested_attributes_options[association_name.to_sym] = options - - if options[:reject_if] == :all_blank - self.nested_attributes_options[association_name.to_sym][:reject_if] = proc { |attributes| attributes.all? {|k,v| v.blank?} } - end + nested_attributes_options[association_name.to_sym] = options + type = (reflection.collection_association? ? :collection : :one_to_one) # def pirate_attributes=(attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) @@ -271,21 +264,11 @@ module ActiveRecord marked_for_destruction? end - # Deal with deprecated _delete. - # - def _delete #:nodoc: - ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead." - _destroy - end - private # Attribute hash keys that should not be assigned as normal attributes. # These hash keys are nested attributes implementation details. - # - # TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are - # removed. - UNASSIGNABLE_KEYS = %w( id _destroy _delete ) + UNASSIGNABLE_KEYS = %w( id _destroy ) # Assigns the given attributes to the association. # @@ -298,13 +281,17 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] attributes = attributes.with_indifferent_access check_existing_record = (options[:update_only] || !attributes['id'].blank?) if check_existing_record && (record = send(association_name)) && (options[:update_only] || record.id.to_s == attributes['id'].to_s) assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) + + elsif attributes['id'] + raise_nested_attributes_record_not_found(association_name, attributes['id']) + elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" if respond_to?(method) @@ -343,7 +330,7 @@ module ActiveRecord # { :id => '2', :_destroy => true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -366,6 +353,8 @@ module ActiveRecord end elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s } assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + else + raise_nested_attributes_record_not_found(association_name, attributes['id']) end end end @@ -382,8 +371,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) || - ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation. + ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) end # Determines if a new record should be build by checking for @@ -394,14 +382,17 @@ module ActiveRecord end def call_reject_if(association_name, attributes) - callback = self.nested_attributes_options[association_name][:reject_if] - - case callback + case callback = nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc - callback.try(:call, attributes) + callback.call(attributes) end end + + def raise_nested_attributes_record_not_found(association_name, record_id) + reflection = self.class.reflect_on_association(association_name) + raise RecordNotFound, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 657ee738c0..55008271b7 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -62,10 +62,21 @@ module ActiveRecord initializer "active_record.notifications" do require 'active_support/notifications' - ActiveSupport::Notifications.subscribe("sql") do |name, before, after, instrumenter_id, payload| + ActiveSupport::Notifications.subscribe("active_record.sql") do |name, before, after, instrumenter_id, payload| ActiveRecord::Base.connection.log_info(payload[:sql], payload[:name], (after - before) * 1000) end end + initializer "active_record.i18n_deprecation" do + require 'active_support/i18n' + + begin + I18n.t(:"activerecord.errors", :raise => true) + warn "[DEPRECATION] \"activerecord.errors\" namespace is deprecated in I18n " << + "yml files, please use just \"errors\" instead." + rescue Exception => e + # No message then. + end + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index a35a6c156b..03359221c3 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -46,7 +46,7 @@ namespace :db do $stderr.puts "Couldn't create database for #{config.inspect}" end end - return # Skip the else clause of begin/rescue + return # Skip the else clause of begin/rescue else ActiveRecord::Base.establish_connection(config) ActiveRecord::Base.connection @@ -246,6 +246,7 @@ namespace :db do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => :environment do require 'active_record/fixtures' + ActiveRecord::Base.establish_connection(Rails.env) base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir @@ -257,7 +258,7 @@ namespace :db do desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :identify => :environment do - require "active_record/fixtures" + require 'active_record/fixtures' label, id = ENV["LABEL"], ENV["ID"] raise "LABEL or ID required" if label.blank? && id.blank? @@ -295,7 +296,7 @@ namespace :db do if File.exists?(file) load(file) else - abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to prevent active_record from loading: config.frameworks -= [ :active_record ]} + abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/boot.rb to limit the frameworks that will be loaded} end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b751c9ad68..d6fad5cf29 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -252,10 +252,33 @@ module ActiveRecord end end + # Returns whether or not this association reflection is for a collection + # association. Returns +true+ if the +macro+ is one of +has_many+ or + # +has_and_belongs_to_many+, +false+ otherwise. + def collection_association? + if @collection_association.nil? + @collection_association = [:has_many, :has_and_belongs_to_many].include?(macro) + end + @collection_association + end + + # Returns whether or not the association should be validated as part of + # the parent's validation. + # + # Unless you explicitely disable validation with + # <tt>:validate => false</tt>, it will take place when: + # + # * you explicitely enable validation; <tt>:validate => true</tt> + # * you use autosave; <tt>:autosave => true</tt> + # * the association is a +has_many+ association + def validate? + !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) + end + private def derive_class_name class_name = name.to_s.camelize - class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro) + class_name = class_name.singularize if collection_association? class_name end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 114095b7ef..487b54f27d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,18 +1,18 @@ module ActiveRecord class Relation - include QueryMethods, FinderMethods, CalculationMethods + include QueryMethods, FinderMethods, CalculationMethods, SpawnMethods delegate :length, :collect, :map, :each, :all?, :to => :to_a attr_reader :relation, :klass attr_writer :readonly, :table - attr_accessor :preload_associations, :eager_load_associations, :include_associations + attr_accessor :preload_associations, :eager_load_associations, :includes_associations, :create_with_attributes def initialize(klass, relation) @klass, @relation = klass, relation @preload_associations = [] @eager_load_associations = [] - @include_associations = [] + @includes_associations = [] @loaded, @readonly = false end @@ -28,38 +28,6 @@ module ActiveRecord with_create_scope { @klass.create!(*args, &block) } end - def merge(r) - raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass - - merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.include_associations) - merged_relation.readonly = r.readonly - - [self.relation, r.relation].each do |arel| - merged_relation = merged_relation. - joins(arel.joins(arel)). - group(arel.groupings). - order(arel.send(:order_clauses).join(', ')). - limit(arel.taken). - offset(arel.skipped). - select(arel.send(:select_clauses)). - from(arel.sources) - end - - merged_wheres = @relation.wheres - - r.wheres.each do |w| - if w.is_a?(Arel::Predicates::Equality) - merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name } - end - - merged_wheres << w - end - - merged_relation.where(*merged_wheres) - end - - alias :& :merge - def respond_to?(method, include_private = false) return true if @relation.respond_to?(method, include_private) || Array.method_defined?(method) @@ -83,13 +51,13 @@ module ActiveRecord :select => @relation.send(:select_clauses).join(', '), :joins => @relation.joins(relation), :group => @relation.send(:group_clauses).join(', '), - :order => @relation.send(:order_clauses).join(', '), + :order => order_clause, :conditions => where_clause, :limit => @relation.taken, :offset => @relation.skipped, :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?) }, - ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @include_associations, nil)) + ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @includes_associations, nil)) rescue ThrowResult [] end @@ -98,7 +66,7 @@ module ActiveRecord end preload = @preload_associations - preload += @include_associations unless find_with_associations + preload += @includes_associations unless find_with_associations preload.each {|associations| @klass.send(:preload_associations, @records, associations) } @records.each { |record| record.readonly! } if @readonly @@ -156,23 +124,13 @@ module ActiveRecord end def reset - @first = @last = @create_scope = @to_sql = nil + @first = @last = @to_sql = @order_clause = @scope_for_create = nil @records = [] self end - def spawn(relation = @relation) - relation = Relation.new(@klass, relation) - relation.readonly = @readonly - relation.preload_associations = @preload_associations - relation.eager_load_associations = @eager_load_associations - relation.include_associations = @include_associations - relation.table = table - relation - end - def table - @table ||= Arel::Table.new(@klass.table_name, Arel::Sql::Engine.new(@klass)) + @table ||= Arel::Table.new(@klass.table_name, :engine => @klass.active_relation_engine) end def primary_key @@ -205,13 +163,15 @@ module ActiveRecord end def with_create_scope - @klass.send(:with_scope, :create => create_scope) { yield } + @klass.send(:with_scope, :create => scope_for_create) { yield } end - def create_scope - @create_scope ||= wheres.inject({}) do |hash, where| - hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality) - hash + def scope_for_create + @scope_for_create ||= begin + @create_with_attributes || wheres.inject({}) do |hash, where| + hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality) + hash + end end end @@ -219,6 +179,10 @@ module ActiveRecord @relation.send(:where_clauses).join(join_string) end + def order_clause + @order_clause ||= @relation.send(:order_clauses).join(', ') + end + def references_eager_loaded_tables? joined_tables = (tables_in_string(@relation.joins(relation)) + [table.name, table.table_alias]).compact.uniq (tables_in_string(to_sql) - joined_tables).any? diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index cf2cc7ba70..5d7bf0b7bc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,7 +6,7 @@ module ActiveRecord end def includes(*associations) - spawn.tap {|r| r.include_associations += Array.wrap(associations) } + spawn.tap {|r| r.includes_associations += Array.wrap(associations) } end def eager_load(*associations) @@ -17,6 +17,10 @@ module ActiveRecord spawn.tap {|r| r.readonly = status } end + def create_with(attributes = {}) + spawn.tap {|r| r.create_with_attributes = attributes } + end + def select(selects) if selects.present? relation = spawn(@relation.project(selects)) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb new file mode 100644 index 0000000000..4ecee8c634 --- /dev/null +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -0,0 +1,85 @@ +module ActiveRecord + module SpawnMethods + def spawn(relation = @relation) + relation = Relation.new(@klass, relation) + relation.readonly = @readonly + relation.preload_associations = @preload_associations + relation.eager_load_associations = @eager_load_associations + relation.includes_associations = @includes_associations + relation.create_with_attributes = @create_with_attributes + relation.table = table + relation + end + + def merge(r) + raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass + + merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.includes_associations) + merged_relation.readonly = r.readonly + + [self.relation, r.relation].each do |arel| + merged_relation = merged_relation. + joins(arel.joins(arel)). + group(arel.groupings). + limit(arel.taken). + offset(arel.skipped). + select(arel.send(:select_clauses)). + from(arel.sources). + having(arel.havings). + lock(arel.locked) + end + + relation_order = r.send(:order_clause) + merged_order = relation_order.present? ? relation_order : order_clause + merged_relation = merged_relation.order(merged_order) + + merged_relation.create_with_attributes = @create_with_attributes + + if @create_with_attributes && r.create_with_attributes + merged_relation.create_with_attributes = @create_with_attributes.merge(r.create_with_attributes) + else + merged_relation.create_with_attributes = r.create_with_attributes || @create_with_attributes + end + + merged_wheres = @relation.wheres + + r.wheres.each do |w| + if w.is_a?(Arel::Predicates::Equality) + merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name } + end + + merged_wheres << w + end + + merged_relation.where(*merged_wheres) + end + + alias :& :merge + + def except(*skips) + result = Relation.new(@klass, table) + result.table = table + + [:eager_load, :preload, :includes].each do |load_method| + result = result.send(load_method, send(:"#{load_method}_associations")) + end + + result.readonly = self.readonly unless skips.include?(:readonly) + result.create_with_attributes = @create_with_attributes unless skips.include?(:create_with) + + result = result.joins(@relation.joins(@relation)) unless skips.include?(:joins) + result = result.group(@relation.groupings) unless skips.include?(:group) + result = result.limit(@relation.taken) unless skips.include?(:limit) + result = result.offset(@relation.skipped) unless skips.include?(:offset) + result = result.select(@relation.send(:select_clauses)) unless skips.include?(:select) + result = result.from(@relation.sources) unless skips.include?(:from) + result = result.order(order_clause) unless skips.include?(:order) + result = result.where(*@relation.wheres) unless skips.include?(:where) + result = result.having(*@relation.havings) unless skips.include?(:having) + result = result.lock(@relation.locked) unless skips.include?(:lock) + + result + end + + end +end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 2dfe2c09ea..0a77ad5fd7 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,5 +1,3 @@ -require "active_support/test_case" - module ActiveRecord class TestCase < ActiveSupport::TestCase #:nodoc: def assert_date_from_db(expected, actual, message = nil) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 12c1f23763..d5adcba3ba 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -11,7 +11,7 @@ module ActiveRecord def initialize(record) @record = record errors = @record.errors.full_messages.join(I18n.t('support.array.words_connector', :default => ', ')) - super(I18n.t('activerecord.errors.messages.record_invalid', :errors => errors)) + super(I18n.t('errors.messages.record_invalid', :errors => errors)) end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index cf763d730a..116c8fc509 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -974,9 +974,9 @@ module AutosaveAssociationOnACollectionAssociationTests end def test_should_default_invalid_error_from_i18n - I18n.backend.store_translations(:en, :activerecord => { :errors => { :models => + I18n.backend.store_translations(:en, :errors => { :models => { @association_name.to_s.singularize.to_sym => { :blank => "cannot be blank" } } - }}) + }) @pirate.send(@association_name).build(:name => '') @@ -985,9 +985,7 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal ["#{@association_name.to_s.titleize} name cannot be blank"], @pirate.errors.full_messages assert @pirate.errors[@association_name].empty? ensure - I18n.backend.store_translations(:en, :activerecord => { :errors => { :models => - { @association_name.to_s.singularize.to_sym => nil } - }}) + I18n.backend = I18n::Backend::Simple.new end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 730d9d8df7..47e7a4bb79 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -2138,8 +2138,11 @@ class BasicsTest < ActiveRecord::TestCase end def test_type_name_with_module_should_handle_beginning + ActiveRecord::Base.store_full_sti_class = false assert_equal 'ActiveRecord::Person', ActiveRecord::Base.send(:type_name_with_module, 'Person') assert_equal '::Person', ActiveRecord::Base.send(:type_name_with_module, '::Person') + ensure + ActiveRecord::Base.store_full_sti_class = true end def test_to_param_should_return_string diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 5a084a611e..ff2322ac15 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -113,6 +113,26 @@ class ImmutableMethodDeveloper < ActiveRecord::Base end end +class OnCallbacksDeveloper < ActiveRecord::Base + set_table_name 'developers' + + before_validation { history << :before_validation } + before_validation(:on => :create){ history << :before_validation_on_create } + before_validation(:on => :update){ history << :before_validation_on_update } + + validate do + history << :validate + end + + after_validation { history << :after_validation } + after_validation(:on => :create){ history << :after_validation_on_create } + after_validation(:on => :update){ history << :after_validation_on_update } + + def history + @history ||= [] + end +end + class CallbackCancellationDeveloper < ActiveRecord::Base set_table_name 'developers' @@ -250,7 +270,18 @@ class CallbacksTest < ActiveRecord::TestCase ], david.history end - def test_save + def test_validate_on_create + david = OnCallbacksDeveloper.create('name' => 'David', 'salary' => 1000000) + assert_equal [ + :before_validation, + :before_validation_on_create, + :validate, + :after_validation, + :after_validation_on_create + ], david.history + end + + def test_update david = CallbackDeveloper.find(1) david.save assert_equal [ @@ -297,6 +328,18 @@ class CallbacksTest < ActiveRecord::TestCase ], david.history end + def test_validate_on_update + david = OnCallbacksDeveloper.find(1) + david.save + assert_equal [ + :before_validation, + :before_validation_on_update, + :validate, + :after_validation, + :after_validation_on_update + ], david.history + end + def test_destroy david = CallbackDeveloper.find(1) david.destroy diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 479970b2fa..fa76e2d57a 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -13,8 +13,6 @@ require 'test/unit' require 'stringio' require 'active_record' -require 'active_record/test_case' -require 'active_record/fixtures' require 'connection' begin diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 73e51fbd91..0672fb938b 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -241,6 +241,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase end def test_instantiation_doesnt_try_to_require_corresponding_file + ActiveRecord::Base.store_full_sti_class = false foo = Firm.find(:first).clone foo.ruby_type = foo.type = 'FirmOnTheFly' foo.save! @@ -259,5 +260,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase # And instantiate will find the existing constant rather than trying # to require firm_on_the_fly. assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } + ensure + ActiveRecord::Base.store_full_sti_class = true end end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 4f559bcaa5..d781a229f4 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -12,6 +12,8 @@ class ModulesTest < ActiveRecord::TestCase [:Firm, :Client].each do |const| @undefined_consts.merge! const => Object.send(:remove_const, const) if Object.const_defined?(const) end + + ActiveRecord::Base.store_full_sti_class = false end def teardown @@ -19,6 +21,8 @@ class ModulesTest < ActiveRecord::TestCase @undefined_consts.each do |constant, value| Object.send :const_set, constant, value unless value.nil? end + + ActiveRecord::Base.store_full_sti_class = true end def test_module_spanning_associations diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 7c3e0f2ca6..6155bfd50a 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/entrant' +require 'models/bird' # So we can test whether Course.connection survives a reload. require_dependency 'models/course' @@ -82,4 +83,9 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal "Ruby Development", Course.find(1).name assert_equal "Ruby Developer", Entrant.find(1).name end + + def test_arel_table_engines + assert_not_equal Entrant.active_relation_engine, Course.active_relation_engine + assert_equal Entrant.active_relation_engine, Bird.active_relation_engine + end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 8891282915..7ca9c416cb 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -34,7 +34,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_should_add_a_proc_to_nested_attributes_options - [:parrots, :birds, :birds_with_reject_all_blank].each do |name| + assert_equal ActiveRecord::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC, + Pirate.nested_attributes_options[:birds_with_reject_all_blank][:reject_if] + + [:parrots, :birds].each do |name| assert_instance_of Proc, Pirate.nested_attributes_options[name][:reject_if] end end @@ -79,12 +82,6 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert ship._destroy end - def test_underscore_delete_is_deprecated - ActiveSupport::Deprecation.expects(:warn) - ship = Ship.create!(:name => 'Nights Dirty Lightning') - ship._delete - end - def test_reject_if_method_without_arguments Pirate.accepts_nested_attributes_for :ship, :reject_if => :new_record? @@ -176,6 +173,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name end + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do + @pirate.ship_attributes = { :id => 1234567890 } + end + end + def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' } @@ -269,6 +272,8 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase + include AssertRaiseWithMessage + def setup @ship = Ship.new(:name => 'Nights Dirty Lightning') @pirate = @ship.build_pirate(:catchphrase => 'Aye') @@ -323,6 +328,12 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase assert_equal 'Arr', @ship.pirate.catchphrase end + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do + @ship.pirate_attributes = { :id => 1234567890 } + end + end + def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' } @@ -384,10 +395,6 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase assert Ship.reflect_on_association(:pirate).options[:autosave] end - def test_should_accept_update_only_option - @ship.update_attribute(:update_only_pirate_attributes, { :id => @pirate.ship.id, :catchphrase => 'Arr' }) - end - def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true @pirate.delete assert_difference('Pirate.count', 1) do @@ -460,6 +467,12 @@ module NestedAttributesOnACollectionAssociationTests assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] end + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do + @pirate.attributes = { association_getter => [{ :id => 1234567890 }] } + end + end + def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @pirate.send(@association_name).destroy_all @pirate.reload.attributes = { @@ -565,7 +578,7 @@ module NestedAttributesOnACollectionAssociationTests assert Pirate.reflect_on_association(@association_name).options[:autosave] end - def test_validate_presence_of_parent__works_with_inverse_of + def test_validate_presence_of_parent_works_with_inverse_of Man.accepts_nested_attributes_for(:interests) assert_equal :man, Man.reflect_on_association(:interests).options[:inverse_of] assert_equal :interests, Interest.reflect_on_association(:man).options[:inverse_of] @@ -582,7 +595,7 @@ module NestedAttributesOnACollectionAssociationTests end end - def test_validate_presence_of_parent__fails_without_inverse_of + def test_validate_presence_of_parent_fails_without_inverse_of Man.accepts_nested_attributes_for(:interests) Man.reflect_on_association(:interests).options.delete(:inverse_of) Interest.reflect_on_association(:man).options.delete(:inverse_of) diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index acd214eb5a..774ab7aa4c 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -4,10 +4,13 @@ require 'models/customer' require 'models/company' require 'models/company_in_module' require 'models/subscriber' +require 'models/ship' require 'models/pirate' require 'models/price_estimate' class ReflectionTest < ActiveRecord::TestCase + include ActiveRecord::Reflection + fixtures :topics, :customers, :companies, :subscribers, :price_estimates def setup @@ -68,22 +71,22 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = ActiveRecord::Reflection::MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil) + reflection = MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end end def test_aggregation_reflection - reflection_for_address = ActiveRecord::Reflection::AggregateReflection.new( + reflection_for_address = AggregateReflection.new( :composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) - reflection_for_balance = ActiveRecord::Reflection::AggregateReflection.new( + reflection_for_balance = AggregateReflection.new( :composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) - reflection_for_gps_location = ActiveRecord::Reflection::AggregateReflection.new( + reflection_for_gps_location = AggregateReflection.new( :composed_of, :gps_location, { }, Customer ) @@ -108,7 +111,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -120,7 +123,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = ActiveRecord::Reflection::AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -137,6 +140,8 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_reflection_in_modules + ActiveRecord::Base.store_full_sti_class = false + assert_reflection MyApplication::Business::Firm, :clients_of_firm, :klass => MyApplication::Business::Client, @@ -172,6 +177,8 @@ class ReflectionTest < ActiveRecord::TestCase :klass => MyApplication::Billing::Nested::Firm, :class_name => 'Nested::Firm', :table_name => 'companies' + ensure + ActiveRecord::Base.store_full_sti_class = true end def test_reflection_of_all_associations @@ -187,7 +194,44 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_through_reflection - assert_kind_of ActiveRecord::Reflection::ThroughReflection, Subscriber.reflect_on_association(:books) + assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books) + end + + def test_collection_association + assert Pirate.reflect_on_association(:birds).collection_association? + assert Pirate.reflect_on_association(:parrots).collection_association? + + assert !Pirate.reflect_on_association(:ship).collection_association? + assert !Ship.reflect_on_association(:pirate).collection_association? + end + + def test_default_association_validation + assert AssociationReflection.new(:has_many, :clients, {}, Firm).validate? + + assert !AssociationReflection.new(:has_one, :client, {}, Firm).validate? + assert !AssociationReflection.new(:belongs_to, :client, {}, Firm).validate? + assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, {}, Firm).validate? + end + + def test_always_validate_association_if_explicit + assert AssociationReflection.new(:has_one, :client, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:belongs_to, :client, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :validate => true }, Firm).validate? + end + + def test_validate_association_if_autosave + assert AssociationReflection.new(:has_one, :client, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:belongs_to, :client, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true }, Firm).validate? + end + + def test_never_validate_association_if_explicit + assert !AssociationReflection.new(:has_one, :client, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:belongs_to, :client, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_many, :clients, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true, :validate => false }, Firm).validate? end private diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 18f6152cc0..195889f1df 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -407,6 +407,11 @@ class RelationTest < ActiveRecord::TestCase end end + def test_relation_merging_with_locks + devs = Developer.lock.where("salary >= 80000").order("id DESC") & Developer.limit(2) + assert devs.locked.present? + end + def test_relation_merging_with_preload [Post.scoped & Post.preload(:author), Post.preload(:author) & Post.scoped].each do |posts| assert_queries(2) { assert posts.first.author } @@ -540,4 +545,23 @@ class RelationTest < ActiveRecord::TestCase assert_equal 'hen', hen.name end + def test_explicit_create_scope + hens = Bird.where(:name => 'hen') + assert_equal 'hen', hens.new.name + + hens = hens.create_with(:name => 'cock') + assert_equal 'cock', hens.new.name + end + + def test_except + relation = Post.where(:author_id => 1).order('id ASC').limit(1) + assert_equal [posts(:welcome)], relation.all + + author_posts = relation.except(:order, :limit) + assert_equal Post.where(:author_id => 1).all, author_posts.all + + all_posts = relation.except(:where, :order, :limit) + assert_equal Post.all, all_posts.all + 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 index 3f96d7973b..15730c2a87 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -6,15 +6,7 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase def setup Topic.reset_callbacks(:validate) @topic = Topic.new - I18n.backend.store_translations :'en', { - :activerecord => { - :errors => { - :messages => { - :taken => "has already been taken", - } - } - } - } + I18n.backend = I18n::Backend::Simple.new end # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index f017f24048..5dfbb1516f 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -4,13 +4,14 @@ require 'models/reply' class I18nValidationTest < ActiveRecord::TestCase repair_validations(Topic, Reply) + def setup Reply.validates_presence_of(:title) @topic = Topic.new - @old_load_path, @old_backend = I18n.load_path, I18n.backend + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations('en', :activerecord => {:errors => {:messages => {:custom => nil}}}) + I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}}) end def teardown @@ -30,75 +31,6 @@ class I18nValidationTest < ActiveRecord::TestCase end end - # ActiveRecord::Errors - def test_errors_generate_message_translates_custom_model_attribute_key - I18n.expects(:translate).with( - :topic, - { :count => 1, - :default => ['Topic'], - :scope => [:activerecord, :models] - } - ).returns('Topic') - - I18n.expects(:translate).with( - :"topic.title", - { :count => 1, - :default => ['Title'], - :scope => [:activerecord, :attributes] - } - ).returns('Title') - - I18n.expects(:translate).with( - :"models.topic.attributes.title.invalid", - :value => nil, - :scope => [:activerecord, :errors], - :default => [ - :"models.topic.invalid", - 'default from class def error 1', - :"messages.invalid"], - :attribute => "Title", - :model => "Topic" - ).returns('default from class def error 1') - - @topic.errors.generate_message :title, :invalid, :default => 'default from class def error 1' - end - - def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti - - I18n.expects(:translate).with( - :reply, - { :count => 1, - :default => [:topic, 'Reply'], - :scope => [:activerecord, :models] - } - ).returns('Reply') - - I18n.expects(:translate).with( - :"reply.title", - { :count => 1, - :default => [:'topic.title', 'Title'], - :scope => [:activerecord, :attributes] - } - ).returns('Title') - - I18n.expects(:translate).with( - :"models.reply.attributes.title.invalid", - :value => nil, - :scope => [:activerecord, :errors], - :default => [ - :"models.reply.invalid", - :"models.topic.attributes.title.invalid", - :"models.topic.invalid", - 'default from class def', - :"messages.invalid"], - :model => 'Reply', - :attribute => 'Title' - ).returns("default from class def") - - Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def' - - end - # validates_uniqueness_of w/ mocha def test_validates_uniqueness_of_generates_message @@ -115,6 +47,25 @@ class I18nValidationTest < ActiveRecord::TestCase @topic.valid? end + # validates_uniqueness_of w/o mocha + + def test_validates_associated_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :errors => {:models => {:topic => {:attributes => {:title => {:taken => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:taken => 'global message'}} + + Topic.validates_uniqueness_of :title + unique_topic.valid? + assert_equal ['custom message'], unique_topic.errors[:replies] + end + + def test_validates_associated_finds_global_default_translation + I18n.backend.store_translations 'en', :errors => {:messages => {:taken => 'global message'}} + + Topic.validates_uniqueness_of :title + unique_topic.valid? + assert_equal ['global message'], unique_topic.errors[:replies] + end + # validates_associated w/ mocha def test_validates_associated_generates_message @@ -132,8 +83,8 @@ class I18nValidationTest < ActiveRecord::TestCase # 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'}}} + I18n.backend.store_translations 'en', :errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}} Topic.validates_associated :replies replied_topic.valid? @@ -141,7 +92,7 @@ class I18nValidationTest < ActiveRecord::TestCase end def test_validates_associated_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}} Topic.validates_associated :replies replied_topic.valid? diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb index 5fa6d3023b..1e71d5d0dd 100644 --- a/activeresource/test/abstract_unit.rb +++ b/activeresource/test/abstract_unit.rb @@ -10,8 +10,6 @@ require 'rubygems' require 'test/unit' require 'active_resource' require 'active_support' -require 'active_support/test_case' -require 'active_model/test_case' $:.unshift "#{File.dirname(__FILE__)}/../test" require 'setter_trap' diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 9b0a84678a..87ec6f2a2c 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,8 @@ *Edge* +* Changed the default ActiveSupport.use_standard_json_time_format from false to true and +ActiveSupport.escape_html_entities_in_json from true to false to match previously announced Rails 3 defaults [DHH] + * Added Object#presence that returns the object if it's #present? otherwise returns nil [DHH/Colin Kelley] * Add Enumerable#exclude? to bring parity to Enumerable#include? and avoid if !x.include?/else calls [DHH] diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 3463000529..51ec87f329 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -66,6 +66,8 @@ module ActiveSupport autoload :StringInquirer autoload :XmlMini end + + autoload :TestCase end require 'active_support/vendor' diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index ad238c1d96..6360a4614e 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -247,13 +247,13 @@ module ActiveSupport expires_in || 0 end - def instrument(operation, key, options, &block) + def instrument(operation, key, options) log(operation, key, options) if self.class.instrument payload = { :key => key } payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument(:"cache_#{operation}", payload, &block) + ActiveSupport::Notifications.instrument("active_support.cache_#{operation}", payload){ yield } else yield end diff --git a/activesupport/lib/active_support/core_ext/float/rounding.rb b/activesupport/lib/active_support/core_ext/float/rounding.rb index 0b1ae4be7e..9bdf5bba7b 100644 --- a/activesupport/lib/active_support/core_ext/float/rounding.rb +++ b/activesupport/lib/active_support/core_ext/float/rounding.rb @@ -1,5 +1,6 @@ class Float - remove_method :round + alias precisionless_round round + private :precisionless_round # Rounds the float with the specified precision. # @@ -12,7 +13,7 @@ class Float magnitude = 10.0 ** precision (self * magnitude).round / magnitude else - super() + precisionless_round end end end diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb index 40bea54c67..8dff217ddc 100644 --- a/activesupport/lib/active_support/core_ext/integer/multiple.rb +++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb @@ -1,6 +1,6 @@ class Integer # Check whether the integer is evenly divisible by the argument. def multiple_of?(number) - self % number == 0 + number != 0 ? self % number == 0 : zero? end end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index c1f0e4bf81..887a40d35e 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -36,7 +36,7 @@ module ActiveSupport end def is_a?(klass) #:nodoc: - klass == Duration || super + Duration == klass || value.is_a?(klass) end # Returns true if <tt>other</tt> is also a Duration instance with the @@ -50,7 +50,9 @@ module ActiveSupport end def self.===(other) #:nodoc: - other.is_a?(Duration) rescue super + other.is_a?(Duration) + rescue NoMethodError + false end # Calculates a new Time or Date that is as far in the future diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index c8415d5449..8ba45f7ea2 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -114,7 +114,8 @@ module ActiveSupport end end - self.escape_html_entities_in_json = true + self.use_standard_json_time_format = true + self.escape_html_entities_in_json = false end CircularReferenceError = Deprecation::DeprecatedConstantProxy.new('ActiveSupport::JSON::CircularReferenceError', Encoding::CircularReferenceError) diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index d9bfcbfcab..3e96decb8c 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -44,11 +44,16 @@ module ActiveSupport class << self attr_writer :notifier - delegate :publish, :subscribe, :instrument, :to => :notifier + delegate :publish, :subscribe, :to => :notifier + delegate :instrument, :to => :instrumenter def notifier @notifier ||= Notifier.new end + + def instrumenter + Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier) + end end class Notifier @@ -67,13 +72,6 @@ module ActiveSupport def wait @queue.wait end - - delegate :instrument, :to => :current_instrumenter - - private - def current_instrumenter - Thread.current[:"instrumentation_#{object_id}"] ||= Notifications::Instrumenter.new(self) - end end end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 0655dd0cb6..3b0b0d8da2 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/module/delegation' module ActiveSupport module Notifications class Instrumenter + attr_reader :id + def initialize(notifier) @id = unique_id @notifier = notifier diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 6b554e7158..710dce78de 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -32,7 +32,6 @@ module ActiveSupport # t.is_a?(Time) # => true # t.is_a?(ActiveSupport::TimeWithZone) # => true class TimeWithZone - def self.name 'Time' # Report class name as 'Time' to thwart type checking end @@ -114,9 +113,9 @@ module ActiveSupport end alias_method :iso8601, :xmlschema - # Coerces the date to a string for JSON encoding. - # - # ISO 8601 format is used if ActiveSupport::JSON::Encoding.use_standard_json_time_format is set. + # Coerces the date to a string for JSON encoding. The default format is ISO 8601. You can get + # %Y/%m/%d %H:%M:%S +offset style by setting ActiveSupport::JSON::Encoding.use_standard_json_time_format + # to false. # # ==== Examples # diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index dda139372e..d91e0415c4 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -13,7 +13,6 @@ require 'mocha' ENV['NO_RELOAD'] = '1' require 'active_support' -require 'active_support/test_case' # Include shims until we get off 1.8.6 require 'active_support/ruby/shim' diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 42b4f10172..6530de1ef4 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -2,6 +2,31 @@ require 'abstract_unit' require 'active_support/time' class DurationTest < ActiveSupport::TestCase + def test_is_a + d = 1.day + assert d.is_a?(ActiveSupport::Duration) + assert d.is_a?(Numeric) + assert d.is_a?(Fixnum) + assert !d.is_a?(Hash) + + k = Class.new + class << k; undef_method :== end + assert !d.is_a?(k) + end + + def test_threequals + assert ActiveSupport::Duration === 1.day + assert !(ActiveSupport::Duration === 1.day.to_i) + assert !(ActiveSupport::Duration === 'foo') + assert !(ActiveSupport::Duration === ActiveSupport::BasicObject.new) + end + + def test_equals + assert 1.day == 1.day + assert 1.day == 1.day.to_i + assert !(1.day == 'foo') + end + def test_inspect assert_equal '0 seconds', 0.seconds.inspect assert_equal '1 month', 1.month.inspect diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb index e1591089f5..fe8c7eb224 100644 --- a/activesupport/test/core_ext/integer_ext_test.rb +++ b/activesupport/test/core_ext/integer_ext_test.rb @@ -5,6 +5,11 @@ class IntegerExtTest < Test::Unit::TestCase def test_multiple_of [ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) } [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) } + + # test the 0 edge case + assert 0.multiple_of?(0) + assert !5.multiple_of?(0) + # test with a prime assert !22953686867719691230002707821868552601124472329079.multiple_of?(2) assert !22953686867719691230002707821868552601124472329079.multiple_of?(3) diff --git a/activesupport/test/isolation_test.rb b/activesupport/test/isolation_test.rb index 39c2166016..a7af5e96f6 100644 --- a/activesupport/test/isolation_test.rb +++ b/activesupport/test/isolation_test.rb @@ -1,10 +1,10 @@ require 'abstract_unit' require 'rbconfig' -# if defined?(MiniTest) || defined?(Test::Unit::TestResultFailureSupport) -# $stderr.puts "Isolation tests can test test-unit 1 only" +if defined?(MiniTest) || defined?(Test::Unit::TestResultFailureSupport) + $stderr.puts "Isolation tests can test test-unit 1 only" -if ENV['CHILD'] +elsif ENV['CHILD'] class ChildIsolationTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 3ba77ae135..62849c3d04 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -5,7 +5,8 @@ module Notifications def setup Thread.abort_on_exception = true - @notifier = ActiveSupport::Notifications::Notifier.new + ActiveSupport::Notifications.notifier = nil + @notifier = ActiveSupport::Notifications.notifier @events = [] @notifier.subscribe { |*args| @events << event(*args) } end @@ -82,13 +83,19 @@ module Notifications end class InstrumentationTest < TestCase + delegate :instrument, :to => ActiveSupport::Notifications + def test_instrument_returns_block_result - assert_equal 2, @notifier.instrument(:awesome) { 1 + 1 } + assert_equal 2, instrument(:awesome) { 1 + 1 } + end + + def test_instrumenter_exposes_its_id + assert_equal 20, ActiveSupport::Notifications.instrumenter.id.size end def test_nested_events_can_be_instrumented - @notifier.instrument(:awesome, :payload => "notifications") do - @notifier.instrument(:wot, :payload => "child") do + instrument(:awesome, :payload => "notifications") do + instrument(:wot, :payload => "child") do 1 + 1 end @@ -108,7 +115,7 @@ module Notifications def test_instrument_publishes_when_exception_is_raised begin - @notifier.instrument(:awesome, :payload => "notifications") do + instrument(:awesome, :payload => "notifications") do raise "OMG" end flunk @@ -123,7 +130,7 @@ module Notifications end def test_event_is_pushed_even_without_block - @notifier.instrument(:awesome, :payload => "notifications") + instrument(:awesome, :payload => "notifications") drain assert_equal 1, @events.size diff --git a/rack b/rack deleted file mode 160000 -Subproject c6805fb93da30e0056b38e0fa6015c3d1bca587 diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 0bc1ea32bc..fc9277bd28 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,9 @@ *Edge* +* Removed config/initializers/new_rails_defaults.rb as all frameworks now follow the settings from it [DHH] + +* Set config.time_zone to UTC by default [DHH] + * Added default .gitignore (this is just recognizing Git market share, don't throw a hissy if you use another SCM) [DHH] * Added cookies.permanent, cookies.signed, and cookies.permanent.signed accessor for common cookie actions [DHH]. Examples: diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index d69e3eea6a..41672325bd 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -32,6 +32,8 @@ end RAILS_ENV = (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup unless defined?(RAILS_ENV) module Rails + autoload :Bootstrap, 'rails/bootstrap' + # Needs to be duplicated from Active Support since its needed before Active # Support is available. Here both Options and Hash are namespaced to prevent # conflicts with other implementations AND with the classes residing in Active Support. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 26ffbe5a58..6034aa8485 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -8,7 +8,7 @@ module Rails class << self attr_writer :config alias configure class_eval - delegate :initialize!, :load_tasks, :to => :instance + delegate :initialize!, :load_tasks, :root, :to => :instance private :new def instance @@ -83,17 +83,19 @@ module Rails end def initializers - initializers = super + initializers = Bootstrap.new(self).initializers plugins.each { |p| initializers += p.initializers } + initializers += super initializers end # TODO: Fix this method def plugins @plugins ||= begin - plugin_names = config.plugins || [:all] - Railtie.plugins.select { |p| plugin_names.include?(:all) || plugin_names.include?(p.plugin_name) }.map { |p| p.new } + - Plugin.all(config.plugins || [:all], config.paths.vendor.plugins) + plugin_names = (config.plugins || [:all]).map { |p| p.to_sym } + Railtie.plugins.select { |p| + plugin_names.include?(:all) || plugin_names.include?(p.plugin_name) + }.map { |p| p.new } + Plugin.all(plugin_names, config.paths.vendor.plugins) end end @@ -102,137 +104,6 @@ module Rails @app.call(env) end - initializer :load_all_active_support do - require "active_support/all" unless config.active_support.bare - end - - # Set the <tt>$LOAD_PATH</tt> based on the value of - # Configuration#load_paths. Duplicates are removed. - initializer :set_load_path do - config.paths.add_to_load_path - $LOAD_PATH.uniq! - end - - # Set the paths from which Rails will automatically load source files, and - # the load_once paths. - initializer :set_autoload_paths do - require 'active_support/dependencies' - ActiveSupport::Dependencies.load_paths = config.load_paths.uniq - ActiveSupport::Dependencies.load_once_paths = config.load_once_paths.uniq - - extra = ActiveSupport::Dependencies.load_once_paths - ActiveSupport::Dependencies.load_paths - unless extra.empty? - abort <<-end_error - load_once_paths must be a subset of the load_paths. - Extra items in load_once_paths: #{extra * ','} - end_error - end - - # Freeze the arrays so future modifications will fail rather than do nothing mysteriously - config.load_once_paths.freeze - end - - # Create tmp directories - initializer :ensure_tmp_directories_exist do - %w(cache pids sessions sockets).each do |dir_to_make| - FileUtils.mkdir_p(File.join(root, 'tmp', dir_to_make)) - end - end - - # Preload all frameworks specified by the Configuration#frameworks. - # Used by Passenger to ensure everything's loaded before forking and - # to avoid autoload race conditions in JRuby. - initializer :preload_frameworks do - ActiveSupport::Autoload.eager_autoload! if config.preload_frameworks - end - - initializer :initialize_cache do - unless defined?(RAILS_CACHE) - silence_warnings { Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(config.cache_store) } - - if RAILS_CACHE.respond_to?(:middleware) - # Insert middleware to setup and teardown local cache for each request - config.middleware.insert_after(:"Rack::Lock", RAILS_CACHE.middleware) - end - end - end - - initializer :initialize_logger do - # if the environment has explicitly defined a logger, use it - next if Rails.logger - - unless logger = config.logger - begin - logger = ActiveSupport::BufferedLogger.new(config.log_path) - logger.level = ActiveSupport::BufferedLogger.const_get(config.log_level.to_s.upcase) - if RAILS_ENV == "production" - logger.auto_flushing = false - end - rescue StandardError => e - logger = ActiveSupport::BufferedLogger.new(STDERR) - logger.level = ActiveSupport::BufferedLogger::WARN - logger.warn( - "Rails Error: Unable to access log file. Please ensure that #{config.log_path} exists and is chmod 0666. " + - "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." - ) - end - end - - # TODO: Why are we silencing warning here? - silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } - end - - # Sets the logger for Active Record, Action Controller, and Action Mailer - # (but only for those frameworks that are to be loaded). If the framework's - # logger is already set, it is not changed, otherwise it is set to use - # RAILS_DEFAULT_LOGGER. - initializer :initialize_framework_logging do - ActiveSupport::Dependencies.logger ||= Rails.logger - Rails.cache.logger ||= Rails.logger - end - - # Sets the dependency loading mechanism based on the value of - # Configuration#cache_classes. - initializer :initialize_dependency_mechanism do - # TODO: Remove files from the $" and always use require - ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load - end - - # Loads support for "whiny nil" (noisy warnings when methods are invoked - # on +nil+ values) if Configuration#whiny_nils is true. - initializer :initialize_whiny_nils do - require('active_support/whiny_nil') if config.whiny_nils - end - - # Sets the default value for Time.zone - # If assigned value cannot be matched to a TimeZone, an exception will be raised. - initializer :initialize_time_zone do - if config.time_zone - require 'active_support/core_ext/time/zones' - zone_default = Time.__send__(:get_zone, config.time_zone) - - unless zone_default - raise \ - 'Value assigned to config.time_zone not recognized.' + - 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.' - end - - Time.zone_default = zone_default - end - end - - # Set the i18n configuration from config.i18n but special-case for the load_path which should be - # appended to what's already set instead of overwritten. - initializer :initialize_i18n do - config.i18n.each do |setting, value| - if setting == :load_path - I18n.load_path += value - else - I18n.send("#{setting}=", value) - end - end - end - # # bail out if gems are missing - note that check_gem_dependencies will have # # already called abort() unless $gems_rake_task is set # return unless gems_dependencies_loaded diff --git a/railties/lib/rails/bootstrap.rb b/railties/lib/rails/bootstrap.rb new file mode 100644 index 0000000000..15d5b2b59d --- /dev/null +++ b/railties/lib/rails/bootstrap.rb @@ -0,0 +1,140 @@ +module Rails + class Bootstrap #< Railtie + include Initializable + + def initialize(application) + @application = application + end + + delegate :config, :root, :to => :'@application' + + initializer :load_all_active_support do + require "active_support/all" unless config.active_support.bare + end + + # Set the <tt>$LOAD_PATH</tt> based on the value of + # Configuration#load_paths. Duplicates are removed. + initializer :set_load_path do + config.paths.add_to_load_path + $LOAD_PATH.uniq! + end + + # Set the paths from which Rails will automatically load source files, and + # the load_once paths. + initializer :set_autoload_paths do + require 'active_support/dependencies' + ActiveSupport::Dependencies.load_paths = config.load_paths.uniq + ActiveSupport::Dependencies.load_once_paths = config.load_once_paths.uniq + + extra = ActiveSupport::Dependencies.load_once_paths - ActiveSupport::Dependencies.load_paths + unless extra.empty? + abort <<-end_error + load_once_paths must be a subset of the load_paths. + Extra items in load_once_paths: #{extra * ','} + end_error + end + + # Freeze the arrays so future modifications will fail rather than do nothing mysteriously + config.load_once_paths.freeze + end + + # Create tmp directories + initializer :ensure_tmp_directories_exist do + %w(cache pids sessions sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(root, 'tmp', dir_to_make)) + end + end + + # Preload all frameworks specified by the Configuration#frameworks. + # Used by Passenger to ensure everything's loaded before forking and + # to avoid autoload race conditions in JRuby. + initializer :preload_frameworks do + ActiveSupport::Autoload.eager_autoload! if config.preload_frameworks + end + + initializer :initialize_cache do + unless defined?(RAILS_CACHE) + silence_warnings { Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(config.cache_store) } + + if RAILS_CACHE.respond_to?(:middleware) + # Insert middleware to setup and teardown local cache for each request + config.middleware.insert_after(:"Rack::Lock", RAILS_CACHE.middleware) + end + end + end + + initializer :initialize_logger do + # if the environment has explicitly defined a logger, use it + next if Rails.logger + + unless logger = config.logger + begin + logger = ActiveSupport::BufferedLogger.new(config.log_path) + logger.level = ActiveSupport::BufferedLogger.const_get(config.log_level.to_s.upcase) + if RAILS_ENV == "production" + logger.auto_flushing = false + end + rescue StandardError => e + logger = ActiveSupport::BufferedLogger.new(STDERR) + logger.level = ActiveSupport::BufferedLogger::WARN + logger.warn( + "Rails Error: Unable to access log file. Please ensure that #{config.log_path} exists and is chmod 0666. " + + "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." + ) + end + end + + # TODO: Why are we silencing warning here? + silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } + end + + # Sets the logger for Active Record, Action Controller, and Action Mailer + # (but only for those frameworks that are to be loaded). If the framework's + # logger is already set, it is not changed, otherwise it is set to use + # RAILS_DEFAULT_LOGGER. + initializer :initialize_framework_logging do + ActiveSupport::Dependencies.logger ||= Rails.logger + Rails.cache.logger ||= Rails.logger + end + + # Sets the dependency loading mechanism based on the value of + # Configuration#cache_classes. + initializer :initialize_dependency_mechanism do + # TODO: Remove files from the $" and always use require + ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load + end + + # Loads support for "whiny nil" (noisy warnings when methods are invoked + # on +nil+ values) if Configuration#whiny_nils is true. + initializer :initialize_whiny_nils do + require('active_support/whiny_nil') if config.whiny_nils + end + + # Sets the default value for Time.zone + # If assigned value cannot be matched to a TimeZone, an exception will be raised. + initializer :initialize_time_zone do + require 'active_support/core_ext/time/zones' + zone_default = Time.__send__(:get_zone, config.time_zone) + + unless zone_default + raise \ + 'Value assigned to config.time_zone not recognized.' + + 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.' + end + + Time.zone_default = zone_default + end + + # Set the i18n configuration from config.i18n but special-case for the load_path which should be + # appended to what's already set instead of overwritten. + initializer :initialize_i18n do + config.i18n.each do |setting, value| + if setting == :load_path + I18n.load_path += value + else + I18n.send("#{setting}=", value) + end + end + end + end +end diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb index f85c17bb94..a2eff377ce 100644 --- a/railties/lib/rails/commands/destroy.rb +++ b/railties/lib/rails/commands/destroy.rb @@ -7,4 +7,4 @@ if ARGV.size == 0 end name = ARGV.shift -Rails::Generators.invoke name, ARGV, :behavior => :revoke +Rails::Generators.invoke name, ARGV, :behavior => :revoke, :destination_root => Rails.root diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb index c5e3ae3529..c1120aad74 100755 --- a/railties/lib/rails/commands/generate.rb +++ b/railties/lib/rails/commands/generate.rb @@ -7,4 +7,4 @@ if ARGV.size == 0 end name = ARGV.shift -Rails::Generators.invoke name, ARGV, :behavior => :invoke +Rails::Generators.invoke name, ARGV, :behavior => :invoke, :destination_root => Rails.root diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index f0a0d5e55e..c180031c61 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -4,11 +4,24 @@ module Rails # Temporarily separate the plugin configuration class from the main # configuration class while this bit is being cleaned up. class Railtie::Configuration - def self.default @default ||= new end + def self.default_middleware_stack + ActionDispatch::MiddlewareStack.new.tap do |middleware| + middleware.use('ActionDispatch::Static', lambda { Rails.public_path }, :if => lambda { Rails.application.config.serve_static_assets }) + middleware.use('::Rack::Lock', :if => lambda { !ActionController::Base.allow_concurrency }) + middleware.use('::Rack::Runtime') + middleware.use('ActionDispatch::ShowExceptions', lambda { ActionController::Base.consider_all_requests_local }) + middleware.use('ActionDispatch::Callbacks', lambda { ActionController::Dispatcher.prepare_each_request }) + middleware.use(lambda { ActionController::Base.session_store }, lambda { ActionController::Base.session_options }) + middleware.use('ActionDispatch::ParamsParser') + middleware.use('::Rack::MethodOverride') + middleware.use('::Rack::Head') + end + end + attr_reader :middleware def initialize(base = nil) @@ -17,7 +30,7 @@ module Rails @middleware = base.middleware.dup else @options = Hash.new { |h,k| h[k] = ActiveSupport::OrderedOptions.new } - @middleware = ActionDispatch::MiddlewareStack.new + @middleware = self.class.default_middleware_stack end end @@ -120,8 +133,8 @@ module Rails end def frameworks(*args) - raise "config.frameworks in no longer supported. See the generated" \ - "config/boot.rb for steps on how to limit the frameworks that" \ + raise "config.frameworks in no longer supported. See the generated " \ + "config/boot.rb for steps on how to limit the frameworks that " \ "will be loaded" end alias frameworks= frameworks @@ -231,6 +244,10 @@ module Rails @log_level ||= RAILS_ENV == 'production' ? :info : :debug end + def time_zone + @time_zone ||= "UTC" + end + def i18n @i18n ||= begin i18n = ActiveSupport::OrderedOptions.new diff --git a/railties/lib/rails/deprecation.rb b/railties/lib/rails/deprecation.rb index 3c5b8bdec7..155166a113 100644 --- a/railties/lib/rails/deprecation.rb +++ b/railties/lib/rails/deprecation.rb @@ -15,11 +15,3 @@ RAILS_ROOT = (Class.new(ActiveSupport::Deprecation::DeprecationProxy) do ActiveSupport::Deprecation.warn(msg, callstack) end end).new - -module Rails - class Configuration - def gem(*args) - ActiveSupport::Deprecation.warn("config.gem has been deprecated in favor of the Gemfile.") - end - end -end
\ No newline at end of file diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 6419961415..3713a38b33 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -117,11 +117,15 @@ module Rails end # Remove the color from output. - # def self.no_color! Thor::Base.shell = Thor::Shell::Basic end + # Track all generators subclasses. + def self.subclasses + @subclasses ||= [] + end + # Generators load paths used on lookup. The lookup happens as: # # 1) lib generators @@ -147,18 +151,10 @@ module Rails end load_paths # Cache load paths. Needed to avoid __FILE__ pointing to wrong paths. - # Rails finds namespaces exactly as thor, with three conveniences: - # - # 1) If your generator name ends with generator, as WebratGenerator, it sets - # its namespace to "webrat", so it can be invoked as "webrat" and not - # "webrat_generator"; + # Rails finds namespaces similar to thor, it only adds one rule: # - # 2) If your generator has a generators namespace, as Rails::Generators::WebratGenerator, - # the namespace is set to "rails:generators:webrat", but Rails allows it - # to be invoked simply as "rails:webrat". The "generators" is added - # automatically when doing the lookup; - # - # 3) Rails looks in load paths and loads the generator just before it's going to be used. + # Generators names must end with "_generator.rb". This is required because Rails + # looks in load paths and loads the generator just before it's going to be used. # # ==== Examples # @@ -166,113 +162,81 @@ module Rails # # Will search for the following generators: # - # "rails:generators:webrat", "webrat:generators:integration", "webrat" - # - # On the other hand, if "rails:webrat" is given, it will search for: + # "rails:webrat", "webrat:integration", "webrat" # - # "rails:generators:webrat", "rails:webrat" - # - # Notice that the "generators" namespace is handled automatically by Rails, - # so you don't need to type it when you want to invoke a generator in specific. + # Notice that "rails:generators:webrat" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. # def self.find_by_namespace(name, base=nil, context=nil) #:nodoc: - name, attempts = name.to_s, [ ] - - case name.count(':') - when 1 - base, name = name.split(':') - return find_by_namespace(name, base) - when 0 - attempts += generator_names(base, name) if base - attempts += generator_names(name, context) if context - end - - attempts << name - attempts += generator_names(name, name) unless name.include?(?:) - attempts.uniq! - - unloaded = attempts - namespaces - lookup(unloaded) + # Mount regexps to lookup + regexps = [] + regexps << /^#{base}:[\w:]*#{name}$/ if base + regexps << /^#{name}:[\w:]*#{context}$/ if context + regexps << /^[(#{name}):]+$/ + regexps.uniq! + + # Check if generator happens to be loaded + checked = subclasses.dup + klass = find_by_regexps(regexps, checked) + return klass if klass + + # Try to require other generators by looking in load_paths + lookup(name, context) + unchecked = subclasses - checked + klass = find_by_regexps(regexps, unchecked) + return klass if klass + + # Invoke fallbacks + invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) + end - attempts.each do |namespace| - klass = Thor::Util.find_by_namespace(namespace) - return klass if klass + # Tries to find a generator which the namespace match the regexp. + def self.find_by_regexps(regexps, klasses) + klasses.find do |klass| + namespace = klass.namespace + regexps.find { |r| namespace =~ r } end - - invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) end # Receives a namespace, arguments and the behavior to invoke the generator. # It's used as the default entry point for generate, destroy and update # commands. - # def self.invoke(namespace, args=ARGV, config={}) - if klass = find_by_namespace(namespace, "rails") + names = namespace.to_s.split(':') + + if klass = find_by_namespace(names.pop, names.shift || "rails") args << "--help" if klass.arguments.any? { |a| a.required? } && args.empty? - klass.start args, config + klass.start(args, config) else puts "Could not find generator #{namespace}." end end # Show help message with available generators. - # def self.help - rails = Rails::Generators.builtin.map do |group, name| - name if group == "rails" - end - rails.compact! - rails.sort! - - puts "Please select a generator." - puts "Builtin: #{rails.join(', ')}." - - # Load paths and remove builtin - paths, others = load_paths.dup, [] - paths.pop - - paths.each do |path| - tail = [ "*", "*", "*_generator.rb" ] - - until tail.empty? - others += Dir[File.join(path, *tail)].collect do |file| - name = file.split('/')[-tail.size, 2] - name.last.sub!(/_generator\.rb$/, '') - name.uniq! - name.join(':') - end - tail.shift - end - end + builtin = Rails::Generators.builtin.each { |n| n.sub!(/^rails:/, '') } + builtin.sort! + lookup("*") + others = subclasses.map{ |k| k.namespace.gsub(':generators:', ':') } + others -= Rails::Generators.builtin others.sort! + + puts "Please select a generator." + puts "Builtin: #{builtin.join(', ')}." puts "Others: #{others.join(', ')}." unless others.empty? end protected - # Return all defined namespaces. - # - def self.namespaces #:nodoc: - Thor::Base.subclasses.map { |klass| klass.namespace } - end - - # Keep builtin generators in an Array[Array[group, name]]. - # + # Keep builtin generators in an Array. def self.builtin #:nodoc: Dir[File.dirname(__FILE__) + '/generators/*/*'].collect do |file| - file.split('/')[-2, 2] + file.split('/')[-2, 2].join(':') end end - # By default, Rails strips the generator namespace to make invocations - # easier. This method generaters the both possibilities names. - def self.generator_names(first, second) #:nodoc: - [ "#{first}:generators:#{second}", "#{first}:#{second}" ] - end - - # Try callbacks for the given base. - # + # Try fallbacks for the given base. def self.invoke_fallbacks_for(name, base) #:nodoc: return nil unless base && fallbacks[base.to_sym] invoked_fallbacks = [] @@ -290,10 +254,10 @@ module Rails # Receives namespaces in an array and tries to find matching generators # in the load path. - # - def self.lookup(attempts) #:nodoc: - attempts = attempts.map { |a| "#{a.split(":").last}_generator" }.uniq - attempts = "{#{attempts.join(',')}}.rb" + def self.lookup(*attempts) #:nodoc: + attempts.compact! + attempts.uniq! + attempts = "{#{attempts.join(',')}}_generator.rb" self.load_paths.each do |path| Dir[File.join(path, '**', attempts)].each do |file| diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 226ae63963..26abb46644 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -12,16 +12,6 @@ module Rails add_runtime_options! - # Always move to rails source root. - # - def initialize(*args) #:nodoc: - if !invoked?(args) && defined?(Rails.root) && Rails.root - self.destination_root = Rails.root - FileUtils.cd(destination_root) - end - super - end - # Automatically sets the source root based on the class name. # def self.source_root @@ -76,17 +66,18 @@ module Rails # # The controller generator will then try to invoke the following generators: # - # "rails:generators:test_unit", "test_unit:generators:controller", "test_unit" + # "rails:test_unit", "test_unit:controller", "test_unit" # - # In this case, the "test_unit:generators:controller" is available and is - # invoked. This allows any test framework to hook into Rails as long as it - # provides any of the hooks above. + # Notice that "rails:generators:test_unit" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. This is what + # allows any test framework to hook into Rails as long as it provides any + # of the hooks above. # # ==== Options # - # This lookup can be customized with two options: :base and :as. The first - # is the root module value and in the example above defaults to "rails". - # The later defaults to the generator name, without the "Generator" ending. + # The first and last part used to find the generator to be invoked are + # guessed based on class invokes hook_for, as noticed in the example above. + # This can be customized with two options: :base and :as. # # Let's suppose you are creating a generator that needs to invoke the # controller generator from test unit. Your first attempt is: @@ -97,7 +88,7 @@ module Rails # # The lookup in this case for test_unit as input is: # - # "test_unit:generators:awesome", "test_unit" + # "test_unit:awesome", "test_unit" # # Which is not the desired the lookup. You can change it by providing the # :as option: @@ -108,18 +99,18 @@ module Rails # # And now it will lookup at: # - # "test_unit:generators:awesome", "test_unit" + # "test_unit:controller", "test_unit" # # Similarly, if you want it to also lookup in the rails namespace, you just # need to provide the :base value: # # class AwesomeGenerator < Rails::Generators::Base - # hook_for :test_framework, :base => :rails, :as => :controller + # hook_for :test_framework, :in => :rails, :as => :controller # end # # And the lookup is exactly the same as previously: # - # "rails:generators:test_unit", "test_unit:generators:controller", "test_unit" + # "rails:test_unit", "test_unit:controller", "test_unit" # # ==== Switches # @@ -151,11 +142,11 @@ module Rails # ==== Custom invocations # # You can also supply a block to hook_for to customize how the hook is - # going to be invoked. The block receives two parameters, an instance + # going to be invoked. The block receives two arguments, an instance # of the current class and the klass to be invoked. # # For example, in the resource generator, the controller should be invoked - # with a pluralized class name. By default, it is invoked with the same + # with a pluralized class name. But by default it is invoked with the same # name as the resource generator, which is singular. To change this, we # can give a block to customize how the controller can be invoked. # @@ -178,11 +169,11 @@ module Rails end unless class_options.key?(name) - class_option name, defaults.merge!(options) + class_option(name, defaults.merge!(options)) end hooks[name] = [ in_base, as_hook ] - invoke_from_option name, options, &block + invoke_from_option(name, options, &block) end end @@ -193,7 +184,7 @@ module Rails # remove_hook_for :orm # def self.remove_hook_for(*names) - remove_invocation *names + remove_invocation(*names) names.each do |name| hooks.delete(name) @@ -219,12 +210,16 @@ module Rails # and can point to wrong directions when inside an specified directory. base.source_root - if base.name && base.name !~ /Base$/ && base.base_name && base.generator_name && defined?(Rails.root) && Rails.root - path = File.expand_path(File.join(Rails.root, 'lib', 'templates')) - if base.name.include?('::') - base.source_paths << File.join(path, base.base_name, base.generator_name) - else - base.source_paths << File.join(path, base.generator_name) + if base.name && base.name !~ /Base$/ + Rails::Generators.subclasses << base + + if defined?(Rails.root) && Rails.root + path = File.expand_path(File.join(Rails.root, 'lib', 'templates')) + if base.name.include?('::') + base.source_paths << File.join(path, base.base_name, base.generator_name) + else + base.source_paths << File.join(path, base.generator_name) + end end end end @@ -263,13 +258,6 @@ module Rails end end - # Check if this generator was invoked from another one by inspecting - # parameters. - # - def invoked?(args) - args.last.is_a?(Hash) && args.last.key?(:invocations) - end - # Use Rails default banner. # def self.banner @@ -290,12 +278,10 @@ module Rails # Rails::Generators::MetalGenerator will return "metal" as generator name. # def self.generator_name - if name - @generator_name ||= begin - if klass_name = name.to_s.split('::').last - klass_name.sub!(/Generator$/, '') - klass_name.underscore - end + @generator_name ||= begin + if generator = name.to_s.split('::').last + generator.sub!(/Generator$/, '') + generator.underscore end end end @@ -339,6 +325,7 @@ module Rails # def self.prepare_for_invocation(name, value) #:nodoc: if value && constants = self.hooks[name] + value = name if TrueClass === value Rails::Generators.find_by_namespace(value, *constants) else super diff --git a/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb index 51c4ad0e2e..7aa049fe80 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb @@ -1,7 +1,6 @@ <!DOCTYPE html> <html> <head> - <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title><%= controller_class_name %>: <%%= controller.action_name %></title> <%%= stylesheet_link_tag 'scaffold' %> </head> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index ee401b1fde..ba3eb67fe0 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -46,15 +46,16 @@ module Rails::Generators def initialize(*args) super - if !options[:no_activerecord] && !DATABASES.include?(options[:database]) + if !options[:skip_activerecord] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end end def create_root self.destination_root = File.expand_path(app_path, destination_root) - empty_directory '.' + valid_app_const? + empty_directory '.' set_default_accessors! FileUtils.cd(destination_root) end @@ -192,8 +193,19 @@ module Rails::Generators @app_name ||= File.basename(destination_root) end + def app_const_base + @app_const_base ||= app_name.gsub(/\W/, '_').squeeze('_').camelize + end + def app_const - @app_const ||= "#{app_name.classify}::Application" + @app_const ||= "#{app_const_base}::Application" + end + + def valid_app_const? + case app_const + when /^\d/ + raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." + end end def app_secret diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 59c6d333e2..7b5c89c3e2 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -5,7 +5,8 @@ gem "rails", "<%= Rails::VERSION::STRING %>" ## Bundle edge rails: <%- if options.dev? -%> -gem "rails", :path => "<%= Rails::Generators::RAILS_DEV_PATH %>" +directory "<%= Rails::Generators::RAILS_DEV_PATH %>", :glob => "{*/,}*.gemspec" +gem "rails", "<%= Rails::VERSION::STRING %>" <%- else -%> <%= "# " unless options.edge? %>gem "rails", :git => "git://github.com/rails/rails.git" <%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb index 9889b52893..2cdf4eae54 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb @@ -1,8 +1,4 @@ -# Filters added to this controller apply to all controllers in the application. -# Likewise, all the methods added will be available for all controllers. - class ApplicationController < ActionController::Base - helper :all protect_from_forgery filter_parameter_logging :password end diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb index 22a7940eb2..de6be7945c 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb @@ -1,3 +1,2 @@ -# Methods added to this helper will be available to all templates in the application. module ApplicationHelper end diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru index acb8435446..2ab821e38d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru +++ b/railties/lib/rails/generators/rails/app/templates/config.ru @@ -1,5 +1,4 @@ -# Require your environment file to bootstrap Rails -require ::File.expand_path('../config/environment', __FILE__) +# This file is used by Rack-based servers to start the application. -# Dispatch the request +require ::File.expand_path('../config/environment', __FILE__) run <%= app_const %>.instance diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index ec0729db04..334820826f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -1,13 +1,13 @@ require File.expand_path('../boot', __FILE__) -module <%= app_name.classify %> +module <%= app_const_base %> class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Add additional load paths for your own custom dirs - # config.load_paths += %W( #{root}/extras ) + # config.load_paths += %W( #{config.root}/extras ) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named @@ -17,15 +17,14 @@ module <%= app_name.classify %> # config.active_record.observers = :cacher, :garbage_collector, :forum_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. - config.time_zone = 'UTC' + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] # config.i18n.default_locale = :de - # Configure generators values. Many other options are available, be sure to - # check the documentation. + # Configure generators values. Many other options are available, be sure to check the documentation. # config.generators do |g| # g.orm :active_record # g.template_engine :erb diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookie_verification_secret.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookie_verification_secret.rb.tt index 9f05cd5a31..451dbe1d1c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookie_verification_secret.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookie_verification_secret.rb.tt @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -ActionController::Base.cookie_verifier_secret = '<%= app_secret %>'; +ActionController::Base.cookie_verifier_secret = '<%= app_secret %>' diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_rails_defaults.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_rails_defaults.rb deleted file mode 100644 index 8ec3186c84..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_rails_defaults.rb +++ /dev/null @@ -1,19 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# These settings change the behavior of Rails 2 apps and will be defaults -# for Rails 3. You can remove this initializer when Rails 3 is released. - -if defined?(ActiveRecord) - # Include Active Record class name as root for JSON serialized output. - ActiveRecord::Base.include_root_in_json = true - - # Store the full class name (including module namespace) in STI type column. - ActiveRecord::Base.store_full_sti_class = true -end - -# Use ISO 8601 format for JSON serialized times and dates. -ActiveSupport.use_standard_json_time_format = true - -# Don't escape HTML entities in JSON, leave that for the #json_escape helper. -# if you're including raw json in an HTML page. -ActiveSupport.escape_html_entities_in_json = false
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt index 4499ab84b6..baff704d3e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt @@ -5,8 +5,8 @@ # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. ActionController::Base.session = { - :key => '_<%= app_name %>_session', - :secret => '<%= app_secret %>' + :key => '_<%= app_name %>_session', + :secret => '<%= app_secret %>' } # Use the database for sessions instead of the cookie-based default, diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html index 88ee108e90..9a48320a5f 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/404.html +++ b/railties/lib/rails/generators/rails/app/templates/public/404.html @@ -1,7 +1,6 @@ <!DOCTYPE html> <html> <head> - <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>The page you were looking for doesn't exist (404)</title> <style type="text/css"> body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html index 9c3c96670b..83660ab187 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/422.html +++ b/railties/lib/rails/generators/rails/app/templates/public/422.html @@ -1,7 +1,6 @@ <!DOCTYPE html> <html> <head> - <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>The change you wanted was rejected (422)</title> <style type="text/css"> body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html index f71c86e652..b80307fc16 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/500.html +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -1,7 +1,6 @@ <!DOCTYPE html> <html> <head> - <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>We're sorry, but something went wrong (500)</title> <style type="text/css"> body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } diff --git a/railties/lib/rails/generators/rails/app/templates/public/index.html b/railties/lib/rails/generators/rails/app/templates/public/index.html index ff2dfd3193..b153ae392f 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/index.html +++ b/railties/lib/rails/generators/rails/app/templates/public/index.html @@ -1,7 +1,6 @@ <!DOCTYPE html> <html> <head> - <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <title>Ruby on Rails: Welcome aboard</title> <style type="text/css" media="screen"> body { diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index a16f587d8b..45b551fc7d 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -3,31 +3,6 @@ require File.expand_path(File.dirname(__FILE__) + "/../config/environment") require 'rails/test_help' class ActiveSupport::TestCase - # Transactional fixtures accelerate your tests by wrapping each test method - # in a transaction that's rolled back on completion. This ensures that the - # test database remains unchanged so your fixtures don't have to be reloaded - # between every test method. Fewer database queries means faster tests. - # - # Read Mike Clark's excellent walkthrough at - # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting - # - # Every Active Record database supports transactions except MyISAM tables - # in MySQL. Turn off transactional fixtures in this case; however, if you - # don't care one way or the other, switching from MyISAM to InnoDB tables - # is recommended. - # - # The only drawback to using transactional fixtures is when you actually - # need to test transactions. Since your test is bracketed by a transaction, - # any transactions started in your code will be automatically rolled back. - self.use_transactional_fixtures = true - - # Instantiated fixtures are slow, but give you @david where otherwise you - # would need people(:david). If you don't want to migrate your existing - # test cases which use the @david style and don't mind the speed hit (each - # instantiated fixtures translates to a database query per test method), - # then set this back to true. - self.use_instantiated_fixtures = false - # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb new file mode 100644 index 0000000000..38a3cbb035 --- /dev/null +++ b/railties/lib/rails/generators/test_case.rb @@ -0,0 +1,238 @@ +require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/hash/reverse_merge' +require 'rails/generators' +require 'fileutils' + +module Rails + module Generators + # Disable color in output. Easier to debug. + no_color! + + # This class provides a TestCase for testing generators. To setup, you need + # just to configure the destination and set which generator is being tested: + # + # class AppGeneratorTest < Rails::Generators::TestCase + # tests AppGenerator + # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # end + # + # If you want to ensure your destination root is clean before running each test, + # you can set a setup callback: + # + # class AppGeneratorTest < Rails::Generators::TestCase + # tests AppGenerator + # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # setup :prepare_destination + # end + # + class TestCase < ActiveSupport::TestCase + include FileUtils + + extlib_inheritable_accessor :destination_root, :current_path, :generator_class, + :default_arguments, :instance_writer => false + + # Generators frequently change the current path using +FileUtils.cd+. + # So we need to store the path at file load and revert back to it after each test. + self.current_path = File.expand_path(Dir.pwd) + self.default_arguments = [] + + setup :destination_root_is_set?, :ensure_current_path + teardown :ensure_current_path + + # Sets which generator should be tested: + # + # tests AppGenerator + # + def self.tests(klass) + self.generator_class = klass + end + + # Sets default arguments on generator invocation. This can be overwritten when + # invoking it. + # + # arguments %w(app_name --skip-activerecord) + # + def self.arguments(array) + self.default_arguments = array + end + + # Sets the destination of generator files: + # + # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # + def self.destination(path) + self.destination_root = path + end + + # Captures the given stream and returns it: + # + # stream = capture(:stdout){ puts "Cool" } + # stream #=> "Cool\n" + # + def capture(stream) + begin + stream = stream.to_s + eval "$#{stream} = StringIO.new" + yield + result = eval("$#{stream}").string + ensure + eval("$#{stream} = #{stream.upcase}") + end + + result + end + alias :silence :capture + + # Asserts a given file exists. You need to supply an absolute path or a path relative + # to the configured destination: + # + # assert_file "config/environment.rb" + # + # You can also give extra arguments. If the argument is a regexp, it will check if the + # regular expression matches the given file content. If it's a string, it compares the + # file with the given string: + # + # assert_file "config/environment.rb", /initialize/ + # + # Finally, when a block is given, it yields the file content: + # + # assert_file "app/controller/products_controller.rb" do |controller| + # assert_instance_method :index, content do |index| + # assert_match /Product\.all/, index + # end + # end + # + def assert_file(relative, *contents) + absolute = File.expand_path(relative, destination_root) + assert File.exists?(absolute), "Expected file #{relative.inspect} to exist, but does not" + + read = File.read(absolute) if block_given? || !contents.empty? + yield read if block_given? + + contents.each do |content| + case content + when String + assert_equal content, read + when Regexp + assert_match content, read + end + end + end + alias :assert_directory :assert_file + + # Asserts a given file does not exist. You need to supply an absolute path or a + # path relative to the configured destination: + # + # assert_no_file "config/random.rb" + # + def assert_no_file(relative) + absolute = File.expand_path(relative, destination_root) + assert !File.exists?(absolute), "Expected file #{relative.inspect} to not exist, but does" + end + alias :assert_no_directory :assert_no_file + + # Asserts a given file does not exist. You need to supply an absolute path or a + # path relative to the configured destination: + # + # assert_migration "db/migrate/create_products.rb" + # + # This method manipulates the given path and tries to find any migration which + # matches the migration name. For example, the call above is converted to: + # + # assert_file "db/migrate/003_create_products.rb" + # + # Consequently, assert_migration accepts the same arguments has assert_file. + # + def assert_migration(relative, *contents, &block) + file_name = migration_file_name(relative) + assert file_name, "Expected migration #{relative} to exist, but was not found" + assert_file file_name, *contents, &block + end + + # Asserts a given migration does not exist. You need to supply an absolute path or a + # path relative to the configured destination: + # + # assert_no_file "config/random.rb" + # + def assert_no_migration(relative) + file_name = migration_file_name(relative) + assert_nil file_name, "Expected migration #{relative} to not exist, but found #{file_name}" + end + + # Asserts the given class method exists in the given content. This method does not detect + # class methods inside (class << self), only class methods which starts with "self.". + # When a block is given, it yields the content of the method. + # + # assert_migration "db/migrate/create_products.rb" do |migration| + # assert_class_method :up, migration do |up| + # assert_match /create_table/, up + # end + # end + # + def assert_class_method(method, content, &block) + assert_instance_method "self.#{method}", content, &block + end + + # Asserts the given method exists in the given content. When a block is given, + # it yields the content of the method. + # + # assert_file "app/controller/products_controller.rb" do |controller| + # assert_instance_method :index, content do |index| + # assert_match /Product\.all/, index + # end + # end + # + def assert_instance_method(method, content) + assert content =~ /def #{method}(\(.+\))?(.*?)\n end/m, "Expected to have method #{method}" + yield $2.strip if block_given? + end + alias :assert_method :assert_instance_method + + # Runs the generator configured for this class. The first argument is an array like + # command line arguments: + # + # class AppGeneratorTest < Rails::Generators::TestCase + # tests AppGenerator + # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # teardown :cleanup_destination_root + # + # test "database.yml is not created when skipping activerecord" do + # run_generator %w(myapp --skip-activerecord) + # assert_no_file "config/database.yml" + # end + # end + # + # You can provide a configuration hash as second argument. This method returns the output + # printed by the generator. + def run_generator(args=self.default_arguments, config={}) + capture(:stdout) { self.generator_class.start(args, config.reverse_merge(:destination_root => destination_root)) } + end + + # Instantiate the generator. + def generator(args=self.default_arguments, options={}, config={}) + @generator ||= self.generator_class.new(args, options, config.reverse_merge(:destination_root => destination_root)) + end + + protected + + def destination_root_is_set? #:nodoc: + raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root + end + + def ensure_current_path #:nodoc: + cd current_path + end + + def prepare_destination + rm_rf(destination_root) + mkdir_p(destination_root) + end + + def migration_file_name(relative) #:nodoc: + absolute = File.expand_path(relative, destination_root) + dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '') + Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb b/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb index 348ec33582..2ca36a1e44 100644 --- a/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb +++ b/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb @@ -1,5 +1,3 @@ require 'rubygems' require 'test/unit' require 'active_support' -require 'active_support/test_case' - diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb index e4bf4035da..9380aa49b6 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb @@ -16,7 +16,7 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase test "should create <%= file_name %>" do assert_difference('<%= class_name %>.count') do - post :create, :<%= file_name %> => { } + post :create, :<%= file_name %> => <%= table_name %>(:one).attributes end assert_redirected_to <%= file_name %>_path(assigns(:<%= file_name %>)) @@ -33,7 +33,7 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase end test "should update <%= file_name %>" do - put :update, :id => <%= table_name %>(:one).to_param, :<%= file_name %> => { } + put :update, :id => <%= table_name %>(:one).to_param, :<%= file_name %> => <%= table_name %>(:one).attributes assert_redirected_to <%= file_name %>_path(assigns(:<%= file_name %>)) end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 2601765065..e76c476bfb 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -2,28 +2,13 @@ # so fixtures are loaded to the right database silence_warnings { RAILS_ENV = "test" } -require 'rack' -require 'rack/test' - require 'test/unit' require 'active_support/core_ext/kernel/requires' -# AP is always present -require 'action_controller/test_case' -require 'action_view/test_case' - -require 'action_mailer/test_case' if defined?(ActionMailer) -require 'active_model/test_case' if defined?(ActiveModel) - if defined?(ActiveRecord) - require 'active_record/test_case' - require 'active_record/fixtures' - class ActiveSupport::TestCase include ActiveRecord::TestFixtures self.fixture_path = "#{Rails.root}/test/fixtures/" - self.use_instantiated_fixtures = false - self.use_transactional_fixtures = true end ActionController::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 2d6983076a..77ef82856a 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -17,7 +17,6 @@ require 'fileutils' require 'active_support' require 'active_support/core_ext/logger' -require 'active_support/test_case' require 'action_controller' require 'rails/all' diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index adb867ca6d..79dfacdcdb 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -24,6 +24,11 @@ module ApplicationTests assert_equal Pathname.new(app_path), Rails.application.root end + test "the application root can be seen from the application singleton" do + require "#{app_path}/config/environment" + assert_equal Pathname.new(app_path), AppTemplate::Application.root + end + test "the application root can be set" do copy_app add_to_config <<-RUBY diff --git a/railties/test/application/initializer_test.rb b/railties/test/application/initializer_test.rb index 3fd0b0e5df..754c0f1839 100644 --- a/railties/test/application/initializer_test.rb +++ b/railties/test/application/initializer_test.rb @@ -83,6 +83,17 @@ module ApplicationTests assert_equal "congratulations", $test_after_initialize_block2 end + test "after_initialize runs after frameworks have been initialized" do + $activerecord_configurations = nil + add_to_config <<-RUBY + config.after_initialize { $activerecord_configurations = ActiveRecord::Base.configurations } + RUBY + + require "#{app_path}/config/environment" + assert $activerecord_configurations + assert $activerecord_configurations['development'] + end + # i18n test "setting another default locale" do add_to_config <<-RUBY diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb new file mode 100644 index 0000000000..397968a4e7 --- /dev/null +++ b/railties/test/application/middleware_test.rb @@ -0,0 +1,78 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class MiddlewareTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + FileUtils.rm_rf "#{app_path}/config/environments" + end + + test "default middleware stack" do + boot! + + assert_equal [ + "ActionDispatch::Static", + "Rack::Lock", + "Rack::Runtime", + "ActionDispatch::ShowExceptions", + "ActionDispatch::Callbacks", + "ActionDispatch::Session::CookieStore", + "ActionDispatch::ParamsParser", + "Rack::MethodOverride", + "Rack::Head", + "ActiveRecord::ConnectionAdapters::ConnectionManagement", + "ActiveRecord::QueryCache" + ], middleware + end + + test "removing activerecord omits its middleware" do + use_frameworks [] + boot! + assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement") + assert !middleware.include?("ActiveRecord::QueryCache") + end + + test "removes lock if allow concurrency is set" do + add_to_config "config.action_controller.allow_concurrency = true" + boot! + assert !middleware.include?("Rack::Lock") + end + + test "removes static asset server if serve_static_assets is disabled" do + add_to_config "config.serve_static_assets = false" + boot! + assert !middleware.include?("ActionDispatch::Static") + end + + test "use middleware" do + use_frameworks [] + add_to_config "config.middleware.use Rack::Config" + boot! + assert_equal "Rack::Config", middleware.last + end + + test "insert middleware after" do + add_to_config "config.middleware.insert_after ActionDispatch::Static, Rack::Config" + boot! + assert_equal "Rack::Config", middleware.second + end + + test "insert middleware before" do + add_to_config "config.middleware.insert_before ActionDispatch::Static, Rack::Config" + boot! + assert_equal "Rack::Config", middleware.first + end + + private + def boot! + require "#{app_path}/config/environment" + end + + def middleware + AppTemplate::Application.instance.middleware.active.map(&:klass).map(&:name) + end + end +end diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb new file mode 100644 index 0000000000..37175783d8 --- /dev/null +++ b/railties/test/application/test_test.rb @@ -0,0 +1,63 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class TestTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + test "truth" do + app_file 'test/unit/foo_test.rb', <<-RUBY + require 'test_helper' + + class FooTest < ActiveSupport::TestCase + def test_truth + assert true + end + end + RUBY + + run_test 'unit/foo_test.rb' + end + + test "integration test" do + controller 'posts', <<-RUBY + class PostsController < ActionController::Base + end + RUBY + + app_file 'app/views/posts/index.html.erb', <<-HTML + Posts#index + HTML + + app_file 'test/integration/posts_test.rb', <<-RUBY + require 'test_helper' + + class PostsTest < ActionController::IntegrationTest + def test_index + get '/posts' + assert_response :success + assert_template "index" + end + end + RUBY + + run_test 'integration/posts_test.rb' + end + + private + def run_test(name) + result = ruby '-Itest', "#{app_path}/test/#{name}" + assert_equal 0, $?.to_i, result + end + + def ruby(*args) + Dir.chdir(app_path) do + `RUBYLIB='#{$:.join(':')}' #{Gem.ruby} #{args.join(' ')}` + end + end + end +end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index b69f23c965..27b6a49566 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -1,8 +1,10 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' class ActionsTest < GeneratorsTestCase + tests Rails::Generators::AppGenerator + arguments [destination_root] + def setup super @git_plugin_uri = 'git://github.com/technoweenie/restful-authentication.git' @@ -178,14 +180,6 @@ class ActionsTest < GeneratorsTestCase protected - def run_generator - silence(:stdout) { Rails::Generators::AppGenerator.start [destination_root] } - end - - def generator(config={}) - @generator ||= Rails::Generators::Base.new([], {}, { :destination_root => destination_root }.merge!(config)) - end - def action(*args, &block) silence(:stdout){ generator.send(*args, &block) } end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 2cba42551e..3e3cd90bed 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -3,6 +3,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' class AppGeneratorTest < GeneratorsTestCase + arguments [destination_root] def setup super @@ -49,17 +50,33 @@ class AppGeneratorTest < GeneratorsTestCase end def test_invalid_database_option_raises_an_error - content = capture(:stderr){ run_generator(["-d", "unknown"]) } + content = capture(:stderr){ run_generator([destination_root, "-d", "unknown"]) } assert_match /Invalid value for \-\-database option/, content end + def test_invalid_application_name_raises_an_error + content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } + assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content + end + + def test_invalid_application_name_is_fixed + run_generator [File.join(destination_root, "things-43")] + assert_file "things-43/config/environment.rb", /Things43::Application\.initialize!/ + assert_file "things-43/config/application.rb", /^module Things43$/ + end + + def test_application_names_are_not_singularized + run_generator [File.join(destination_root, "hats")] + assert_file "hats/config/environment.rb", /Hats::Application\.initialize!/ + end + def test_config_database_is_added_by_default run_generator assert_file "config/database.yml", /sqlite3/ end def test_config_database_is_not_added_if_skip_activerecord_is_given - run_generator ["--skip-activerecord"] + run_generator [destination_root, "--skip-activerecord"] assert_no_file "config/database.yml" end @@ -76,13 +93,13 @@ class AppGeneratorTest < GeneratorsTestCase end def test_prototype_and_test_unit_are_skipped_if_required - run_generator ["--skip-prototype", "--skip-testunit"] + run_generator [destination_root, "--skip-prototype", "--skip-testunit"] assert_no_file "public/javascripts/prototype.js" assert_no_file "test" end def test_shebang_is_added_to_files - run_generator ["--ruby", "foo/bar/baz"] + run_generator [destination_root, "--ruby", "foo/bar/baz"] %w( about @@ -97,7 +114,7 @@ class AppGeneratorTest < GeneratorsTestCase end def test_shebang_when_is_the_same_as_default_use_env - run_generator ["--ruby", Thor::Util.ruby_command] + run_generator [destination_root, "--ruby", Thor::Util.ruby_command] %w( about @@ -113,11 +130,11 @@ class AppGeneratorTest < GeneratorsTestCase def test_template_from_dir_pwd FileUtils.cd(Rails.root) - assert_match /It works from file!/, run_generator(["-m", "lib/template.rb"]) + assert_match /It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]) end def test_template_raises_an_error_with_invalid_path - content = capture(:stderr){ run_generator(["-m", "non/existant/path"]) } + content = capture(:stderr){ run_generator([destination_root, "-m", "non/existant/path"]) } assert_match /The template \[.*\] could not be loaded/, content assert_match /non\/existant\/path/, content end @@ -127,7 +144,7 @@ class AppGeneratorTest < GeneratorsTestCase template = %{ say "It works!" } template.instance_eval "def read; self; end" # Make the string respond to read - generator(:template => path, :database => "sqlite3").expects(:open).with(path).returns(template) + generator([destination_root], :template => path, :database => "sqlite3").expects(:open).with(path).returns(template) assert_match /It works!/, silence(:stdout){ generator.invoke } end @@ -151,28 +168,20 @@ class AppGeneratorTest < GeneratorsTestCase end def test_dev_option - run_generator ["--dev"] + run_generator [destination_root, "--dev"] rails_path = File.expand_path('../../..', Rails.root) - dev_gem = %(gem "rails", :path => #{rails_path.inspect}) + dev_gem = %(directory #{rails_path.inspect}, :glob => "{*/,}*.gemspec") assert_file 'Gemfile', /^#{Regexp.escape(dev_gem)}$/ end def test_edge_option - run_generator ["--edge"] + run_generator [destination_root, "--edge"] edge_gem = %(gem "rails", :git => "git://github.com/rails/rails.git") assert_file 'Gemfile', /^#{Regexp.escape(edge_gem)}$/ end protected - def run_generator(args=[]) - silence(:stdout) { Rails::Generators::AppGenerator.start [destination_root].concat(args) } - end - - def generator(options={}) - @generator ||= Rails::Generators::AppGenerator.new([destination_root], options, :destination_root => destination_root) - end - def action(*args, &block) silence(:stdout){ generator.send(*args, &block) } end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 56bc688ad0..8e2fd3b9ed 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -1,12 +1,12 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/controller/controller_generator' class ControllerGeneratorTest < GeneratorsTestCase + arguments %w(Account foo bar) def test_help_does_not_show_invoked_generators_options_if_they_already_exist content = run_generator ["--help"] - assert_no_match /Helper options:/, content + assert_no_match /Helper options\:/, content end def test_controller_skeleton_is_created @@ -66,15 +66,8 @@ class ControllerGeneratorTest < GeneratorsTestCase run_generator assert_file "app/controllers/account_controller.rb" do |controller| - assert_instance_method controller, :foo - assert_instance_method controller, :bar + assert_instance_method :foo, controller + assert_instance_method :bar, controller end end - - protected - - def run_generator(args=["Account", "foo", "bar"]) - silence(:stdout) { Rails::Generators::ControllerGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/generator_generator_test.rb b/railties/test/generators/generator_generator_test.rb index aea3f4da51..28377f23b0 100644 --- a/railties/test/generators/generator_generator_test.rb +++ b/railties/test/generators/generator_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/generator/generator_generator' class GeneratorGeneratorTest < GeneratorsTestCase + arguments %w(awesome) def test_generator_skeleton_is_created run_generator @@ -16,11 +16,4 @@ class GeneratorGeneratorTest < GeneratorsTestCase assert_file "lib/generators/awesome/awesome_generator.rb", /class AwesomeGenerator < Rails::Generators::NamedBase/ end - - protected - - def run_generator(args=["awesome"], config={}) - silence(:stdout) { Rails::Generators::GeneratorGenerator.start args, config.merge(:destination_root => destination_root) } - end - end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 4ce48a453b..35567f7929 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -10,93 +10,23 @@ end Rails.application.config.root = Rails.root require 'rails/generators' +require 'rails/generators/test_case' + require 'rubygems' require 'active_record' require 'action_dispatch' -CURRENT_PATH = File.expand_path(Dir.pwd) -Rails::Generators.no_color! - -class GeneratorsTestCase < ActiveSupport::TestCase - include FileUtils - - def destination_root - File.join(Rails.root, "tmp") - end +class GeneratorsTestCase < Rails::Generators::TestCase + destination File.join(Rails.root, "tmp") + setup :prepare_destination - def setup - cd CURRENT_PATH - rm_rf(destination_root) - mkdir_p(destination_root) + def self.inherited(base) + base.tests Rails::Generators.const_get(base.name.sub(/Test$/, '')) + rescue + # Do nothing. end def test_truth - # don't complain, test/unit - end - - def capture(stream) - begin - stream = stream.to_s - eval "$#{stream} = StringIO.new" - yield - result = eval("$#{stream}").string - ensure - eval("$#{stream} = #{stream.upcase}") - end - - result - end - alias :silence :capture - - def assert_file(relative, *contents) - absolute = File.join(destination_root, relative) - assert File.exists?(absolute), "Expected file #{relative.inspect} to exist, but does not" - - read = File.read(absolute) if block_given? || !contents.empty? - yield read if block_given? - - contents.each do |content| - case content - when String - assert_equal content, read - when Regexp - assert_match content, read - end - end - end - - def assert_no_file(relative) - absolute = File.join(destination_root, relative) - assert !File.exists?(absolute), "Expected file #{relative.inspect} to not exist, but does" + # Don't cry test/unit end - - def assert_migration(relative, *contents, &block) - file_name = migration_file_name(relative) - assert file_name, "Expected migration #{relative} to exist, but was not found" - assert_file File.join(File.dirname(relative), file_name), *contents, &block - end - - def assert_no_migration(relative) - file_name = migration_file_name(relative) - assert_nil file_name, "Expected migration #{relative} to not exist, but found #{file_name}" - end - - def assert_class_method(content, method, &block) - assert_instance_method content, "self.#{method}", &block - end - - def assert_instance_method(content, method) - assert content =~ /def #{method}(\(.+\))?(.*?)\n end/m, "Expected to have method #{method}" - yield $2.strip if block_given? - end - - protected - - def migration_file_name(relative) - absolute = File.join(destination_root, relative) - dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '') - - migration = Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first - File.basename(migration) if migration - end -end +end
\ No newline at end of file diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb index f8bfc517a2..cf18782986 100644 --- a/railties/test/generators/helper_generator_test.rb +++ b/railties/test/generators/helper_generator_test.rb @@ -1,4 +1,3 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/helper/helper_generator' @@ -6,6 +5,7 @@ ObjectHelper = Class.new AnotherObjectHelperTest = Class.new class HelperGeneratorTest < GeneratorsTestCase + arguments %w(admin) def test_helper_skeleton_is_created run_generator @@ -50,11 +50,4 @@ class HelperGeneratorTest < GeneratorsTestCase end end end - - protected - - def run_generator(args=["admin"]) - silence(:stdout) { Rails::Generators::HelperGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb index 6a504ceea2..88e18be5b2 100644 --- a/railties/test/generators/integration_test_generator_test.rb +++ b/railties/test/generators/integration_test_generator_test.rb @@ -1,18 +1,11 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/integration_test/integration_test_generator' class IntegrationTestGeneratorTest < GeneratorsTestCase + arguments %w(integration) def test_integration_test_skeleton_is_created run_generator assert_file "test/integration/integration_test.rb", /class IntegrationTest < ActionController::IntegrationTest/ end - - protected - - def run_generator(args=["integration"]) - silence(:stdout) { Rails::Generators::IntegrationTestGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 251474ad16..ee4346eb71 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/mailer/mailer_generator' class MailerGeneratorTest < GeneratorsTestCase + arguments %w(notifier foo bar) def test_mailer_skeleton_is_created run_generator @@ -42,11 +42,4 @@ class MailerGeneratorTest < GeneratorsTestCase assert_file "app/models/notifier.rb", /def foo/ assert_file "app/models/notifier.rb", /def bar/ end - - protected - - def run_generator(args=["notifier", "foo", "bar"]) - silence(:stdout) { Rails::Generators::MailerGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/metal_generator_test.rb b/railties/test/generators/metal_generator_test.rb index 80bf342892..5d6a277561 100644 --- a/railties/test/generators/metal_generator_test.rb +++ b/railties/test/generators/metal_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/metal/metal_generator' class MetalGeneratorTest < GeneratorsTestCase + arguments %w(foo) def test_metal_skeleton_is_created run_generator @@ -13,11 +13,4 @@ class MetalGeneratorTest < GeneratorsTestCase content = capture(:stderr){ run_generator ["object"] } assert_match /The name 'Object' is either already used in your application or reserved/, content end - - protected - - def run_generator(args=["foo"]) - silence(:stdout) { Rails::Generators::MetalGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 35172a8be4..2fd3e5c056 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -1,32 +1,30 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/migration/migration_generator' class MigrationGeneratorTest < GeneratorsTestCase - def test_migration - @migration = "change_title_body_from_posts" - run_generator - assert_migration "db/migrate/#{@migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration/ + migration = "change_title_body_from_posts" + run_generator [migration] + assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration/ end def test_migration_with_class_name - @migration = "ChangeTitleBodyFromPosts" - run_generator - assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{@migration} < ActiveRecord::Migration/ + migration = "ChangeTitleBodyFromPosts" + run_generator [migration] + assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/ end def test_add_migration_with_attributes - @migration = "add_title_body_to_posts" - run_generator [@migration, "title:string", "body:text"] + migration = "add_title_body_to_posts" + run_generator [migration, "title:string", "body:text"] - assert_migration "db/migrate/#{@migration}.rb" do |content| - assert_class_method content, :up do |up| + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_class_method :up, content do |up| assert_match /add_column :posts, :title, :string/, up assert_match /add_column :posts, :body, :text/, up end - assert_class_method content, :down do |down| + assert_class_method :down, content do |down| assert_match /remove_column :posts, :title/, down assert_match /remove_column :posts, :body/, down end @@ -34,26 +32,19 @@ class MigrationGeneratorTest < GeneratorsTestCase end def test_remove_migration_with_attributes - @migration = "remove_title_body_from_posts" - run_generator [@migration, "title:string", "body:text"] + migration = "remove_title_body_from_posts" + run_generator [migration, "title:string", "body:text"] - assert_migration "db/migrate/#{@migration}.rb" do |content| - assert_class_method content, :up do |up| + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_class_method :up, content do |up| assert_match /remove_column :posts, :title/, up assert_match /remove_column :posts, :body/, up end - assert_class_method content, :down do |down| + assert_class_method :down, content do |down| assert_match /add_column :posts, :title, :string/, down assert_match /add_column :posts, :body, :text/, down end end end - - protected - - def run_generator(args=[@migration]) - silence(:stdout) { Rails::Generators::MigrationGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index e073b11e1e..051a43706b 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' class ModelGeneratorTest < GeneratorsTestCase + arguments %w(Account name:string age:integer) def test_help_shows_invoked_generators_options content = run_generator ["--help"] @@ -84,13 +84,13 @@ class ModelGeneratorTest < GeneratorsTestCase run_generator ["product", "name:string", "supplier_id:integer"] assert_migration "db/migrate/create_products.rb" do |m| - assert_class_method m, :up do |up| + assert_class_method :up, m do |up| assert_match /create_table :products/, up assert_match /t\.string :name/, up assert_match /t\.integer :supplier_id/, up end - assert_class_method m, :down do |down| + assert_class_method :down, m do |down| assert_match /drop_table :products/, down end end @@ -126,7 +126,7 @@ class ModelGeneratorTest < GeneratorsTestCase run_generator ["account", "--no-timestamps"] assert_migration "db/migrate/create_accounts.rb" do |m| - assert_class_method m, :up do |up| + assert_class_method :up, m do |up| assert_no_match /t.timestamps/, up end end @@ -171,11 +171,4 @@ class ModelGeneratorTest < GeneratorsTestCase content = capture(:stderr){ run_generator ["object"] } assert_match /The name 'Object' is either already used in your application or reserved/, content end - - protected - - def run_generator(args=["Account", "name:string", "age:integer"], config={}) - silence(:stdout) { Rails::Generators::ModelGenerator.start args, config.merge(:destination_root => destination_root) } - end - end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 98cbf9b8f6..8c1df3b992 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,4 +1,3 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' @@ -13,9 +12,10 @@ module ActiveRecord end class NamedBaseTest < GeneratorsTestCase + tests Rails::Generators::ScaffoldControllerGenerator def test_named_generator_attributes - g = Rails::Generators::ScaffoldControllerGenerator.new ["admin/foo"] + g = generator ["admin/foo"] assert_equal 'admin/foo', g.name assert_equal %w(admin), g.class_path assert_equal 1, g.class_nesting_depth @@ -28,12 +28,12 @@ class NamedBaseTest < GeneratorsTestCase def test_named_generator_attributes_without_pluralized ActiveRecord::Base.pluralize_table_names = false - g = Rails::Generators::ScaffoldControllerGenerator.new ["admin/foo"] + g = generator ["admin/foo"] assert_equal "admin_#{g.singular_name}", g.table_name end def test_scaffold_plural_names - g = Rails::Generators::ScaffoldControllerGenerator.new ["ProductLine"] + g = generator ["ProductLine"] assert_equal "ProductLines", g.controller_name assert_equal "ProductLines", g.controller_class_name assert_equal "product_lines", g.controller_file_name diff --git a/railties/test/generators/observer_generator_test.rb b/railties/test/generators/observer_generator_test.rb index 6fed2998dd..44d9e4a9f3 100644 --- a/railties/test/generators/observer_generator_test.rb +++ b/railties/test/generators/observer_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/observer/observer_generator' class ObserverGeneratorTest < GeneratorsTestCase + arguments %w(account) def test_invokes_default_orm run_generator @@ -23,11 +23,4 @@ class ObserverGeneratorTest < GeneratorsTestCase content = run_generator ["account", "--test-framework=rspec"] assert_match /rspec \[not found\]/, content end - - protected - - def run_generator(args=["account"]) - silence(:stdout) { Rails::Generators::ObserverGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/performance_test_generator_test.rb b/railties/test/generators/performance_test_generator_test.rb index d19128f79a..099575ea1d 100644 --- a/railties/test/generators/performance_test_generator_test.rb +++ b/railties/test/generators/performance_test_generator_test.rb @@ -1,18 +1,11 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/performance_test/performance_test_generator' class PerformanceTestGeneratorTest < GeneratorsTestCase + arguments %w(performance) def test_performance_test_skeleton_is_created run_generator assert_file "test/performance/performance_test.rb", /class PerformanceTest < ActionController::PerformanceTest/ end - - protected - - def run_generator(args=["performance"]) - silence(:stdout) { Rails::Generators::PerformanceTestGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index f5b8b6ffb6..f84b8b6d50 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/plugin/plugin_generator' class PluginGeneratorTest < GeneratorsTestCase + arguments %w(plugin_fu) def test_plugin_skeleton_is_created run_generator @@ -46,11 +46,4 @@ class PluginGeneratorTest < GeneratorsTestCase run_generator run_generator ["plugin_fu"], :behavior => :revoke end - - protected - - def run_generator(args=["plugin_fu"], config={}) - silence(:stdout) { Rails::Generators::PluginGenerator.start args, config.merge(:destination_root => destination_root) } - end - end diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index dff3908ea1..15c0ca0f01 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/resource/resource_generator' class ResourceGeneratorTest < GeneratorsTestCase + arguments %w(account) def setup super @@ -50,8 +50,8 @@ class ResourceGeneratorTest < GeneratorsTestCase run_generator ["account", "--actions", "index", "new"] assert_file "app/controllers/accounts_controller.rb" do |controller| - assert_instance_method controller, :index - assert_instance_method controller, :new + assert_instance_method :index, controller + assert_instance_method :new, controller end assert_file "app/views/accounts/index.html.erb" @@ -96,11 +96,4 @@ class ResourceGeneratorTest < GeneratorsTestCase assert_no_match /resources :accounts$/, route end end - - protected - - def run_generator(args=["account"], config={}) - silence(:stdout) { Rails::Generators::ResourceGenerator.start args, config.merge(:destination_root => destination_root) } - end - end diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 02155c295c..7593c14dd9 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -1,4 +1,3 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' @@ -8,6 +7,7 @@ module Unknown end class ScaffoldControllerGeneratorTest < GeneratorsTestCase + arguments %w(User name:string age:integer) def test_controller_skeleton_is_created run_generator @@ -15,35 +15,35 @@ class ScaffoldControllerGeneratorTest < GeneratorsTestCase assert_file "app/controllers/users_controller.rb" do |content| assert_match /class UsersController < ApplicationController/, content - assert_instance_method content, :index do |m| + assert_instance_method :index, content do |m| assert_match /@users = User\.all/, m end - assert_instance_method content, :show do |m| + assert_instance_method :show, content do |m| assert_match /@user = User\.find\(params\[:id\]\)/, m end - assert_instance_method content, :new do |m| + assert_instance_method :new, content do |m| assert_match /@user = User\.new/, m end - assert_instance_method content, :edit do |m| + assert_instance_method :edit, content do |m| assert_match /@user = User\.find\(params\[:id\]\)/, m end - assert_instance_method content, :create do |m| + assert_instance_method :create, content do |m| assert_match /@user = User\.new\(params\[:user\]\)/, m assert_match /@user\.save/, m assert_match /@user\.errors/, m end - assert_instance_method content, :update do |m| + assert_instance_method :update, content do |m| assert_match /@user = User\.find\(params\[:id\]\)/, m assert_match /@user\.update_attributes\(params\[:user\]\)/, m assert_match /@user\.errors/, m end - assert_instance_method content, :destroy do |m| + assert_instance_method :destroy, content do |m| assert_match /@user = User\.find\(params\[:id\]\)/, m assert_match /@user\.destroy/, m end @@ -108,7 +108,7 @@ class ScaffoldControllerGeneratorTest < GeneratorsTestCase assert_file "app/controllers/users_controller.rb" do |content| assert_match /class UsersController < ApplicationController/, content - assert_instance_method content, :index do |m| + assert_instance_method :index, content do |m| assert_match /@users = User\.all/, m end end @@ -127,7 +127,7 @@ class ScaffoldControllerGeneratorTest < GeneratorsTestCase assert_file "app/controllers/users_controller.rb" do |content| assert_match /class UsersController < ApplicationController/, content - assert_instance_method content, :index do |m| + assert_instance_method :index, content do |m| assert_match /@users = User\.find\(:all\)/, m assert_no_match /@users = User\.all/, m end @@ -135,11 +135,4 @@ class ScaffoldControllerGeneratorTest < GeneratorsTestCase ensure Unknown::Generators.send :remove_const, :ActiveModel end - - protected - - def run_generator(args=["User", "name:string", "age:integer"]) - silence(:stdout) { Rails::Generators::ScaffoldControllerGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 0b961cee19..4ddc7b1c89 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -1,8 +1,8 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold/scaffold_generator' class ScaffoldGeneratorTest < GeneratorsTestCase + arguments %w(product_line title:string price:integer) def setup super @@ -32,35 +32,35 @@ class ScaffoldGeneratorTest < GeneratorsTestCase assert_file "app/controllers/product_lines_controller.rb" do |content| assert_match /class ProductLinesController < ApplicationController/, content - assert_instance_method content, :index do |m| + assert_instance_method :index, content do |m| assert_match /@product_lines = ProductLine\.all/, m end - assert_instance_method content, :show do |m| + assert_instance_method :show, content do |m| assert_match /@product_line = ProductLine\.find\(params\[:id\]\)/, m end - assert_instance_method content, :new do |m| + assert_instance_method :new, content do |m| assert_match /@product_line = ProductLine\.new/, m end - assert_instance_method content, :edit do |m| + assert_instance_method :edit, content do |m| assert_match /@product_line = ProductLine\.find\(params\[:id\]\)/, m end - assert_instance_method content, :create do |m| + assert_instance_method :create, content do |m| assert_match /@product_line = ProductLine\.new\(params\[:product_line\]\)/, m assert_match /@product_line\.save/, m assert_match /@product_line\.errors/, m end - assert_instance_method content, :update do |m| + assert_instance_method :update, content do |m| assert_match /@product_line = ProductLine\.find\(params\[:id\]\)/, m assert_match /@product_line\.update_attributes\(params\[:product_line\]\)/, m assert_match /@product_line\.errors/, m end - assert_instance_method content, :destroy do |m| + assert_instance_method :destroy, content do |m| assert_match /@product_line = ProductLine\.find\(params\[:id\]\)/, m assert_match /@product_line\.destroy/, m end @@ -89,7 +89,7 @@ class ScaffoldGeneratorTest < GeneratorsTestCase def test_scaffold_on_revoke run_generator - run_generator :behavior => :revoke + run_generator ["product_line"], :behavior => :revoke # Model assert_no_file "app/models/product_line.rb" @@ -117,14 +117,4 @@ class ScaffoldGeneratorTest < GeneratorsTestCase # Stylesheets (should not be removed) assert_file "public/stylesheets/scaffold.css" end - - protected - - def run_generator(config={}) - silence(:stdout) do - Rails::Generators::ScaffoldGenerator.start ["product_line", "title:string", "price:integer"], - config.merge(:destination_root => destination_root) - end - end - end diff --git a/railties/test/generators/session_migration_generator_test.rb b/railties/test/generators/session_migration_generator_test.rb index 34fb996b7f..251ffb19ed 100644 --- a/railties/test/generators/session_migration_generator_test.rb +++ b/railties/test/generators/session_migration_generator_test.rb @@ -1,9 +1,7 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/session_migration/session_migration_generator' class SessionMigrationGeneratorTest < GeneratorsTestCase - def test_session_migration_with_default_name run_generator assert_migration "db/migrate/add_sessions_table.rb", /class AddSessionsTable < ActiveRecord::Migration/ @@ -24,11 +22,4 @@ class SessionMigrationGeneratorTest < GeneratorsTestCase ensure ActiveRecord::SessionStore::Session.table_name = "sessions" end - - protected - - def run_generator(args=[]) - silence(:stdout) { Rails::Generators::SessionMigrationGenerator.start args, :destination_root => destination_root } - end - end diff --git a/railties/test/generators/stylesheets_generator_test.rb b/railties/test/generators/stylesheets_generator_test.rb index 15263d4bb8..d9079327ba 100644 --- a/railties/test/generators/stylesheets_generator_test.rb +++ b/railties/test/generators/stylesheets_generator_test.rb @@ -1,9 +1,7 @@ -require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/stylesheets/stylesheets_generator' class StylesheetsGeneratorTest < GeneratorsTestCase - def test_copy_stylesheets run_generator assert_file "public/stylesheets/scaffold.css" @@ -11,14 +9,7 @@ class StylesheetsGeneratorTest < GeneratorsTestCase def test_stylesheets_are_not_deleted_on_revoke run_generator - run_generator :behavior => :revoke + run_generator [], :behavior => :revoke assert_file "public/stylesheets/scaffold.css" end - - protected - - def run_generator(config={}) - silence(:stdout) { Rails::Generators::StylesheetsGenerator.start [], config.merge(:destination_root => destination_root) } - end - end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 4b7b80c7f5..2df218debc 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -9,6 +9,11 @@ class GeneratorsTest < GeneratorsTestCase Gem.stubs(:respond_to?).with(:loaded_specs).returns(false) end + def test_invoke_add_generators_to_raw_lookups + TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) + Rails::Generators.invoke("test_unit:model", ["Account"]) + end + def test_invoke_when_generator_is_not_found output = capture(:stdout){ Rails::Generators.invoke :unknown } assert_equal "Could not find generator unknown.\n", output @@ -51,12 +56,6 @@ class GeneratorsTest < GeneratorsTestCase assert_equal "foobar:foobar", klass.namespace end - def test_find_by_namespace_add_generators_to_raw_lookups - klass = Rails::Generators.find_by_namespace("test_unit:model") - assert klass - assert_equal "test_unit:generators:model", klass.namespace - end - def test_find_by_namespace_lookup_to_the_rails_root_folder klass = Rails::Generators.find_by_namespace(:fixjour) assert klass @@ -96,7 +95,7 @@ class GeneratorsTest < GeneratorsTestCase end def test_builtin_generators - assert Rails::Generators.builtin.include? %w(rails model) + assert Rails::Generators.builtin.include?("rails:model") end def test_rails_generators_help_with_builtin_information @@ -107,7 +106,7 @@ class GeneratorsTest < GeneratorsTestCase def test_rails_generators_with_others_information output = capture(:stdout){ Rails::Generators.help }.split("\n").last - assert_equal "Others: active_record:fixjour, fixjour, foobar, mspec, rails:javascripts.", output + assert_equal "Others: active_record:fixjour, fixjour, foobar:foobar, mspec, rails:javascripts, xspec.", output end def test_warning_is_shown_if_generator_cant_be_loaded @@ -178,6 +177,8 @@ class GeneratorsTest < GeneratorsTestCase end assert_equal false, klass.class_options[:generate].default + ensure + Rails::Generators.subclasses.delete(klass) end def test_source_paths_for_not_namespaced_generators diff --git a/railties/test/plugins/vendored_test.rb b/railties/test/plugins/vendored_test.rb index 9a2d40cad8..b3b85891b2 100644 --- a/railties/test/plugins/vendored_test.rb +++ b/railties/test/plugins/vendored_test.rb @@ -191,5 +191,11 @@ module PluginsTest boot_rails assert_equal [:a, :c, :b], $arr end + + test "plugin order array is strings" do + add_to_config "config.plugins = %w( c_plugin all )" + boot_rails + assert_equal [:c, :a, :b], $arr + end end -end
\ No newline at end of file +end diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 435bd34925..edab27465e 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -1,6 +1,5 @@ require 'abstract_unit' require 'action_controller' -require 'action_controller/test_case' require 'rails/info' require 'rails/info_controller' |